还不错,是么?
如果你使用以linux 2.6 x86 为内核的GNU/Linux 操作系统(在这里为Slackware Linux 打个小广告),这段代码应该没什么问题,如果你对这段代码的生成方式有点兴趣,不妨看完这篇文章 ;p
当然,如果你是一个C语言的初学者,你可能需要学会怎样玩转函数,因为这是本文的一个前置知识。另外你需要有一点点汇编基础,这没有什么难度,只消你随便下载一本汇编书看半个小时即可,你需要做得最后一件事就是百度一下AT&T 汇编格式和Intel 汇编格式的那点可怜的区别。
当然,你现在大可不必特别留心这些东西,只不过当你在往下的阅读中遇到了什么瓶颈,千万不要忘了考虑下上面一段文字;p
另外代码本身就有点投机取巧,大神们口下留情呀 ;p
------------------------------OK,我们开始---------------------------
不知道你是不是一眼就看出了这段代码的工作方式,我们在f[2] 也就是main 函数返回值的地方用数组a 的首地址覆盖了main 函数的返回地址,于是当main 函数返回的时候便跳转到了数组a 的首地址,从而执行了数组a。 至于数组a 里面的东东则是一段机器码,调用了write 和exit 系统调用。至于数组的书写方式嘛,主要是为了让C语言初学者迷糊,然而如果你对C语言稍有了解就会明白所有的数组都会改写成“指针+偏移”的形式。
如果直观的表示一下则是这个样子:
- 3[a] == *(3+a) == *(a+3) == a[3];
好了,既然把这段代码的原理说清楚了,剩下的就是怎么实现的问题。那么我们的任务现在主要就是这么几点:
A. 用汇编调用write 系统调用输出"Hello World!\n",然后调用exit 系统调用安全地退出程序。
B. 得到这个程序的机器码。
C. 将机器码嵌入C 程序并调用之。
这就是我们的任务,并不是很多,不是么?
-----------------------------TASK: A------------------------------
在GNU/Linux 里,程序可以通过软中断0x80 来调用系统调用(syscall,你可以在得到一份linux syscall 的列表)。一个系统调用的过程分为如下几步:
1. 把希望执行的syscall 的编号扔到eax。
2. 把调用的参数依此压入其他的寄存器(ebx、ecx、edx、esi、edi 和ebp),不要太担心这么几个寄存器够不够用,看看syscall 的列表就知道,大多数的syscall 都用不到6 个参数。
3. 执行软中断 0x80。
4. CPU 切换到内核模式并执行syscall (这一步是CPU 制造商和 你OS的开发者的事情了)。
我们来拿最简单的exit 系统调用做个实验:
我们写的程序应该完成如下任务:
把1 扔到eax(看看那份syscall 列表就知道为什么了)。
< b>把0 扔到ebx(exit 的参数)。
代码:
- #filename: myexit.s
-
- .section .text
- .globl _start
- _start:
- movl $1, %eax
- movl $0, %ebx
- int $0x80
speller@SHELL-LAB:~/code/c/myhello/tmp$ as -o myexit.o myexit.s
speller@SHELL-LAB:~/code/c/myhello/tmp$ ld -o myexit myexit.o
speller@SHELL-LAB:~/code/c/myhello/tmp$ ./myexit
speller@SHELL-LAB:~/code/c/myhello/tmp$
好像没什么不正常。
下面让我们再进一步,写一个write 的系统调用后跟exit 的系统调用。write 调用的步骤大约如下:
把系统调用编号4 扔到eax
< b>把第一个参数扔到ebx(这里是1,代表stdout)。
如果对write 调用的参数不是很清楚,可以在你的GNU/Linux 发行版上运行指令
- man 2 write
但是容我打断一下:这里我们有一个问题,write 调用需要用一个字符串的首地址作第二个参数,那么这个首地址该怎么确定呢?
我们固然可以用硬编码的方式将地址写死在程序中,可是一旦这样程序便失去了通用性,每一台计算机上必须重新确定一个首地址。其实前辈们已经用一些方法巧妙的解决了这个问题,其中简单易行的一种方法就是用call 指令确定相对地址。
这种方法的编码类似如下:
- .section .text
- .globl _start
- _start:
- jmp Begin
-
- Shellcode:
-
popl %esi
-
# other code
-
- Begin:
- call Shellcode
- .ascii "Hello world!\n"
让我们实现一下:
- #filename: write_sc.s
-
- .section .text
- .globl _start
- _start:
- jmp Begin
-
- Shellcode:
- # write ()
- popl %esi
- movl $0, %eax
- movl $0, %ebx
- movl $0, %edx
-
- movl $0x4, %eax
- movl $0x1, %ebx
- lea (%esi), %ecx
- movl $0xe, %edx
- int $0x80
-
- # exit()
- movl $0, %ebx
- movl $0x1, %eax
- int $0x80
-
- Begin:
- call Shellcode
- .ascii "Hello world!\n"
speller@SHELL-LAB:~/code/c/myhello/tmp$ as -o write_sc.o write_sc.s
speller@SHELL-LAB:~/code/c/myhello/tmp$ ld -o write_sc write_sc.o
speller@SHELL-LAB:~/code/c/myhello/tmp$ ./write_sc
Hello world!
speller@SHELL-LAB:~/code/c/myhello/tmp$
WOW!大功告成,我们用汇编调用syscall 完成了一个Hello world 程序!
-----------------------------TASK: B------------------------------
好了,既然我们得到了我们想要的,那么更进一步得到这段Hello world程序的机器码吧。在这里我们用到了GNU/Linux 最常用的工具之一objdump :
speller@SHELL-LAB:~/code/c/myhello/tmp$ objdump -d write_sc
write_sc: file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: eb 2f jmp 8048085
08048056
8048056: 5e pop %esi
8048057: b8 00 00 00 00 mov $0x0,%eax
804805c: bb 00 00 00 00 mov $0x0,%ebx
8048061: ba 00 00 00 00 mov $0x0,%edx
8048066: b8 04 00 00 00 mov $0x4,%eax
804806b: bb 01 00 00 00 mov $0x1,%ebx
8048070: 8d 0e lea (%esi),%ecx
8048072: ba 0e 00 00 00 mov $0xe,%edx
8048077: cd 80 int $0x80
8048079: bb 00 00 00 00 mov $0x0,%ebx
804807e: b8 01 00 00 00 mov $0x1,%eax
8048083: cd 80 int $0x80
08048085
8048085: e8 cc ff ff ff call 8048056
804808a: 48 dec %eax
804808b: 65 gs
804808c: 6c insb (%dx),%es:(%edi)
804808d: 6c insb (%dx),%es:(%edi)
804808e: 6f outsl %ds:(%esi),(%dx)
804808f: 20 77 6f and %dh,0x6f(%edi)
8048092: 72 6c jb 8048100
8048094: 64 21 0a and %ecx,%fs:(%edx)
speller@SHELL-LAB:~/code/c/myhello/tmp$
前面80480* 是每条指令的地址,随后则是相应的、用十六进制表示的机器码,然后则是返汇编语句。很给力,不是么 ;p
我们现在得到这段程序的机器码了,然而这里还有点小小的问题:这段机器码存在大量的NULL 字符——而在C语言中NULL字符可以结束一个字符串。当然,需要说明的是在开篇展示的Hello world 程序中这个并没有什么大碍,然而我们最好还是有点敬业精神为好。实话告诉你:这个标签起名叫Shellcode 不是没有原因的——Shellcode 正是蠕虫病毒的核心所在。一个蠕虫无法把一个中间带有很多NULL 字符的机器码注入其他程序,因为C 程序的一个约定规则是如果字符串中出现了'\0',则代表这个字符串在此处结束了,这样蠕虫只能传递一个不完整的Shellcode ——通常这根本没有什么意义。
很抱歉的突然扯到蠕虫病毒,不过这里可不是教你怎么写蠕虫,只是根据敬业精神我们需要得到一段没有NULL 的机器码。仔细一看这其中的NULL 字符都是汇编程序中某些指令的不合适引起的,例如
- movl $0, %eax
- movl $0x4, %eax
我们需要小心翼翼的完成这个任务:选择合适的指令来确保产生的机器码当中不含有NULL 字符:
-
#filename: write_sc.s
-
- .section .text
- .globl _start
- _start:
- jmp Begin
-
- Shellcode:
- # write ()
- popl %esi
- xor %eax, %eax
- xor %ebx, %ebx
- xor %edx, %edx
-
- movb $0x4, %al
- movb $0x1, %bl
- lea (%esi), %ecx
- movb $0xe, %dl
- int $0x80
-
- # exit()
- xor %ebx, %ebx
- movb $0x1, %al
- int $0x80
-
- Begin:
- call Shellcode
- .ascii "Hello world!\n"
speller@SHELL-LAB:~/code/c/myhello$ as -o write_sc.o write_sc.s
speller@SHELL-LAB:~/code/c/myhello$ ld -o write_sc write_sc.o
speller@SHELL-LAB:~/code/c/myhello$ objdump -d write_sc > dump.msg
speller@SHELL-LAB:~/code/c/myhello$ cat dump.msg
write_sc: file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: eb 17 jmp 804806d
08048056
8048056: 5e pop %esi
8048057: 31 c0 xor %eax,%eax
8048059: 31 db xor %ebx,%ebx
804805b: 31 d2 xor %edx,%edx
804805d: b0 04 mov $0x4,%al
804805f: b3 01 mov $0x1,%bl
8048061: 8d 0e lea (%esi),%ecx
8048063: b2 0e mov $0xe,%dl
8048065: cd 80 int $0x80
8048067: 31 db xor %ebx,%ebx
8048069: b0 01 mov $0x1,%al
804806b: cd 80 int $0x80
0804806d
804806d: e8 e4 ff ff ff call 8048056
8048072: 48 dec %eax
8048073: 65 gs
8048074: 6c insb (%dx),%es:(%edi)
8048075: 6c insb (%dx),%es:(%edi)
8048076: 6f outsl %ds:(%esi),(%dx)
8048077: 20 77 6f and %dh,0x6f(%edi)
804807a: 72 6c jb 80480e8
804807c: 64 21 0a and %ecx,%fs:(%edx)
speller@SHELL-LAB:~/code/c/myhello$
之所以把objdump 的输出扔到文本dump.msg 中是因为这样可以方便你使用各种工具来提取中间部分的机器码,现在考验你才智的时候到了,你可以用任何你喜欢的方法来提取中间的机器码并且在每2个十六进制前面加一个"\x"。什么方法都可以,只要你喜欢:用Vim、用Emacs、用Perl、用Python、用Shell、用C……甚至你可以一个一个地抄在本子上然后敲到电脑上 ;p
最后我们得到的东西大约就是这样:
- \xeb\x17\x5e\x31\xc0\x31\xdb\x31\xd2\xb0\x04\xb3\x01\x8d\x0e\xb2\x0e\xcd\x80\x31\xdb\xb0\x01\xcd\x80\xe8\xe4\xff\xff\xff\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21\x0a
-----------------------------TASK: C------------------------------
我们用覆盖函数返回值的办法来测试一下这段机器码是否工作良好(你知道函数的实质是什么,对么?):
-
/* filename: test.c */
-
-
char shellcode [] = "\xeb\x17\x5e\x31\xc0\x31\xdb\x31\xd2\xb0\x04\xb3\x01\x8d\x0e\xb2\x0e\xcd\x80\x31\xdb\xb0\x01\xcd\x80\xe8\xe4\xff\xff\xff\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21\x0a";
-
-
int main
-
(void) {
-
-
int *p = (int *)&p + 2;
-
*p = (int)shellcode;
-
-
return 0;
- }
如果你作为一个C语言的初学者对这段代码感到云里雾里,建议你考虑一下开头写的那段建议 ;p
speller@SHELL-LAB:~/code/c/myhello$ gcc -o test test.c
speller@SHELL-LAB:~/code/c/myhello$ ./test
Hello world!
speller@SHELL-LAB:~/code/c/myhello$
漂亮,我们得到的机器码确实工作良好!
现在为了让程序的行为更奇特一点,我们全局定义一个数组a,并在main 函数中对数组的每个元素进行赋值,然后将利用指针p 来修改main 返回值的行为用数组来完成,这样显得比较整齐划一。当然这个时候有时考察你聪明才智的时候了,你需要把shellcode[] 中\xeb 的形式变成0xeb 的形式然后进行赋值,一个不算是太丑的办法就是在vim 里执行指令
- :3s/\\x/\,\ [a]\ =\ 0x/g
最后我们的小程序看起来就是这个样子:
- /* filename: hello.c */
-
-
char a [43];
-
-
int main
-
(void) {
-
-
int f[1];
-
0 [a] = 0xeb, 1 [a] = 0x17, 2 [a] = 0x5e;
-
3 [a] = 0x31, 4 [a] = 0xc0, 5 [a] = 0x31;
-
6 [a] = 0xdb, 7 [a] = 0x31, 8 [a] = 0xd2;
-
9 [a] = 0xb0, 10[a] = 0x04, 11[a] = 0xb3;
-
12[a] = 0x01, 13[a] = 0x8d, 14[a] = 0x0e;
-
15[a] = 0xb2, 16[a] = 0x0e, 17[a] = 0xcd;
-
18[a] = 0x80, 19[a] = 0x31, 20[a] = 0xdb;
-
21[a] = 0xb0, 22[a] = 0x01, 23[a] = 0xcd;
-
24[a] = 0x80, 25[a] = 0xe8, 26[a] = 0xe4;
-
27[a] = 0xff, 28[a] = 0xff, 29[a] = 0xff;
-
30[a] = 0x48, 31[a] = 0x65, 32[a] = 0x6c;
-
33[a] = 0x6c, 34[a] = 0x6f, 35[a] = 0x20;
-
36[a] = 0x77, 37[a] = 0x6f, 38[a] = 0x72;
-
39[a] = 0x6c, 40[a] = 0x64, 41[a] = 0x21;
-
42[a] = 0x0a, 2 [f] = (int)a;
-
-
return 0;
- }
我们看看效果如何:
speller@SHELL-LAB:~/code/c/myhello$ gcc -o hello hello.c
speller@SHELL-LAB:~/code/c/myhello$ ./hello
Hello world!
speller@SHELL-LAB:~/code/c/myhello$
WOW,很棒,不是么,你可以拉一个C 语言新手过来给他看看这段代码,并和他打赌这段代码编译后执行会有什么效果,如果你每次押注20块,我敢保证你一天下来会赢一大把钱 ;p
另外如果你对蠕虫病毒产生了兴趣,那么可以动用你的搜索引擎来看个究竟,不过切记这一切只能为了兴趣之故,你万万不能为了攻击而研究这些东西——如果你想成为一名合格的黑客的话。