Rating:
# PoliCTF 2015: johnthepacker
----------
## Challenge details
| Contest | Challenge | Category | Points |
|:---------------|:--------------|:----------|-------:|
| PoliCTF 2015 | johnthepacker | Reversing | 350 |
**Description:**
>*John's greatest skill is to [pack everything and everywhere](challenge/topack) with everyone. He doesn't want that someone reverse his super secret program. So he wrote a magic packing system. Can you show to John that his packing system is not a good anti-reversing solution?
N.B. Unfortunately John The Packer has multiple solution, so if you have a solution that is not accepted by the scoreboard (but is accepted by the binary) please contact an OP on IRC*
----------
## Write-up
We identify & get pseudocode of the binary
>```bash
>$ file topack
>topack; ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, stripped
>```
The main routine (functions renamed by us) is straightforward:
>```c
>int main_routine()
>{
> decrypt_and_execute(hidden_func, 83);
> return 0;
>}
>```
It simply calls a function to decrypt & execute a memory area consisting of 83 DWORDs:
>```asm
>.text:080485E0 decrypt_and_execute proc near ; CODE XREF: char_check_1+55?p
>.text:080485E0 ; char_check_0+3A?p ...
>.text:080485E0
>.text:080485E0 arg_0 = dword ptr 8
>.text:080485E0 arg_4 = dword ptr 0Ch
>.text:080485E0
>.text:080485E0 push ebp
>.text:080485E1 mov ebp, esp
>.text:080485E3 sub esp, 8
>.text:080485E6 mov eax, [ebp+arg_0]
>.text:080485E9 and eax, 0FFFFF000h
>.text:080485EE sub esp, 4
>.text:080485F1 push 7 ; prot
>.text:080485F3 push 1000h ; len
>.text:080485F8 push eax ; addr
>.text:080485F9 call _mprotect
>.text:080485FE add esp, 10h
>.text:08048601 mov ecx, [ebp+arg_0]
>.text:08048604 mov edx, 66666667h
>.text:08048609 mov eax, ecx
>.text:0804860B imul edx
>.text:0804860D sar edx, 1
>.text:0804860F mov eax, ecx
>.text:08048611 sar eax, 1Fh
>.text:08048614 sub edx, eax
>.text:08048616 mov eax, edx
>.text:08048618 mov edx, eax
>.text:0804861A shl edx, 2
>.text:0804861D add edx, eax
>.text:0804861F mov eax, ecx
>.text:08048621 sub eax, edx
>.text:08048623 mov edx, off_804A294[eax*4]
>.text:0804862A mov eax, [ebp+arg_0]
>.text:0804862D mov ecx, [ebp+arg_4]
>.text:08048630 add esp, 8
>.text:08048633 push eax
>.text:08048634 mov edx, [edx]
>.text:08048636
>.text:08048636 loc_8048636: ; CODE XREF: decrypt_and_execute+5C?j
>.text:08048636 xor [eax], edx
>.text:08048638 add eax, 4
>.text:0804863B dec ecx
>.text:0804863C jnz short loc_8048636
>.text:0804863E pop eax
>.text:0804863F call eax
>.text:08048641 sub esp, 8
>.text:08048644 push [ebp+arg_4]
>.text:08048647 push [ebp+arg_0]
>.text:0804864A call sub_804859B
>.text:0804864F add esp, 10h
>.text:08048652 nop
>.text:08048653 leave
>.text:08048654 retn
>```
There are two approaches we can take, we can either run the binary in gdb, break on 0x0804863F (the call eax which transfers control to the decrypted memory area) and dump the binary as it is decrypted or we can look up the static keytable and write a IDA patching routine ourselves. We chose the latter approach. We can see the XOR key in the above decryption routine (xor [eax], edx) is loaded from an address in a pointer table at off_804A294:
>```asm
>.data:0804A294 off_804A294 dd offset unk_8048CE0 ; DATA XREF: sub_804859B+27?r
>.data:0804A294 ; decrypt_and_execute+43?r
>.data:0804A298 dd offset unk_8048CE5
>.data:0804A29C dd offset aB00b ; "B00B"
>.data:0804A2A0 dd offset aDead ; "DEAD"
>.data:0804A2A4 dd offset unk_8048CF4
>```
Which points to the DWORD-sized key table:
>```asm
>.rodata:08048CE0 unk_8048CE0 db 1 ; DATA XREF: .data:off_804A294?o
>.rodata:08048CE1 db 2
>.rodata:08048CE2 db 3
>.rodata:08048CE3 db 4
>.rodata:08048CE4 db 0
>.rodata:08048CE5 unk_8048CE5 db 10h ; DATA XREF: .data:0804A298?o
>.rodata:08048CE6 db 20h
>.rodata:08048CE7 db 30h ; 0
>.rodata:08048CE8 db 40h ; @
>.rodata:08048CE9 db 0
>.rodata:08048CEA aB00b db 'B00B',0 ; DATA XREF: .data:0804A29C?o
>.rodata:08048CEF aDead db 'DEAD',0 ; DATA XREF: .data:0804A2A0?o
>.rodata:08048CF4 unk_8048CF4 db 0FFh ; DATA XREF: .data:0804A2A4?o
>.rodata:08048CF5 db 0FFh
>.rodata:08048CF6 db 0FFh
>.rodata:08048CF7 db 0FFh
>```
Giving us the keys: 0x04030201, 0x40302010, 0x42303042, 0x44414544.
We will proceed by decrypting whatever encrypted blobs are present in the binary and reverse them subsequently (if you want to run the modified binary afterwards don't forget to patch out the xor [eax], edx instruction!). Let's first do that with hidden_func which is encrypted with key 0x04030201 giving us, after decryption, the following pseudocode:
>```c
>int __cdecl hidden_func(int a1, int a2, int a3, int a4, signed int a5, int a6)
>{
> int v6; // ST08_4@4
> int v7; // ST1C_4@4
> int v8; // ST08_4@4
> int v9; // ST1C_4@4
> int v10; // ST08_4@4
> int v11; // ST1C_4@4
> int v12; // ST08_4@4
> int v13; // ST1C_4@4
> int v14; // ST08_4@4
> int v15; // ST1C_4@4
> int v16; // ST08_4@4
> int v17; // ST1C_4@4
> int v18; // ST08_4@4
> int result; // eax@5
>
> if ( a5 <= 1 )
> {
> printf("Usage:\n %s flag{<key>}\n", *(_DWORD *)a6);
> exit(0);
> }
> v6 = *(_DWORD *)(a6 + 4);
> v7 = decrypt_and_execute(header_check, 17);
> v8 = *(_DWORD *)(a6 + 4);
> v9 = decrypt_and_execute(0x804869A, 17) + v7; // footer_check
> v10 = *(_DWORD *)(a6 + 4);
> v11 = decrypt_and_execute(0x80486DE, 23) + v9;// ascii_check
> v12 = *(_DWORD *)(a6 + 4);
> v13 = decrypt_and_execute(0x8048A42, 24) + v11;// char_check_0
> v14 = *(_DWORD *)(a6 + 4);
> v15 = decrypt_and_execute(0x80489A9, 38) + v13;// char_check_1
> v16 = *(_DWORD *)(a6 + 4);
> v17 = decrypt_and_execute(0x804890B, 39) + v15;// deadboob_check
> v18 = *(_DWORD *)(a6 + 4);
> if ( decrypt_and_execute(0x80488E4, 9) + v17 == 7 )// len_check
> result = printf("\x1B[1;37mYou got the flag: \x1B[1;32m%s\x1B[0m\n", *(_DWORD *)(a6 + 4));
> else
> result = printf("\x1B[1;31mLoser\n\x1B[0m");
> return result;
>}
>```
Which is the main routine. It consists of several 'decrypt_and_execute' calls to various other functions (which turn out to be checks on the flag) and adds their results. If all checks passed, we apparently have a valid flag. We wrote [the following IDA plugin](solution/john_patch.py) script to decrypt all encrypted areas at once (including two additional ones called to by char_check_0 and char_check_1):
>```python
>#!/usr/bin/python
>#
># PoliCTF 2015
># johnthepacker (REVERSING/350)
>#
># IDA decryption plugin
>#
># @a: Smoke Leet Everyday
># @u: https://github.com/smokeleeteveryday
>#
>
>areas = [(0x08048AA5, 83, 0x04030201),
> (0x08048655, 17, 0x40302010),
> (0x0804869A, 17, 0x04030201),
> (0x080486DE, 23, 0x44414544),
> (0x08048A42, 24, 0x40302010),
> (0x080489A9, 38, 0x44414544),
> (0x0804890B, 39, 0x04030201),
> (0x080488E4, 9, 0x40302010),
> (0x0804873A, 54, 0x04030201),
> (0x08048813, 52, 0x42303042)]
>
>for loc, size, key in areas:
> for i in range(size):
> d = Dword(loc+(i*4))
> decoded_dword = d ^ key
> PatchDword(loc+(i*4), decoded_dword)
>```
If we load this in IDA with File -> Script file -> john_patch.py it will automatically decrypt all relevant memory areas so we can reverse them statically, giving us the following pseucode for the check routines:
>```c
>signed int __cdecl header_check(int a1, int a2, int a3, int a4, const char *haystack)
>{
> signed int result; // eax@2
>
> if ( strstr(haystack, "flag{") == haystack )
> {
> result = 1;
> }
> else
> {
> printf("wrong Header for %s\n", haystack);
> result = 0;
> }
> return result;
>}
>
>signed int __cdecl footer_check(int a1, int a2, int a3, int a4, char *s)
>{
> signed int result; // eax@2
>
> if ( s[strlen(s) - 1] == 125 )
> {
> result = 1;
> }
> else
> {
> printf("wrong End for %s\n", s);
> result = 0;
> }
> return result;
>}
>
>signed int __cdecl ascii_check(int a1, int a2, int a3, int a4, char *s)
>{
> size_t v6; // [sp+8h] [bp-10h]@1
> signed int i; // [sp+Ch] [bp-Ch]@1
>
> v6 = strlen(s);
> for ( i = 0; i < (signed int)v6; ++i )
> {
> if ( s[i] < 0 )
> {
> printf("Not ascii character in %s\n", s);
> return 0;
> }
> }
> return 1;
>}
>
>signed int __cdecl char_check_0(int a1, int a2, int a3, int a4, int a5)
>{
> int v5; // ebx@2
> signed int i; // [sp+Ch] [bp-Ch]@1
>
> for ( i = 1; i <= 6; ++i )
> {
> v5 = *(_BYTE *)(i + 4 + a5);
> if ( v5 != decrypt_and_execute(pow_check_0, 54) )
> return 0;
> }
> return 1;
>}
>
>signed int __cdecl pow_check_0(int a1, int a2, int a3, int a4, signed int a5)
>{
> double v5; // ST10_8@1
> double v6; // ST10_8@1
> double v7; // ST10_8@1
> float v8; // ST2C_4@1
>
> v5 = pow((long double)a5, 5.0) * 0.5166666688;
> v6 = v5 - pow((long double)a5, 4.0) * 8.125000037;
> v7 = pow((long double)a5, 3.0) * 45.83333358 + v6;
> v8 = v7 - pow((long double)a5, 2.0) * 109.8750007 + (long double)a5 * 99.65000093 + 83.99999968;
> return (signed int)ffloor(v8);
>}
>
>signed int __cdecl char_check_1(int a1, int a2, int a3, int a4, int a5)
>{
> int v5; // ST10_4@2
> int v6; // ST0C_4@2
> int v7; // ST08_4@2
> int some_table[23]; // [sp+0h] [bp-78h]@1
> int i; // [sp+5Ch] [bp-1Ch]@1
>
> qmemcpy(some_table, &mystery_buffer, 0x58u);
> for ( i = 0; i <= 10; ++i )
> {
> v5 = some_table[2 * i + 1];
> v6 = some_table[2 * i];
> v7 = *(_BYTE *)(i + 11 + a5);
> if ( !decrypt_and_execute(pow_check_1, 52) )
> return 0;
> if ( !(*(_BYTE *)(a5 + 17) & 1) )
> return 0;
> }
> return 1;
>}
>
>BOOL __cdecl pow_check_1(int a1, int a2, int a3, int a4, signed int a5, __int64 a6)
>{
> long double v6; // fst7@1
> unsigned __int64 v7; // rax@2
>
> v6 = pow(2.0, (long double)a5);
> if ( v6 >= 9.223372036854776e18 )
> v7 = (signed __int64)(v6 - 9.223372036854776e18) ^ 0x8000000000000000LL;
> else
> v7 = (signed __int64)v6;
> return 4 * v7 + 21 == a6;
>}
>
>signed int __cdecl deadboob_check(int a1, int a2, int a3, int a4, char *s, int a6)
>{
> signed int result; // eax@2
>
> if ( a6 + 22 < strlen(s) )
> {
> if ( (magic_table[a6] ^ (unsigned __int8)s[a6 + 20]) == s[a6 + 21] )
> result = deadboob_check(0xDEADB00B, 0xDEADB00B, 0xDEADB00B, 0xDEADB00B, s, a6 + 1);
> else
> result = 0;
> }
> else
> {
> result = 1;
> }
> return result;
>}
>
>BOOL __cdecl len_check(int a1, int a2, int a3, int a4, char *s)
>{
> return strlen(s) == 33;
>}
>```
The functions header_check, footer_check, ascii_check and len_check simply impose restrictions upon the flag specifying it should be of the format:
>flag{SECRET}
where SECRET is ASCII-printable characters only and the total flag length is 33.
The function char_check_0 iterates over the first 6 bytes of the secret and checks whether pow_check_0 holds, that is, it effectively defines the first 6 secret characters using the following polynomial:
![alt poly](poly.png)
The function char_check_1 checks for the next 11 bytes whether pow_check_1 holds (given the corresponding QWORD entry in the 'magic table') over them which effectively defines the next 11 secret characters as follows:
![alt check1](check1.png)
Taking into account the possible wrap-around with 9.223372036854776e18.
The final routine deadboob_check is a recursive routine that checks whether a XOR between the successive final 11 bytes of the secret match the byte entries in another magic table, defining the final 10 characters of the secret as:
![alt check2](check2.png)
Putting all this together gives us the [following keygen](solution/john_keygen.py):
>```python
>#!/usr/bin/python
>#
># PoliCTF 2015
># johnthepacker (REVERSING/350)
>#
># @a: Smoke Leet Everyday
># @u: https://github.com/smokeleeteveryday
>#
>
>from math import floor, log
>from struct import unpack
>
>def pow_0(arg):
> a5 = float(arg)
> v5 = float(pow(a5, 5.0)) * 0.5166666688
> v6 = v5 - float(pow(a5, 4.0)) * 8.125000037
> v7 = float(pow(a5, 3.0)) * 45.83333358 + v6
> v8 = v7 - float(pow(a5, 2.0)) * 109.8750007 + a5 * 99.65000093 + 83.99999968
> return int(floor(v8))
>
>def pow_1(arg):
> a1 = ((arg - 21) / 4)
> if(a1 == 0):
> a1 ^= 0x8000000000000000
> a1 += 9.223372036854776e18
> return int(log(a1, 2))
>
>def key_gen():
> magic_table_0 = "\x15\x00\x00\x00\x00\x80\x00\x00\x15\x00\x00\x00\x00\x00\x08\x00\x15\x00\x00\x00\x00\x00\x80\x00\x15\x00\x00\x00\x00\x80\x00\x00\x15\x00\x00\x00\x00\x00\x40\x00\x15\x00\x00\x00\x00\x80\x00\x00\x15\x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x00\x00\x00\x40\x00\x15\x00\x00\x00\x00\x00\x08\x00\x15\x00\x00\x00\x00\x00\x00\x80\x15\x00\x00\x00\x00\x80\x00\x00"
> magic_table_1 = "\x44\x07\x43\x59\x1C\x5B\x1E\x19\x47\x00"
>
> key = ""
>
> # First 6 bytes
> for i in xrange(6):
> key += chr(pow_0(i+1))
>
> # Next 11 bytes
> for i in xrange(11):
> index = (2 * i) * 4
> magic_value = unpack('<Q', magic_table_0[index: index + 8])[0]
> char_val = pow_1(magic_value)
>
> # ( !(*(_BYTE *)(a5 + 17) & 1) )
> if((i == 6) and (char_val & 1 == 0)):
> # Wrap-around compensation
> char_val -= 1
>
> key += chr(char_val)
>
> # Final 10 bytes
> for i in xrange(10):
> key += chr(ord(key[len(key)-1]) ^ ord(magic_table_1[i]))
>
> return key
>
>print "[+]Flag: [flag{%s}]" % key_gen()
>```
Which gives us the flag:
>```bash
>$ ./john_keygen.py
>[+]Flag: [flag{packer-15-4-?41=-in-th3-4ss}]
>```