Ich, der Assembler

Was tut man mit einem Stück Closed-Source Software, was den einen Vorteil hat genau das zu tun was man will (und nicht mehr), aber leider den Nachteil regelmäßig abzustürzen?
Und was tut man, wenn dieses Stück der KernelMode-Treiber einer Paketfilter-Firewall ist und „abstürzen“ bedeutet dass man einen Bluescreen bekommt?

Richtig, man holt WinDbg und IDA raus und fängt an den Fehler zu suchen 😉 Dank ersterem bekommt man aus dem Crashdump (hier: Minidump) relativ schnell raus, wo der Fehler aufgetreten ist, und mit IDA bekommt man dann raus was dort passiert.
In diesem Fall wird im TDI-Modul zuerst ein Eintrag in der Liste der verbundenen Anwendungen gesucht und dann später darauf gearbeitet. Das Funktioniert wunderbar, wenn man nur einen Kern hat (laut About-Dialog ist das Ding von 2002), aber sobald man mehr hat ist nicht mehr gesagt, dass ein gefundener Eintrag 20 Instruktionen weiter immer noch da ist. Konkret ist genau das hier passiert: ICMP-Pakete haben ja keine Connection, und dementsprechend ist ihr Eintrag sofort weg nachdem der Request erledigt ist. Alle 1000 Pakete (ungefähr) hat sich das dann so ausgewirkt, dass der eigentlich gefundene Eintrag schon wieder gelöscht war, als dann versucht wurde die gecachete MD5-Summe zu lesen.

mov     ecx, [ebp+fname]
push    ecx
call    connection_by_fname
mov     [ebp+index], eax
cmp     [ebp+index], 0FFFFFFFFh        ; index = -1 ?
jz      short newapp                   ; unknown app
mov     ecx, [ebp+len]
mov     edx, [ebp+index]
mov     eax, P_conn_table
mov     esi, [eax+edx*4+11Ch]          ; Array-Zugriff: eax+11Ch, Element edx
add     esi, 1D8h                      ; cached md5 in esi+1D8h
mov     edi, [ebp+md5buf]
mov     edx, ecx                       ; strncpy(edi,esi,ecx)
shr     ecx, 2
rep movsd
mov     ecx, edx
and     ecx, 3
rep movsb
xor     eax, eax
jmp     exit

Was also tun? Locking geht an der Stelle nicht, das quittiert uns Windows mit einem IRQL_NOT_LESS_OR_EQUAL – wie ich leidvoll erfahren musste, nachem ich das ganze Folgende mit diesem Ansatz durchexerziert hatte.

Möglichkeit zwei ist die weit weniger garantierte Möglichkeit einfach esi auf Null zu prüfen. Dann besteht zwar immer noch die Chance, zwischen dieser Prüfung und der Verwendung danach die Daten zu verlieren, aber das passiert anscheinend wesentlich seltener. Dazu müssen wir aber Code einfügen zwischen dem Array-Zugriff und der darauffolgenden Zeile, und zwar eigentlich nur das:

test    esi,esi
jz      short newapp

. Leider ist da aber kein Platz (warum auch). Also durch die Binary gesucht und nach den bekannten Alignment-Blöcken gesucht. Die sind nur Füllstoff, da können wir also unseren eigenen Code unterbringen. Praktischerweise sind 12 Byte direkt hinter der problematischen Funktion verfügbar. Die kürzeste Lösung die mir eingefallen ist sieht so aus (der erste Teil steht nach dem Arrayzugriff, „space“ ist der Füllblock):

TEST    esi,esi
JZ      newapp
CALL    space
...
 
space:
ADD     esi, 1D8h
MOV     edi, [ebp+0Ch]
RET

Und jetzt fangen die Probleme an.
IDA kann zwar in Win32PE assemblieren, aber nicht für SYS-Dateien. Überhaupt hab ich keinen passenden Assembler gefunden, der sowas in-place kann. Also hab ich mich mit einem Hex-Editor (hier: Tiny Hexer) bewaffent und die passende Stelle im Code gesucht. War einfach, der CALL an connection_by_fname ist nur dort zu finden 😉 Weniger einfach war das Übersetzen des Codes in Bytecode nur mit Hilfe von einer Referenz – aber möglich, hier das was ich gebastelt hab:

85 F6              TEST    esi,esi
74 1A              JZ      newapp
E8 A5 00 00 00     CALL    space
...
 
space:
81 C6 D8 01 00 00  ADD     esi, 1D8h
8B 7D 0C           MOV     edi, [ebp+0Ch]
C3                 RET

Damit hab ich unten sogar noch 2 Byte übrig, und der Code oben passt genau in den gleichen Platz rein wie 2 Instruktionen, die ich dann weiter unten nachgebaut hab.
Ein Call ohne Stackframebehandlung mag zwar seltsam aussehen, aber es erfüllt seinen Zweck und ist wesentlich kürzer als ein JMP/32, was hier leider nötig gewesen wäre: der Sprung ist A8h lang und damit zu weit für einen JMP/8 (der ja Vorzeichenbehaftet rechnet (was ich auch erst übersehen hatte)).

Oh und, eins noch: die Checksum im PE-Header muss unbedingt angepasst werden, sonst weigert sich Windows, den Treiber zu laden.

Somit hab ich jetzt wieder einen funktionierenden Paketfilter, auch wenn ich tausende von Pings versende 🙂