Reversing +Aesculapius
A complete explanation of a very good assembler protection
28 February 1998
by Jack of Shadows
Courtesy of Reverser's page of reverse engineering
Oh my, we have awakened a 'dormient' mighty cracker as it seems! :-)
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!
our protections
Our protections
There is a crack, a crack in everything That's how the light gets in
( )Beginner ( )Intermediate (x)Advanced ( )Expert

A very nice protection deserves a complete explanation. Here it is.
Reversing +Aesculapius
A complete explanation of a very good assembler protection
Written by Jack of Shadows

It was a while ago when I cracked my last program (it was a HyperDisk in 1992, do you remember this GREAT disk cache?). So I received Aesculapiuses protection as a gift from above, a chance to put myself back in shape. It took me about 4 hours to completely understand his interesting concept. Crack was then very simple thing, indeed.

Tools required
Turbo Debugger (just for testing some hypothesis)
Turbo Pascal (to write two small helper programs)
Tea (preferably green)

Target's URL/FTP

For those who don't want to know, here is the crack:
  f: file;
  enc: word;
  x: word;
  x := $1330 xor enc;

All others please read further.

First, a word of explanation. I have done most of the analysis in IDA and used Turbo Debugger only on few occasions to test some of my thoughts. Cracking could be done without debugger, but not without IDA (it's a great tool).

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


Disassemble the and you'll find a bunch of garbage. The program is somehow encoded. Let's take a look at the method Aesculapius is using.

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 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!). It will decrypt the code and NOP the call to sub_0_169. Turbo Pascal, of course (you won't see me C):

  f: file;
  i: integer;
  w: word;
  nop: longint;
  enc: word;
  nop := $909090;
  for i := 1 to $8AD do begin
    w := w XOR enc;

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, independent of real file name. Completely unnecessary and wrong but helpful for us.


Load now the unwrapped into IDA. (Patched) entry point now only jumps to the real entry point:
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 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 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 loc_0_110D:
seg000:110D      push ax
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 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 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 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 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 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 It opens the file (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).


So what do we have to do to crack the program. Open a file, read current encryption constant, XOR it with 1330h and write result to the appropriate offset. Cracking program was presented at the beginning of the essay.

Final Notes
As you can clearly see, Aesculapiuses protection is not very tough when we understand it. However keep in mind that we are dealing with a minimal program which is not using any other protectionist techniques. If we spread this protection over some medium sized program and intermix it with other tricks, it would be very hard to trace and crack, indeed.

You are deep inside reverser's page of reverse engineering, choose your way out:

Back to our protections

redhomepage redlinks redsearch_forms red+ORC redstudents' essays redacademy database
redreality cracking redhow to search redjavascript wars
redtools redanonymity academy redcocktails redantismut CGI-scripts redmail_reverser
redIs reverse engineering legal?