Level : intermediate
This tutorial explains unpacking of ExeCryptor official crackme published on crackmes.de.
ExeCryptor official crackme landed on crackmes.de somewhere around 2004. At that time ExeCryptor looked like really hard bone and I saved crackme on my hard drive with hope that one day I will have skills to defeat it.
The 2006 year closley comes to end and I have finally managed to break this crackme. Even more, crackme doesn't look too hard now. Main strength of ExeCryptor is in feature called "Code Morphing". Code morphing is another name for obfuscation, junk, garbage, transformation of original opcodes to huge and unreadable equivalent code. Code that is doing same thing as original code, but it is almost impossibe to trace trough it. ExeCryptor layer is also obfuscated with tons of such junk which jumps all around. But even in such environment, it is possible to find weak spots and break protection.
Kao already solved this crackme, but in this my solution (that comes late, I know) you will see what anti-debug tricks ExeCryptor uses and how to make good dump. Also, Kao's dump isn't fixed to the end - on my Windows XP Pro SP2 I get errors. Reason why is that happened is also explained in this tutorial.
Tools that I used are OllyDbg 1.10 with some script plugin, LordPE and ImpREC.
I think that this version of ExeCryptor is 2.1.17. Altough this version is obsolete, remember that you always can learn something from it :).
2. Debugging tricks
- ExeCryptor uses TLS callbacks to execute code before OEP so protected executable will run under Olly without stoping on OEP. Side effect is that other anti debug tricks will be executed , Olly will be detected and it will ExeCryptor will terminate it. To avoid this auto-running, we can set Olly to break on system breakpoint instead on executable OEP (debugging options -> events -> and set "Make first pause at" - System breakpoint).
- Protector uses lot of exceptions, especially those with LOCK prefix.
- Hardware breakpoints are corrupted and we cannot use them.
- Software breakpoints are detected. ExeCryptor examnes whole APIs. This is example on IsDebuggerPresent API:
0048859C 8A00 MOV AL,BYTE PTR DS:[EAX] ;Take first byte from API,
0047BF8B 2C 99 SUB AL,99 ;Subtract it with 99,
0047BF8D 8B55 FC MOV EDX,DWORD PTR SS:[EBP-4] ;EDX=IsDebuggerPresent,
00470EA9 F62A IMUL BYTE PTR DS:[EDX]
00462F3A 3C A4 CMP AL,0A4 ;Compare is byte=0A4,
00462F3C 0F85 A6050000 JNZ EXECrypt.004634E8 ;No breakpoint on API,
00462F42 E9 9E070000 JMP EXECrypt.004636E5 ;Breakpoint detected.
That is just example for first byte, further it performs similar checks for the rest of bytes. Since software breakpoints are detected, hardware are corupted, we must use memory breakpoints.
- ExeCryptor examnes possible API redirections and hooks:
00488DC5 8038 E9 CMP BYTE PTR DS:[EAX],0E9 ;Checks for long JMP on a API start.
00488DC8 ^0F84 8355FEFF JE unpacked.0046E351
00463F91 8038 EB CMP BYTE PTR DS:[EAX],0EB ;Checks for a short JMP.
00463F94 0F84 B7A30000 JE unpacked.0046E351
00456056 8038 E8 CMP BYTE PTR DS:[EAX],0E8 ;Checks for a CALL.
00456059 0F85 50E40200 JNZ unpacked.004844AF
- First check is IsDebuggerPresent trick.
- Second check is FindWindowA , where ExeProtector looks for window with "OLLYDBG" class.
- Third trick is more interesting. ExeCryptor will enumerate windows to retrieve PID numbers with GetWindowThreadProcessId. Then it examnes PE header of each process with OpenProcess and ReadProcessMemory. Example, it reads 40h bytes from 400000:
0012F720 00476B51 /CALL to ReadProcessMemory from EXECrypt.00476B4C
0012F724 0000002C |hProcess = 0000002C
0012F728 00400000 |pBaseAddress = 400000
0012F72C 0012F898 |Buffer = 0012F898
0012F730 00000040 |BytesToRead = 40 (64.)
0012F734 0012F8DC pBytesRead = 0012F8DC
Then it checks buffer for MZ signature:
00467A37 66:817D A4 4D5A CMP WORD PTR SS:[EBP-5C],5A4D
00467A3D ^0F85 C27DFFFF JNZ EXECrypt.0045F805
00467A43 ^E9 FFF5FEFF JMP EXECrypt.00457047
If data in buffer is OK, it will read PE offset information and from there it wil locate export table offset. In Olly 1.10 export section is at 50F000. ExeCryptor will examne all export names in order to find two possible OllyDbg exports. Again it uses ReadProcessMemory to read every 40h bytes of table:
0012F738 5F 41 64 64 73 6F 72 74 65 64 64 61 74 61 00 5F _Addsorteddata._
0012F748 41 64 64 74 6F 6C 69 73 74 00 5F 41 6E 61 6C 79 Addtolist._Analy
0012F758 73 65 63 6F 64 65 00 5F 41 6E 69 6D 61 74 65 00 secode._Animate.
0012F768 5F 41 73 73 65 6D 62 6C 65 00 5F 41 74 74 61 63 _Assemble._Attac
Blacklisted exports are:
_Setdisasm ;I sow that it checks for this one.
_SendMessageToWDM@20 ;This one doesn't exist in Olly 1.10 but it checks for this one too.
_Setdumptype ;This one is checked to (altough I didn't spot that).
To get rid of this protection, we can change exports (some plugins will not work then), or patch checks. You can run crackme under Olly then.
3. Finding OEP
Protected file is Delphi program which make things easier. Delphi OEP is always at the end of code section. We can run app within Olly (or attach with Olly) and scroll to the end of section:
00442FD0 8C264400 DD EXECrypt.0044268C
00442FD4 84264400 DD EXECrypt.00442684
00442FD8 54264400 DD EXECrypt.00442654
00442FDC 4C264400 DD EXECrypt.0044264C
00442FE0 1C264400 DD EXECrypt.0044261C
00442FE4 2C274400 DD EXECrypt.0044272C
00442FE8 FC264400 DD EXECrypt.004426FC
00442FEC 64274400 DD EXECrypt.00442764
00442FF0 34274400 DD EXECrypt.00442734
00442FF4 D4274400 DD EXECrypt.004427D4
00442FF8 A4274400 DD EXECrypt.004427A4
00442FFC 9C274400 DD EXECrypt.0044279C
00443000 6C274400 DD EXECrypt.0044276C
00443004 302A4400 DD EXECrypt.00442A30
00443008 D4294400 DD EXECrypt.004429D4
0044300C AC2E4400 DD EXECrypt.00442EAC
00443010 642E4400 DD EXECrypt.00442E64
00443014 00 DB 00
00443015 00 DB 00
00443016 00 DB 00
00443017 00 DB 00
00443018 B42E4400 DD EXECrypt.00442EB4
0044301C .-E9 71940300 JMP EXECrypt.0047C492 <----------------- Here it should be OEP!!!
00443021 9F DB 9F
00443022 49 DB 49 ; CHAR 'I'
00443023 66 DB 66 ; CHAR 'f'
00443024 B1 DB B1
00443025 CB DB CB
00443026 D2 DB D2
00443027 . AB STOS DWORD PTR ES:[EDI]
00443028 . F62E IMUL BYTE PTR DS:[ESI]
0044302A . 58 POP EAX ; user32.77D493F5
0044302B . 5A POP EDX ; user32.77D493F5
0044302C .-E9 8BBA0000 JMP EXECrypt.0044EABC
00443031 >-E9 E5C60200 JMP EXECrypt.0046F71B
00443036 0F DB 0F
00443037 8C DB 8C
00443038 90 NOP
00443039 . C603 00 MOV BYTE PTR DS:[EBX],0
0044303C . 59 POP ECX ; user32.77D493F5
0044303D . C1C5 0B ROL EBP,0B
00443040 . C1C1 15 ROL ECX,15
00443043 . 81F1 C5E80D07 XOR ECX,70DE8C5
00443049 . 81C1 0B20AF36 ADD ECX,36AF200B
0044304F .-E9 76BC0200 JMP EXECrypt.0046ECCA
00443054 . 81DF A44999AA SBB EDI,AA9949A4
0044305A . 8731 XCHG DWORD PTR DS:[ECX],ESI
0044305C . 8D45 C7 LEA EAX,DWORD PTR SS:[EBP-39]
0044305F . 50 PUSH EAX
00443060 .-E9 981E0400 JMP EXECrypt.00484EFD
00443065 . 81C1 E798052F ADD ECX,2F0598E7
0044306B .^E9 498FFCFF JMP EXECrypt.0040BFB9
00443070 85 DB 85
00443071 F5 DB F5
00443072 45 DB 45 ; CHAR 'E'
00443073 CB DB CB
00443074 2E DB 2E ; CHAR '.'
00443075 BA DB BA
00443076 E8 DB E8
00443077 15 DB 15
00443078 07 DB 07
00443079 FC DB FC
0044307A FF DB FF
0044307B 90 NOP
0044307C 00 DB 00
0044307D 00 DB 00
0044307E 00 DB 00
0044307F 00 DB 00
00443080 00 DB 00
00443081 00 DB 00
00443082 00 DB 00
Line 0044301C is where OEP should be but instead of usuall PUSH EBP opcode , we can see some jump to ExeCryptor's code. Protector has replaced OEP code with it's own obfuscation and "reall OEP" is at the location of that jump:
0047C492 E8 7369FEFF CALL EXECrypt.00462E0A <----------------- This is OEP!
0047C497 68 9A5A90E7 PUSH E7905A9A
0047C49C ^E9 2730FEFF JMP EXECrypt.0045F4C8
0047C4A1 61 POPAD
0047C4A2 -FF25 88654400 JMP DWORD PTR DS: ; user32.GetClassInfoA
0047C4A8 C3 RETN
0047C4A9 60 PUSHAD
0047C4AA -E9 20BFFCFF JMP EXECrypt.004483CF
0047C4AF 59 POP ECX ; user32.77D493F5
0047C4B0 C3 RETN
0047C4B1 E9 06210000 JMP EXECrypt.0047E5BC
0047C4B6 ^E9 BDFAFEFF JMP EXECrypt.0046BF78
0047C4BB ^E9 4F57FEFF JMP EXECrypt.00461C0F
0047C4C0 67:64:FF36 0000 PUSH DWORD PTR FS:
0047C4C6 67:64:8926 0000 MOV DWORD PTR FS:,ESP
0047C4CC ^E9 97BBFEFF JMP EXECrypt.00468068
0047C4D1 8945 A4 MOV DWORD PTR SS:[EBP-5C],EAX
0047C4D4 8B45 D8 MOV EAX,DWORD PTR SS:[EBP-28]
To break at that OEP, we can restart target, place memory breakpoint on access on that line, and run (Shift+F9) untill we stop there.
I don't know what to think about import protection. It is not hard , but it is not too easy either. Most imports are redirected to ExeCryptor code where we have tons of garbage code. There imports are stored as some hashes. ExeCryptor then examnes all ordinals in specified DLL to find one with correct hash. When import is found, it jumps on it with RETN or JMP. JMP type import is writen in IAT when import with correct hash is found so this decryption is performed only once. RETN type is never writen, instead import always needs to be decrypted. I didn't intend to fix imports manually so I needed somehow to automate job. Because that, I divided import protection to 3 types.
- Example of first type:
004011D8 $-FF25 94614400 JMP DWORD PTR DS: ; EXECrypt.0047AE8C
Import jump goes into ExeCryptor code where we can find random junky code:
0047AE8C ^0F80 EEFDFFFF JO EXECrypt.0047AC80
0047AE92 ^E9 9B65FFFF JMP EXECrypt.00471432
00471432 ^E9 5BD9FFFF JMP EXECrypt.0046ED92
0046ED92 8B05 5CFE4500 MOV EAX,DWORD PTR DS:[45FE5C]
0046ED98 09C0 OR EAX,EAX
0046ED9A -0F85 16EFFDFF JNZ EXECrypt.0044DCB6
0046EDA0 ^E9 37F1FFFF JMP EXECrypt.0046DEDC
0046DEDC E9 ECDC0000 JMP EXECrypt.0047BBCD
0047BBCD ^0F84 4A6CFEFF JE EXECrypt.0046281D
0046281D 68 5DB7978E PUSH 8E97B75D
00462822 58 POP EAX
00462823 E9 EE650200 JMP EXECrypt.00488E16
00488E16 ^E9 B180FEFF JMP EXECrypt.00470ECC
00470ECC 81F0 EC9AEF06 XOR EAX,6EF9AEC
00470ED2 F7C0 00000200 TEST EAX,20000
00470ED8 ^E9 12AFFEFF JMP EXECrypt.0045BDEF
0045BDEF E9 564A0000 JMP EXECrypt.0046084A
0046084A ^0F84 3144FFFF JE EXECrypt.00454C81
00454C81 81C8 40EC7B0E OR EAX,0E7BEC40
00454C87 0F85 B6B70400 JNZ EXECrypt.004A0443
004A0443 C1C0 16 ROL EAX,16
004A0446 2B05 008E4900 SUB EAX,DWORD PTR DS:[498E00]
004A044C ^E9 D3BCFDFF JMP EXECrypt.0047C124
0047C124 81C0 A45747A7 ADD EAX,A74757A4
0047C12A E8 296B0000 CALL EXECrypt.00482C58
Every import of this type has different junk so it looked like impossible for automating process of finding imports. But it is not possible that every new jump has decryption routine just for it. That would result with huge protector code. So it must be that only first couple opcodes are random to prevent tracing and auto tools, but soon or later all must end in one main routine. And that was the case. I checked references to the last call destination and I found numerous references:
References in EXECrypt:wxphotmy to 00482C58
Address Disassembly Comment
00454988 CALL EXECrypt.00482C58
00454F21 CALL EXECrypt.00482C58
004552DB CALL EXECrypt.00482C58
0045663E CALL EXECrypt.00482C58
0045691A JMP EXECrypt.00482C58
00456F5F CALL EXECrypt.00482C58
0049F79E JMP EXECrypt.00482C58
0049F9EE CALL EXECrypt.00482C58
0049FB19 CALL EXECrypt.00482C58
004A034A JMP EXECrypt.00482C58
004A04E2 CALL EXECrypt.00482C58
So what is at that address? Just more junk:
00482C58 -0F82 18A0FCFF JB EXECrypt.0044CC76
00482C5E 8B15 38414800 MOV EDX,DWORD PTR DS:
00482C64 09D2 OR EDX,EDX ; EXECrypt.0047C492
00482C66 ^0F85 9FAFFFFF JNZ EXECrypt.0047DC0B
00482C6C ^E9 18BDFDFF JMP EXECrypt.0045E989
00482C71 8B2A MOV EBP,DWORD PTR DS:[EDX]
00482C73 8919 MOV DWORD PTR DS:[ECX],EBX
But stack is place where we will find important information:
$-4 > 0047C12F RETURN to EXECrypt.0047C12F from EXECrypt.00482C58
$ ==> > 7C816D4F RETURN to kernel32.7C816D4F
$+4 > 7C910738 ntdll.7C910738
$+8 > FFFFFFFF
$+C > 7FFDE000
$+10 > 8054B038
First value in stack is address where we will return after import is found. And when we break there, check registers window and you will see that EAX contains our import:
EAX 7C809B77 kernel32.CloseHandle
It is same for all imports of this type. "Magic address" for this type is 482C58 but we have one more at 4584D3.
- Second type:
004012B0 $-FF25 A0614400 JMP DWORD PTR DS:[4461A0] ; EXECrypt.0046A5B8
On that location we have this code:
0046A5B8 60 PUSHAD
0046A5B9 B8 9EDBAE8D MOV EAX,8DAEDB9E
0046A5BE E8 909B0100 CALL EXECrypt.00484153
0046A5C3 51 PUSH ECX ; kernel32.7C809BC7
It starts with PUSHAD opcode so we can use hardware preakpoint (execute PUSHAD, place hw bp at ESP value in dump and run) that will bring us to:
00465E0D -FF25 A0614400 JMP DWORD PTR DS:[4461A0] ; user32.LoadStringA
And this import is also writen in IAT. Check original jump:
004012B0 $-FF25 A0614400 JMP DWORD PTR DS:[4461A0] ; user32.LoadStringA
Don't forget to remove hw bp.
- Third type:
Third type is similar to second one but it is not convinient for script. Import starts with some random junk, but eventually it has PUSHAD opcode and from there it is same thing.
I wrote script that fixed type 1 and 2, but there was some problems. Target would ventually crush and I had to restart and copy-paste partialy fixed IAT. But it was pretty fast and soon I recovered whole IAT. I dumped target and used ImpREC to rebuild new IAT. At this point target was unpacked. Dump was running fine altough there was some interaction between ExeCryptor code and target code. I mean at OEP code, plus some inside functions. But dump would crush on others machines.
5. Dump problems
First problem that acctualy is not problem at all, is TLS information. I left TLS info like in original file and on every startup EC code triggered with TLS callback would start. That is not problem but it's little annoyance if we try to debug dump. To fix that you can open any Delphi executable on your machine and see how TLS looks on that file. Set similar value in dump and problem is solved.
The reall problem lies in fact that dump still uses EC layer which cannot be removed. Why? Because this is the EC main strength - to mutate code so we cannot recower it. But that is not exact problem. Problem is, that EC used some imports in the process of unpacking (and it will use them later). Those imports are not part of IAT and they are not placed in IAT section. This is how that part of EC code works:
Step1: EC uppon startup loads some DLLs (kernel32.dll, ntdll.dll, rpcrt4.dll, etc...). After loading, EC places image base of DLL to apropriate variable. Variable is NULL DWORD (empty 4 bytes) that is placed inside EC code (as part of code).
Step2: EC uses some custom procedure to find APIs in that DLL. It takes that DLL base, then examnes PE header searching for export table etc. When ordinal is found, EC places API address into new variable (NULL DWORD for APIs). But before that EC checked is variable=NULL. If yes, it means that it needs to find API. If not, it thinks that API is already loaded so it will just go to that address. And here is problem.
When we dump file, in our dump those variables will not be equal to NULL. They will hold API address. But that address is static and on another machine dump will fail to work. Check my first dump:
00480E89 833D BC814700 00 CMP DWORD PTR DS:[4781BC],0
You see? EC checks is varable empty. But there is address of GetCurrentId API that was dumped to my dump. EC will see that variable is not NULL and it will go straight to that address. But if I set variable to NULL, EC will find that API again. And that is solution of that problem. How to find and fix all such imports? There are only few of them. Use Olly great feature "Right click on CPU -> Search for -> All commands" , enter "CMP DWORD[CONST],0" and you will find all variables. All that holds some import needs to be filled with 0.
Problem two comes from a step one. Dump file holds those DLL bases that will be incorrect on some other machines. Check this example:
0046A671 CMP DWORD PTR DS:,0 DS:=77DD0000 (advapi32.77DD0000)
You need to set those variables to NULL too. But problem still remains because those variables are never filled again after OEP is reached! So we need to place correct DLL bases at those into those variables. I solved that by injecting some code and changing OEP that will fill those variables:
00443120 > $ 68 80304400 PUSH unpacked.00443080 ; /FileName = "advapi32.dll"
00443125 . FF15 60515100 CALL DWORD PTR DS:[<&kernel32.LoadLibrar>; LoadLibraryA
0044312B . A3 04694600 MOV DWORD PTR DS:,EAX
00443130 . 90 NOP
00443131 . 90 NOP
00443132 . 68 90304400 PUSH unpacked.00443090 ; /FileName = "comctl32.dll"
00443137 . FF15 60515100 CALL DWORD PTR DS:[<&kernel32.LoadLibrar>; LoadLibraryA
0044313D . A3 472C4800 MOV DWORD PTR DS:[482C47],EAX
00443142 . 90 NOP
00443143 . 90 NOP
00443144 . 68 A0304400 PUSH unpacked.004430A0 ; /FileName = "gdi32.dll"
00443149 . FF15 60515100 CALL DWORD PTR DS:[<&kernel32.LoadLibrar>; LoadLibraryA
0044314F . A3 392C4800 MOV DWORD PTR DS:[482C39],EAX
00443154 . 90 NOP
00443155 . 90 NOP
00443156 . 68 B0304400 PUSH unpacked.004430B0 ; /FileName = "kernel32.dll"
0044315B . FF15 60515100 CALL DWORD PTR DS:[<&kernel32.LoadLibrar>; LoadLibraryA
00443161 . A3 10B84400 MOV DWORD PTR DS:[44B810],EAX
00443166 . 90 NOP
00443167 . 90 NOP
00443168 . 68 F0304400 PUSH unpacked.004430F0 ; /FileName = "oleaut32.dll"
0044316D . FF15 60515100 CALL DWORD PTR DS:[<&kernel32.LoadLibrar>; LoadLibraryA
00443173 . A3 282C4800 MOV DWORD PTR DS:[482C28],EAX
00443178 . 90 NOP
00443179 . 90 NOP
0044317A . 68 10314400 PUSH unpacked.00443110 ; /FileName = "user32.dll"
0044317F . FF15 60515100 CALL DWORD PTR DS:[<&kernel32.LoadLibrar>; LoadLibraryA
00443185 . A3 68804800 MOV DWORD PTR DS:,EAX
0044318A . 90 NOP
0044318B . 90 NOP
0044318C . 90 NOP
0044318D . 90 NOP
0044318E . 90 NOP
0044318F . 90 NOP
00443190 .^E9 87FEFFFF JMP unpacked.0044301C
Now variables will hold correct bases and dump will work. But one more thing about that. One variable hodl image base of NTDLL.DLL that is used only on NT systems (I have unpacked file on Window XP). On Win 9x systems this DLL doesn't exist. There is variable for this DLL, but it doesn't need to be filled with image base. Instead it just needs to be set to NULL. Then dump will work on all machines :)
Btw, these are problems that I found in kao's dump. Kao didn't noticed it, but after I installed SP2 to my Win XP, his dump didn't worked (exception at startup, not window tile, no serial check). And I fixed all of these problems in his dump too, to see will it work.
7. The End
This ExeCryptor version looked like 2.1.17 to me, but I checked evaluation version 2.1.17 and it has couple more tricks. After IsDebuggerPresent, it calls CheckRemoteDebuggerPresent and then it goes to FindWindowA.
And that would be all.
Unpacked crackme + rtf tutorial is here http://www.reversing.be/binaries/articles/20061201110937698.rar
haggar , 2006