Friday, December 01 2006 @ 11:16 AM CET Contributed by: haggar Views: 8630
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:
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:
_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:
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.
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:
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:
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:
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:
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.