Shell Code 原理深入剖析

這兩天都一直在解析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....空間內。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章