如何编写一个简单的shellcode
1、写在前面
最近在准备《安全编程》的考试,感觉shellcode学起来懵懵的> <但是!!!身为一个信息安全专业的学生,不会写shellcode简直就是死咸鱼啊!Shellcode是探测到漏洞时执行的代码,是漏洞利用时十分重要的一块内容~请跟本咸鱼一起来学习一个[围笑]。
2、系统调用函数execve
示例代码1:
1 | //shellcode.c |
*通过系统调用execve函数来返回shell。
从上述程序中可以看出,在c语言中调用execve函数来返回shell时,需要包含相应的头文件,在主函数中调用execve函数,同时传入三个参数。
execve函数的介绍:
execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。execve是内核级调用函数。
函数定义 int execve(const char filename, char const argv[], char* const envp[])
返回值 执行成功时没有返回值,执行失败时的返回值为-1.
函数说明 execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以NULL指针结束,最后一个参数则为传递给执行文件的新环境变量数组。
示例代码2:
1 |
|
上述代码实现的功能:与在bin目录下执行 ls -al /etc/passwd 是相同的。
通过在http://syscalls.kernelgrok.com查询到的系统调用表,可以获得sys_execve函数的相关信息:
execve函数的系统调用号为11,对应的寄存器中保留的参数值分别为:eax:0x0b(11!); ebx:char _user*; ecx:char _user* user*; edx:char _user* user*; esi:struct pt_regs *; edi:—-。
3、用汇编语言来编写shellcode
通过反编译获得的汇编代码来研究如何用汇编语言编写shellcode。
对示例代码1进行如下操作:
1、编译所编写的代码。
1 | [scz@ /home/scz/src]> gcc -o shellcode –g gdb -static shellcode.c |
2、用gdb调试执行代码,对main进行反汇编。
1 | [scz@ /home/scz/src]> gdb shellcode |
3、对execve函数进行反汇编。
1 | (gdb) disas __execve <-- -- -- 输入 |
4、研究main函数的汇编代码。
1 | 0x80481a0 : pushl %ebp # 保存原来的栈基指针 |
5、研究execve函数的汇编代码。Linux在寄存器里传递它的参数给系统调用,用软件中断跳到kernel模式(int
$0x80)。
1 | 0x804b9b0 <__execve>: pushl %ebx # ebx压栈 |
Shellcode的开发过程:
从上面的分析可以看出,完成execve() 系统调用,我们所要做的不过是这么几项而已:
a) 在内存中有以NULL结尾的字符串"/bin/ksh"
b) 在内存中有”/bin/ksh”的地址,其后是一个 unsigned long 型的NULL值
c) 将0xb拷贝到寄存器EAX中
d) 将”/bin/ksh”的地址拷贝到寄存器EBX中
e) 将”/bin/ksh”地址的地址拷贝到寄存器ECX中
f) 将 NULL 拷贝到寄存器EDX中
g) 执行中断指令int $0x80
如果execve()调用失败的话,程序将继续从堆栈中获取指令并执行,而此时堆栈中的数据是随机的,通常这个程序会coredump。我们希望如果execve调用失败的话,程序可以正常退出,因此我们必须在execve调用后增加一个exit系统调用。它的C语言程序如下:
1 | //shellcode_exit.c |
汇编语言代码如下:
1 | [scz@ /home/scz/src]> gcc -o shellcode_exit -static shellcode_exit.c |
我们可以看到,exit系统调用将0x1放到EAX中(这是它的syscall索引值),将退出码放入EBX中,然后执行”int$0x80”。大部分程序正常退出时返回0值,我们也在EBX中放入0。
现在我们所要完成的工作又增加了三项:
h) 将0x1拷贝到寄存器EAX中
i) 将0x0拷贝到寄存器EBX中
j) 执行中断指令int $0x80
同时,要注意不能出现0x00,要避免中途被截断。理清任务之后,可以很容易地用汇编语言写出shellcode:
1 | section .text |
根据之前int 0x80中断指令调用形式,要求eax存放系统调用号;ebx、ecx、edx分别存放参数部分。
汇编源码中,首先是第4行eax清零;之后第5行压栈;然后第6行,第7行字符串压栈,这样在栈中就构造了以”\x00”结尾的字符串”/bin//sh”。注意这里的“/bin//sh”与“/bin/sh”同样效果。此时的ESP指针指向了这个字符串首地址,第8行将该首地址赋给ebx,这样就有了int 0x80中断指令的第一个参数ebx;第9行中eax入栈,此时eax值还是0;第10行ebx入栈也就是把字符串”/bin//sh”地址入栈,两次压栈,此时栈中就有了字符串地址和一个0,刚好构成了一个指针数组;第11行将该指针数组的地址也就是esp赋给ecx,系统调用的第2个参数ecx中就保持了指针数组的地址;第12行edx清零,刚好是系统调用的第3个参数为零。第13行将系统调用号0xB赋给al,这样可以避免出现坏字符。最后调用软中断指令执行。
4、练习和示例
*FreeBSD 堆栈 ;Linux 寄存器
1、编写如下c语言程序对应的shellcode:
1 | int main() { |
jmp/call方法,FreeBSD
1 | global _start |
2、编写如下c语言程序对应的shellcode:
1 | int main() { |
push方法,FreeBSD
1 | BITS 32 |
3、在Linux环境下分别用jmp/call方法和push方法各写一个汇编程序,完成如下c语言程序所要完成的功能,以便形成对应的shellcode。
1 | int main(void) { |
push方法:
1 | BITS 32 |
jmp/call方法:
1 | BITS 32 |
4、汇编语言直接编写shellcode(jmp/call方法,FreeBSD环境下)执行:
1 | int main() { |
jmp/call方法:
1 | global _start |