Actual Engineering
The story of a man who insists on solving hardware issues with software.
I'm using an Asus laptop, and it has this nice feature where pressing fn+esc toggles the function keys row between hotkey mode and FN key mode. Hotkey mode lets you change brightness, volume, keyboard backlight, and so on. The FN key mode sends the actual F1-F12 codes to the OS. It's a nice feature, but sometimes I'm using a mouse only, cup of tea in another, and can't be bothered to reach the keyboard.
Looking up the subject online gives a lot of irrelevant results but the idea is that since the BIOS controls the keyboard it's impossible to control from the software side. And yet, Asus has a program that lets you toggle it from the software side. I wanted to figure out how that worked. This was my first time ever attempting reverse engineering anything, and I'll document here a lot of things I learned.
I'll be using a lot of technical terminology and references to various Windows systems. I'll try to insert a link to anything newly introduced but I do encourage to do your own internet searches on anything that piques your interest.
TL;DR: AsKeybdHk.exe searches for a process with title and class name "HControl" and posts a message (
PostMessage
) to the process with a magic string and magic number (0x8080
or0x8081
). HControl.exe is bundled with the install. It gets a handle for a named device object"\\.\ATKACPI"
, and decodes these numbers into even more magic numbers to be sent toDeviceIoControl
."\\.\ATKACPI"
is symlinked by the driver I assume to"\Device\ATKACPI"
. Check the code sample below for something usable.
Our analysis target was the Asus Hotkey windows program, found on the Windows Store. Downloading that and jumping through a lot of hoops to get access to the files shows a folder containing various executables and DLLs. Most of them had a relatively small file size, except for the main executable. AsKeybdHk.exe was the launch program, and was the focus of most of the research in here.
Every good reverse engineer knows exactly the behavior he's analyzing, and knows what he wants to find out. I didn't exactly go in with a clear idea in mind, which is definitely a bad thing when researching online about your issue.
I found out online that the function key switching is most likely being handled by the BIOS, or something ACPI related. A very vague idea, but it did let me know that I'm looking for a driver communication call. On linux, that's IOCTL. On windows, that's DeviceIoControl()
.
Looking up examples for DeviceIoControl show that it's coupled with a call to CreateFile()
to acquire a handle. So whatever function I'm looking for probably gets a handle to an ACPI driver and uses that in a call to DeviceIoControl.
I installed Ghidra for the first time, and got myself accustomed with the very intimidating user interface. Ghidra is a static binary analyzer, and most of my work took place in it. It takes some time getting used to it, and knowing what it can and cannot do.
Importing a binary into a Ghidra project and analyzing it was a rather automatic process. Simply let the default settings do their job, and you'll quickly get a list of functions and kernel calls. After that, there's a couple more tools one could use to begin.
The first step during static analysis is searching for strings. The binary I was analyzing at the time had the MFC library statically embedded, and so had a lot of unused strings cluttering my search. Sometimes, a single keyword is all it takes to figure out the code path. Searching for strings that begin with '-' is a good way to guess potential command line options, and searching for strings that begin with or contain a backslash '\' can help identify important files and directories.
The second step is finding out what system calls an executable references. The imports panel lists all DLLs referenced and the functions called. Many programs use the C runtime library and tend to have the main function hidden behind layers of runtime setup. You learn to pinpoint it quickly by sight with practice, but searching for common startup functions like GetCommandLine()
is a good way to get into the actual program loop. All windows programs end up performing a call to a system DLL function such as kernel32 or user32. These are libraries not statically linked, unless you're dealing with a state actor, and must be announced before being used. Dynamic loading with GetProcAddress
can throw simple analysis off, but isn't a good technique by itself in my opinion.
Static analysis can only take you so far, and sometimes it can simply be too much to interpret what the decompilation is trying to convey. Debuggers allow you to stop execution at any point in program flow to inspect memory values and track down exactly what responds to user interaction.
The Asus Keyboard Hotkeys program "AsKeybdHk.exe", is a huge 2MB executable. Opening it in Ghidra and following the entry quickly shows that this isn't a straightforward program. First thing I saw was a call to AfxWinInit
, meaning that this was an MFC application. MFC applications are usually built by handing off the main function to a library and instead supplying callbacks and events. This meant that the real WinMain is a generic Get/Translate/DispatchMessage loop handled by CWinApp, and any reference to the actual user implemented functions was going to be stored in a data segment inside the binary.
Modern windows applications are built around sending and receiving messages. Moving your mouse, pressing a key button, anything that represents user interaction has a message that represents it. MFC message maps are a way to specify how each class responds to the messages it receives. These are static, readonly structures that MSVC usually places in the .rdata segment of PE binaries. Finding the message map entries involves tracking down a call to AfxFindMessageEntry
, and using that to find the message map entry. This function is implemented in inline assembly for 32-bit targets (wincore.cpp, line 2010) and MSVC never touches __asm
blocks, making it quite easy to find it with a simple instruction pattern search.
Once you find the function, a debugger shows that the pointer is stored at the top of ESP. Following that pointer, I used IDA free to interpret the values in there. I've linked below the PDF that has the IDA scripts to detect and interpret these message maps. The debugger revealed more than one message map, since each class has it's own static map. Unfortunately, that was also a dead end for me, and I couldn't seem to find any interesting message codes. Perhaps if I had more experience with disassembler scripts I might've found something useful in there.
C++ compilers tend to have RTTI enabled by default, so poking around the program data segments could reveal what classes are into play. In this case, the class string identifiers began with the prefix ".?AV"
. One particular class, CTransparentButton
, isn't a MFC built in class, meaning it's custom made. That explains a bit of the troubles I've been having locating the button.
Nevertheless, using WinDBG to break on calls to Kernel32!DeviceIoControl
shows that the program communicates with the driver only once at startup, presumably just testing for hardware support. Ghidra found only this reference to DeviceIoControl
. The payload never seemed to change, so I started looking elsewhere.
The specific payload is different from the one used to change the FN button state. As shown by reading the registers in WinDBG:
int lpDataIn[0xc] = { 0 };
lpDataIn[0] = 0x53545344; // Equals "STSD"
lpDataIn[1] = 0x4;
lpDataIn[2] = 0x100023;
BOOL CommSuccess = DeviceIoControl(ATKACPI, 0x22240c, lpDataIn, 0xc,
&OutBuffer, 0x400, &BytesReturned, (LPOVERLAPPED)0x0);
After that, I was stuck. I didn't know exactly what to do, since the program wasn't doing the thing I was expecting it to do. Searching for 'fn' in the strings tab shows a lot of calls to the registry key, "Computer\HKEY_LOCAL_MACHINE\SOFTWARE\ASUS\ASUS System Control Interface\AsusOptimization\ASUS Keyboard Hotkeys\Fn Switch"
. I over-relied on static analysis, when another tool would've been much more fitting to use. Using Sysinternals' Process Monitor to monitor the AsKeybdHk.exe shows that it queries this registry key on startup. This further supports my theory the DeviceIoCall is just a hardware test. Adding a filter for accesses to that key shows that AsusOptimization.exe comes into play. And yet, I still didn't quite understand how did AsKeybdHk.exe communicated with that process.
Visual Studio comes bundled with a tool called Spy++. A weirdly named tool, this one lets you inspect components on a window and record messages sent between them.
In the image above, clicking (or rather, on mouse up) one of the two buttons causes a broadcasted message "Transparent Button Click Message". The message is always sent with lParam=0
, and wParam
set to 0x8080
for off or 0x8081
for on. Spy++ allows you to also see the destination of each message, and the handle here referred to another program, called "HControl"
Note that there's an actual button in there, but it didn't send or receive any mouse related messages. I'm somewhat glad I found out that there was a custom button in play.
Presumably standing for Hotkey Control, this seems to be the main communication bridge between the driver and various other Asus crapware utilities. Inspecting the main function shows a lot of driver and registry key setup. Searching for references to DeviceIoControl
with the ATKACPI
handle leads to a function with a large if chain found at HControl+0x2b70. Further searching for the magic string "ATKConfig Application Notification to ATKHotkey"
shows this snippet:
The function I named "ATKACPI_DeviceIoControl"
contains a call to DeviceIoControl. At this point, I didn't want to spend any more time trying to decode assembly and instead attached WinDBG to the HControl
process running on my computer. Doing bu HControl+1510
places a breakpoint right before the driver comm call. Doing dd edi L20
shows the numbers passed to the function.
In conclusion, here's the function that controls the FN switch light, in all it's un-highlighted glory. I couldn't quite figure out how to ask the driver about the current status, so implementing a 'toggle' function will need some userspace data saved. Incidentally, that's what all the registry key queries are probably about. Maybe there isn't actually a way to ask the driver about the state.
int GetDriverData()
{
HANDLE ATKACPI = CreateFileW(L"\\\\.\\ATKACPI", 0xc0000000, 3, (LPSECURITY_ATTRIBUTES)0x0, 3, 0, (HANDLE)0x0);
if (ATKACPI == 0x0 || ATKACPI == INVALID_HANDLE_VALUE) {
printf("Failed to find ATKACPI handle.\n");
return -1;
}
enum FN_SWITCH_STATE {
FN_SWITCH_OFF = 0x0,
FN_SWITCH_ON = 0x1,
};
// Special thanks to Daft Punk and WinDBG
int lpDataIn[0x10] = { 0 };
// Magic numbers extracted from debugging stack memory (edi register)
lpDataIn[0] = 0x53564544; // Equals "SVED". Probably authentication?
lpDataIn[1] = 0x8; // Signifying FN switch?
lpDataIn[2] = 0x100023;
lpDataIn[3] = FN_SWITCH_OFF; // Message to driver
// Output data from the driver
DWORD OutBuffer[0x400u] = { 0 };
DWORD BytesReturned = 0;
BOOL Err = DeviceIoControl(ATKACPI, 0x22240Cu, lpDataIn, 0x10, OutBuffer, 0x400u, &BytesReturned, 0);
if (Err == FALSE) {
printf("DeviceIoControl returned false\n");
} else {
printf("DeviceIoControl returned true\n%d bytes returned\n", BytesReturned);
printf("Data returned in hex:\n");
for (int i = 0; i < BytesReturned; i++) {
printf("%x ", OutBuffer[i]);
}
}
// Remember to cleanup :O
CloseHandle(ATKACPI);
return 0;
}
Here's the github repo containing the code for a more usable program. Note that the code doesn't update the registry keys, meaning that any of the Asus tools relying on them will be wrong on startup.
While recording the messages posted around, I noticed a message sent "from the BIOS". Maybe there is a way for the driver to communicate it's state to userspace, but I believe that's an adventure for another time.This was my first time doing any reverse engineering, first time using Ghidra, IDA, a debugger with no source code, and I do understand why people consider them powerful tools. I'm quite satisfied with my work here, and I could put all of that in a Powershell script.
I also learned that static linking is evil. An IDA plugin like ClassInformer could've helped a lot with the initial investigations