Manual Unpacking

Packer : UPX 1.23

Objectif :

Cible :

Outils nécessaires :

Fichiers joints :

Index

  1. Introduction
  2. Collecte des infos sur la cible
  3. Dump full et examen du dump sous un éditeur hexa
  4. Reconstruction de l'exe original
    1. MZ header
    2. PE header + table des sections
    3. Assemblage des sections
  5. Correction de la section des imports
  6. Correction des ressources
  7. Infos pratiques sur UPX 1.23

1. Introduction

UPX est très facile à unpacker manuellement afin de restituer l'exe tel qu'il l'était à l'origine. Le seul petit hic se trouvant au niveau des imports comme à l'habitude des programmes packés, mais rien de bien méchant rassurez vous. Il n'y a pas besoin d'unpackeur générique pour UPX puisque UPX lui même fourni une option de décompression (-d), à ce sujet certains tools appelés upx Scrambler permettent d'empêcher cette décompression mais tout ce qui est dans ce tut sera réalisable sur des programme packé par upx et qui ont été "scramblés". Ce tutorial vous permettra de découvrir comment fonctionne UPX et vous permettra de reconstituer l'exe original avant la compression. C'est aussi une bonne introduction pour s'entraîner à reconstruire un exe en entier ce qui nous servira pour d'autres packers.

2. Collecte des infos sur la cible

On ouvre la cible avec Lord-PE et on récupère les infos suivantes :

Entry-point : 000080E0
Import-Table : RVA = 00009058, size = 000000A4


Sections Table :

names		Voffset		Vsize		Roffset		Rsize		Flags
UPX0		00001000	00005000	00000400	00000000	E0000080
UPX1		00006000	00003000	00000400	00002400	E0000040
.rsrc		00009000	00001000	00002800	00000200	C0000040

Première constatation, on voit deux section portant les noms UPX0 et UPX1 (attention ne pas se fier aux noms des sections pour identifier un packer, en effet le nom n'a pas grand intérêt et on peux très bien mettre n'importe quoi à la place, ça ne changerait rien). Si on examine plus attentivement on remarque que ces 2 sections commencent au même offset sur le disque (Roffset) et que la taille de UPX0 est de 0 (Rsize), ce qui signifie que UPX0 n'existe pas sur le disque mais seulement en mémoire. L'entry-point se situe dans UPX1 et la table des imports actuelle dans la section .rsrc.

3. Dump full et examen du dump sous un éditeur hexa

On dump la cible avec Lord-PE ou un autre process dumper en s'assurant que ces options soit cochés avant de dumper :

A présent on va étudier le dump sous un éditeur hexa où l'on va découvrir des choses très intéressantes.

Apres avoir étudié UPX j'ai remarqué qu'il gardait intact le PE-header d'origine et qu'il le "cachait" quelques part dans la section UPX1. Si nous retrouvons le PE header d'origine après ça va être du gâteau pour reconstituer l'exe tel qu'il l'était avant la compression. Pour le trouver, rien de plus simple, on lance une recherche hexa sur le dword 5045 qui caractérise les lettres ascii PE, représentant ce qu'on appelle le début du PE-header. Si nous trouvons plusieurs références s'assurer que la suite ressemble à un PE-header valide.

Sur la cible on trouvera 4 références aux offsets :

J'estime que vous avez un minimum de connaissances sur le PE ou sinon je vous invite à lire une doc afin de mieux comprendre comment se repérer dans le PE.

Dans ce PE est contenu toutes les informations sur la structure de l'exe original. Ce qui nous intéresse le plus c'est la table des sections qui nous montre comment étaient structurées les sections avec leur offset et taille d'origine. Pour retrouver la table des sections, il faut partir de l'offset du PE qui nous servira de base :

Ici, pour l'OptionalHeaderSize on lit : E000 qui se traduit en 00E0. Donc notre table des sections commence en 18+E0 à partir du PE que nous avons trouvé. Ici c'est facile de trouver le début de cette table visuellement , puisqu'il suffit de regarder les noms des sections et de prendre comme base le début du nom de la 1ere section (".text") qui marquera le début de notre table des sections. Mais je vous ai donné la méthode au cas où les noms des sections soient vides.

Examinons cette table des sections maintenant où j'ai colorié de couleur différente chaque section :

0000724C 73707269 6E746641 00014765 74446C67 4974656D 54657874 4100014D sprintfA..GetDlgItemTextA..M
00007268 65737361 6765426F 78410001 53656E64 4D657373 61676541 00014465 essageBoxA..SendMessageA..De
00007284 7374726F 7957696E 646F7700 01446961 6C6F6742 6F785061 72616D41 stroyWindow..DialogBoxParamA
000072A0 00000000 00005045 00004C01 04004228 673C0000 00000000 0000E000 ......PE..L...B(g<..........
000072BC 0F010B01 06000030 00000030 00000000 0000B011 00000010 00000040 .......0...0...............@
000072D8 00000000 40000010 00000010 00000400 00000000 00000400 00000000 ....@.......................
000072F4 00000070 00000010 00000000 00000200 00000000 10000010 00000000 ...p........................
00007310 10000010 00000000 00001000 00000000 00000000 00001444 00003C00 .......................D..<.
0000732C 00000060 0000A801 00000000 00000000 00000000 00000000 00000000 ...`........................
00007348 00000000 00000000 00000000 00000000 00000000 00000000 00000000 ............................
00007364 00000000 00000000 00000000 00000000 00000000 00000000 00000040 ...........................@
00007380 0000B000 00000000 00000000 00000000 00000000 00000000 00000000 ............................
0000739C 00002E74 65787400 00002E2A 00000010 00000030 00000010 00000000 ...text....*.......0........
000073B8 00000000 00000000 00002000 00602E72 64617461 0000E807 00000040 .......... ..`.rdata.......@
000073D4 00000010 00000040 00000000 00000000 00000000 00004000 00402E64 .......@..............@..@.d
000073F0 61746100 0000DC0A 00000050 00000010 00000050 00000000 00000000 ata........P.......P........
0000740C 00000000 00004000 00C02E72 73726300 0000A801 00000060 00000010 ......@....rsrc........`....
00007428 00000060 00000000 00000000 00000000 00004000 00400060 00000045 ...`..............@..@.`...E
00007444 00000000 A6620000 E7220AC0 7A04F800 95CF6DAE 4DAF7A08 90849434 .....b..."..z.....m.M.z....4
00007460 A900526B DB7CF142 0CA522C2 2ADD2CCB 6406FE0B 7D29C4B1 CD355899 ..Rk.|.B..".*.,.d...})...5X.

Voici la structure d'une section :

BYTE[8]	Name
DWORD Virtual Size (Vsize)
DWORD Virtual Adresse (Voffset)
DWORD Size of raw data (Rsize)
DWORD Pointer to raw data (Roffset)
DWORD PointerToRelocation
DWORD PointerToLinenumbers
WORD NumberOfRelocation
WORD NumberOfLinenumbers
DWORD Caracteristiques (Flags)

La taille de cette structure est de 28h bytes. En relevant les valeurs de chaque sections on peut dresser la table des sections tels que :

Sections Table :

names		Voffset		Vsize		Roffset		Rsize		Flags
.text		00001000	00002A2E	00001000	00003000	60000020
.rdata		00004000	000007E8	00004000	00001000	40000040
.data		00005000	00000ADC	00005000	00001000	C0000040
.rsrc		00006000	000001A8	00006000	00001000	40000040

On va pour reconstruire l'exe original d'après ce format en respectant les colonnes Roffset et Rsize.

4. Reconstruction de l'exe original

Sous hex WorkShop on créer un nouveau fichier, celui-ci est vierge et on va y insérer tous les éléments de notre exe. On ouvre aussi le dump où on l'on ira découper nos morceaux.

a) MZ header

Cette 1ere partie est en gros l'en-tête d'un exe (toute la partie avant le repère PE). Celui du Dump fera très bien l'affaire. Normalement il faut regarder en 0000003C et relever le dword, celui-ci nous donne l'offset où commence le PE header. Ici c'est en 000000D8.

dans le dump :

début : 00000000
fin : 000000D8
taille = D8h

On copie ce bloc du dump et on le colle dans le nouveau fichier.

b) PE header + Table des sections

Le PE original du programme. La partie qu'on a trouvée tout à l'heure, qui donne toutes les infos sur l'exe, et qui s'étend jusqu'à la fin de la table des sections.

dans le dump :

début : 000072A6
fin : 0000743E
taille = 198h bytes

On copie ce bloc et on le colle à la suite dans le nouveau fichier. A présent on à l'en-tête et le PE entièrement reconstitués, il ne nous reste qu'à assembler les différentes sections. On sait que la première section (".text") commence à l'offset 1000h on va donc compléter la suite du PE avec des 0 jusqu'a l'offset 1000. Pour l'instant la taille total de notre fichier occupe 270h bytes il nous faut donc un bloc de 1000h - 270h = D90h bytes de 00. Sous Hex Workshop on se place à la fin de notre nouveau fichier on clique droit et on fais Insert on met une valeur D90h rempli avec 00 en hex byte, puis OK.

c) Assemblage des sections

On se place dans le dump et on découpe nos sections selon notre table des sections, il suffit de copier les blocs suivants et de les coller dans le nouveau fichier les un à la suite des autres.

Name		Roffset		Rsize

.text		00001000	00003000
.rdata		00004000	00001000
.data		00005000	00001000
.rsrc		00006000	00001000

Sous Hex WorkShop pour sélectionner un bloc il faut aller dans le menu EDIT / Select block. On se place au début de la section et on sélectionne un block correspondant à sa taille.

Une fois cela fait, sauvegarder le tout sous un nom genre rebuild.exe et si vous avez bien bosser le nouveau fichier devrait ressembler quasiment à ce qu'était l'exe avant d'être packé par UPX. Celui-ci n'est pas fonctionnel encore car il faut reconstruire les imports que notre dump a modifiés et qu'UPX a détruits en partie. Mais on peut déjà désassembler l'exe sous wdasm et obtenir le code et les datas. On peut aussi l'ouvrir avec un éditeur de PE afin de l'examiner plus amplement.

5. Correction de la section des imports

Passons à la partie la plus délicate de ce tutorial car jusque là, ce n'était que du découpage. En effet il nous faut reconstruire les imports car si UPX n'a pas touché aux autres sections, on ne peut pas en dire autant de la section qui concerne les import à savoir ici .rdata.

Si nous Examinons dans notre exe reconstruit ce que nous avons dans cette section on pourra voir 2 parties bien distinctes :

00003FFC 00000000 94E4E577 C8E0E577 54C9E477 7EDEE577 31A0E577 FCA7E577 .......w...wT..w~..w1..w...w
00004018 61D9E577 86ADE577 7E17E477 58E3E577 42D1E577 FD98E577 B816E477 a..w...w~..wX..wB..w...w...w
00004034 B9E6E577 C030E777 A9ADE577 4275E777 95E5E577 49A9E577 F294E477 ...w.0.w...wBu.w...wI..w...w
00004050 85E5E577 51E3E577 FBE3E577 1689E577 D2E2E577 CB15E677 6B15F477 ...wQ..w...w...w...w...wk..w
0000406C 440CF577 3AF1E577 5EE3E577 8BB0E577 9725E577 A116F477 72ACE577 D..w:..w^..w...w.%.w...wr..w
00004088 5F8CF477 32B3E577 00000000 A2E7D177 F678D577 B211D377 2D5AD177 _..w2..w.......w.x.w...w-Z.w
000040A4 74C4D177 34E9D377 00000000
FFFFFFFF 87124000 9B124000 72756E74 t..w4..w..........@...@.runt
000040C0 696D6520 6572726F 72200000 0D0A0000 544C4F53 53206572 726F720D ime error ......TLOSS error.
000040DC 0A000000 53494E47 20657272 6F720D0A 00000000 444F4D41 494E2065 ....SING error......DOMAIN e
000040F8 72726F72 0D0A0000 52363032 380D0A2D 20756E61 626C6520 746F2069 rror....R6028..- unable to i
00004114 6E697469 616C697A 65206865 61700D0A 00000000 52363032 370D0A2D nitialize heap......R6027..-
00004130 206E6F74 20656E6F 75676820 73706163 6520666F 72206C6F 77696F20 not enough space for lowio
0000414C 696E6974 69616C69 7A617469 6F6E0D0A 00000000 52363032 360D0A2D initialization......R6026..-
00004168 206E6F74 20656E6F 75676820 73706163 6520666F 72207374 64696F20 not enough space for stdio
00004184 696E6974 69616C69 7A617469 6F6E0D0A 00000000 52363032 350D0A2D initialization......R6025..-
000041A0 20707572 65207669 72747561 6C206675 6E637469 6F6E2063 616C6C0D pure virtual function call.
000041BC 0A000000 52363032 340D0A2D 206E6F74 20656E6F 75676820 73706163 ....R6024..- not enough spac
000041D8 6520666F 72205F6F 6E657869 742F6174 65786974 20746162 6C650D0A e for _onexit/atexit table..
000041F4 00000000 52363031 390D0A2D 20756E61 626C6520 746F206F 70656E20 ....R6019..- unable to open
00004210 636F6E73 6F6C6520 64657669 63650D0A 00000000 52363031 380D0A2D console device......R6018..-
0000422C 20756E65 78706563 74656420 68656170 20657272 6F720D0A 00000000 unexpected heap error......
00004248 52363031 370D0A2D 20756E65 78706563 74656420 6D756C74 69746872 R6017..- unexpected multithr
00004264 65616420 6C6F636B 20657272 6F720D0A 00000000 52363031 360D0A2D ead lock error......R6016..-
00004280 206E6F74 20656E6F 75676820 73706163 6520666F 72207468 72656164 not enough space for thread
0000429C 20646174 610D0A00 0D0A6162 6E6F726D 616C2070 726F6772 616D2074 data.....abnormal program t
000042B8 65726D69 6E617469 6F6E0D0A 00000000 52363030 390D0A2D 206E6F74 ermination......R6009..- not
000042D4 20656E6F 75676820 73706163 6520666F 7220656E 7669726F 6E6D656E enough space for environmen
000042F0 740D0A00 52363030 380D0A2D 206E6F74 20656E6F 75676820 73706163 t...R6008..- not enough spac
0000430C 6520666F 72206172 67756D65 6E74730D 0A000000 52363030 320D0A2D e for arguments.....R6002..-
00004328 20666C6F 6174696E 6720706F 696E7420 6E6F7420 6C6F6164 65640D0A floating point not loaded..
00004344 00000000 4D696372 6F736F66 74205669 7375616C 20432B2B 2052756E ....Microsoft Visual C++ Run
00004360 74696D65 204C6962 72617279 00000000 0A0A0000 52756E74 696D6520 time Library........Runtime
0000437C 4572726F 72210A0A 50726F67 72616D3A 20000000 2E2E2E00 3C70726F Error!..Program: .......<pro
00004398 6772616D 206E616D 6520756E 6B6E6F77 6E3E0000 4765744C 61737441 gram name unknown>..GetLastA
000043B4 63746976 65506F70 75700000 47657441 63746976 6557696E 646F7700 ctivePopup..GetActiveWindow.
000043D0 4D657373 61676542 6F784100 75736572 33322E64 6C6C0000 00000000 MessageBoxA.user32.dll......
000043EC 00000000 FFFFFFFF AE334000 B2334000 FFFFFFFF 62344000 66344000 .........3@..3@.....b4@.f4@.
00004408 FFFFFFFF E6354000 EA354000
00000000 00000000 00000000 00000000 .....5@..5@.................
00004424 00000000 00000000 00000000 00000000 00000000 00000000 00000000 ............................

(La suite de la section ne contient que des 00)

La partie rouge n'est pas vraiment intéressante, à vrai dire elle ne nous sert a rien, c'est une partie qui à été ajouté par le compilateur à noté ici Microsoft visual studio C++ comme indiqué.

En bleu, c'est les tableaux des IAT (Import Address Table) pointés par les membres FirstThunk de la table des imports et reconnaissables avec les adresses des fonctions comme : 94E4E577 qui se lit : 77E5E494. et que l'on peut aussi séparer en 2 parties (pour les 2 dll importés). La fin d'un tableau est marquée par un DWORD 00000000.

00003FFC 00000000 94E4E577 C8E0E577 54C9E477 7EDEE577 31A0E577 FCA7E577 .......w...wT..w~..w1..w...w
00004018 61D9E577 86ADE577 7E17E477 58E3E577 42D1E577 FD98E577 B816E477 a..w...w~..wX..wB..w...w...w
00004034 B9E6E577 C030E777 A9ADE577 4275E777 95E5E577 49A9E577 F294E477 ...w.0.w...wBu.w...wI..w...w
00004050 85E5E577 51E3E577 FBE3E577 1689E577 D2E2E577 CB15E677 6B15F477 ...wQ..w...w...w...w...wk..w
0000406C 440CF577 3AF1E577 5EE3E577 8BB0E577 9725E577 A116F477 72ACE577 D..w:..w^..w...w.%.w...wr..w
00004088 5F8CF477 32B3E577 00000000
A2E7D177 F678D577 B211D377 2D5AD177 _..w2..w.......w.x.w...w-Z.w
000040A4 74C4D177 34E9D377 00000000

On note ici que le 1er tableau commence en 000040000 et contient 36 éléments + 1 vide et le 2eme tableau commence en 00004094 et contient 6 éléments + 1 vide. Le nombre d'élément d'un tableau détermine le nombre de fonctions importées par DLL.

Il nous manque 2 éléments pour compléter cette section des imports :

Pour trouver les noms des fonctions il faut retourner dans le dump et on les trouvera qui débutent très clairement à l'offset : 00007007. On remarque au passage qu'il n'y a pas les noms des DLL alors qu'habituellement ceux ci s'y trouvent mêlés. Pour trouver les noms des DLL il suffit de regarder dans le dump à l'adresse de l'import-table (00009058). En effet UPX regroupe les noms des dll du programme à la suite la table des imports de l'exe packé.

Ici on voit qu'il 'y a 2 dll importées : KERNEL32.dll et USER32.dll. On retient ces 2 noms que nous ajouterons à notre exe reconstruit.

Quand à la table des imports original du programme, il va falloir la construire nous même car celle-ci est introuvable dans le dump. Si on ouvre avec un éditeur de PE notre exe reconstruit on verra que la table des imports devait normalement se trouver à l'offset 00004414 et de taille 3C. Très bien, nous la reconstruirons à cet endroit.

On ouvre notre exe reconstruit on va à l'offset 00004414 et on laisse une place de 3C.

A la suite on va écrire le nom des 2 DLL importés, dans la partie Ascii de notre éditeur. En n'oubliant pas de terminer le nom de chaque DLL par un byte 00. (le byte 00 doit être dans la partie hexa). l'ordre n'a pas d'importance, puisque nous l'indiquerons au moment où l'on écrira la table des imports.

On retourne dans le dump à l'offset 00007007 (début des noms des fonctions (oublier pas l'index avant le nom d'une fonction, c'est pour ça qu'on ne commence pas directement sur le nom de la première fonction mais 1 word plutôt)) On sélectionne tout le bloc qui contient des noms de fonction et on Copie (on regarde la taille de ce bloc en même temps) et dans l'exe reconstruit, à la suite des nom de DLL (après le byte 00 marquant le fin de nom de la dll), on sélectionne un bloc égal à la taille du bloc que nous venons de copier et on le supprime. Ensuite on colle celui du dump à la place.

Retournons à l'offset de la table des imports (00004414) et écrivons notre table. Pour cela il nous faut relevés 2 éléments nécessaires que sont les offsets des noms des DLL et les offset des 2 tableaux IAT relevés un peu plus tôt :

Comment savoir à quelle DLL appartiennent les tableaux ? Il suffit de prendre le nom d'une fonction dans celles listés et de regarder avec une doc à quelle DLL elle appartient (le MSDN ou win32.hlp font très bien l'affaire) ensuite on recherche avec les API LoadLibrary et GetProcAddress l'adresse relative de cette fonction. L'adresse obtenue est forcement dans l'un des tableaux d'IAT de notre exe, il suffit de faire une recherche hexa sur cette adresse et nous tomberont dans le tableau correspondant à la DLL. (n'oubliez pas d'inverser l'ordre des bytes de l'adresse retourné pour la trouver dans le fichier)

Je vous ai facilité les chose avec un petit tools que j'ai codé pour ça que vous pourrez trouver dans les fichiers joins à ce tutorial. Pour s'en servir, il suffit d'entrer un nom de DLL et un nom de fonction et le programme nous retournera son adresse. (attention de bien respecter la syntaxe des noms des dll et des fonctions ainsi que les majuscules et les minuscules, exemple correct : USER32.dll et MessageBoxA)

J'ai maintenant identifié que le premier tableau correspond aux import de KERNEL32.dll et le deuxième à USER32.dll je peux à présent écrire ma table des imports :

OriginalFirstThunk TimeDateStamp ForwarderChain DllName FirstThunk
00000000 00000000 00000000 50440000 00400000 (Kernel32.dll)
000000000 00000000 00000000 5D440000 94400000 (user32.dll)
000000000 000000000 000000000 000000000 000000000 (vide)

On se place à l'offset de notre table des import en 00004414 et on écrit notre table ce qui donne vu sous un éditeur hexa :

0000004368 72617279 00000000 0A0A0000 52756E74 696D6520 4572726F rary........Runtime Erro
0000004380 72210A0A 50726F67 72616D3A 20000000 2E2E2E00 3C70726F r!..Program: .......<pro
0000004398 6772616D 206E616D 6520756E 6B6E6F77 6E3E0000 4765744C gram name unknown>..GetL
00000043B0 61737441 63746976 65506F70 75700000 47657441 63746976 astActivePopup..GetActiv
00000043C8 6557696E 646F7700 4D657373 61676542 6F784100 75736572 eWindow.MessageBoxA.user
00000043E0 33322E64 6C6C0000 00000000 00000000 FFFFFFFF AE334000 32.dll...............3@.
00000043F8 B2334000 FFFFFFFF 62344000 66344000 FFFFFFFF E6354000 .3@.....b4@.f4@......5@.
0000004410 EA354000 00000000 00000000 00000000 50440000 00400000 .5@.............PD...@..
0000004428 00000000 00000000 00000000 5D440000 94400000 00000000 ............]D...@......
0000004440 00000000 00000000 00000000 00000000 4B45524E 454C3332 ................KERNEL32
0000004458 2E646C6C 00555345 5233322E 646C6C00 00014765 74537464 .dll.USER32.dll...GetStd
0000004470 48616E64 6C650001 47657453 7472696E 67547970 65570001 Handle..GetStringTypeW..
0000004488 47657453 7472696E 67547970 65410001 4C434D61 70537472 GetStringTypeA..LCMapStr
00000044A0 696E6757 00014C43 4D617053 7472696E 67410001 4D756C74 ingW..LCMapStringA..Mult
00000044B8 69427974 65546F57 69646543 68617200 014C6F61 644C6962 iByteToWideChar..LoadLib

Voila notre job est presque fini, il faut juste corriger les IAT pour pointer non pas vers les adresses des fonctions en mémoire mais vers le nom des fonctions dans notre fichier. Pour ça on va utiliser la fonction Rebuild Import Table de LordPE qui nous évitera un travail manuel laborieux.

Notre section des import est à présent valide et nous pouvons désassembler l'exe sous wdasm en obtenant les imports, les datas et le code, Par contre si on essaye de lancer l'exe il ne se passe rien, il nous reste un dernier point à corriger : les ressources.

6. Correction des ressources

Notre dump a bien dumpé les ressources mais l'index qui permet au programme d'identifier les ressources est manquant. On retourne dans le dump on regarde à quelle offset se situe les ressources avec un éditeur de PE et on relève dans le PE directory info :

Ressource : offset = 00009000 , size = 58

Maintenant notre exe se lance bien et est entièrement reconstruit quasiment comme à son origine.

7. Infos pratiques sur UPX 1.23

:0040822E 61                      popad
:0040822F E97C8FFFFF jmp 004011B0 (OEP)
:00408234 00000000000000000000 BYTE 10 DUP(0)
:0040823E 00000000000000000000 BYTE 10 DUP(0)
:00408248 00000000000000000000 BYTE 10 DUP(0)
:00408252 00000000000000000000 BYTE 10 DUP(0)
:0040825C 00000000000000000000 BYTE 10 DUP(0)
:00408266 00000000000000000000 BYTE 10 DUP(0)
:00408270 00000000000000000000 BYTE 10 DUP(0)