Chapter 1: Setting arbitrary values into registers
Introduction
Being able to set a particular register to an arbitrary value is mandatory when deploying exploits. Sometimes there simply aren’t any ROP-gadgets for that and so it seems we cannot write our ROP-chain to gain a shell, or to run arbitrary commands or to write to some address we control. On occasions like this, we need to think out of the box, we need to be creative. See Introduction in Chapter 2 to review ret2dlresolve,SROP and ret2csu techniques (not fun!).
Setting an arbitrary value into RAX
Re-purposing a function within the binary
On x86, RAX always holds the return code of the last called function. Knowing this, if you already have control of the instruction pointer and you can construct a simple ROP-chain to call any function within the binary, you may re-purpose a function by manipulating its arguments so it ends up returning the value you need into RAX.
Re-purposing a libc6 function
Any libc6 function called by the binary can be used as well. If you haven’t (yet) leaked a libc6 address, you can construct a simple ROP-chain that calls the functions in the .plt section instead to achieve the same results. For example, let’s imagine our vulnerable program has this in its .got section:
[0x403fc0] abort@GLIBC_2.2.5 → 0x7ffff7e0f3df
[0x403fc8] puts@GLIBC_2.2.5 → 0x7ffff7e5e820
[0x403fd0] memset@GLIBC_2.2.5 → 0x7ffff7f33600
[0x403fd8] alarm@GLIBC_2.2.5 → 0x7ffff7eb8e10
[0x403fe0] gets@GLIBC_2.2.5 → 0x7ffff7e5df80
[0x403fe8] setvbuf@GLIBC_2.2.5 → 0x7ffff7e5ee30
You can call some of these functions without leaking any of them in order to set RAX to the value you want. Using the man page for each function, you read the RETURN VALUE section right away to determine if this returned value could be of use to you or not. For example, if you read puts man page:
RETURN VALUE
puts() and fputs() return a non negative number on success, or EOF on error.
So this function may or may not work for you sometimes. Let’s say you want to set the value 0x3b into RAX on this vulnerable program. puts() wouldn’t achieve that, it’s obvious. How about, say, alarm()?:
RETURN VALUE
alarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no
previously scheduled alarm
So a ROP-chain like this one would do the trick:
# First, we disable any previously alarm set by the program:
payload += p64(pop_rdi)
payload += p64(0x0)
payload += p64(alarm_plt) # alarm(0)
# Next, we set a new alarm to 0x3b:
payload += p64(pop_rdi)
payload += p64(0x3b) # 0x3b = syscall execve!
payload += p64(alarm_plt) # alarm(0x3b); eax = 0x3b!
# We know set a new alarm again to 0x3b, so alarm() returns
# the value 0x3b into RAX:
payload += p64(pop_rdi)
payload += p64(0x3b) # 0x3b = syscall execve!
payload += p64(alarm_plt) # alarm(0x3b); eax = 0x3b!
If the program does something when the alarm is triggered and you do not want that, you need to add more ROP-gadgets right after to use the current value at RAX (0x3b) and then disable the alarm by calling alarm_plt(0) again.
Leveraging function side-effects
Yeah, it sounds a bit esoteric but it works. Some of the functions within a binary or some libc6 functions have some side-effects that you can leverage in your favour to do things you thought not possible at first! Of course, if you leverage libc6 functions side-effects, your exploit may not work on a different libc6 version (for example, your exploit works well in local but it fails miserably against the live instance). Otherwise, you will be fine. These side-effects typically involved leaving a register to some value that suits your needs. For example, open(); after calling open with a valid filename and if it succeeds, you will end up setting RDX to 0 (seen on GLIBC 2.36, 2.27).
There’s an example of this in the exploit I wrote to solve UAM challenge porroPwnPwn here: UAM/porroPwnPwn/exploit.py, option SHELLNOLEAK.
List of common libc6 functions that sets RAX to a certain value
The following list is by no means exhaustive, and more functions will be added to it:
- read
Your ROP-chain will need to read up to as many bytes as the value you want into EAX. The desired value must be set into RDX.
- write
Your ROP-chain will need to write up to as many bytes as the value you want into EAX. The desired value must be set into RDX.
- open
Your ROP-chain will need to keep opening files until reaching the value you want into RAX-3 (for stdin, stdout and stderr; unless the vulnerable program has opened more files already, in this case you would need to compensate for that).
- fseek
Your ROP-chain needs to seek to the value you want into EAX as the offset on the opened file. The desired value must be set into RSI.
- alarm
Your ROP-chain needs to disable any previous alarm with a call to alarm(0) first; then make two calls to set the alarm to the value you want into RAX: alarm(value);alarm(value). The desired value must be set into RDI (first, rdi = 0, then two consecutive calls with RDI == desired value for RAX).
- tolower,toupper
Your ROP-chain needs to set the value you want into RAX as the parameter for this function. The desired value must be set into RDI.