首先编译,为了理解执行流和溢出本身本次编译需要把system模块编译出来
练习目的是理解溢出和执行流
一开始需要在main的入口打个断点
pwndbg的信息比较详尽 如果凌乱那么使用gdb即可 考虑到后期需要转换Windows 而pwndbg和windbg的一些命令相同 所以继续用pwndbg
非常直观的看到断点位置

简单理解一下汇编的部分
关于空间分配的部分,见注释1
为了直观看汇编部分
找几个关键点,要完成溢出关注sub实际分配的空间

关注str的实际位置 解释查看注释2
在gets函数调用后打断点查看输入

来查看传入的参数(eax)和此时str变量当中存储的值

查看内存布局

代码顺着执行接下来会走到printf的部分

按照预期来说当程序走到这里会正常输出8个A
打个断点验证

正常看到了这个输出
接下来分支有两条
第一条就是单纯的修改执行流,现在程序的逻辑是收到什么字符串就printf什么字符串,所以接下来的事情,是想要把收到的字符串当作命令执行,也就是理解执行流
重新让程序跑起来
这次跑起来以后不要再输入单纯的字符A
而是输入一个命令比如id

在继续之前先来找找命令执行的相关函数,由于在编译时已经把system编译进去了 可以直接查询

这里找到system相关的地址挑第一个来用do_system不用处理多余的东西直接就可以用
注意在本例当中system和do_system的传参原理虽然不通,但是都通过eax所以接下来的调试过程通用
当前代码在 call printf 处使用的是直接寻址,而不是通过 GOT 表做间接跳转
直接在这里打断点
在 32 位 x86 汇编中,直接调用函数的 call <target> 采用的是相对近调用
也就是这个指令是两个部分
call 指令 + 4字节的一个地址
然后call指令本身所在的地址是0x80498df
所以要修改call function的地址要在call指令+1的位置调整即可
这里要稍微标记一下 call function 这条指令本身不是 call后面的地址本身
而是 这样的一个公式call call指令本身地址-目的函数的相对距离
做个假设 比如 call指令本身的地址是0x1234 目标函数是0x1244那么想要调用目标函数 不是call 0x1244而是 call 0x10(0x1244-0x1234) 但是注意 call 0x1234这个指令本身要占5个字节 也就是说如果直接call 0x10那么是从第五个字节向后算的所以 call的最终公式应该如下(套用0x1234和0x1244)
call ( 0x1244(目的函数地址)-0x1234(call指令地址)-0x5(call指令长度) )
最终如果想要从0x1234的call指令地址成功调用位于0x1244的函数的话这个计算结果就是
call 0xb
之后查看当前调用

然后让他运行下去

中间的报错暂时不用管,看到最后这个地方就说明确实是调用了 do_system 这里报错是%s这个命令没有找到
这里的主要原因是printf 和 do_system 的函数原型与参数解析方式完全不同,看注释3
也就是说我们的command是在%s+4的位置存着 所以重新运行在这里也下一个断点
这里由于传参方式的不同所以需要set的点不一样

然后还需要设置一下 否则会陷入一个循环 输出也不直观
设置 GDB 仅保留在父进程,子进程自由执行并脱离

最终成功调用do_system执行命令
但是这样就发现一个事情完全没有用到get输入的参数,此时这个参数无论是什么都不影响我们后续的部分,反正后续通过eax传参
用于理解执行流
在现代 x86/GCC 中,出于性能优化以及满足 SSE 指令集的 16 字节对齐要求,编译器开辟局部变量空间时,会按照 16 字节(0x10)的倍数向上取整。
也就是说当前的代码
str分配的空间小于0x10 所以向上取证
但是当这个部分大于0x10时比如等于18那么还是会向上取证
编译

注意此处的lea eax, [ebp-0x14]与lea -0x14(%ebp),%eax 在机器码层面完全相同,唯一的区别是汇编代码的语法风格(Syntax Flavor)不同:前者是 Intel 语法,后者是AT&T语法。
printf 是格式化输出函数
system 是命令执行函数
do_system 是 glibc 内部核心执行函数
理解了执行流以后来尝试溢出的部分 , 溢出首先就不能手动去更改执行流 , 而是需要通过输入被动修改执行流
直白的说就是 : 不可能直接调试目标程序 , 所以要从程序的输入当中破坏程序的预期执行流 , 让程序走向我的预期执行流
在基础理解部分已经写出了分配的空间大小 , 以及原因 , 直接看关键部分代码
这里分配了0x10空间大小 , 在gets调用结束后打断点
一个本办法找到当前的内存空间 esp和ebp是栈顶和栈底 那其实只需要查看一下esp 和 ebp 就知道范围
大略过一下地址情况
这个地址ffffcc30很明显是str字符串的存储位置
接下来的ffffcc40 + 8的位置是ebp 那么ffffcc40 + 12的位置是返回地址的位置
查看这个地方 稍微做一下注解

在 Linux C 语言环境中,程序的第一个执行入口并不是 main 函数,而是编译器链接进去的 _start 汇编桩
在 __libc_start_call_main 执行 call main 的瞬间,CPU 硬件机制自动将紧接着的下一条指令地址(即 0x08049e45,也就是 __libc_start_call_main+101)压入栈中作为返回地址(Return Address)。
简单来说在这种情况下 把main函数看做一个普通函数这个函数返回会向着__libc_start_call_main这里去返回
溢出攻击的终极目标是覆盖掉返回地址(Return Address),让程序在 ret 时跳去执行预期的代码。
内存布局如下
所以这个部分的距离计算如下
0xffffcc4c - 0xffffcc34 = 0x18 = 24字节
可以理解一共分配了0x10(16)字节的空间大小那么ebp在0x10 + 4的位置然后返回地址在ebp+4
所以常规状态下 这个距离也可以这样算
分配大小 + ebp(4字节) + 一个到return address的padding 一般为(4字节)
也就是在本例当中0x10(16)+0x4(4)+0x4(4)=0x18(24)也就是从第25个字节开始覆盖到返回地址
按照这个逻辑输入到24个A看到的内存布局应该是A紧连返回地址

来查看这个内存布局 继续向下执行一定是崩溃的
在崩溃之前有一个点需要理解
在这里ffffcc40+0xc这个位置原本的返回地址从08049e45->08049e00这里并不是返回地址变了
从这个变化恰恰应证了溢出是正常覆盖了的00是因为 gets() 会在读取完字符串末尾自动追加一个空字节 \0
综上 尝试28个A时应该会让返回地址变成0x41414141然后由于没有这样的地址所以报错
由于我们gcc编译器版本的原因 所以导致GCC 编译器生成了一种特殊的函数收尾机制,它不是直接 ret,而是:
所以预期如下


但是此刻无法完成溢出执行命令这样的执行流需要用到进阶的技术
目前到这里完成溢出练习
这里题目设计的问题所以这个命令执行是有一点复杂的
精准修复 ECX(Stack Pivoting / 栈迁移变种):
溢出时不能全填 A,必须把 0xffffcc40 位置精准填入一个合法的栈地址(比如原始的 0xffffcc60)。这样 lea esp, [ecx-4] 算出来的还是合法栈顶,程序就能活着执行 ret。
构造标准的 ret2libc 栈结构: 在 ret 后面不能只填函数地址,必须按照 32 位函数调用的栈格式来排布:
按照这样的payload结构如下
注意这里存在一个问题 : 硬编码栈地址的局限性
也就是说接下来我所有的操作都只针对我当前的环境和我本次调试
但是理论是正确的 , 理解理论是可以完成同样的操作的
如果按照这个payload 去写一个脚本 生成bin文件加载进去会发现报错 这个payload的理论只是我们推演的雏形 , 是有问题的 按照这个思路来说 , 写出来的payload大概这样

但是 , 这样去加载就会发现有问题
一切如预期 但是崩溃了

这里存在的问题有几个
大量文字来袭
1.栈帧布局错位
在内存中,SYSTEM_ADDR 之后紧接着写入了 EAX_ADDR(0xffffcc34)当 ret 跳转到 system 时,system 内部从栈顶读取数据:
system 接收到的参数指针不是 "id;",而是错误的垃圾地址,导致命令无法执行。