一个有意思的 Hello World

2779阅读 3评论2011-03-20 shell_way
分类:LINUX

话说几个月没写代码了,闲来敲一点比较有意思的小程序,依旧,我想尽快展示一些能够吸引住你的眼球的东西(本文所有的演示都基于32 位系统和编译器):




还不错,是么?
如果你使用以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语言稍有了解就会明白所有的数组都会改写成“指针+偏移”的形式
如果直观的表示一下则是这个样子:
  1. 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 的参数)。
执行软中断0x80。

代码:
  1. #filename: myexit.s

  2. .section .text
  3.          .globl _start
  4. _start:
  5.          movl $1, %eax
  6.          movl $0, %ebx
  7.          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)。
把第二个参数(一个字符串的首地址)扔到ecx。
把地三个参数(要输出字符的个数)扔到edx。
执行int 0x80
如果对write 调用的参数不是很清楚,可以在你的GNU/Linux 发行版上运行指令
  1. man 2 write
来看个究竟。

但是容我打断一下:这里我们有一个问题,write 调用需要用一个字符串的首地址作第二个参数,那么这个首地址该怎么确定呢?
我们固然可以用硬编码的方式将地址写死在程序中,可是一旦这样程序便失去了通用性,每一台计算机上必须重新确定一个首地址。其实前辈们已经用一些方法巧妙的解决了这个问题,其中简单易行的一种方法就是用call 指令确定相对地址。
这种方法的编码类似如下:
  1. .section .text
  2.          .globl _start
  3. _start:
  4.          jmp Begin

  5. Shellcode:
  6.         popl %esi
  7.         # other code

  8. Begin:
  9.          call Shellcode
  10.          .ascii "Hello world!\n"
真正的代码在Shellcode 标签之后,开始我们跳转到Begin 标签,然后会调用Shellcode。最重要的细节就在这里,call 指令会把下一条指令的地址压栈,这里压栈的便是"Hello World!\n"的地址。跳转到Shellcode 标签之后第一条popl 指令会出栈这个地址并扔到esi,这样esi 里便是我们想要的数组首地址了。我们便用这样的方法得到了不用硬编码地址而通用性很好的代码。
让我们实现一下:
  1. #filename: write_sc.s

  2. .section .text
  3.          .globl _start
  4. _start:
  5.          jmp Begin

  6. Shellcode:
  7.          # write ()
  8.          popl %esi
  9.          movl $0, %eax
  10.          movl $0, %ebx
  11.          movl $0, %edx

  12.          movl $0x4, %eax
  13.          movl $0x1, %ebx
  14.          lea (%esi), %ecx
  15.          movl $0xe, %edx
  16.          int $0x80

  17.          # exit()
  18.          movl $0, %ebx
  19.          movl $0x1, %eax
  20.          int $0x80

  21. Begin:
  22.          call Shellcode
  23.          .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 字符都是汇编程序中某些指令的不合适引起的,例如
  1. movl $0, %eax

  1. movl $0x4, %eax
前者显式的出现了0,后者则是隐式的,因为0x4 仅仅填充了al,eax 其他的24个bit 并没有被使用,默认地,系统将用0填充其他24个bit。
我们需要小心翼翼的完成这个任务:选择合适的指令来确保产生的机器码当中不含有NULL 字符:
  1. #filename: write_sc.s

  2. .section .text
  3.          .globl _start
  4. _start:
  5.          jmp Begin

  6. Shellcode:
  7.          # write ()
  8.          popl %esi
  9.          xor %eax, %eax
  10.          xor %ebx, %ebx
  11.          xor %edx, %edx

  12.          movb $0x4, %al
  13.          movb $0x1, %bl
  14.          lea (%esi), %ecx
  15.          movb $0xe, %dl
  16.          int $0x80

  17.          # exit()
  18.          xor %ebx, %ebx
  19.          movb $0x1, %al
  20.          int $0x80

  21. Begin:
  22.          call Shellcode
  23.          .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$
太棒了,我们一次性去掉了所有的NULL 字符!
之所以把objdump 的输出扔到文本dump.msg 中是因为这样可以方便你使用各种工具来提取中间部分的机器码,现在考验你才智的时候到了,你可以用任何你喜欢的方法来提取中间的机器码并且在每2个十六进制前面加一个"\x"。什么方法都可以,只要你喜欢:用Vim、用Emacs、用Perl、用Python、用Shell、用C……甚至你可以一个一个地抄在本子上然后敲到电脑上 ;p
最后我们得到的东西大约就是这样:
  1. \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
很漂亮的一段机器码,Well Done!


     -----------------------------TASK: C------------------------------

我们用覆盖函数返回值的办法来测试一下这段机器码是否工作良好(你知道函数的实质是什么,对么?):
  1. /* filename: test.c */

  2. 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";

  3. int main
  4. (void) {

  5.         int *p = (int *)&p + 2;
  6.         *p = (int)shellcode;

  7.         return 0;
  8. }
请不要担心不可执行栈这样的东西,linux目前还没有在堆栈上禁止运行代码,因为这样并没有什么意义,返回libc 就可以很简单的突破不可执行栈,况且不可执行栈会造成效率的损失。如果你的系统中栈受到保护,那么不妨用mprotect 调用来重新设置堆栈属性。
如果你作为一个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 里执行指令
  1. :3s/\\x/\,\ [a]\ =\ 0x/g
接下来你只需要敲每个语句的左值就好了,如果还是太懒用perl 实现之。
最后我们的小程序看起来就是这个样子:

  1. /* filename: hello.c */

  2. char a [43];

  3. int main
  4. (void) {

  5.         int f[1];
  6.         0 [a] = 0xeb, 1 [a] = 0x17, 2 [a] = 0x5e;
  7.         3 [a] = 0x31, 4 [a] = 0xc0, 5 [a] = 0x31;
  8.         6 [a] = 0xdb, 7 [a] = 0x31, 8 [a] = 0xd2;
  9.         9 [a] = 0xb0, 10[a] = 0x04, 11[a] = 0xb3;
  10.         12[a] = 0x01, 13[a] = 0x8d, 14[a] = 0x0e;
  11.         15[a] = 0xb2, 16[a] = 0x0e, 17[a] = 0xcd;
  12.         18[a] = 0x80, 19[a] = 0x31, 20[a] = 0xdb;
  13.         21[a] = 0xb0, 22[a] = 0x01, 23[a] = 0xcd;
  14.         24[a] = 0x80, 25[a] = 0xe8, 26[a] = 0xe4;
  15.         27[a] = 0xff, 28[a] = 0xff, 29[a] = 0xff;
  16.         30[a] = 0x48, 31[a] = 0x65, 32[a] = 0x6c;
  17.         33[a] = 0x6c, 34[a] = 0x6f, 35[a] = 0x20;
  18.         36[a] = 0x77, 37[a] = 0x6f, 38[a] = 0x72;
  19.         39[a] = 0x6c, 40[a] = 0x64, 41[a] = 0x21;
  20.         42[a] = 0x0a, 2 [f] = (int)a;
  21.         
  22.         return 0;
  23. }
注意这里把数组下标写在了前面,你在实际代码中千万不能用这样的方法
我们看看效果如何:
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
另外如果你对蠕虫病毒产生了兴趣,那么可以动用你的搜索引擎来看个究竟,不过切记这一切只能为了兴趣之故,你万万不能为了攻击而研究这些东西——如果你想成为一名合格的黑客的话。
上一篇:程序的自动改写
下一篇:shell_way 死了

文章评论