On va voir aujourd’hui un grand classique des packer et aussi des shellcodes; Comment trouver et parser la liste des DLL afin de trouver le début d’une DLL mappée en mémoire;
Désormais on sait que grace au PEB + 0xC on peut trouver l’emplacement de la structure Loader Data.
Loader Data nommé aussi PEB_LDR_DATA va contenir la liste des DLL et a quel offset mémoire trouver les DLL de notre process. Mais si on regarde à cet emplacement on tombe sur une structure fort déroutante (Moi je vous jure, j’ai mis du temps à comprendre et avec le livre de Sir Russinovich sur les genoux !). Cette structure ne propose pas une seul liste de DLL chargée mais 3 listes. Ces listes utilisent un format de liste chainée. Ce que chez M. Windows on nomme des LIST_ENTRY.
Il y a 3 listes disponible concernant les DLL en mémoire ;
- La liste de DLL chargée par windows au démarrage de l’exécutable (InLoadOrderModuleList)
- La liste de DLL actuellement en mémoire. (InMemoryOrderModuleList)
- La liste de DLL initialisée (InInitialisationOrderModuleList)
Je vous l’accorde pour l’instant c’est pas trop parlant; Autant les deux premier on comprend bien la différence, si un exécutable après initialisation charge avec la fonctions LoadLibrary une nouvelle DLL on ne la retrouvera pas dans la première liste. La 3eme liste en fait ne comprend pas notre exécutable lui même. C’est donc équivalent à la seconde option sans notre propre process, car oui le listing des DLL en mémoire contient notre propre process.
Bon ça c’était la partie facile. Voyons ces listes chainées. Une liste chainée utilise deux pointeurs FLINK (Forward Link) et BLINK (BackWard link). Et c’est à partir de là qu’il faut sortir un schéma. Voici un petit exemple de cette structure. Cet exemple contient 3 records.
Personne est tombé dans les pommes ? J’ai un ancien collègue qui aurait probablement éructé un “C’est quoi c’te structure de Nazi”. Alors petite explication. Chaque Flink donne l’adresse du Flink suivant, idem pour le Blink qui donne lui l’adresse du Blink précédent. Les deux premiers records sont les header il n’ont pas de data, seul le prochain Flink vous permettra d’arriver à la structure qui nous intéresse.
Donc si on veut voir la structure 2, il faut lire le 1re Flink, puis le Second, et enfin sous le Flink on trouvera le record qui vous intéresse.Mais il reste une petite subtilitée… Dans notre cas, Il y a 3 listes, donc chaque entrée comporte en header 3 couples de flink/blink. Donc en fait pour une entrée la structure est :
Donc si on suis les flink de InLoadOrderModuleList notre structure sera 0X18 bytes plus loin. Si on suit les Flink de InInitialisationOrderModuleList, la structure n’est qu’a 0x8 bytes plus loin. Funny n’est-ce pas.
Ha ! Et en plus quand on cherche quelque chose, il faut retenir l’offset du premier Flink des headers sous peine de tourner en boucle Ad Vitam. Donc pour aider à la comprenette, prenons un exemple. Dans cet exemple 32 bits, le Loader data d’après le PEB est situé en 0x211EA0. Le format de loader data au final c’est :
- 0x0 Dword Lenght (Bullshit, on s’en sert pas)
- 0x4 Dword Unused
- 0x8 Dword SSHandle (On s’en fout aussi)
Et on attaque les fameuses list entry.
- 0x0C List_Entry (Donc 2 Dword, le Flink et le Blink) InLoadOrderModuleList
- 0x14 List_Entry InMemoryOrderModuleList
- 0x1C List_Entry InInitialisationOrderModuleList
Ok donc admettons que l’on souhaite voir la première entrée de la liste InInitialisationOrderModuleList, le Flink de ce header nous informe d’allez voir le premier record en 0x241F58 (regardez en 0x1c). Je suis en train de suivre le flink de la 3eme liste donc j’ai ma structure si chèrement désirée en flink + 0x8. Et devinez ce qu’on y trouve…
Victoire enfin une structure tant recherchée. En 241F58 j’ai bien le flink suivant, mais surtout ;
On trouve en 0X241f60 l’offset en mémoire de notre image PE loadée de ntdll.dll. Je sais que c’est ntdll.dll car en 0x241F78 j’ai l’offset d’une string unicode donnant de nom de la DLL.
cette structure, Qui démarre au flink de la 1ere listes se nomme LDR_MODULE. En deux mots pour les packers il y a deux trucs juteux
- En 0x18 la base adresse, ici en 0x24160 qui nous dit que cette dll est loadée en 0x7C900000
- En 0x30 un pointeur sur la chaine unicode du nom de la DLL (ici en 0x241f78 qui pointe vers 0x7C92040c)
Alors après il est rare que les packers testent le nom de la dll directement. Soit certains savent que kernel32.dll c’est toujours la seconde et que la 1ere c’est toujours ntdll.dll. Soit ils utilisent un genre de hash du pauvre pour retrouver le nom de la DLL.
Petit exemple de “Use the force luke” tiré d’un malware quelconque..
push 30h pop ecx mov esi, fs:[ecx] ; PEB (FS:[0x30]) mov esi, [esi+0Ch] ; ESI = LoaderData mov esi, [esi+1Ch] ; ESI = Flink InInitialisationOrderModuleList mov ebp, [esi+8] ; EBP = Base addresse de ntdll mov ds:ntdllbase, ebp
Un autre générique et chiadé a moi :) (Oui je t’ai vu toi dans le fond ricaner, on peut faire mieux)
; Find DLL , à apeller avec le hash convoité ; Return la base addresse dans EAX HASH_SFT equ 0x7 HASH_NTDLL.DLL equ 0xBC1A1445 HASH_KERNEL32.DLL equ 0xA9B35F35 HASH_USER32.DLL equ 0x2D37FEFB push ebp mov ebp,esp mov ebx, [fs:0x30] ; pointer sur PEB fs:0x30 mov ebx, [ebx+0x0C] ; pointeur sur PEB->Ldr mov edx, ebx+0x14 ; addr du 1er flink... alias le point de sortie a sauver mov ebx, [ebx+0x14] ; flink premier module de la liste InMemoryOrder xor ecx,ecx xor eax,eax jmp .startlist .nextmod: cmp ebx,edx je .tfini .startlist: mov esi, [ebx+0x28] ; pointeur sur la liste (unicode) .readchar: lodsw ; lis un Word (unicode), x00\Char test al,al ; Fin de la string ? jz .stopreadchar cmp al,0x60 ; Si minuscule convert to Majuscule, jbe .stoschar sub al,0x20 ; pass en majuscule .stoschar xor cl,al ; Hash du pauvre, rolxor rol ecx,HASH_SFT jmp .readchar .stopreadchar: cmp ecx,[ebp+0x8] ; Parametre 1, Hash DLL Name je .tfinifound mov ecx,0 ; Reset the hash mov ebx, [ebx] ; choppe le module suivant mov esi, [ebx+0x4] ; module base address jmp .nextmod .tfinifound mov eax, [ebx+0x10] ; module base address jmp .tgohome .tfini: mov eax,0 ; pas trouvé l'offset return 0 .tgohome: mov esp,ebp pop ebp retn 4
et un autre petit exemple “Use the force en 64 Bits”. Tout est pareil, sauf la taille des adresse évidemment ce qui décale toute notre affaire.
push 60h pop rcx mov rax,[gs:rcx] ; Peb mov rax,[rax+18h] ; LoaderData mov rsi,[rax+30h] ; Ldr.InInitializationOrderModuleList lodsq ; skip ntdll.dll mov rbx,[rax+10h] ; kernel32.dll base
Et voila, la structure est bien chelou mais désormais vous êtes aptes à retrouver l’offset de n’importe quelle DLL mappée en mémoire et/ou détecter cette action dans le packer de votre choix.
a+