Posts Codefest 2020 - tooeasy [Reversing]
Post
Cancel

Codefest 2020 - tooeasy [Reversing]

tooeasy is a reversing challenge with 17 solves. The description reads:

I mean lets get to basics, I have my tried to get in how real malware authors would hide there stuff but still this chall is tooeasy.

Going by the text, it seems that the binary uses some kind of malware technique to prevent debugging. It is an ELF binary, so we can run it under Linux. If we do so, it asks us for a flag:

1
2
3
4
$ ./tooeasy 
[*]Please give me flag
flag 
[*] This aint flag try again

If we load the binary in Ghidra to statically analyze it, the results are not pretty. The decompiled code seems too convoluted to analyze by hand. A good rule of thumb is to switch to dynamic analysis when static analysis becomes too complicated. In order to properly analyze the binary’s behavior, we can launch it with strace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ strace -i -s64 -Xabbrev -y ./tooeasy
[00007f6f51cbca07] execve("./tooeasy", ["./tooeasy"], 0x7ffe2eaa3f60 /* 47 vars */) = 0
[000000000045298f] mmap(0x800000, 3038765, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, 0</dev/pts/1>, 0) = 0x800000
[0000000000852aa0] readlink("/proc/self/exe", "/home/carlos/codefest/tooeasy/tooeasy", 4096) = 37
[0000000000852af4] mmap(0x400000, 2981888, PROT_NONE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x400000
[0000000000852af4] mmap(0x400000, 855157, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x400000
[0000000000852af4] mprotect(0x400000, 855157, PROT_READ|PROT_EXEC) = 0
[0000000000852af4] mmap(0x6d1000, 21208, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0xd1000) = 0x6d1000
[0000000000852af4] mprotect(0x6d1000, 21208, PROT_READ|PROT_WRITE) = 0
[0000000000852af4] mmap(0x6d7000, 2592, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x6d7000
[000000000040000e] munmap(0x800000, 3038765) = 0
...
[0000000000449411] write(1</dev/pts/1>, "[*]Please give me flag\n", 23[*]Please give me flag
) = 23
[0000000000449133] fstat(0</dev/pts/1>, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}) = 0
[000000000044933e] read(0</dev/pts/1>, flag
"flag\n", 1024) = 5
[0000000000448a51] nanosleep({tv_sec=5, tv_nsec=0}, 0x7ffc470b9c60) = 0
[0000000000449411] write(1</dev/pts/1>, "[*] This aint flag try again\n", 29[*] This aint flag try again
) = 29
...

It seems that the executable is going through some unpacking by mapping several memory regions and extracting itself (the author later revealed that it is a UPX binary). In order to view the final executable, we set a breakpoint at the call to munmap, right after all the unpacking is done. We can easily do this in gdb with catch syscall unmap, and then running the program:

1
2
3
4
5
6
7
$ gdb -q ./tooeasy
Reading symbols from ./tooeasy...(no debugging symbols found)...done.
(gdb) catch syscall munmap
Catchpoint 1 (syscall 'munmap' [11])
(gdb) run
...
Catchpoint 1 (call to syscall munmap), 0x000000000040000e in ?? ()

We can then dump the binary to disk. We chose to do this with the dump gdb command. In order for dump to work, we must specify the start and end address of the region we want to extract; these can be obtained from /proc/<pid>/maps. The process’ PID can be retrieved by running info inferiors in gdb.

1
2
3
4
5
6
7
8
$ cat /proc/1688/maps 
00400000-004d1000 r-xp 00000000 00:00 0 
004d1000-006d1000 ---p 00000000 00:00 0 
006d1000-006d8000 rw-p 00000000 00:00 0                                  [heap]
00800000-00ae6000 rwxp 00000000 00:00 0 
7ffff7ffa000-7ffff7ffd000 r--p 00000000 00:00 0                          [vvar]
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0                          [vdso]
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]

We are only interested in the lower memory regions, where the code resides:

1
(gdb) dump binary memory out.bin 0x400000 0x06d8000

We can now load out.bin in Ghidra. The loaded binary only exports the entry function, which in turn takes us to the main function (FUN_00400b8d). After renaming some variables for clarity, this is the output for said 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
37
38
39
40
41
42
int FUN_00400b8d(void) {
	int out;
	long len;
	long in_FS_OFFSET;
	uint i;
	char ok [18];
	char please [23];
	char buf [25];
	char fail [29];
	char input [104];
	long local_10;
	
	local_10 = *(in_FS_OFFSET + 0x28);
	/* ... */
	FUN_00410780(please);
	FUN_0040faa0("%s", input);
	FUN_004489d0(5);
	len = FUN_004004e0(input);
	if (len == 0x18) {
		i = 1;
		while (i < 0x19) {
			if ((input[i - 1] ^ i) != buf[i - 1]) {
				FUN_00410780(fail);
				out = 1;
				goto LAB_00400efa;
			}
			i = i + 1;
		}
		FUN_00410780(ok);
		out = 0;
	} else {
		FUN_00410780(fail);
		out = 1;
	}

LAB_00400efa:
	if (local_10 != *(in_FS_OFFSET + 0x28)) {
		/* WARNING: Subroutine does not return */
		FUN_0044b9b0();
	}
	return out;
}

The variables please, fail and ok contain the strings displayed at program startup, incorrect and correct input respectively, and have been omitted for brevity. buf contains what seem to be random characters. Given FUN_0040faa0’s arguments, it looks like it reads data from stdin and writes it to input, much like scanf. It also seems that FUN_004004e0 returns the length of the string passed to it. FUN_004489d0 corresponds to the nanosleep observed in the last lines of the strace output above.

If the input’s length is equal to 0x18 (24), each character in the input is compared to some value in a loop. If every test passes, the success message is displayed. With the contents of buf, we can reverse the operation in order to form the desired input. This can be done in a few lines:

1
2
3
4
buf = "bmgacct|r~ce~QfcNpr!|ude"
for i, c in enumerate(list(buf), start=1):
        print(chr(ord(c) ^ i), end="")
print("")

Which outputs the flag: codefest{this_is_ba5ics}.

This post is licensed under CC BY 4.0 by the author.