Pwning the Chip8 Emulator with Blind Format Strings
This is a continuation of my previous post, if you have not read it you can find it here.
As I was browsing through X I came across this post.
When trying to exploit the emulator before I realized I could call printf
with an arbitrary format string, I also checked
quickly resources online, but mainly they exploit format string bugs which are not blind (you can see and read the output
of the printf
). Here the situation is different as the output would be on the terminal of the remote computer which we can’t
read. However, as I saw the above post I realized blind format string exploits are a thing and I decided to investigate whether
they would work here.
For context I reccomend you read the post I linked as well as keep the man page for printf open while you read and refer to it often.
Blind printf
exploit
The general idea is using printf
to first leak a libc
address into the Chip8 memory. Then we process this using the Chip8’s
assembly to compute the address of system
. Finally we use printf
again to overwrite the GOT entry of puts
and we have
arbitrary code execution.
First step is placing a pointer to some Chip8 memory onto the stack. We will need this because with the format %n
(which is the only writing primitive in format strings) we can write only to pointers which are on the stack.
I decided to use address 0x405220
which corresponds to the VM address 0x100
. This is what the stack looks like at a printf
call
pwndbg> stack 15
00:0000│ rsp 0x7ffdd9892408 —▸ 0x7fa214d6be59 (Mix_FadeInMusicPos+265) ◂— mov ebp, eax
01:0008│ 0x7ffdd9892410 —▸ 0x7ffdd9892568 —▸ 0x7ffdd989419f ◂— '/home/luca/Documents/CHIP8-Emulator-C/src/Chip8'
02:0010│ 0x7ffdd9892418 —▸ 0x7ffdd9892450 ◂— 2
03:0018│ 0x7ffdd9892420 —▸ 0x7fa214fcd000 (_rtld_global) —▸ 0x7fa214fce2e0 ◂— 0
04:0020│ 0x7ffdd9892428 —▸ 0x4013fd (main+295) ◂— movzx eax, byte ptr [rip + 0x5556]
05:0028│ 0x7ffdd9892430 —▸ 0x7ffdd9892568 —▸ 0x7ffdd989419f ◂— '/home/luca/Documents/CHIP8-Emulator-C/src/Chip8'
06:0030│ 0x7ffdd9892438 ◂— 0x254442d18
07:0038│ 0x7ffdd9892440 ◂— 0x3fe0000000000000
08:0040│ 0x7ffdd9892448 ◂— 0xb00000000
09:0048│ 0x7ffdd9892450 ◂— 2
0a:0050│ 0x7ffdd9892458 —▸ 0x7fa214b91dba (__libc_start_call_main+122) ◂— mov edi, eax
0b:0058│ 0x7ffdd9892460 ◂— 0x3ada368abac064c3
0c:0060│ 0x7ffdd9892468 —▸ 0x4012d6 (main) ◂— push rbp
0d:0068│ 0x7ffdd9892470 ◂— 0x2bac064c3
0e:0070│ 0x7ffdd9892478 —▸ 0x7ffdd9892568 —▸ 0x7ffdd989419f ◂— '/home/luca/Documents/CHIP8-Emulator-C/src/Chip8'
I decided to write the address 0x405220
at the stack address 0x7ffdd9892568
, which is as you can see, where the program name
is stored on the stack. To do so I used to following code
asm.write_bytes(0x0, b"%" + str(PRINTF_LEAK_ADDR).encode("ascii") + b"c%6$ln\x00")
asm.native_call_primitive(PRINTF_PLT)
Here PRINTF_LEAK_ADDR
is 0x405220
. In particular the format stores the value 0x405220
into the character counter and then
dumps it using the %ln
format with the appropriate argument index.
Now that we have the address we want to write too we can leak there some libc
address. In particular we have the address of
__libc_start_call_main+122
, 0x7fa214b91dba
, on the stack. To leak this we need to use first the format %*15$c
which
reads the argument with index 15 into the character counter. In particular the *15$
part says that the 15th argument, which
is supposed to be an int
, stores the value that need to be used as the field width (refer to the man pages for details).
This is precisely the argument index of the __libc_start_call_main+122
address.
Unfortunately, we do not control the type here, the value is interpreted as int
, so we can only read the lower 32 bits. However
this will prove sufficient, because we will just overwrite the lower 32 bits of the GOT entry of puts
.
Further it is a signed int
which will lead to some weirdness when the MSB of the lower 32 bits is set
(whether this is set or not depends on ASLR, so it is random).
Now that we read the value we dump it into the chip8’s memory at 0x405220
using the format %49$ln
. So the final code looks
like
asm.write_bytes(0x0, b"%*15$c%49$ln\x00")
asm.native_call_primitive(PRINTF_PLT)
Now let me go back to the problem with signed int
s. For example let me take the case where __libc_start_call_main+122
is
0x7ffff7bc1dba
. The lower 32 bits are 0xf7bc1dba
and thus have the sign bit set. In this case what gets written at the
leak location is the value 0x0843e246
. However after some thinking about this and how the printf
code may work (I did not
actually read the code) I realized that we always have actual_lower_32_bits + leak_value = 0x100000000
(i.e. the leaked value
is the two’s complement of the real value) so that we can always recover the actual value from the leaked one anyhow. If the
sign bit is not set then the leaked value is just the real value of the lower 32 bits.
So we process the leak as follows:
- look at the page offset of the leaked value (this is independent of ASLR): if it is
0xdba
the leak is the actual value and we need to do nothing, - otherwise compute the actual value by getting the two’s complement.
After implementing this in the chip8’s instructions, we add the offset to system
to the leak with more instructions.
Then we can leverage printf
again to overwrite the lower 32-bits of some GOT entry (let me chose puts
which is at
address 0x405038
). To keep the previous setup in place I write this to a different stack location with format strings.
pwndbg> stack 15
00:0000│ rsp 0x7ffc39a50a00 —▸ 0x7ffc39a50b58 —▸ 0x405220 (chip+256) ◂— 0xffffffffb0e8f050
01:0008│-038 0x7ffc39a50a08 —▸ 0x7ffc39a50a40 —▸ 0x405038 (puts@got[plt]) —▸ 0x7ff7b0eb8e60 (puts) ◂— push r14
02:0010│-030 0x7ffc39a50a10 —▸ 0x7ff7b12a4000 (_rtld_global) —▸ 0x7ff7b12a52e0 ◂— 0
03:0018│-028 0x7ffc39a50a18 —▸ 0x4013fd (main+295) ◂— movzx eax, byte ptr [rip + 0x5556]
04:0020│-020 0x7ffc39a50a20 —▸ 0x7ffc39a50b58 —▸ 0x405220 (chip+256) ◂— 0xffffffffb0e8f050
05:0028│-018 0x7ffc39a50a28 ◂— 0x254442d18
06:0030│-010 0x7ffc39a50a30 ◂— 0x3fe0000000000000
07:0038│-008 0x7ffc39a50a38 ◂— 0xb00000000
08:0040│ rbp 0x7ffc39a50a40 —▸ 0x405038 (puts@got[plt]) —▸ 0x7ff7b0eb8e60 (puts) ◂— push r14
09:0048│+008 0x7ffc39a50a48 —▸ 0x7ff7b0e68dba (__libc_start_call_main+122) ◂— mov edi, eax
0a:0050│+010 0x7ffc39a50a50 ◂— 0x3ada368abac064c3
0b:0058│+018 0x7ffc39a50a58 —▸ 0x4012d6 (main) ◂— push rbp
0c:0060│+020 0x7ffc39a50a60 ◂— 0x2bac064c3
0d:0068│+028 0x7ffc39a50a68 —▸ 0x7ffc39a50b58 —▸ 0x405220 (chip+256) ◂— 0xffffffffb0e8f050
0e:0070│+030 0x7ffc39a50a70 —▸ 0x7ffc39a50b58 —▸ 0x405220 (chip+256) ◂— 0xffffffffb0e8f050
As you can see we both have the leak address and the puts
GOT address somewhere on the stack now.
Now comes the annoying part: we need to generate the format string that will overwrite the GOT entry ourself in the chip8
program. Ideally one would use the format %<value to write in decimal ascii>c
to load the value in the character counter.
However converting to decimal is not so easy as there is no division operation on the chip8.
What I ended up doing is checking the values bit by bit and adding the format strings %1$<some power of 2>c
: for example the
binary number 1011
would convert to %1$1c%1$2c%1$8c
. I also spitted the write in two steps as this seemed to work better
and it also produce smaller format strings, so each time I write only 16 bits using the format %14$hn
(14
references the
pointer to puts@got[plt]
we previously written on the stack). Splitting the write in two also means we need to update the GOT address on the stack
before the second write.
After this is done the exploit is completed using again the arbitrary call primitive to call puts@plt
with any command we
want to run as first argument.
Conclusion
The exploit works fully reliably, however, unless the output of the program is piped into /dev/null
it is extremely slow as
a lot of characters need to actually be printed. This was just a learning project anyhow and I learned a lot about format string
exploits along the way, I hope at the end of this journey you have learned something to, dear reader.
Anyhow here is the exploit code which will generate a malicious ROM that spawns a calculator