# 堆栈不平衡

通常是由于函数调用和返回过程中堆栈操作的不匹配。每当调用一个函数时,程序会将当前的栈指针(SP)和返回地址压入堆栈,这会影响到后续的堆栈操作。

例如,假设一个函数调用 func ,在进入 func 时,栈指针会增加(即压入返回地址)。如果在 func 中未正确地平衡堆栈(比如未能弹出相应的值),那么在返回时,栈指针可能会指向一个不正确的位置,导致堆栈不平衡。

至于栈指针的修改,可以通过调整指令来确保调用的函数有正确的返回地址。例如,修改 call 指令的目标地址为 0 ,可以防止调用某个函数,这在某些逆向工程中是用于跳过不必要的函数调用或陷阱,达到对程序行为的控制。

简单来说,理解堆栈的操作顺序和调用约定是非常重要的。通过反汇编工具分析这些操作,可以更好地理解程序的执行流程和潜在的漏洞。你可以从调试程序,观察栈的变化入手,加深对这些概念的理解。

要深入理解堆栈不平衡的问题,我们可以从堆栈的工作原理和汇编代码角度进行分析。

# 1. 堆栈的基本概念

堆栈是一种后进先出(LIFO)的数据结构,用于存储函数调用的信息,包括返回地址、局部变量和参数。堆栈的顶端通常由栈指针(SP)指向。

# 2. 函数调用的过程

当一个函数被调用时,会执行以下步骤:

  1. 保存返回地址:调用指令(如 call func )会将当前指令的下一条地址压入堆栈。
  2. 调整栈指针:栈指针会减少(在 x86 架构中,栈向下增长)。
  3. 进入函数体:函数内部可以通过栈来存储局部变量。

# 示例代码

push ebp           ; 保存旧的基指针
mov ebp, esp      ; 设置新的基指针
sub esp, 16       ; 为局部变量分配空间

; 函数体

mov esp, ebp      ; 恢复栈指针
pop ebp           ; 恢复旧的基指针
ret                ; 返回

# 3. 堆栈不平衡的原因

如果在函数中未正确弹出压入的值,或者在返回之前没有恢复堆栈状态,就会导致堆栈不平衡。比如,如果你在函数内部多次压入值而没有相应的弹出,堆栈将继续增长,最终导致溢出。

# 4. 栈指针的修改

栈指针的修改通常涉及 pushpop 指令。例如,如果需要跳过某个函数的执行,可以将 call 指令的目标地址更改为 0 ,这样不会实际调用任何函数,从而避免栈指针的进一步修改。

# 伪代码示例

call 0x0         ; 不会改变堆栈状态

# 5. 可视化结构图

[ ESP ]   -->  | Return Address | <-- call func
               | Local Var 1    |
               | Local Var 2    |
               | ...            |
               | Old EBP        |
[ EBP ]   -->  | Base Pointer   |

# 6. 逆向工程的实用技巧

  • 使用调试工具:例如 GDB 或 x64dbg,逐步执行代码,观察堆栈的变化。
  • 分析调用约定:了解不同平台(如 x86 和 x64)的调用约定,这对逆向非常重要。
  • 编写脚本:可以使用 Python 等语言结合工具(如 IDA Pro 或 Ghidra)自动化分析任务。

掌握这些基础知识后,继续深入学习汇编语言和计算机体系结构将有助于你成为真正的逆向工程大师!

# 布栈

教我做一道 CTF 中的 pwn 题: #!/usr/bin/env python from pwn import * sh = process ('./ret2libc1') binsh_addr = 0x8048720 system_plt = 0x08048460 payload = flat ([b'a' * 112, system_plt, b'b' * 4, binsh_addr]) sh.sendline (payload) sh.interactive () 这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以 'bbbb' 作为虚假的地址,其后参数对应的参数内容。 你看他布栈的时候,为什么要 system_plt 后面要布一个虚假的地址,然后才是参数呢,这里没太懂

这道题目是典型的 ret2libc 攻击 (Return-to-libc),目标是通过覆盖栈上的返回地址,绕过程序的正常流程,转而调用 system() 函数来执行命令(在这个例子中是 /bin/sh )。为了理解为什么 system_plt 后面需要填充一个虚假的地址( b'b' * 4 ),我们需要详细分析下栈结构和函数调用的过程。

# 背景知识

x86 架构的程序中,函数调用的基本过程如下:

  1. 压栈:当一个函数被调用时,程序会将当前的 返回地址 压入栈中,这个返回地址是程序调用该函数后,程序应该跳转回来的地址。
  2. 参数传递:在栈上,函数的参数会紧随返回地址之后依次被压入栈中。
  3. 栈帧结构:每次函数调用时,都会为该函数分配一个栈帧,栈帧中包含:
    • 返回地址:指向调用函数之后的代码位置。
    • 参数:该函数的参数。

# 攻击思路

在这道题中,我们要利用程序中的漏洞,覆盖栈上的返回地址,使得程序在执行 ret (返回指令)时跳到我们指定的地址,也就是 system() 函数的地址。

# 分析代码

#!/usr/bin/env python
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
  1. binsh_addr = 0x8048720 :这是我们要传给 system() 函数的参数地址,也就是 /bin/sh 字符串的地址。 system() 函数需要一个字符串参数,在这里我们传递的是 /bin/sh 的地址, system() 会执行该命令,启动一个 shell。

  2. system_plt = 0x08048460 :这是 system 函数的地址,在程序中的 PLT(Procedure Linkage Table) 区域。PLT 是程序用来调用动态库函数的一个表,每当程序要调用动态库中的函数时,都会先跳转到 PLT 处,由 PLT 去调用函数的实际地址。

  3. payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])

    • b'a' * 112 :这里填充的是 112 个字节 a ,这是覆盖栈帧到返回地址的部分。程序会首先执行一些前期的操作(例如栈的初始化、局部变量等),然后遇到返回地址。因为栈的布局是从高地址到低地址的,所以我们填充 a 字符来覆盖返回地址的空间,确保在覆盖掉返回地址后,后面的内容是我们想要填充的内容。
    • system_plt :这是我们覆盖的返回地址。当栈上的返回地址被覆盖为 system_plt 后,程序将跳转到 system() 函数。这个 system_plt 的地址是硬编码的,它会让程序跳转到 system() 的实际地址。注意,这个地址是 PLT 表中的地址,在动态链接的程序中,PLT 是一个跳转表,用于跳转到 system 函数的实际实现地址。
    • b'b' * 4 :这是一个 虚假的返回地址,也就是说这是我们填充在 system_plt 后的一个占位符。这个部分的大小是 4 字节(通常是 x86 架构的指针大小)。为什么要填充这个虚假的返回地址呢?因为 system() 函数的调用约定是需要一个参数(即 /bin/sh 的地址)。这个虚假的返回地址用于模拟栈上 system() 函数执行完之后返回的地址。我们这样做是为了让栈结构保持正确,在调用 system() 时,栈帧结构要完整,所以我们需要一个假的返回地址来确保 system() 执行完成后能正确返回。实际上,在这道题中,虚假的地址 b'b' * 4 会被 system() 执行后丢弃。
    • binsh_addr :这是我们传递给 system() 函数的参数 /bin/sh 字符串的地址。 system() 函数需要一个字符串作为参数,这个参数是 system() 在执行时使用的命令,告诉它启动一个 shell。
  4. sh.sendline(payload) :这一行是将构造好的 payload 发送到程序中,触发栈的溢出和返回地址的修改。

  5. sh.interactive() :执行完 payload 后,程序将进入交互模式,让我们与程序进行交互,通常在这种 CTF 题目中,这意味着我们已经成功地获得了一个 shell,可以执行命令。

# 为什么要填充虚假的地址?

在调用 system() 函数时,通常在栈上需要有一个返回地址,指示函数执行完之后应该跳转到哪里。由于 system() 是一个库函数,它执行完成后并不会返回到一个特定的地址,而是会根据调用栈返回到调用它的地方。但是,由于我们要用 ret2libc 攻击来绕过正常的程序流程,我们通过修改返回地址,使得程序跳转到 system() 函数。但是 system() 函数的调用约定仍然需要栈上有一个正确的返回地址(即调用 system() 后程序会跳回的地址)。

  • 由于 system() 在执行后会返回,我们需要一个虚假的返回地址来填补栈上的空隙,确保栈帧结构完整。
  • 我们用 b'b' * 4 来填充这个虚假的返回地址。这里的 4 字节是因为在 x86 架构中,返回地址是一个指针,占用 4 字节。

# 总结

  • system_plt 是我们想跳转到的地址,即 system() 函数的入口。
  • 虚假的返回地址 ( b'b' * 4 ) 用于保证栈帧的完整性,它只是一个占位符。 system() 执行后会跳过这个虚假的地址,并且返回到 system() 函数的调用者。
  • binsh_addr 是传递给 system() 函数的参数,即 /bin/sh 的地址, system() 会执行它,启动一个 shell。

这个过程通过巧妙地控制栈上的返回地址,利用系统中已经存在的库函数来执行恶意代码,从而达到绕过程序控制并获取 shell 的目的。