The ELF challenge is an x86 stripped ELF for Linux. Relevant code from main():
int __cdecl main(int argc, char ** argv) { sub_804861C(); [...] mbuf1 = (char *)malloc(v2); for ( i = 0; strlen(argv1) > i; ++i ) mbuf1[i] = argv1[(i - xor_var) % strlen(argv1)]; sleep(1u); puts("done\n"); ++xor_var; [...] mbuf2 = (char *)malloc(v3); puts("Calculating phase 2 ..."); for ( i = 0; ; ++i ) { v4 = strlen(argv1); if ( v4 <= i ) break; mbuf2[i] = xor_var ^ off_804A198[i] ^ mbuf1[i]; } sleep(1u); puts("done\n"); [...] mbuf3 = (char *)malloc(v5); for ( i = 0; ; ++i ) { v6 = strlen(argv1); if ( v6 <= i ) break; mbuf3[i] = xor_var; } [...] for ( i = 0; i <= 2; ++i ) { printf("Calculating phase %u ...\n", i + 3); for ( j = 0; ; ++j ) { v7 = strlen(argv1); if ( v7 <= j ) break; mbuf3[j] ^= mbuf2[j] ^ off_804A198[(i + j + xor_var) % strlen(argv1)]; } sleep(1u); puts("done\n"); } [...] if ( mbuf3_0_4 != 0x58326011 || mbuf3_4_8 != 0x22516561 || mbuf3_8_12 != 0x5E6B6266 || mbuf3_12_16 != 0x556E454B ) puts("Flag wrong!"); else puts("Flag correct!"); return 0; }
The flag provided in argv[1] is transformed by several xor functions into 3 malloced buffers and the final buffer's bytes are compared to some arbitrary value. Each xor function involves the global variable xor_var located at 0x804a194 and initialized to 10, whose value seems to be changed only twice.
Before all this main() calls the function located at 0x804861c:
int __cdecl anti_dbg() { if ( getenv("LD_PRELOAD") ) ++xor_var; v3 = fork(); if ( !v3 ) { v2 = getppid(); if ( ptrace(PTRACE_ATTACH, v2, 0, 0) < 0 ) exit(1); sleep(1u); ptrace(PTRACE_DETACH, v2, 0, 0); exit(0); } wait(&retcode); result = retcode; if ( retcode ) { sleep(1u); result = xor_var++ + 1; } return result; }
So this is a simple anti debug function, whose main purpose is to check that the environment variable LD_PRELOAD isn't set, and that the process can be ptraced. It silently alters xor_var if one of these conditions is not true. To bypass these checks, one can simply run the executable LD_PRELOADing a library that modifies getenv() and ptrace() return values. I actually just patched the PLT entries of getenv(), ptrace() and sleep(), as sleeps get pretty annoying during debug:
$ objdump -S -j .plt reverse_me_patched [...] 08048470 <sleep@plt>: 8048470: c3 ret [...] 08048490 <getenv@plt>: 8048490: 31 c0 xor %eax,%eax 8048492: c3 ret [...] 08048520 <ptrace@plt>: 8048520: 31 c0 xor %eax,%eax 8048522: c3 ret
Those modifications just bypass the GOT calls, and ensure that ptrace() always returns 0 and getenv() always returns NULL. From there on it should just be a matter of inversing the xor functions: xor_var should be 0xa in the first loop, 0xb in the second, and 0xe in the third and fourth loops. Doing so gives the flag "DbC~ai\x19{=Di^~>ny" that still gets a "Wrong flag" output. Checking the xor_var value at the end of main():
$ gdb -q reverse_me_patched (gdb) b *0x08048e18 Breakpoint 1 at 0x8048e18 (gdb) r `echo -en 'DbC~ai\x19{=Di^~>ny'` [...] Breakpoint 1, 0x08048e18 in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x00000024
So xor_var finishes at 0x24, pretty far away from the expected 0xe. There must be some hidden code executed, but no constructors are set, xor_var is indeed 0xa at the beginning of main() and there isn't any additional code to be found within .text. Debugging step by step shows surprising xor_var incrementations after some libc function calls. The only code that should be involved between main() and libc functions is dynamic relocation, so we check PLT and GOT entries:
// objdump -h to get ELF sections description // start/size/offset are hex 12 .plt start=08048450, size=e0, offset=450 17 .eh_frame start=08048fbc, size=90, offset=fbc 18 .init_array start=0804a04c, size=4, offset=204c 22 .got start=0804a148, size=4, offset=2148 23 .got.plt start=0804a14c, size=40, offset=214c // objdump -s -j .got.plt to get GOT entries 0x804a158 <printf@got.plt>: 0x080491af 0x804a15c <sleep@got.plt>: 0x08048476 0x804a160 <wait@got.plt>: 0x08048486 0x804a164 <getenv@got.plt>: 0x08048496 0x804a168 <malloc@got.plt>: 0x080492d1 0x804a16c <puts@got.plt>: 0x08049060 0x804a170 <__gmon_start__@got.plt>: 0x080484c6 0x804a174 <exit@got.plt>: 0x080484d6 0x804a178 <strlen@got.plt>: 0x0804945f 0x804a17c <__libc_start_main@got.plt>: 0x080484f6 0x804a180 <fork@got.plt>: 0x08048506 0x804a184 <getppid@got.plt>: 0x08048516 0x804a188 <ptrace@got.plt>: 0x08048526
PLT first instructions call the functions pointed to by their respective GOT entries. Before any call is made, those entries should point back to PLT addresses (ranging from 0x08048450 to 08048430) for dynamic relocation. After symbol resolution, the relevant GOT entry is replaced by the function's actual address. We can see here that printf, malloc, puts and strlen GOT entries do not point back to PLT, but to 0x08049XXX addresses. This segment does not appear in objdump -h and seems to lie in between eh_frame and init_array_start. It doesn't matter anyway as the kernel copies whole ELFs in memory during execve syscalls, so these addresses remain valid and reachable. To study them, one may for instance load the ELF as a binary in IDA with the proper loading offset (0x08048000). There is probably a prettier workaround with segments definition though.
Two of those hidden functions (puts and strlen) look like main()'s anti-debugging prologue: getenv/ptrace and some xor_var incrementations. The other two increment xor_var by as many 0xcc as they find in puts and strlen wrappers code. Those four functions start with a call to 0x804940b that copies the addresses of those wrappers in their GOT entries again. They end with a relocation call, that will eventually overwrite GOT entries with actual libc values. This means that two consecutive PLT calls to one of those functions trigger the wrapper only *once*. The wrapper can be called again after one of the remaining three wrapped functions is executed, overwriting the GOT entries once again.
The last thing to do is to set up breakpoints before and after each one of those functions in our patched executable to see how they affect xor_var:
$ gdb -q reverse_me_patched (gdb) b *0x8048739 Breakpoint 1 at 0x8048739 // before puts (gdb) b *0x8048739+5 Breakpoint 2 at 0x804873e // after puts (gdb) b *0x8048755 Breakpoint 3 at 0x8048755 // before strlen (gdb) b *0x8048755+5 Breakpoint 4 at 0x804875a // after strlen (gdb) b *0x804875d Breakpoint 5 at 0x804875d // before malloc (gdb) b *0x804875d+5 Breakpoint 6 at 0x8048762 // after malloc (gdb) r test Breakpoint 1, 0x08048739 in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x0000000a (gdb) c Continuing. Calculating phase 1 ... Breakpoint 2, 0x0804873e in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x0000000b (gdb) c Continuing. Breakpoint 3, 0x08048755 in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x0000000b (gdb) c Continuing. Breakpoint 4, 0x0804875a in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x0000000c (gdb) c Continuing. Breakpoint 5, 0x0804875d in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x0000000c (gdb) c Continuing. Breakpoint 6, 0x08048762 in ?? () (gdb) x/xw 0x0804a194 0x804a194: 0x0000000d
So each one basically does xor_var++, and printf should do the same thing than malloc. We can deduce xor_var values for each xor loop: for the first loop it's 0xe, second 0x14, third 0x1b, and the three executions of the fourth get respectively 0x1d, 0x20 and 0x23. After the final puts("done\n"), xor_var is indeed 0x24 as seen earlier. Calculating the inverse of the xor functions is now pretty straightforward:
#!/usr/bin/python mbuf1 = "" mbuf2 = "" mbuf3 = "\x11\x60\x32\x58\x61\x65\x51\x22\x66\x62\x6b\x5e\x4b\x45\x6e\x55" arbstr = "fluxFluxfLuxFLuxflUxFlUxfLUxFLUxfluXFluXfLuXFLuXflUXFlUXfLUXFLUX" for i in range(0, 16): mbuf2 += chr(ord(mbuf3[i]) ^ ord(arbstr[(i+0x1d)%16]) ^ ord(arbstr[(i+0x20+1)%16]) ^ ord(arbstr[(i+0x23+2)%16]) ^ 0x1b) for i in range(0, 16): mbuf1 += chr(ord(mbuf2[i]) ^ ord(arbstr[i]) ^ 0x14) ibuf = ['']*16 for i in range(0,len(mbuf1)): ibuf[(i - 0xe)%16] = mbuf1[i] print repr("".join(ibuf))
And we get the key: lD4v0idsS3cTions. sqall01's home-made lib used to create the challenge is available here.
x)
fu
Just the last python script, but with wrong values for the global variable:
0xa instead of 0xe
0xb instead of 0x14
0xe instead of the other four
nice, I've only one doubt: where you retrieve "DbC~ai\x19{=Di^~>ny" ?
thanks!
Nice job on the chall, especially with the way IDA handles GOT entries and undocumented sections, that's pretty deceptive at first.