Chapter 2: Spawning shells creatively
Introduction
Sometimes, lacking some ROP-gadgets or dealing with a custom libc6 implementation on a live instance may deem the spawning of a shell almost impossible. There are well-known techniques to deal with different situations where it looks (almost) impossible to gain a shell:
SROP [1] , when you have a lack of ROP-gadgets to control most of or part of the usual suspects to pop a shell, like RDI,RSI,RDX…
ret2dlresolve [2], when there is no way to leak anything from the program. For example, see exercises 1 to 4 in Chapter1: Exercises.
ret2csu [3], when you have a considerable lack of ROP-gadgets to set the usual registers, you can use this technique to populate most of them at once.
But not all of these techniques may be available for some particular binaries. For example, if the binary has not been linked with the __libc_csu_init function, ret2csu will not be an option. This function, along with __libc_csu_fini where removed starting GLIBC 2.34. So for modern challenges, this technique won’t be available at all.
As for ret2dlresolve, it won’t work in two cases: when the binary has been statically linked and when Full RelRO is enabled.
How about SROP? You still need to set the value 0xf into EAX and then call syscall. Have a look at Setting an arbitrary value into RAX when you do not have a ROP-gadget to populate RAX. If there isn’t a syscall ROP-gadget, not everything is lost: read on!
Spawning a shell when leaking is not an option without using ret2dlresolve
Let’s consider a vulnerable program that does not make any calls to write,puts,printf, etc. so there is no way we can leak libc6 addresses from it. Besides, we are not going to use ret2dlresolve. These are the binary protections:
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
If the binary is non PIE, then at least we can use ROP-gadgets right away. The level of protection for the got is Partial RELRO, which means we can overwrite it with an arbitrary value.
1. Have a look at the got table
First, get a list of libc6 functions the program calls by looking at its got table:
gdb ./vuln
gef➤ got
[0x404000] getpid@GLIBC_2.2.5 → 0x7ffff7e78320
[0x404008] memset@GLIBC_2.2.5 → 0x7ffff7ef5fc0
[0x404010] alarm@GLIBC_2.2.5 → 0x7ffff7e76b60
[0x404018] gets@GLIBC_2.2.5 → 0x7ffff7e19fa0
2. Search for a function that has the syscall opcode
For each libc6 function from the got, disassemble it in order to look for the syscall op-code. For example, if we disassemble getpid():
r2 /lib/x86_64-linux-gnu/libc.so.6
[0x00027390]> aaaa
[0x00027390]> pdf @ sym.getpid
;-- getpid:
; XREFS(25)
┌ 8: sym.__getpid ();
│ rg: 0 (vars 0, args 0)
│ bp: 0 (vars 0, args 0)
│ sp: 0 (vars 0, args 0)
│ 0x000d5320 b827000000 mov eax, 0x27 ; '\''
│ 0x000d5325 0f05 syscall
└ 0x000d5327 c3 ret
If we disassemble alarm():
[0x00027390]> pdf @ sym.alarm
┌ 33: sym.alarm ();
│ 0x000d3b60 b825000000 mov eax, 0x25 ; '%'
│ 0x000d3b65 0f05 syscall
│ 0x000d3b67 483d01f0ffff cmp rax, 0xfffffffffffff001
│ ┌─< 0x000d3b6d 7301 jae 0xd3b70
│ │ 0x000d3b6f c3 ret
│ │ ; CODE XREF from sym.alarm @ 0xd3b6d(x)
│ └─> 0x000d3b70 488b0d69e2.. mov rcx, qword [0x001d1de0] ; [0x1d1de0:8]=0
│ 0x000d3b77 f7d8 neg eax
│ 0x000d3b79 648901 mov dword fs:[rcx], eax
│ 0x000d3b7c 4883c8ff or rax, 0xffffffffffffffff
└ 0x000d3b80 c3 ret
Well, we have two candidates with the classic structure:
mov eax, syscallid
syscall
These are basically libc6 stubs that do practically nothing in user-mode and just set the syscallid into EAX before transferring control to kernel-mode by issuing a syscall. These are the functions we are most interested in.
3. Pick one that does not crash the program and overwrite its got entry
So now we have two got entries we could overwrite: getpid and alarm. The idea is to jump to the syscall instruction, right after the mov eax, syscallid instruction. So we need to pick one that does not make the program crash - for that, you need to read the disassembled code - and then overwrite the last byte with the one with the syscall instruction. For example, let’s consider we pick getpid().:
0x000d5320 b827000000 mov eax, 0x27 0x000d5325 0f05 syscall
The last byte of its got entry has the value 0x20, so we need to overwrite this value with 0x25. Remember, the offsets don’t change! So now, if the program or our ROP-chain calls getpid(), the control flow will end up directly on the syscall instruction.
Note
Which function to choose? That depends on your ROP-chain and the program’s logic. For example, imagine you don’t have any means to set 0x3b into RAX except the technique described in Re-purposing a libc6 function. You wouldn’t use alarm() in this case then!
4. Find a way to set 0x3b into RAX
You need to find a way to set 0x3b into RAX (execve) (see Setting an arbitrary value into RAX) Alternatively, you can also use execveat, syscallid=0x142 (see man execveat). Don’t forget to set RDI,RSI and RDX to valid values (usual ROP-chain for calling execve()) and you’ll be all set!
Try to solve ex3, ex4 and ex5 from Chapter1: Exercises, they are all solved using the idea described here.
Good libc6 stubs with syscall
If you have the libc6 version the live instance is running, you can use r2 to get a list of the stubs that may prove useful:
r2 /path/to/libc.so.6
[0x00027390]> "/ad/ mov eax, *[0-9].+;syscall"
0x0003c1e0 b83e000000 mov eax, 0x3e
0x0003c1e5 0f05 syscall
0x0003c215 b87f000000 mov eax, 0x7f
0x0003c21a 0f05 syscall
0x0003c24e b882000000 mov eax, 0x82
0x0003c253 0f05 syscall
...
You can also look for stubs that, right after calling syscall, they return:
[0x00027390]> "/ad/ mov eax, *[0-9].+;syscall;ret"
0x000913c0 b818000000 mov eax, 0x18
0x000913c5 0f05 syscall
0x000913c7 c3 ret
0x000d5320 b827000000 mov eax, 0x27
0x000d5325 0f05 syscall
0x000d5327 c3 ret
0x000d5340 b866000000 mov eax, 0x66
0x000d5345 0f05 syscall
0x000d5347 c3 ret
0x000d5360 b868000000 mov eax, 0x68
0x000d5365 0f05 syscall
0x000d5367 c3 ret
0x000d55b0 b86f000000 mov eax, 0x6f
0x000d55b5 0f05 syscall
0x000d55b7 c3 ret
0x000f7b30 b85f000000 mov eax, 0x5f
0x000f7b35 0f05 syscall
0x000f7b37 c3 ret
Note
For read(), you won’t find mov eax, 0; syscall of course! Yeah, you are right:
┌─< 0x000f8097 7417 je 0xf80b0 │ │ 0x000f8099 31c0 xor eax, eax │ │ 0x000f809b 0f05 syscall │ │ 0x000f809d 483d00f0ffff cmp rax, 0xfffffffffffff000 │ ┌──< 0x000f80a3 775b ja 0xf8100 │ ││ 0x000f80a5 c3 ret
Dumping code
As long as the vulnerable program allows you to output something, you can leak addresses the usual way: constructing a simple ROP-chain that performs the usual puts(addr_to_leak), receiving the address leaked afterwards and working from there. But you can leverage this in order to leak code as well. Of course the code in the .text segment of the binary is something we normally have, but how about libc6? Imagine the live instance is running a custom libc6 version and our leaks give us unknown offsets. Well, you can try to dump its code from a certain and known address in order to find the exact address you are interested in and then returning just there. This is how I solved the retired HTB challenge Finale [4].
Dumping code with puts
puts() will stop printing data as soon as a NULL-byte is reached. So your dumping function should take that into account. On some challenges, it’s common to call alarm() first to set a timer; you may need to unset the alarm before attempting to dump code with a ROP-chain performing alarm(0) first. Besides, puts() will end up with a new line character (x0a), so you must remove the last byte returned by puts always.
Inputting addresses to leak with gets
Now, if you write a routine that dumps memory addresses within a range [start,end], bear in mind that gets() will stop reading when x0a or 0xd is inputted. So if the vulnerable program is calling gets and you are sending your payload and one of your computed addresses to dump bytes from end up having any of its bytes to x0a or x0d, the computed address received by outs() will miss a byte or a bunch of them, thus segfaulting. So you need to write some logic to consider these cases.
What to look for
The idea is to find the shortest offset from any known address previously leaked to, say, system(), execve() or execveat(). For example, consider alarm(). On a Debian GNU/Linux Bookworm, with GLIBC 2.36, execve() is 3504 bytes away from alarm():
gef➤ p/d execve - alarm
$3 = 3504
So if the vulnerable program is running on a live instance with a custom libc, but you can leak, say, alarm() because it’s on the got table, then you may start dumping code from there until finding execve(). Some other functions may be used as well. Of course, you can try to reach system() too, but you will need to dump more bytes:
gef➤ p/d alarm - system
$4 = 554944
Note
Some functions are n bytes before a leaked function whereas some others are n bytes after it. For example, execve() is n bytes after alarm(), but system() is n bytes before execve(). With trivial observations like this one, you can do great things sometimes!
Overwriting functions in the got to “transform” one into another without leaks
Some of the functions a vulnerable program calls can be turned into a different one by just overwriting its last byte in the got table. For example, on GLIBC 2.19 and GLIBC 2.23, functions read() and write() are so close to one another, that only the last byte differs. I wrote an unintended way to exploit a real challenge that was supposed to be solved using ret2dlresolve: ch77/exploit.py (you can also see the generated list of functions grouped by offsets in chapter2/libc-2.19-funcs.txt generated with my script chapter2/genlist.py).
Now, on modern versions of glibc, read() and write() are further apart from each other and this trick no longer works:
gef➤ p read
$1 = {ssize_t (int, void *, size_t)} 0x7ffff7e9b090 <__GI___libc_read>
gef➤ p write
$2 = {ssize_t (int, const void *, size_t)} 0x7ffff7e9b130 <__GI___libc_write>
But there are other functions we can still turn. Some of these transformations will allow us to do something we were not supposed at first, like outputting data (leaks!). Of course, this heavily depends on the glibc version. An exploit may work for a particular version and then fail miserably when tried against another version (older or newer).
For example, let’s consider these functions (GLIBC 2.36):
0x00035020 3 41 sym.tolower
0x00035050 3 41 sym.toupper
With no leaks at all, we can just turn one into the other by overwriting the last byte. If the vulnerable program transforms a character to lowercase by calling, at some point:
int lower_c = tolower(0x41)
We can alter the control flow by overwriting tolower@got with byte 0x50. Then, next time the program or us call tolower@plt we are calling toupper instead. Easy peasy. It’s overwriting the got all over again, but we don’t need leaks this time!
Libc6 functions and their offsets
You can get a list of all libc6 functions using radare2 and sort the output by their offset, so you can easily spot which ones can be turned:
r2 /lib/x86_64-linux-gnu/libc.so.6
[0x00027390]> aaaa
[0x00027390]> afl~sym.|sort
I wrote a python script that automates this process: chapter2/genlist.py. You can use it like this:
python3 genlist.py
Alternatively, you can pick a different libc version by running it this way:
python3 genlist.py /path/to/libc.so.6