一道很有趣的题目 原理基础ret2shellcode 但是考查了汇编代码的阅读以及栈结构的理解 还有内平栈
涉及到的知识点还是很多的 这里讲细一点
内平栈
内平栈(In—Place Stack)是一种特殊的栈,可以在一个给定的数组中存储元素,并可以支持压栈和弹栈操作,且不需要额外的内存空间。保持堆栈平衡的关键是使用一个变量(称为指针,即esp)来跟踪堆栈顶部的位置。当元素被压入堆栈时,指针会指向新元素,表示它是当前堆栈的顶部。当元素被弹出堆栈时,指针会移动到上一个元素的位置,表示它是当前堆栈的新顶部。这样,指针就可以在堆栈操作过程中保持堆栈平衡。
外平栈
外平栈(External Stack)是一种特殊的栈,可以存储元素,并可以支持压栈和弹栈操作,但需要额外的内存空间。与内平栈不同,外平栈不使用指针来跟踪堆栈顶部的位置,而是使用两个变量:一个用于标识堆栈顶部的位置(esp),另一个用于标识堆栈底部的位置(ebp)。当元素被压入堆栈时,顶部变量会指向新元素,表示它是当前堆栈的顶部。当元素被弹出堆栈时,底部变量会指向上一个元素的位置,表示它是当前堆栈的新底部。这样,两个变量就可以在堆栈操作过程中保持堆栈平衡。
程序分析
checksec看一下保护机制
[*] '/home/chen/pwn' |
保护机制全部关闭 再ida看一下
.text:08048060 public _start |
应该不是用c语言编译的 很干净 只有一个exit函数和start函数 反编译后看不出什么所以然
并且可以看到没有ebp指针 说明这题是内平栈 是利用esp指针来控制程序执行流
__int64 start() |
这里直接解读汇编吧
.text:08048060 push esp |
前面6行对这个栈帧进行了初始化的操作 push入栈了esp 接着入栈了返回地址为exit函数
随后对于四个寄存器进行了归零操作
.text:0804806E push 3A465443h |
16进制转化成人话就是: Let’s start the CTF:
这里的五次push 使得esp指向减少了0x14
此时的栈帧结构为:
.text:08048087 mov ecx, esp ; addr |
接着把esp所指向的内存空间赋值给了ecx 这里可以看到ida识别出来了 其被作为一个函数的addr参数 这个函数是什么先别急 我们一行一行看
这里出现了一个新的寄存器名称 为什么只是dl? 并且在ida中我们选中其 会和edx一起关联起来 二者有什么关系?
32位CPU有4个32位的通用寄存器EAX、EBX、ECX和EDX。对低16位数据的存取,不会影响高16位的数据。这些低16位寄存器分别命名为:AX、BX、CX和DX,它和先前的CPU中的寄存器相一致。 |
相当于给这个函数赋值了长度参数(ida识别出来的len)
那么接下来的bl寄存器同理 相当于ebx的低8位 赋值为了1
重点来了 eax的低八位被赋值为了4 随后进行了int 0x80 也就是系统调用
那么我们查询一下调用号为4的函数是什么 发现是write函数
(5条消息) linux 系统调用号表_Anciety的博客-CSDN博客_系统调用号
那么上述汇编代码翻译成c语言也就是
write(1,esp,0x14) |
所以 相当于输出了上面push入栈的那0x14字节的数据 也就是输出了相应的字符串
但是这里注意一下 栈仍然是不变的 此时栈的结构和esp指针仍然是保留原状(即上文的图所示)
接下来再看
.text:08048091 xor ebx, ebx |
赋值了两个参数 edx(参数len)为0x3c eax(系统调用号)为3 还有xor ebx,ebx相当于ebx赋为0
调用号3是read函数 也就是
read(0,esp,0x3c) |
所以我们可以在esp指针指向的地方 输入0x3c字节数据
接着看剩下的代码
.text:08048099 add esp, 14h |
使esp寄存器的值增加0x14 此时esp指向的是存放offset _exit地址的空间
随后retn指令相当于pop eip 也就是控制程序执行流到exit函数 进行一个退出
在retn过后 此时栈帧就只剩下一个old esp了
分析完了程序 那么我们来想一下利用的思路
我们拥有栈溢出的机会 并且得知了具体哪一块栈空间可以用来控制程序执行流 并且保护全关
没有system函数也没有泄露libc的机会 也没有足够的栈溢出长度来供我们使用onegadget生成的shellcode直接pwn
那么这时候想到的是ret2shellcode 我们只能在栈上写入shellcode 但是没有bss段供我们写入的机会 所以只能写在栈上
那么要做的就是泄露栈上的地址 想一下 程序唯一能够利用输出的地方也就是write函数那边了
.text:08048087 mov ecx, esp ; addr |
看到第一个 mov ecx,esp 我们再联想到 retn指令执行完了以后 栈帧只剩下了old esp 并且此时esp仍然指向old esp 此时如果执行
这一行指令 那么ecx的值就会被赋为old esp 也就是程序会输出old esp 也就是泄露了栈地址
所以我们第一次写入的payload为
payload = cyclic(0x14)+p32(0x8048087) |
此时程序就会输出old esp 那么接下来又要怎么利用
我们知道 控制程序执行流利用的是retn这个指令 但是在其生效前 esp指针会被抬高0x14字节
所以我们需要0x14字节的垃圾数据 然后再往栈上写入shellcode存放的地址 然后控制程序执行流到shellcode
这一系列的操作用图演示为;
所以此时eip接收到了stack_addr(我们泄露的栈地址)+0x14
其寻址到shllcode的栈空间 于是我们就获取了shell
完整的exp:
from pwn import* |