A complete explanation of a very good assembler protection
Judging from this work I can only hope that Jack of Shadows' re-awakened passion for reversing will produce MANY more capable works like this one!
I hope to receive soon Aesculapius' observations about this... and I have to confess that I myself was playing with this crack, yet I was still inside the 'convolute' xorings when Jack's solution came... and now, with hindsight, everything seems easy... THEREFORE AN ADVICE TO MY MOST CAPABLE READERS: DO NOT READ THIS if you have not already started working on Aesculapius' protection! You would spoil a very nice learning chance if you do...
Once you have already had a couple of cracking sessions on Aesculapius' protection, on the countrary, you'll sip this beautiful essay as it should be done: chill, refreshing, at the correct mind 'temperature'... well, do whatever you will... if you carry on reading: ENJOY!
var f: file; enc: word; x: word; begin assign(f,'aescul.com'); reset(f,1); seek(f,$6); blockread(f,enc,2); seek(f,$FF9); x := $1330 xor enc; blockwrite(f,x,2); close(f); end.
All others please read further.
So let's see what Aesculapius has prepared for us. Run the program. It just prints "unregistered". OK, so the game is to change this to "registered".
Startup code is simple:
seg000:0100 public start seg000:0100 start proc near seg000:0100 call sub_0_169 seg000:0103 jmp loc_0_1309 seg000:0103 start endp
So let's take a look at sub_0169:
seg000:0169 sub_0_169 proc near seg000:0169 mov si, 1E5h seg000:016C mov cx, 8ADh seg000:016F mov ax, cs:word_0_106 seg000:0173 seg000:0173 loc_0_173: seg000:0173 xor [si], ax seg000:0175 inc si seg000:0176 inc si seg000:0177 loop loc_0_173 seg000:0179 retn seg000:0179 sub_0_169 endp
No magic here, just a simple XOR with a constant. Let's put together a simple unwrapper (don't forget to make a backup copy of aescul.com!). It will decrypt the code and NOP the call to sub_0_169. Turbo Pascal, of course (you won't see me C):
var f: file; i: integer; w: word; nop: longint; enc: word; begin assign(f,'aescul.com'); reset(f,1); nop := $909090; blockwrite(f,nop,3); seek(f,$6); blockread(f,enc,2); seek(f,$E5); for i := 1 to $8AD do begin blockread(f,w,2); w := w XOR enc; Seek(f,FilePos(f)-2); blockwrite(f,w,2); end; close(f); end.
Run the unwrapped program to check if it is working (it is). Run it again, it will crash. Aesculapius is encrypting the disk file every time we run the program. Later we'll see why and how, for now just unwrap the program from backup copy and rename it to something else. Run it twice, it will work both times. OK, so program is always encrypting the aescul.com, independent of real file name. Completely unnecessary and wrong but helpful for us.
seg000:0100 public start seg000:0100 start proc near seg000:0100 nop seg000:0101 nop seg000:0102 nop seg000:0103 jmp loc_0_1309 seg000:0103 start endp
Let's take a look at it:
seg000:1309 loc_0_1309: seg000:1309 call sub_0_1107 seg000:130C call sub_0_1ED seg000:130F mov cs:byte_0_168, 0 ; some initialization? seg000:1315 mov cs:word_0_166, 0 seg000:131C mov cx, 0F0Ah ; NOP out the whole block seg000:131F mov bx, 1EDh seg000:1322 seg000:1322 loc_0_1322: seg000:1322 mov byte ptr [bx], 90h seg000:1325 inc bx seg000:1326 loop loc_0_1322 seg000:1328 call sub_0_17A seg000:132B seg000:132B loc_0_132B: ; exit to DOS seg000:132B mov ax, 4C00h seg000:132E int 21h
First we call two functions, then initialize two variables and wipe out large block of data. Data? Hey, we are wiping out the function sub_0_1ED we have just called! Definitely a part of protection. So maybe then initialization is not a initialization at all but just a cleanup? Remember this 1ED, we'll return to it. After NOPping the block we call another function and return to DOS. Smiple.
And, hey! a surprise! (just kidding) Right after this is lying a string "Registered." Our little hypothesis seems to be correct. We do have to make a program write this string out.
seg000:1330 aRegistered_ db 0Ah seg000:1330 db 0Dh,'Registered.$',0
So let's follow our dead listing trace:
seg000:1107 sub_0_1107 proc near seg000:1107 mov di, 1EDh ; remember this constant? seg000:110A mov ax, 0FFFh seg000:110D seg000:110D loc_0_110D: seg000:110D push ax seg000:110E seg000:110E loc_0_110E: seg000:110E mov ax, 6 seg000:1111 call sub_0_12E8 seg000:1114 cmp ax, cs:word_0_15B seg000:1119 jz oc_0_110E seg000:111B mov cs:word_0_15B, ax seg000:111F add ax, ax seg000:1121 mov si, ax seg000:1123 call cs:off_0_10FB[si] ; jump table seg000:1128 pop ax seg000:1129 cmp cs:word_0_166, 0F00h seg000:1130 jnb loc_0_1135 seg000:1132 dec ax seg000:1133 jnz loc_0_110D seg000:1135 seg000:1135 loc_0_1135: seg000:1135 call sub_0_12DE seg000:1138 mov cs:word_0_106, ax ; decryption operator seg000:113C xor bx, bx seg000:113E xor cx, cx seg000:1140 xor si, si seg000:1142 xor di, di seg000:1144 xor bp, bp seg000:1146 retn seg000:1146 sub_0_1107 endp
What's going on here? First, DI is initialized to magic constant 1ED and AX to FFF to serve as counter (looped between loc_0_110D: and dec ax/jnz loc_0_110D). At the end of loop something is called, AX is stored and some registers are cleared. But wait, what is this? Word_0_106 was used as a decryption constant when the program was unwrapped and it is now changed. Weird.
Let's go deeper:
seg000:12DE sub_0_12DE proc near seg000:12DE in ax, 40h seg000:12E0 xor ax, 0FFFFh seg000:12E3 mov cs:word_0_159, ax seg000:12E7 retn seg000:12E7 sub_0_12DE endp seg000:12E8 sub_0_12E8 proc near seg000:12E8 push bx seg000:12E9 push cx seg000:12EA push dx seg000:12EB xchg ax, bx seg000:12EC call sub_0_12DE seg000:12EF xor dx, dx seg000:12F1 div bx seg000:12F3 xchg ax, dx seg000:12F4 pop dx seg000:12F5 pop cx seg000:12F6 pop bx seg000:12F7 retn seg000:12F7 sub_0_12E8 endp
To shorten story a little let's first take a look at sub_0_12DE. It is very simple - it just reads a port $40 (real-time clock!) and stores result into some global variable.
Sub_0_12E8 is much more interesting and not so easy to understand. It calls sub_0_12DE and then divides resulting value with number which was stored in AX when sub was called. It then discards a integer part of result and returns modulo in AX. We can therefore conclude that it is working as a random generator which returns numbers in the range [0..AX-1].
Return to the sub_0_1107. Now we can understand a lot more. Let's take a look at it again:
seg000:110E loc_0_110E: seg000:110E mov ax, 6 seg000:1111 call sub_0_12E8 ; random number generator seg000:1114 cmp ax, cs:word_0_15B seg000:1119 jz loc_0_110E seg000:111B mov cs:word_0_15B, ax ; last random number seg000:111F add ax, ax seg000:1121 mov si, ax seg000:1123 call cs:off_0_10FB[si] ; jump table
Random number generator is used to return number in the range [0..5]. It is compared to the last random number and whole process is repeat until we get different number. This is then used as an index into a jump table which, not surprisingly, contains 6 offsets:
seg000:10FB off_0_10FB seg000:10FB dw offset loc_0_11C3 seg000:10FD dw offset loc_0_11F2 seg000:10FF dw offset loc_0_121D seg000:1101 dw offset loc_0_1255 seg000:1103 dw offset loc_0_1280 seg000:1105 dw offset loc_0_12AB
Let's examine the first function:
seg000:11C3 loc_0_11C3: seg000:11C3 xor ax, ax seg000:11C5 xor bx, bx seg000:11C7 mov bl, 50h seg000:11C9 mov ax, 5 seg000:11CC call sub_0_12E8 ; our old friend, random generator seg000:11CF mov si, ax seg000:11D1 or bl, cs:[si+114Ah] seg000:11D6 mov al, bl seg000:11D8 stosb ; store somewhere seg000:11D9 mov bl, 58h seg000:11DB mov ax, 5 seg000:11DE call sub_0_12E8 ; again seg000:11E1 mov si, ax seg000:11E3 or bl, cs:[si+114Ah] seg000:11E8 mov al, bl seg000:11EA stosb ; and again seg000:11EB add cs:word_0_166, 2 seg000:11F1 retn
Our old friend random generator is back in action. Returning value is ORed with 50 and stored somewhere. Where? Where DI points, of course. And that is our old friend, buffer that starts at address 1ED.
Other five functions produce similar "nonsense" (it makes a lot of sense, you'll see later) into the same buffer.
But there is one more meaningful part - add cs:word_0_166,2. We have stored 2 bytes into 1ED buffer and incremented this value by 2. We can confirm this behavior on the other 5 functions. Four of them put 2 bytes into buffer and increment word_0_166 by two, just the last one puts 4 bytes into buffer and increments word_0_166 by four. And this word_0_166 was used before, in the sub_0_1107 where this code fragment was executed:
seg000:1129 cmp cs:word_0_166, 0F00h seg000:1130 jnb loc_0_1135
We will only fill F00 bytes with this "garbage" and then we will stop. AX therefore never reaches 0 (in function sub_0_1107).
Let's unwind back to main entry point:
seg000:1309 loc_0_1309: seg000:1309 call sub_0_1107 seg000:130C call sub_0_1ED
After sub_0_1107 we execute sub_0_1ED. But wait, that is the "garbage" that 6 randomly called functions has just created. Maybe that is not garbage after all? And really, it is not garbage but a code. Randomly generated but code nonetheless.
Let's take another look at loc_0_11C3. Offset 114A points to a buffer with 5 elements: 7,1,3,5,6. If we OR them with 50 we get opcodes for push bp, push cx, push bx, push si, and push di. ORed with 58 they produce corresponding pop-s. Loc_0_11C3 therefore generates a PUSH/POP pairs (with same or different registers). If we examine other five routines, we can produce following table:
function produces loc_0_11C3 push bx/cx/bp/si/di pop bx/cx/bp/si/di loc_0_11F2 move bx/cx/bp/si/di, bx/cx/bp/si/di loc_0_121D sbb/or/sub/cmp/xor/add/and bx/cx/bp/si/di, bx/cx/bp/si/di loc_0_1255 xchg bx/cx/bp/si/di, bx/cx/bp/si/di loc_0_1280 mov bl/bh/cl/ch,[bx/bx+di/bp+di/di] loc_0_12AB mov bl/bh/cl/ch,[(bx/bx+di/bp+di/di) + random offset]
Sub_0_1107 is therefore a code generator. Produced code is nonsensical but completely correct and functional. It will modify only registers bx, cx, bp, si, and di (remember, all were (unnecessarily) cleared at the end of sub_0_1107). It is stored in the buffer, starting at offset 1ED and ending with offset 10F6. On offset 10F7 we can find already prepared RETN which will return as to the caller.
But what is the use of such code? To hide something inside! That "something" must be the guts of the protection system. And indeed it is and it is hiding in the last randomly called function, loc_0_12AB:
seg000:12AB loc_0_12AB: seg000:12AB xor ax, ax seg000:12AD mov bx, 8A80h seg000:12B0 mov ax, 5 seg000:12B3 call sub_0_12E8 seg000:12B6 mov si, ax seg000:12B8 or bl, cs:[si+114Ah] seg000:12BD mov ax, 4 seg000:12C0 call sub_0_12E8 seg000:12C3 mov si, ax seg000:12C5 or bl, cs:[si+114Fh] seg000:12CA mov ax, bx seg000:12CC xchg ah, al seg000:12CE stosw seg000:12CF mov ax, cs:word_0_159 seg000:12D3 stosw seg000:12D4 call sub_0_1156 seg000:12D7 add cs:word_0_166, 4 seg000:12DD retn
First part is only a code generator but at the end much more interesting function sub_0_1156 is called:
seg000:1156 sub_0_1156 proc near seg000:1156 push cx seg000:1157 push si seg000:1158 cmp cs:word_0_159, 0B00h ; compare with timer seg000:115F jb loc_0_11BE seg000:1161 cmp cs:byte_0_168, 1 ; state seg000:1167 jz loc_0_1190 seg000:1169 cmp cs:byte_0_168, 2 seg000:116F jz loc_0_11A7 seg000:1171 cmp cs:byte_0_168, 3 seg000:1177 jz loc_0_11BE seg000:1179 cmp cs:word_0_166, 0A00h seg000:1180 jbe loc_0_1190 seg000:1182 mov cx, 3 seg000:1185 mov si, 1147h seg000:1188 rep movsb ; move data to code buffer seg000:118A add cs:byte_0_168, 1 seg000:1190 seg000:1190 loc_0_1190: seg000:1190 cmp cs:word_0_166, 0B00h seg000:1197 jbe loc_0_11A7 seg000:1199 mov cx, 3 seg000:119C mov si, 10F8h seg000:119F rep movsb ; move data to code buffer seg000:11A1 add cs:byte_0_168, 1 seg000:11A7 seg000:11A7 loc_0_11A7: seg000:11A7 cmp cs:word_0_166, 0C00h seg000:11AE jbe loc_0_11BE seg000:11B0 mov cx, 2 seg000:11B3 mov si, 11C1h seg000:11B6 rep movsb ; move data to code buffer seg000:11B8 add cs:byte_0_168, 1 seg000:11BE seg000:11BE loc_0_11BE: seg000:11BE pop si seg000:11BF pop cx seg000:11C0 retn seg000:11C0 sub_0_1156 endp
Now THAT is an interesting code! First it compares current timer with 0B00 and exits if smaller. A little randomized behavior can never hurt (and can confuse a cracker). Then an internal state variable is compared to 1/2/3 and an appropriate code is executed. First fragment moves an opcodes for "mov ax, 900h" (but only if we have reached offset A00) into buffer and increments internal state so it will never be executed again. Second moves an opcodes for "mov dx, 12F8h" (if we have reached offset B00) and increments internal state. Third moves an opcodes for "int 21h" (if we have reached offset C00) and again increments internal state meaning that we have completed our task. All further calls to sub_0_1156 will just return.
In the now famous 1ED buffer we now have following fragment (mixed with harmless randomly generated instructions; harmless because then don't change registers AX and DX):
... random instructions ... mov ax, 900h ... random instructions ... mov dx, 12F8h ... random instructions ... int 21h ... random instructions ...
So what is this interrupt? Output to screen, of course, and 12F8 is the offset of string "Unregistered."! We only have to patch offset 12F8 and change it into 1330 (offset of "Registered." string). We'll return to this later, just after finishing our reverse engineering survey. We just have to check function sub_0_17A, called at the end of the program:
seg000:017A sub_0_17A proc near seg000:017A mov ax, 1A00h seg000:017D mov dx, 115h seg000:0180 int 21h ; DOS - SET DISK TRANSFER AREA ADDRESS seg000:0182 jb loc_0_1E5 seg000:0184 mov ax, 4301h seg000:0187 xor cx, cx seg000:0189 mov dx, 108h seg000:018C int 21h ; DOS - 2+ - SET FILE ATTRIBUTES seg000:018E jb loc_0_1E5 seg000:0190 mov ax, 3D02h seg000:0193 mov dx, 108h seg000:0196 int 21h ; DOS - 2+ - OPEN DISK FILE WITH HANDLE seg000:0198 jb loc_0_1E5 seg000:019A mov cs:word_0_113, ax seg000:019E mov ax, 4200h seg000:01A1 mov cx, 0 seg000:01A4 mov dx, 0 seg000:01A7 int 21h ; DOS - 2+ - MOVE FILE READ/WRITE POINTER (LSEEK) seg000:01A9 call sub_0_169 ; encryption seg000:01AC mov ax, 4000h seg000:01AF mov cx, 123Fh seg000:01B2 mov bx, cs:word_0_113 seg000:01B7 mov dx, 100h seg000:01BA int 21h ; DOS - 2+ - WRITE TO FILE WITH HANDLE seg000:01BC jb loc_0_1E5 seg000:01BE mov ax, 3E00h seg000:01C1 int 21h ; DOS - 2+ - CLOSE A FILE WITH HANDLE seg000:01C3 mov ax, 5701h seg000:01C6 mov cx, cs:word_0_15E seg000:01CB mov dx, cs:word_0_160 seg000:01D0 int 21h ; DOS - 2+ - SET FILE'S DATE/TIME seg000:01D2 mov ax, 4301h seg000:01D5 mov dx, 108h seg000:01D8 xor cx, cx seg000:01DA mov cl, cs:byte_0_15D seg000:01DF int 21h ; DOS - 2+ - SET FILE ATTRIBUTES seg000:01E1 call sub_0_169 ; encryption seg000:01E4 retn seg000:01E5 seg000:01E5 loc_0_1E5: ; just some error recovery seg000:01E5 mov ax, 3E00h seg000:01E8 int 21h ; DOS - 2+ - CLOSE A FILE WITH HANDLE seg000:01EA jmp loc_0_132B seg000:01EA sub_0_17A endp
This part of the program is responsible for the crash we experienced after unwrapping aescul.com. It opens the file aescul.com (first setting attributes to 0), encrypts the program (but with a new key! remember, it was changed at seg000:1138 in sub_0_1107), writes it to file, closes a file, sets file date/time to zero (don't know why) and attributes to zero (don't know why either). At the end it encrypts program again (actually it decrypts it since double XORing with the same value is a do-nothing operation).