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