The “4N006135” by borismilner contains a total of four x86 binaries, each of them with an increasing level of difficulty. Level-0 and Level-1 are pretty straightforward, while Level-2 and Level-3 took me a bit more time and the Intel Software Developer’s Manual at hand.
In this post I’ll focus on (some aspects of) Level-2 and how I built the corresponding keygen in Python.
Let’s jump into it, and start with the following disassembled section in .text:
.text:004013E0 mov edi, 40D020h .text:004013E5 mov ecx, 20h .text:004013EA mov al, 4Fh; 4Fh is the letter "O" .text:004013EC rep stosb
REP executes an instruction (STOS) and decrease the value of ECX until ECX is 0. The value of ECX is 20h so this means 32 times.
STOS(B) copies the value (1 byte) contained in AL to [EDI] and increments/decrements (depending on the Direction Flag) EDI by 1. B can be replaced by W or D to copy the value of 2 or 4 bytes respectively.
In Python this would look something like this:
s = bytearray(chr(0x4f) * 32)
I’m using bytearray instead of string because we’ll need to modify part of this value (strings are immutable in Python).
Skip a few lines of useless code until:
.text:00401401 rdtsc .text:00401403 mov ebx, eax .text:00401405 push ebx .text:00401406 push 40903Dh .text:0040140B call _printf
From the Intel manual:
The RDPMC (read performance-monitoring counter) and RDTSC (read time-stamp counter) instructions allow application programs to read the processor’s performance-monitoring and time-stamp counters, respectively.
The RDTSC instruction loads the current count of the time-stamp counter into the EDX:EAX registers.
The instruction is used as a pseudorandom number generator. The EAX part of the result is used as a User ID and stored in EBX for later usage.
Our keygen will ask for this value:
uid = int(raw_input("Enter User ID: "))
.text:00401413 mov edi, 40D020h .text:00401418 bt ebx, 0 .text:0040141C jnb short zero_bit_set .text:0040141E mov byte ptr [edi], 2Ah .text:00401421 zero_bit_set:
The value 40D020h came up before, but what is it? By observing all the remaining instructions of the program we can deduct that this is the memory location that will hold the final key (that we have to
guess calculate), computed using a custom algorithm and the user ID random value.
A quick recap so far:
EDI – pointer to the memory location where the key is stored. This at the moment contains a string of 32 “O”s
EBX – the random User ID
The BT instruction takes the bit 0 of EBX and stores the result in the Carry Flag (CF). JNB jumps if CF is 0.
We are basically checking if the user ID number is even or not. If the number is odd the first value of the key will be 2Ah (symbol “*”) otherwise remains “O”.
Let’s add this to our python script:
if ((uid % 2) != 0): s = chr(0x2a)
Then the program checks if the number of user ID is bigger than 0B16B00B5h
; zero_bit_set: .text:00401421 inc edi .text:00401422 cmp ebx, 0B16B00B5h .text:00401428 ja short above_b16b00b5h .text:0040142A mov byte ptr [edi], 2Ah .text:0040142D above_b16b00b5h:
if not, it changes the second value of the key to “*”
if (uid <= 0x0b16b00b5): s = chr(0x2a)
Then the Parity Flag (PF) is evaluated. This is, however, done over the value of EDI, which now will be 40D022h (we have performed “inc EDI” twice so far). 404D022h in binary is “10000001101000000100000” and so the PF will always be 0.
; above_b16b00b5h: .text:0040142D inc edi .text:0040142E jnp short no_parity .text:00401430 mov byte ptr [edi], 2Ah .text:00401433 no_parity:
The jump won’t be taken and the third value of the key will always be “*”.
Same for the 4th as shown in the next two lines:
; no_parity: .text:00401433 inc edi .text:00401434 mov byte ptr [edi], 2Ah
Let’s implement this in Python:
s = chr(0x2a) s = chr(0x2a)
and analyse the next instructions:
.text:00401437 mov ecx, 1Ch .text:0040143C get_byte: .text:0040143C shr ebx, 1 .text:0040143E mov edx, 0 .text:00401443 mov eax, ebx .text:00401445 mov esi, 1Ah .text:0040144A div esi .text:0040144C test ecx, 1 .text:00401452 jz short add_97 .text:00401454 add edx, 41h .text:00401457 mov [edi], dl .text:00401459 inc edi .text:0040145A loop get_byte .text:0040145C jmp short go_on .text:0040145E add_97: .text:0040145E add edx, 61h .text:00401461 mov [edi], dl .text:00401463 inc edi .text:00401464 loop get_byte .text:00401466 .text:00401466 go_on:
This is obviously a loop as we can see from the instruction at address 00401464.
LOOP decreases ECX and jumps to the specified address until ECX is 0. This is done 28 times (ECX is 1Ch).
PS.: remember that our EDI register still points to the third value of our key, since EDI was not increased after the last mov instruction at 00401434
What every cycle in the loop does is:
- shift right the value of User ID by 1 bit
- take the remainder of the division between User ID and 1Ah (User ID mod 26)
- if the value of ECX is even, add 61h to the remainder; else, add 41h to the remainder
- save the remainder value to EDI (our key, remember?)
- increase EDI
- decrease ECX
for x in range(28,0,-1): uid = uid >> 1 z = uid % 26 if ((x % 2) == 0): z += 0x61 else: z += 0x41 s[31-x] = chr(z) # starts from s
At the end we’ll have the final key value.
The remaining instructions of the program ask the user to insert the key, and the value entered is compared with the one just computed in memory.
If they are equal… we have found the right key!
Our final python script:
s = bytearray(chr(0x4f)*32) uid = int(raw_input("Enter User ID: ")) if ((uid % 2) != 0): s = chr(0x2a) if (uid <= 0x0b16b00b5): s = chr(0x2a) s = chr(0x2a) s = chr(0x2a) for x in range(28,0,-1): uid = uid >> 1 z = uid % 26 if ((x % 2) == 0): z += 0x61 else: z += 0x41 s[31-x] = chr(z) # starts from s print ("Password: %s" % s )