Points: 50
Category: re
Description
I found this onion in my kitchen, may I ask you to dissect it?
Solution
The file we downloaded is an ELF 64-bit executable. First, let’s try running it:
1 2 3 |
$ ./zwiebel Input key: hxp{test} :( |
So our task will be getting the flag out of binary. Next step is running it in gdb. The binary is not stripped, so we can start by looking at the list of functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
gdb-peda$ info functions All defined functions: Non-debugging symbols: 0x00000000004005f8 _init 0x0000000000400630 _exit@plt 0x0000000000400640 puts@plt 0x0000000000400650 mmap@plt 0x0000000000400660 printf@plt 0x0000000000400670 __libc_start_main@plt 0x0000000000400680 fgets@plt 0x0000000000400690 memcpy@plt 0x00000000004006a0 fflush@plt 0x00000000004006b0 ptrace@plt 0x00000000004006d0 _start 0x0000000000400700 deregister_tm_clones 0x0000000000400740 register_tm_clones 0x0000000000400780 __do_global_dtors_aux 0x00000000004007a0 frame_dummy 0x00000000004007d0 __printf 0x0000000000400800 main 0x0000000000400880 __libc_csu_init 0x00000000004008f0 __libc_csu_fini 0x00000000004008f4 _fini |
First thing to notice is that ptrace is on the list of functions. Binaries in CTF tasks commonly use this function to detect if they’re being run in debugger. Calling ptrace(PTRACE_TRACEME, …) will return -1 if the process is being run in debugger, so it’s a very simple check to implement. Indeed if we run the binary in gdb it will just print “:(“, without even asking us for password. Fortunately the check is also very easy to work around it – just put a breakpoint in ptrace, step out of it and swap the return value to 0 (by overriding register).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
gdb-peda$ b ptrace fin gdb-peda$ x/5i $rip => 0x4007e0 <__printf+16>: test rax,rax 0x4007e3 <__printf+19>: jne 0x4007e9 <__printf+25> 0x4007e5 <__printf+21>: xor eax,eax 0x4007e7 <__printf+23>: pop rdx 0x4007e8 <__printf+24>: ret set $rax=0 si si => 0x4007e5 <__printf+21>: xor eax,eax 0x4007e7 <__printf+23>: pop rdx 0x4007e8 <__printf+24>: ret |
Now that we have taken care of that let’s take a look at main function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
gdb-peda$ disas main Dump of assembler code for function main: 0x0000000000400800 <+0>: push r15 0x0000000000400802 <+2>: push r14 0x0000000000400804 <+4>: push rbx 0x0000000000400805 <+5>: mov edi,0x400907 0x000000000040080a <+10>: xor eax,eax 0x000000000040080c <+12>: call 0x400660 <printf@plt> 0x0000000000400811 <+17>: mov rdi,QWORD PTR [rip+0x225788] # 0x625fa0 <stdout@@GLIBC_2.2.5> 0x0000000000400818 <+24>: call 0x4006a0 <fflush@plt> 0x000000000040081d <+29>: mov rdx,QWORD PTR [rip+0x22578c] # 0x625fb0 <stdin@@GLIBC_2.2.5> 0x0000000000400824 <+36>: mov r15d,0x601280 0x000000000040082a <+42>: mov edi,0x601280 0x000000000040082f <+47>: mov esi,0x90 0x0000000000400834 <+52>: call 0x400680 <fgets@plt> 0x0000000000400839 <+57>: mov edi,0x0 0x000000000040083e <+62>: mov esi,0x24c8d 0x0000000000400843 <+67>: mov edx,0x7 0x0000000000400848 <+72>: mov ecx,0x22 0x000000000040084d <+77>: mov r8d,0xffffffff 0x0000000000400853 <+83>: xor r9d,r9d 0x0000000000400856 <+86>: call 0x400650 <mmap@plt> 0x000000000040085b <+91>: mov r14,rax 0x000000000040085e <+94>: mov esi,0x601310 0x0000000000400863 <+99>: mov edx,0x24c8d 0x0000000000400868 <+104>: mov rdi,r14 0x000000000040086b <+107>: call 0x400690 <memcpy@plt> 0x0000000000400870 <+112>: mov rbx,r15 0x0000000000400873 <+115>: xor eax,eax 0x0000000000400875 <+117>: call r14 0x0000000000400878 <+120>: xor eax,eax 0x000000000040087a <+122>: pop rbx 0x000000000040087b <+123>: pop r14 0x000000000040087d <+125>: pop r15 0x000000000040087f <+127>: ret End of assembler dump. |
Not much going on in here – it asks for password, reads user input allocates some memory and copies a whole bunch of something (about 150KB) into it. Finally it jumps into whatever it is that it copied. Let’s set a breakpoint at jump location and see what’s going on in there.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
gdb-peda$ x/20i $rip => 0x7ffff7fad000: lea esi,[rsi+0x0] 0x7ffff7fad006: lea esi,[rsi+riz*1+0x0] 0x7ffff7fad00a: lea esi,[rsi+riz*1+0x0] 0x7ffff7fad011: lea edi,[rdi+riz*1+0x0] 0x7ffff7fad018: mov rax,rbx 0x7ffff7fad01b: mov al,BYTE PTR [rax+0x0] 0x7ffff7fad01e: and al,0x40 0x7ffff7fad020: je 0x7ffff7fad038 0x7ffff7fad022: lea rsi,[rip+0x34] # 0x7ffff7fad05d 0x7ffff7fad029: lods eax,DWORD PTR ds:[rsi] 0x7ffff7fad02a: mov rcx,rax 0x7ffff7fad02d: lods eax,DWORD PTR ds:[rsi] 0x7ffff7fad02e: xor DWORD PTR [rsi],eax 0x7ffff7fad030: add rsi,0x4 0x7ffff7fad034: loop 0x7ffff7fad02e 0x7ffff7fad036: jmp 0x7ffff7fad065 0x7ffff7fad038: push 0xa283a 0x7ffff7fad03d: mov eax,0x1 0x7ffff7fad042: mov edi,0x0 0x7ffff7fad047: mov rsi,rsp gdb-peda$ x/5i 0x7ffff7fad065 0x7ffff7fad065: pop sp 0x7ffff7fad067: (bad) 0x7ffff7fad068: cmp al,0xa3 0x7ffff7fad06a: movabs eax,ds:0x3e9f3e6235abb69e 0x7ffff7fad073: (bad) |
We can see that the code takes the first character of our input (rax+0x0), then takes a single bit out of it and checks it value. If the 7th bit is 0 the result of ‘and’ operation will be 0 and the conditional jump in 8th line will take us to a short piece of code that print the sad face (“:(“) and terminates the program. That’s not what we want to happen, so let’s examine the other branch. We can easily pass the check by setting up a breakpoint in the line with and al, 0x40 and just changing the value of al register.
After checking the single bit of our input it runs some loop. Afterwards it performs a jump into total garbage. It seems that the loop must be decrypting the code before we jump into it. Let’s see what’s going on in there. The loop translated into pseudocode looks roughly like this:
1 2 3 4 5 6 |
eax = encrypted[rsi++] rcx = eax eax = encrypted[rsi++] for (; rcx > 0; --rcx) { encrypted[rsi++] ^= eax } |
So basically it reads number of bytes to decrypt and a key and then performs a simple xor using that key. Ok, let’s set a breakpoint in the jump location and see how the code looks after decrypting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
gdb-peda$ x/20i $rip => 0x7ffff7fad065: lea esi,[rsi+riz*1+0x0] 0x7ffff7fad069: mov rax,rbx 0x7ffff7fad06c: mov al,BYTE PTR [rax+0x1d] 0x7ffff7fad06f: and al,0x2 0x7ffff7fad071: je 0x7ffff7fad089 0x7ffff7fad073: lea rsi,[rip+0x34] # 0x7ffff7fad0ae 0x7ffff7fad07a: lods eax,DWORD PTR ds:[rsi] 0x7ffff7fad07b: mov rcx,rax 0x7ffff7fad07e: lods eax,DWORD PTR ds:[rsi] 0x7ffff7fad07f: xor DWORD PTR [rsi],eax 0x7ffff7fad081: add rsi,0x4 0x7ffff7fad085: loop 0x7ffff7fad07f 0x7ffff7fad087: jmp 0x7ffff7fad0b6 0x7ffff7fad089: push 0xa283a 0x7ffff7fad08e: mov eax,0x1 0x7ffff7fad093: mov edi,0x0 0x7ffff7fad098: mov rsi,rsp 0x7ffff7fad09b: mov edx,0x3 0x7ffff7fad0a0: syscall 0x7ffff7fad0a2: mov eax,0x3c |
Turns out this chunk looks pretty much the same as the previous one. Except it checks a different bit. We can repeat the whole process and get yet another chunk of similar code, checking yet another bit. And so on, probably for at least a few hundred times. Theoretically we could get the whole password that way, but it would take ages to do that manually. Instead I decided to automate the process by writing a python script.
First I dumped the memory from gdb to a file (dump binary memory packed.dump 0x7ffff7fad000 0x7ffff7fad000+0x24c8d). This dump can be deassembled using objdump (objdump -D -b binary -mi386 -Mintel,x86-64 packed.dump > packed.objdump). Of course only the already decrypted code will make sense. However, the code is very repetitive and has a clear pattern. For example we just need to look for mov al,BYTE PTR [rax+0x{offset}] to see which character is being examined by a given chunk of a code. And we can easily parse the following line to get specific bit.
So I wrote a python script that runs a loop, parsing a chunk, using the collected data to decrypt the next chunk and calling objdump to deassemble it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
import os import struct def write_data(fout, data, written, write_offset): skip = write_offset - written fout.write(data[skip:]) return written + len(data) def decode(fin, fout, start_offset, write_offset): assert start_offset < write_offset written = 0 data = fin.read(start_offset) written = write_data(fout, data, written, write_offset) data = fin.read(4) written = write_data(fout, data, written, write_offset) data = fin.read(4) key = struct.unpack('@I', data)[0] print 'key=', hex(key) written = write_data(fout, data, written, write_offset) while fin: data = fin.read(4) try: x = struct.unpack('@I', data) except: # looks like the remainder of file was less than 4 bytes # we're likely already after the part we wanted to decrypt # so who cares? return data = struct.pack('@I', x[0] ^ key) written = write_data(fout, data, written, write_offset) def decode_next(fin, objdumped): STATE_NONE = 1 STATE_READ_BIT = 2 STATE_READ_VALUE = 3 STATE_GOT_DATA = 4 STATE_READ_JMP = 5 STATE_GOT_DECODE = 6 next_decode_start = None next_jump = None char_index = None char_bit = None expected_value = None state = STATE_NONE reading_header = True for l in objdumped: if reading_header: if '' in l: reading_header = False continue parts = l.split() if state == STATE_NONE: if parts[-2] == 'PTR' and parts[-3] == 'al,BYTE': char_index = int(parts[-1][5:-1], 16) state = STATE_READ_BIT elif state == STATE_READ_BIT: assert parts[-2] == 'and' char_bit = int(parts[-1][3:], 16) state = STATE_READ_VALUE elif state == STATE_READ_VALUE: if parts[-2] == 'je': expected_value = 1 it elif parts[-2] == 'jne': expected_value = 0 else: raise ValueError("STATE_READ_VALUE failed") state = STATE_GOT_DATA elif state == STATE_GOT_DATA: if parts[-2] == '#': next_decode_start = int(parts[-1], 16) state = STATE_GOT_DECODE elif state == STATE_GOT_DECODE: if parts[-2] == 'loop': state = STATE_READ_JMP elif state == STATE_READ_JMP: assert parts[-2] == 'jmp' next_jump = int(parts[-1], 16) break return char_index, char_bit, expected_value, next_jump, next_decode_start def get_flag(data): result = [0] * 100 for byte, bit, val in data: if val: result[byte] |= bit return [chr(x) for x in result] if __name__ == '__main__': fin = open('packed.dump', 'r') objfile = 'code.objdump' total_offset = 0 info = [] try: for index in xrange(10000): fin.seek(0) outfile = 'haxed_{}'.format(index) objdumped = open(objfile, 'r') fout = open(outfile, 'w') result = decode_next(fin, objdumped) if any(x is None for x in result): raise ValueError("Failed to parse some data in decode_next") print [hex(x) for x in result] info.append(result[:3]) decode(fin, fout, total_offset + result[4], total_offset + result[3]) total_offset += result[3] objdumped.close() fout.close() objfile = 'objdump_{}'.format(index) os.system('objdump -D -b binary -mi386 -Mintel,x86-64 {} > {}'.format(outfile, objfile)) infile = outfile finally: print info print get_flag(info) fin.close() |
After ~1600 loops the script produced the flag:
hxp{1_h0p3_y0u_d1dnt_p33l_th3_0ni0n_by_h4nd}. Very appropriate 🙂
This was a very cool task, big thanks to organisers for preparing it.
Leave a Reply
You must be logged in to post a comment.