FR EN

ELF - RE 400

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.

6 messages

  1. FrizN 26/10/13 17:40

    x)

  2. acez 25/10/13 17:29

    fu

  3. FrizN 25/10/13 11:49

    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

  4. foo 25/10/13 11:01

    nice, I've only one doubt: where you retrieve "DbC~ai\x19{=Di^~>ny" ?
    thanks!

  5. FrizN 25/10/13 08:51

    Nice job on the chall, especially with the way IDA handles GOT entries and undocumented sections, that's pretty deceptive at first.