- with no command line parameters
- with a command line parameter
- with strace and a command line parameter
maze3@maze:~$ /maze/maze3 ./level4 ev0lcmds! maze3@maze:~$ maze3@maze:~$ /maze/maze3 A maze3@maze:~$ maze3@maze:~$ strace /maze/maze3 A execve("/maze/maze3", ["/maze/maze3", "A"], [/* 17 vars */]) = 0 strace: [ Process PID=4859 runs in 32 bit mode. ] mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC) = 0 exit(1) = ?
Let's disassemble the maze3 with objdump:
maze3@maze:~$ objdump -d /maze/maze3 /maze/maze3: file format elf32-i386 Disassembly of section .text: 08048060 <_start>: 8048060: 58 pop %eax 8048061: 48 dec %eax 8048062: 75 32 jne 8048096 <fine> 8048064: e8 14 00 00 00 call 804807d <_start+0x1d> 8048069: 2e 2f cs das 804806b: 6c insb (%dx),%es:(%edi) 804806c: 65 76 65 gs jbe 80480d4 <d1+0x9> 804806f: 6c insb (%dx),%es:(%edi) 8048070: 34 20 xor $0x20,%al 8048072: 65 76 30 gs jbe 80480a5 <fine+0xf> 8048075: 6c insb (%dx),%es:(%edi) 8048076: 63 6d 64 arpl %bp,0x64(%ebp) 8048079: 73 21 jae 804809c <fine+0x6> 804807b: 0a 00 or (%eax),%al 804807d: b8 04 00 00 00 mov $0x4,%eax 8048082: bb 01 00 00 00 mov $0x1,%ebx 8048087: 59 pop %ecx 8048088: ba 14 00 00 00 mov $0x14,%edx 804808d: cd 80 int $0x80 804808f: b8 01 00 00 00 mov $0x1,%eax 8048094: cd 80 int $0x80 08048096 <fine>: 8048096: 58 pop %eax 8048097: b8 7d 00 00 00 mov $0x7d,%eax 804809c: bb 60 80 04 08 mov $0x8048060,%ebx 80480a1: 81 e3 00 f0 ff ff and $0xfffff000,%ebx 80480a7: b9 97 00 00 00 mov $0x97,%ecx 80480ac: ba 07 00 00 00 mov $0x7,%edx 80480b1: cd 80 int $0x80 80480b3: 8d 35 cb 80 04 08 lea 0x80480cb,%esi 80480b9: 89 f7 mov %esi,%edi 80480bb: b9 2c 00 00 00 mov $0x2c,%ecx 80480c0: ba 78 56 34 12 mov $0x12345678,%edx 080480c5 <l1>: 80480c5: ad lods %ds:(%esi),%eax 80480c6: 31 d0 xor %edx,%eax 80480c8: ab stos %eax,%es:(%edi) 80480c9: e2 fa loop 80480c5 <l1> 080480cb <d1>: 80480cb: 20 d7 and %dl,%bh 80480cd: 0c cc or $0xcc,%al 80480cf: b8 61 27 67 61 mov $0x61672761,%eax 80480d4: 67 f4 addr16 hlt 80480d6: 42 inc %edx 80480d7: 10 79 1b adc %bh,0x1b(%ecx) 80480da: 61 popa 80480db: 10 3e adc %bh,(%esi) 80480dd: 1b 70 11 sbb 0x11(%eax),%esi 80480e0: 38 bd f1 28 05 bd cmp %bh,-0x42fad70f(%ebp) 80480e6: f3 49 repz dec %ecx 80480e8: 84 84 19 b5 d6 8c 13 test %al,0x138cd6b5(%ecx,%ebx,1) 80480ef: 78 56 js 8048147 <d1+0x7c> 80480f1: 34 23 xor $0x23,%al 80480f3: a3 .byte 0xa3 80480f4: 15 .byte 0x15 80480f5: f9 stc 80480f6: 92 xchg %eax,%edx
There are four parts:
- Main code (start)
- function fine()
- function l1()
- function d1()
Please note that the code of the functions are all subsequents: if no jump changes execution flow, the function l1() is executed just after fine(), and d1() is executed just after l1().
Let's look into fine() procedure
- calls mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC): this means that 151 bytes starting from 0x8048000 can be read, written and executed. A godd area we can use to store some arbitrary code to be executed
- after that it stores in %edi and %esi the address of function d1 ()
- it stores 0x2c (decimal 44) in ecx
- it stores 0x12345678 (decimal 305419896) into edx
After that l1() is executed because it is just the following code.
But what does l1() do?
Well, it's a loop based on lods xor and stos instructions.
For each loop execution:
- stores into eax the content of memory at esi
- xor eax with edx (containing a fixed value 0x12345678) and stores the result in eax
- copy the value in eax into memory address edi
- (automatically) lods and stos automatically increase esi and edi
- (automatically) loop decrease ecx
The result is that the string at esi (i.e. d1() code location) is replaced by other code using an xor decoding function that rewrites 44 bytes.
This also means that l1() replaces code of d1() at runtime.
Let's verify this by placing a break at the last line of fine() and execute 176 steps.
Why 176 steps? Because every loop execution of l1() takes 4 steps that multiplied by the length of d1() code (44 bytes) makes 176 steps in total.
(gdb) disas 0x080480cb Dump of assembler code for function d1: => 0x080480cb <+0>: pop %eax 0x080480cc <+1>: cmpl $0x1337c0de,(%eax) 0x080480d2 <+7>: jne 0x80480ed <d1+34> 0x080480d4 <+9>: xor %eax,%eax 0x080480d6 <+11>: push %eax 0x080480d7 <+12>: push $0x68732f2f 0x080480dc <+17>: push $0x6e69622f 0x080480e1 <+22>: mov %esp,%ebx 0x080480e3 <+24>: push %eax 0x080480e4 <+25>: push %ebx 0x080480e5 <+26>: mov %esp,%ecx 0x080480e7 <+28>: xor %edx,%edx 0x080480e9 <+30>: mov $0xb,%al 0x080480eb <+32>: int $0x80 0x080480ed <+34>: mov $0x1,%eax 0x080480f2 <+39>: xor %ebx,%ebx 0x080480f4 <+41>: inc %ebx 0x080480f5 <+42>: int $0x80 End of assembler dump.
And we see that d1() code is totally different and it seems a shellcode.
The lines 0x080480d7 push "/bin//sh" and the line 0x080480eb calls an execve()
Now let's dive into the new d1() code: The third line compares a static value of 0x1337c0de to the value at a memory address contained in (%eax)
If they are different jumps to the end of d1() and execute an exit(1)
if not, it executes the interesting part of the shellcode (i.e. the execve())
Let's advance one step in execution and reach the cmpl instruction
(gdb) stepi 0x080480cc in d1 () (gdb) info reg eax 0xffffd8c0 -10048 ecx 0x0 0 edx 0x12345678 305419896 ebx 0x8048000 134512640 esp 0xffffd79c 0xffffd79c ebp 0x0 0x0 esi 0x804817b 134513019 edi 0x804817b 134513019 eip 0x80480cc 0x80480cc <d1+1> eflags 0x206 [ PF IF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x0 0 (gdb) x/wx 0xffffd8c0 0xffffd8c0: 0x434c0041
We show content of memory at %eax and it contains a 0x41 (ASCII "A").
This seems to be our command line parameter
Retry with "AAAA" parameter:
(gdb) set args AAAA
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /maze/maze3 AAAA
Breakpoint 1, 0x080480c0 in fine ()
(gdb) stepi 177
0x080480cb in d1 ()
(gdb) stepi
0x080480cc in d1 ()
(gdb) info reg
eax 0xffffd8bd -10051
ecx 0x0 0
edx 0x12345678 305419896
ebx 0x8048000 134512640
esp 0xffffd79c 0xffffd79c
ebp 0x0 0x0
esi 0x804817b 134513019
edi 0x804817b 134513019
eip 0x80480cc 0x80480cc <d1+1>
eflags 0x206 [ PF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
(gdb) x/wx 0xffffd8bd
0xffffd8bd: 0x41414141
The code compares our parameter with 0x1337c0de
So far it seems that:
- the main function call mprotect to make part of the code section writable and executable
- a decoding function is called to turn part of the code to a shellcode
- the shellcode checks if the argv[1] is equal to a fixed value and execute the remaining part of the shellcode if the argv[1] satisfies the condition
So now we only have to use a specific value for the command line parameter to match 0x1337c0de.
Recalling that Intel is little endian:
maze3@maze:~$ /maze/maze3 $(python -c 'print "\xde\xc0\x37\x13"') $ id uid=15003(maze3) gid=15003(maze3) euid=15004(maze4) groups=15003(maze3) $ cat /etc/maze_pass/maze4 d*******k
$
Very fun challenge!
It took a while even if it didn't require strange shellcodes or buffer overwrites..
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.