Bypassing Lenovo's WiFi Module Authorization
I recently resurrected an old Lenovo notebook (a Lenovo Yoga 2 Pro specifically) as a test machine for various things. For being such an old machine (first released in 2014) it still performs relatively well, the display, SSD, CPU and memory are still fine for smaller tasks. But the integrated WiFi module is terrible, especially in newer networks. I run my main network only in the 5GHz band as the 2.4GHz band is very congested here and it can't even connect as it does not have any support for the 5GHz band at all. As WiFi modules are relatively inexpensive (<20$) I decided to replace it with a modern Intel AX210 as it is cheap and known to work well on Linux.
I went ahead and replaced the module in my notebook and started it. Upon booting, the firmware printed "Unauthorized Wireless network card is plugged in. Power off and remove it" and refused to do anything after that. This is because Lenovo has a module built into the firmware which checks if the WiFi module has the exact same PCI ID as the one originally shipped and aborts the boot if it detects any other ID.
A workaround (at least on Linux) is to boot with the original module installed (otherwise the PCIe port doesn't get initialized by the firmware), delete the PCI device for the module (echo 1 > /sys/class/pci_bus/[your_bus_num]/device/delete
), suspend the notebook, replace the module while in suspend, resume and rescan the bus (echo 1 > /sys/class/pci_bus/0000:00/rescan
). You have to do this every time you reboot though, which makes this rather tedious. On Linux you can use kexec to soft-reboot without going through the firmware, but there are still things which need a hard reboot.
To actually fix the problem at the root, I need to patch the firmware which is stored on a flash chip on the motherboard. There are two ways to get to it, via the chipset SPI controller while the machine is booted or from an external clip probe while the machine is without power. The first method does not require opening the notebook nor additional hardware but is risky. If the modified firmware you flash has any issues booting you essentially have a brick unless you have the external clip probe and flasher for the second method.
I have the necessary hardware, so I decided to go with the safer method. I disassembled the notebook until I got access to the SPI flash chip containing the firmware. This is in 99% of cases a single SOIC-8 chip with 25 somewhere in the part number. For the Lenovo Yoga 2 Pro this is a W25Q64FVSSIG, a 3V 64Mbit flash chip made by Winbond. I used a CH341a-based flasher set to 3.3V (make sure that you're not feeding too much voltage into your IC) with a SOIC-8 clip probe to access it. On the software side I used flashrom, which has support for tons of SPI flash chips including this one. I first read the existing contents with flashrom -p ch341a_spi --read=stock.rom
, then did the same thing again and compared checksums to make sure I have a valid copy of the stock contents should anything go wrong. With the flash contents in hand, I needed to hack out the check.
Since this notebook, same as most since ~2011 has UEFI-based firmware, I used UEFITool to open the flash contents. It successfully detects a bunch of data structures, including two UEFI FFSv2 filesystems. I extracted the complete body of both filesystems and called strings
on it to find the one containing the messge I was seeing. Since UEFI contains both single-byte-based ASCII/UTF-8 text as well as UCS2/UTF-16 text I needed to call it with strings -e l
as well (note UEFI is always little-endian). I spotted the error message string in the second strings
call of the first FFSv2 volume in close proximity to the OneKeyRecovery executable as well as the UEFIL05BIOSLock executable. I decided to extract the PE body of both (in UEFITool) and called strings on both again, determining that UEFIL05BIOSLock is the executable containing the WiFi module authorization code. Since this executable is just a special Windows PE file, I loaded it into Ghidra to figure out how it operates.
I searched Ghidra for the error message (Search -> For strings) and jumped to the sole function it is referenced from, which I called checkHandler:
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
void checkHandler(EFI_EVENT *Event,void *context)
{
EFI_STATUS status;
bool consoleCleared;
bool stopBoot;
stopBoot = false;
consoleCleared = false;
if ((enableL05WWANLock) && (_wwanState == 2)) {
clearConsole();
consoleCleared = true;
print(L"\nUnauthorized WWAN network card is plugged in. Power off and remove it");
stopBoot = true;
}
if ((((enableL05WLANLock != false) && (wlan_pci_buses != (wlan_pci_bus_desc *)0x0)) &&
(authorized_wlan_devices != (authorized_wlan_device *)0x0)) &&
(status = checkWLANAuthorized(), status == 0x8000000000000003)) {
if (!consoleCleared) {
clearConsole();
}
print(L"\nUnauthorized Wireless network card is plugged in. Power off and remove it");
stopBoot = true;
}
if (stopBoot) {
do {
} while( true );
}
return;
}
Note that this is the fully reverse-engineered function, after I've given everything names and fixed any issues with data structures. This is very different from what you get when you're just opening this executable in Ghidra. If you find yourself needing to reverse-engineer (U)EFI executables, I can highly recommend uefi-firmware-tools by Alex James which I also used to assist to reverse-engineer this module.
In a nutshell what this function does is it checks if the WWAN (mobile network) or WLAN lock global variables are set, if they are it checks if the authorized device global variables aren't null pointers and if that succeeds it tries to check if the device is authorized. In case of the WWAN device the check has already been performed prior to this function being called and its result is stored in _wwanState
. If either the WWAN or WLAN device are not in the authorized list it sets stopBoot
to true and prints the aformentioned message to the screen. Finally it just enters an infinite loop to prevent the system from booting.
The actual check is in checkWLANAuthorized which is a relatively lengthy function which I'm not going to show here reading the PCI device config space for each bus in a list of buses, reading the vendor and product IDs from each of those devices and comparing it against a list of authorized devices.
checkHandler
is referenced from the executable entry point (the low-level equivalent of a main function), where it gets passed to a function (efiRegisterNotify
) which registers it as an EFI event handler to be executed just before the boot. In this function one can also see where most of the data in global variables is coming from: A custom Lenovo EFI protocol with GUID a98e0db6-796e-4c19-b3a23036ad5f02a7 containing a pointer to a rather large data structure with the list of approved devices as well as the bus identifiers (PCIe for WiFi, USB for WWAN) which need to be checked.
EFI_STATUS _ModuleEntryPoint(EFI_HANDLE ImageHandle,EFI_SYSTEM_TABLE *SystemTable)
{
EFI_STATUS status;
undefined local_res18 [16];
setHandles(ImageHandle,SystemTable);
enableL05WLANLock = enableL05WLANLock;
enableL05WWANLock = enableL05WWANLock;
if ((lenovo_ext_con_proto == 0) &&
(status = (*bootServices->LocateProtocol)
(&EFI_GUID_180001a50,(void *)0x0,(void **)&lenovo_ext_con_proto),
(longlong)status < 0)) {
return status;
}
// Lenovo's authorized devices protocol
// GUID a98e0db6-796e-4c19-b3a23036ad5f02a7
status = (*bootServices->LocateProtocol)(&lenovo_auth_proto_guid,(void *)0x0,&lenovo_auth_proto);
if ((longlong)status < 0) {
status = 0;
}
else {
if (enableL05WLANLock != false) {
wlan_pci_buses = lenovo_auth_proto->auth_data->wlan_pci_buses;
authorized_wlan_devices = lenovo_auth_proto->auth_data->authorized_wlan_devices;
}
if (enableL05WWANLock != false) {
wwan_usb_buses = lenovo_auth_proto->auth_data->wwan_usb_buses;
authorized_wwan_devices = lenovo_auth_proto->auth_data->authorized_wwan_devices;
if ((wwan_usb_buses != (undefined **)0x0) &&
(authorized_wwan_devices != (authorized_wwan_device *)0x0)) {
registerWWANCheck();
}
}
status = efiRegisterNotify(8,checkHandler,0,local_res18);
}
return status;
}
void efiRegisterNotify(EFI_TPL template,EFI_EVENT_NOTIFY notify_func,void *ctx,EFI_EVENT *event)
{
if (notify_func == (EFI_EVENT_NOTIFY)0x0) {
notify_func = (EFI_EVENT_NOTIFY)&DAT_180000934;
}
(*bootServices->CreateEventEx)(0x200,template,notify_func,ctx,&EfiEventReadyToBootGuid,event);
return;
}
So now that we understand how this executable works, I needed to find a way to bypass it. The logically simplest way would be to just delete it, but it creates custom events in functions not shown here, the absence of these events might cause unknown problems and I don't have the time to reverse-engineer the rest of the firmware to make sure it doesn't. Another way would be to add my new WiFi module's IDs to the list of approved devices, but that list is not in here and this would require further reverse-engineering. Or I could just use my knowledge of this executable to hack it so it never triggers the infinite loop.
In this case I'm actually quite lucky as the global variables enableL05WLANLockStatic
and enableL05WWANLockStatic
are statically defined in this executable's data section which I can modify. Setting both of those to zero would essentially bypass all of this module's harmful features as none of the checkHandler's conditions would evaluate to true.
They are also in a neat OEM Switch section, meaning they are likely intended to be toggled by the OEM while testing and toggling them thus should not cause any adverse effects. Because they have a nice string description just in front of the values (be aware of the trailing null bytes also in front of them though) I chose to just open the executable in a hex editor, find the two relevant strings and changed the two bytes from 0x01 to 0x00, thus disabling the lock. To confirm the mod, one could load the resulting binary back into Ghidra but I was quite confident in my change, so I just went ahead and used UEFITool again to replace the PE body of the executable in question with the modded one. Note that if the menu entry to do that is greyed out, make sure you use the old engine version of UEFITool to perform the mod as the new engine cannot yet replace parts of the firmware.
After saving the resulting firmware file from UEFITool I used flashrom again to write back my changes to the SPI flash chip, reassembled the notebook and booted it. And indeed, it booted straight into the operating system, even with the "unauthorized" WiFi module installed!
Now, could I have guessed how to patch this just from the strings
output and a bit of trial & error? Most likely, but to me it's a lot more fun and educational to solve the problem a bit more thoroughly.
A few notes for last:
- I am in no way responsible for anything you do to your devices as a result of this blog post. This is explicitly unsupported by the vendor and you do it on your own risk.
- This post has been written specifically using the Lenovo Yoga 2 Pro's firmware. Please exercise caution and don't blindly apply it to other models.