這兩天都一直在解析3D模型資料。今天閑暇之餘寫了段測試代碼。分析下駭客們用的緩衝區溢位攻擊原理及Shell code原理。好,直接進入正題。有什麼說得不對的地方還望大家糾正。嘿嘿!
首先來這麼一段小小的測試代碼:
void test( void )
{
cout << "Success!" << endl;
}
int main( void )
{
int a[ 1 ];
a[ 3 ] = ( int )test;
return 0;
}
上面這段代碼,可以簡單的解釋緩衝區溢位的原理,首先定義了一個整形數組a,紅色部分代碼已經寫入越界。導致的結果就是會輸出:Success!
這裡就有一個疑問了,為什麼在程式裡沒有調用test函數,也執行了test函數裡面的代碼呢?
這裡就是緩衝區溢位導致的結果。這裡要從函數的調用原理來解釋這種現象,在函數被調用時,會儲存函數的棧幀。會將ebp, eip進行壓棧儲存。順序就是:
高地址 低地址
[ eip ] [ ebp ] [ a[ 0 ] ]
在彙編層面,調用函數會call [ 地址 ],call又兩步驟,一是push eip到堆棧進行儲存作為函數的ret返回地址,eip的值就是當前call指令的下一條指令的地址。當函數結束時,執行ret指令就會跳轉到eip所存的地址,也就是主調函數的棧幀裡面,完成調用。
說了這麼多,這裡的a[ 3 ]將test的地址當好覆蓋到存放eip的地址上了。main函數的ret指令便跳轉到test的代碼空間裡了。所以便輸出了Success! 當然這樣程式會崩潰,因為執行到test的ret指令時,此時的eip的值已經未知了,堆棧已經不平衡。因此跳轉將到未知地方,便崩潰了。
從上面的例子中不難看出,我們可以通過Buffer Overflow來改變在堆棧中存放的函數返回地址,從而改變整個程式的流程,使它轉向任何我們想要它去的地方。這就為駭客們提供了可乘之機。
最常見的方法是:在長字串中嵌入一段代碼(就是通過溢出越界寫入,覆蓋掉函數的返回地址),並將函數的返回地址覆蓋為這段代碼的地址, 這樣當函數返回時,程式就轉而開始執行這段我們自編的代碼了。 一般來說,這段代碼都是執行一個Shell程式(如/bin/sh),因為這樣的話,當我們入侵一個帶有Buffer Overflow缺陷且具有suid-root屬性的程式時。 我們會獲得一個具有root許可權的shell,在這個shell中我們可以幹任何事。因此,這段代碼一般被稱為Shell Code。
下面我們來舉個例子說明下Shell Code的原理:
int Code( int a, int b )
{
return a + b;
}
void TestShell( void )
{
int result = 0;
BYTE FuncByte[ 512 ];
BYTE* JmpAddr = ( BYTE* ) Code;
DWORD ofsFuncAddr = *( ( DWORD* )( JmpAddr + 1 ) ) + 5;
BYTE* FuncAddr = ( BYTE* )( ( ( DWORD )JmpAddr ) + ofsFuncAddr );
BYTE* pFuncBuff = FuncAddr;
BYTE* pInput = FuncByte;
while ( true )
{
if ( (*pInput++ = *pFuncBuff++ ) == 0xC3 )
break;
}
__asm
{
lea eax, FuncByte
push 100
push 200
mov ecx, 1
call __label
__label:
cmp ecx, 0
je __ret
sub ecx, 1
jmp eax
__ret:
mov result, eax
add esp, 8
}
cout << result << endl;
system( "pause" );
}
int main( void )
{
TestShell ();
return 0;
}
上面的FuncByte用於儲存Code函數的位元組碼,JmpAddr指向jmp到Code函數的jmp指令地址。call [目標函數] 會先跳轉到jmp [ 函數地址 ]指令的地址上,然後才會jmp到目標函數的首地址上。ofsFuncAddr用於儲存當前jmp指令5個位元組中後4個位元組儲存的函數地址位移量(這裡暫不管跳轉的遠近,這裡是無條件轉移,就粗略認為是4個位元組存放的是位移)。
指令地址 位元組碼 指令 目標函數地址
0041954B E9 10 1D 00 00 jmp TestShell (41B260h)
從上面的jmp指令可以看出,E9就是jmp指令的位元組碼,後面藍色的4個位元組就是:目標函數的地址 - jmp指令地址 - jmp指令的5個位元組。也就是:0x41B260 - 0x41954B - 5 = 0x001D10。FuncAddr儲存的是目標函數的首地址。之後的while就是將Code函數的位元組碼拷貝到FuncByte裡。因為函數結束會執行ret指令,ret指令的位元組碼就是0xC3。所以我們以它來終止迴圈停止拷貝。
之後的彙編代碼是為了執行我們拷貝的位元組碼,並維護堆棧平衡,讓跳轉地址正確跳轉到__ret後面的 mov result, eax 語句。但是要怎麼樣才能讓執行了我們拷貝的位元組碼後正確跳轉到我們想要的位置呢?這裡我們使用call指令來完成這項工作,紅色的call __label會將下一條彙編語句的地址壓入堆棧,作為函數的返回地址。由於FuncByte裡面存放的是Code函數的位元組碼,因此執行FuncByte裡面的位元組碼與Code函數的效果是一樣的。這裡執行FuncByte直接用jmp eax來進行跳轉。執行到0xC3(ret)位元組碼後,就會跳轉到cmp ecx, 0這條語句上。這裡我做了個限制使用ecx計數讓ret回來後因為ecx為零(sub ecx, 1 ),執行je __ret。紅色的程式碼片段也可以用 push __ret 一條指令來替換,相當於把返回地址push到堆棧,當拷貝的位元組碼執行完後返回到__ret:。這裡只是為了說明CALL指令的原理。 然後將傳回值賦給result。之後pop掉兩個參數100, 200。維持堆棧平衡。之後就是列印result的值:300。實現了ShellCode的原型。
好了,基本上是說完了!這裡的Code函數裡面只是簡單的一條語句,如果有複雜的操作還需要進一步處理FuncByte裡面的位元組碼。比如,Code函數裡面有函數調用,將會有jmp跳轉。而jmp跳轉使用的是距當前語句的指令地址的位移量。FuncByte是一臨時的位元組數組,執行的位元組碼的指令地址也將在臨時的地址空間裡。位元組碼不變的情況下,jmp指令的地址變了,自然jmp同樣的位移是不會跳轉到正確的目標函數地址的。我的初步想法是在拷貝位元組碼的同時對使用位移的指令進行特殊計算處理。讓在臨時地址空間中也能正確跳轉。暫時留個思緒,拋磚引玉!各位多多指教! - -
如果Code函數裡面有函數調用:
int Code( int a, int b )
{
cout << a + b << endl;
return a + b;
}
下面是我們拷貝的位元組碼和Code函數的位元組碼對比:
FuncByte的拷貝位元組碼:
0013FBF8 55 push ebp
0013FBF9 8B EC mov ebp,esp
0013FBFB 81 EC C0 00 00 00 sub esp,0C0h
0013FC01 53 push ebx
0013FC02 56 push esi
0013FC03 57 push edi
0013FC04 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
0013FC0A B9 30 00 00 00 mov ecx,30h
0013FC0F B8 CC CC CC CC mov eax,0CCCCCCCCh
0013FC14 F3 AB rep stos dword ptr [edi]
0013FC16 68 D8 94 41 00 push 4194D8h
0013FC1B 8B 45 08 mov eax,dword ptr [ebp+8]
0013FC1E 03 45 0C add eax,dword ptr [ebp+0Ch]
0013FC21 50 push eax
0013FC22 B9 88 86 45 00 mov ecx,458688h
0013FC27 E8 2B E2 FF FF call 0013DE57
0013FC2C 8B C8 mov ecx,eax
0013FC2E E8 10 E7 FF FF call 0013E343
0013FC33 8B 45 08 mov eax,dword ptr [ebp+8]
0013FC36 03 45 0C add eax,dword ptr [ebp+0Ch]
0013FC39 5F pop edi
0013FC3A 5E pop esi
0013FC3B 5B pop ebx
0013FC3C 81 C4 C0 00 00 00 add esp,0C0h
0013FC42 3B EC cmp ebp,esp
0013FC44 E8 D0 E8 FF FF call 0013E519
0013FC49 8B E5 mov esp,ebp
0013FC4B 5D pop ebp
0013FC4C C3 ret
Code函數本身位元組碼:
0041B770 55 push ebp
0041B771 8B EC mov ebp,esp
0041B773 81 EC C0 00 00 00 sub esp,0C0h
0041B779 53 push ebx
0041B77A 56 push esi
0041B77B 57 push edi
0041B77C 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
0041B782 B9 30 00 00 00 mov ecx,30h
0041B787 B8 CC CC CC CC mov eax,0CCCCCCCCh
0041B78C F3 AB rep stos dword ptr [edi]
0041B78E 68 D8 94 41 00 push offset std::endl (4194D8h)
0041B793 8B 45 08 mov eax,dword ptr [a]
0041B796 03 45 0C add eax,dword ptr [b]
0041B799 50 push eax
0041B79A B9 88 86 45 00 mov ecx,offset std::cout (458688h)
0041B79F E8 56 DE FF FF call operator<< (4195FAh)
0041B7A4 8B C8 mov ecx,eax
0041B7A6 E8 40 E3 FF FF call operator<< (419AEBh)
0041B7AB 8B 45 08 mov eax,dword ptr [a]
0041B7AE 03 45 0C add eax,dword ptr [b]
0041B7B1 5F pop edi
0041B7B2 5E pop esi
0041B7B3 5B pop ebx
0041B7B4 81 C4 C0 00 00 00 add esp,0C0h
0041B7BA 3B EC cmp ebp,esp
0041B7BC E8 00 E5 FF FF call (__RTC_CheckEsp) (419CC1h)
0041B7C1 8B E5 mov esp,ebp
0041B7C3 5D pop ebp
0041B7C4 C3 ret
從紅色的3個call可以看出,我們的位元組碼沒有變,也就是同樣的位移值。計算出來的call地址是不一樣的。拷貝的在0x0013....空間內。而正確的應該是0x0041....空間內。