PWN學習之棧溢出
前言
我記得我在最開始學編程的時候,經常會聽到老師說輸入
的時候要注意大小,不要超過數組大小否則會造成緩沖區溢出
導致程序崩潰
的。
當時就覺得溢出就溢出咯,崩潰就崩潰咯,難不成還能導致電腦被攻擊嗎?就偏偏不控制輸入長度。
寫bug
先讓我們來寫個bug體驗一下,下面這段程序要求用戶輸入字符串並且把數據給buffer數組,如果超過12長度的字符串就會造成緩沖區溢出!
bug.cpp源碼
#include <stdio.h>
void bug()
{
char buffer[12]={"1"};
scanf("%s",buffer);
}
int main()
{
__asm //加nop是為了方便在OD里面定位到
{
nop;
nop;
nop;
}
bug();
__asm //可以忽略不用理會
{
nop;
nop;
nop;
}
return 0;
}
目標環境:
- Windows 7旗艦版(64位)
- VC++ 6.0
- Notepad++
用cl來編譯源碼並且生成帶.asm
的匯編源碼文件,編譯命令是cl.exe -FAS ./bug.cpp
進入到lab01
目錄編譯源碼后,生成了.exe 和 .asm文件。
然后我們運行bug.exe測試下輸入超過12長度的字符串看看程序反應會怎么樣,程序不出意外的崩潰掉了。
OD動態調試bug.exe
OK我們用OD進行調試來看看堆棧中實際的情況。
我這里用的是x32dbg
,載入后找到3條nop指令就可以定位到調用bug
函數附近的匯編代碼了。
在0x0040103A
位置下斷點。
接着F9讓程序運行到這里,此時我們注意觀察堆棧,接着馬上要按F7了。
按F7讓程序進入bug函數的內部
,並且這時候仔細看堆棧。
我們會發現進入函數內部后首先是棧頂發生了變化,之前的棧頂為0x0018FF3C
,現在的棧頂為0x0018FF38
,棧頂減少了4字節,說明發生了push操作,push xx
等於esp-4
也就是0x0018FF38
的地址,並且此時把內容送入esp(棧頂)
地址。
可以發現送入esp地址的內容是調用bug函數的下一句代碼的地址,也就是當F7 進入call bug
函數的時候,其實是先執行了push eip
的操作,因為此時eip=下一句代碼地址,然后再jmp bug函數
處執行代碼。
OD調試觀察溢出
好了接下來我們繼續F8單步執行,當F8后此時注意ebp被壓入了堆棧,估計這個問題會有很多新手做ctf pwn的時候被坑,他們計算出溢出大小后直接+4進行了此地址的覆蓋,以為覆蓋到了ret地址,其實ret地址在下面,導致拿不到flag。
當F8執行完sub esp,0xC
匯編指令后,棧頂位置減了12字節,用來存放buffer數組的數據。
之后我們一路執行到call
函數這里的時候,發現又有兩個數據入棧了,這是調用scanf
函數術后的參數,我們可以不用管它,然后我們在F8步過call后,程序要求我們輸入字符串,我們輸入AAAAAAAAAAAABBBBCCCC
12個A代表填滿buffer數組,BBBB代表溢出覆蓋ebp寄存器,CCCC代表溢出覆蓋ret地址。
-----------------------------------溢出前--------------------------------
0018FF20 00407034 bug.00407034 |參數1:"%s"
0018FF24 0018FF28 L"1" |參數2:buffer數組地址
0018FF28 00000031 ;buffer數組起始位置
0018FF2C 00000000 ;buffer
0018FF30 00000000 ;buffer
0018FF34 0018FF48 ;ebp寄存器
0018FF38 0040103F ;call bug函數的下一句匯編指令地址 |返回到 bug.00401046 自 bug.00401000
-----------------------------------溢出后----------------------------------
0018FF20 00407034 bug.00407034 |參數1:"%s"
0018FF24 0018FF28 "AAAAAAAAAAAABBBBCCCC" |參數2:buffer數組地址
0018FF28 41414141 ;buffer數組起始位置
0018FF2C 41414141 ;buffer
0018FF30 41414141 ;buffer
0018FF34 42424242 ;ebp寄存器
0018FF38 43434343 ;call bug函數的下一句匯編指令地址 |返回到 bug.00401046 自 bug.00401000
棧溢出攻擊之突破密碼驗證
這里其實主要就是利用棧溢出覆蓋掉局部變量的值,讓其改變流程。
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[8];
authenticated = strcmp(password,PASSWORD);
_asm nop;
strcpy(buffer,password);//緩沖區溢出!!!!
return authenticated;
}
int main()
{
int valid_flag = 0;
char password[1024];
while(1)
{
printf("Please input password:");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("密碼錯誤\n\n");
}else
{
printf("恭喜!密碼輸入正確!\n");
break;
}
}
}
只有當密碼是1234567
的時候才會停止循環,並且輸出密碼正確。
用溢出方式來破解密碼程序,首先strcmp
函數的返回值是當s1<s2時,返回為負數;當s1=s2時,返回值= 0;當s1>s2時,返回正數。所以我們想辦法讓authenticated
變量等於0,這樣條件才能為假,才能進入恭喜!密碼輸入正確!
的分支。
所以我們只要輸入8位數字,這時候\0 結尾符剛好能覆蓋到 authenticated
變量使其等於0,從而改變程序判斷。
可以看到je會走到密碼正確的分支。
x64位棧溢出
源碼還是采用bug.cpp的然后用VisualStudio編譯成x64的。
載入IDA來看一下他的匯編代碼,可以發現32位寄存器都變成了64位寄存器。
還有x64和x86的還有個區別就是函數調用棧的區別,在x64中只有fastcall
函數調用約定,定義如下。
參數1、參數2、參數3、參數4分別保存在 RCX、RDX、R8D、R9D ,剩下的參數從右往左依次入棧,被調用者實現棧平衡,返回值存放在 RAX 中。
#C語言代碼
int fastcall_sum = fastcall_add(1, 2, 3, 4, 5, 6, 7);
#----------------------匯編代碼--------------------------------
00007FF6577A366E mov dword ptr [rsp+30h],7 ;超過參數4保存在棧中
00007FF6577A3676 mov dword ptr [rsp+28h],6 ;超過參數4保存在棧中
00007FF6577A367E mov dword ptr [rsp+20h],5 ;超過參數4保存在棧中
00007FF6577A3686 mov r9d,4 ;參數4
00007FF6577A368C mov r8d,3 ;參數3
00007FF6577A3692 mov edx,2 ;參數2
00007FF6577A3697 mov ecx,1 ;參數1
#調用fastcall_add函數
00007FF6577A369C call fastcall_add (07FF6577A11C2h)
00007FF6577A36A1 mov dword ptr [fastcall_sum],eax # 返回值
#--------------------------------------------------------------
#---------------------fastcall_add函數--------------------------
int __fastcall fastcall_add(int a, int b, int c, int d, int e, int f, int g)
{
00007FF6D22D1790 mov dword ptr [rsp+20h],r9d ;參數4給臨時變量
00007FF6D22D1795 mov dword ptr [rsp+18h],r8d ;參數3給臨時變量
00007FF6D22D179A mov dword ptr [rsp+10h],edx ;參數2給臨時變量
00007FF6D22D179E mov dword ptr [rsp+8],ecx ;參數1給臨時變量
00007FF6D22D17A2 push rbp ;rbp入棧 ,和x86一樣待會會用到
00007FF6D22D17A3 push rdi ;rdi入棧 , 不清楚
00007FF6D22D17A4 sub rsp,0E8h ;將棧頂向下拉232個字節
00007FF6D22D17AB mov rbp,rsp ;把rsp棧頂給了rbp 方便尋址定位
00007FF6D22D17AE mov rdi,rsp ;把rsp棧頂也給了rdi
00007FF6D22D17B1 mov ecx,3Ah ;循環變量58
00007FF6D22D17B6 mov eax,0CCCCCCCCh ;燙燙燙燙
00007FF6D22D17BB rep stos dword ptr [rdi] ;循環58此
00007FF6D22D17BD mov ecx,dword ptr [rsp+108h]
int sum = a+b+c+d+e+f+g;
00007FF6D22D17C4 mov eax,dword ptr [b]
00007FF6D22D17CA mov ecx,dword ptr [a]
00007FF6D22D17D0 add ecx,eax ;a+b
00007FF6D22D17D2 mov eax,ecx
00007FF6D22D17D4 add eax,dword ptr [c];+c
00007FF6D22D17DA add eax,dword ptr [d];+d
00007FF6D22D17E0 add eax,dword ptr [e];+e
00007FF6D22D17E6 add eax,dword ptr [f];+f
00007FF6D22D17EC add eax,dword ptr [g];+g
return sun;
00007FF6D22D17F2 mov dword ptr [sum],eax;存放總和
}
00007FF6D22D17F8 lea rsp,[rbp+0E8h]
00007FF6D22D17FF pop rdi
00007FF6D22D1800 pop rbp
00007FF6D22D1801 ret # 沒做棧平衡
我們在匯編代碼中找到調用bug函數的地方,因為我們bug.cpp中bug函數並沒有參數,所以不需要關心這些參數調用方式。
然后注意看bug函數的這個地方,我們發現棧頂被拉低了96個字節,也就是12*8其中在這里一個char占用了8位,然后12個元素x8就是96個字節了。然后我們需要輸入超過8x4=32
個字符才能溢出。
在x64dbg中進行實驗,看看真實效果。