The Birth of the World's First Self-Healing Micropatch
By Luka Treiber, 0patch team
And now for something completely different: we just published our first patch for 0patch Agent itself. A self-healing patch, so to speak.
It's been almost three months since 0patch open beta has been released and users gave it a warm reception. Among the feedback given there were not only bug reports, improvement requests and thank-yous, but also patches you would like to see in our 0patch database. Being a patch developer I added those to our already oversized wish list and figured it would be hard to set priorities. But then - allow me to add a little drama here - as the 0patch Agent development team, with one foot on vacation, already rescheduled tasks to fix reported bugs in the agent and started another rush to release a new version, I got a brainwave:
Let's 0patch it!
Not only did the dev team heave a sigh of relief (and switched back to the original schedule) - I also finally got my priority bug to patch.
This bug that I'll share with you raised concerns that in rare circumstances some 0patches may become ineffective. Although hardly noticeable the bug was kind of a big deal to us as we want to make 0patch as reliable as possible.
The flaw, a logical error, is located in function updatePatches which is in charge of applying patches to modules in a process.
Here's the relevant source:
3564: void updatePatches(bool forceSync,
bool firstCall,
HMODULE hTriggerModule) { //... 3664: // Get a list of all the modules in this process. 3665: if(EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { //... 3670: if (hTriggerModule != NULL && hTriggerModule == INVALID_HANDLE_VALUE) 3671: hMods[nMods++]=hTriggerModule;
What the developer tried to do here was not to add an invalid handle to the hMods list. However, instead, the code adds only invalid handles to the hMods list. Obviously the second condition check at line 3670 is mistyped. Instead of != the coder used == , consequently processing only modules with invalid handles. Because of this, a newly loaded module might get ignored by our agent under certain circumstances.
The function updatePatches synchronizes the list of patched modules kept by 0patch Loader with the actual list of modules returned by EnumProcessModules (line 3665). Whenever a new module load event is detected, updatePatches is called in order to apply any available patches to the new module. However, the list of modules provided by EnumProcessModules does not always contain the latest module that triggered updatePatches, so we pass along the module's handle and append it to the list after checking that the handle is not invalid - which apparently got miscoded.
Now to the patch. We have to change the operator == to !=. Should be simple so let's start. How does this source code translate to assembly?
5c1a5428 mov eax,dword ptr [ebp-3Ch] ; eax = hTriggerModule
5c1a542b cmp eax,ebx ; hTriggerModule != NULL ?
5c1a542d je 0PatchLoader!updatePatches+0x2d6 (5c1a5446)
5c1a542f cmp eax,0FFFFFFFFh ; hTriggerModule == INVALID_HANDLE_VALUE ?
5c1a5432 jne 0PatchLoader!updatePatches+0x2d6 (5c1a5446)
5c1a5434 mov eax,dword ptr [ebp-1Ch]
5c1a5437 mov dword ptr [ebp+eax*4-1054h],0FFFFFFFFh ; hMods[nMods]
=INVALID_HANDLE_VALUE
5c1a5442 inc eax
5c1a5443 mov dword ptr [ebp-1Ch],eax ; nMods++
5c1a5446 cmp byte ptr [ebp+214h],0
5c1a542b cmp eax,ebx ; hTriggerModule != NULL ?
5c1a542d je 0PatchLoader!updatePatches+0x2d6 (5c1a5446)
5c1a542f cmp eax,0FFFFFFFFh ; hTriggerModule == INVALID_HANDLE_VALUE ?
5c1a5432 jne 0PatchLoader!updatePatches+0x2d6 (5c1a5446)
5c1a5434 mov eax,dword ptr [ebp-1Ch]
5c1a5437 mov dword ptr [ebp+eax*4-1054h],0FFFFFFFFh ; hMods[nMods]
=INVALID_HANDLE_VALUE
5c1a5442 inc eax
5c1a5443 mov dword ptr [ebp-1Ch],eax ; nMods++
5c1a5446 cmp byte ptr [ebp+214h],0
In the code snippet above we see that at 5c1a5428 hTriggerModule is stored in eax. Then the two conditions of the flawed if statement from line 3670 are checked: first hTriggerModule against NULL (at 5c1a542b) and then against INVALID_HANDLE_VALUE (at 5c1a542f). We want to patch that second condition check so we place our patchlet at address 5c1a5432. As a first patchlet instruction we will use a je which effectively replaces the erroneous jne marked in red above. This way we microsurgically cut out a flawed instruction and replace it with a correct one and - Voila! We should have a patch for our Agent before you could say JMP! Or so I thought...
Looking at the code snippet above again (only this time more closely) I noticed something unexpected: at 5c1a5437 the compiler apparently optimized the code to always add INVALID_HANDLE_VALUE to hMods list or in c++ terms:
hMods[nMods++]=INVALID_HANDLE_VALUE;
Because honestly, it is hard to see any other intent in that buggy if statement. So what I had to do was copy the body of the if statement at line 3671 (code between 5c1a5434 and 5c1a5443) to the patch and modify that copied code to again resemble
hMods[nMods++]=hTriggerModule;
I packed these modifications into a 0patch Factory source file:
MODULE_PATH "C:\Progra~2\0patch\Agent\0patchLoader.dll"
PATCH_ID 247
PATCH_FORMAT_VER 2
VULN_ID 1630
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x5432
JUMPOVERBYTES 20
N_ORIGINALBYTES 5
code_start
je end_patch ; replace jne with je
mov ebx,eax ; temporarily store hTriggerModule to ebx
mov eax,dword [ebp-1Ch]
mov dword [ebp+eax*4-1054h],ebx ; store ebx to hMods array
inc eax
mov dword [ebp-1Ch],eax ; nMods++
xor ebx,ebx ; restore ebx to NULL as it was before
end_patch:
code_end
patchlet_end
PATCH_ID 247
PATCH_FORMAT_VER 2
VULN_ID 1630
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x5432
JUMPOVERBYTES 20
N_ORIGINALBYTES 5
code_start
je end_patch ; replace jne with je
mov ebx,eax ; temporarily store hTriggerModule to ebx
mov eax,dword [ebp-1Ch]
mov dword [ebp+eax*4-1054h],ebx ; store ebx to hMods array
inc eax
mov dword [ebp-1Ch],eax ; nMods++
xor ebx,ebx ; restore ebx to NULL as it was before
end_patch:
code_end
patchlet_end
I hit 0patch Factory's Build Patch button and Presto! The patch is born. After it gets applied the disassembly looks as follows:
5c1a5428 mov eax,dword ptr [ebp-3Ch]
5c1a542b cmp eax,ebx
5c1a542d je 0PatchLoader!updatePatches+0x2d6 (61725446)
5c1a542f cmp eax,0FFFFFFFFh
004c007c je 5c1a5446
004c007e mov ebx,eax
004c0080 mov eax,dword ptr [ebp-1Ch]
004c0083 mov dword ptr [ebp+eax*4-1054h],ebx
004c008a inc eax
004c008b mov dword ptr [ebp-1Ch],eax
004c008e xor ebx,ebx
5c1a5446 cmp byte ptr [ebp+214h],0
5c1a542b cmp eax,ebx
5c1a542d je 0PatchLoader!updatePatches+0x2d6 (61725446)
5c1a542f cmp eax,0FFFFFFFFh
004c007c je 5c1a5446
004c007e mov ebx,eax
004c0080 mov eax,dword ptr [ebp-1Ch]
004c0083 mov dword ptr [ebp+eax*4-1054h],ebx
004c008a inc eax
004c008b mov dword ptr [ebp-1Ch],eax
004c008e xor ebx,ebx
5c1a5446 cmp byte ptr [ebp+214h],0
Note that to improve readability I inlined the patch code (marked in blue) in the original code.
Still pretty simple. The 0patch contains 7 instructions and it took me an hour to analyze and develop for both 32 and 64 bit Agent versions. No restarts and no reinstalling, that's how we operate. If you already have 0patch Agent installed, patches ZP-247 and ZP-248 should already be deployed on your machine.
This is a great example of how 0patch could be used by software vendors to quickly release emergency updates without affecting regular release (and vacation) schedules.
Luka Treiber
@0patch
P.S.: It just struck me that 0patch Agent may actually be the first