前言
逆向工程(RE)与漏洞利用(Pwn)看似是两个独立的技术方向,但核心都围绕「理解程序执行逻辑」与「控制程序运行流程」展开
文章前半部分以理论先行,后程以实战为导向,不讲冗余理论,快速掌握逆向控制流劫持的核心技巧,轻松实现程序校验绕过
其实操作起来是很简单的,理论是为了更好厘清逻辑,如果觉得比较难理解,可以先看实操演示
Pwn栈原理
接下来我要先引入一个概念,在我们学的ret2libc1中,32位程序下,在编写EXP时,payload的system地址和/bin/sh地址之间是要填入一串四字节的垃圾数据的。看似随意的一个操作,其实在正常而非攻击的32位程序的情况下,是call一个函数之后,执行结束后的返回地址。比如我执行完system函数,我要返回main函数,main函数的地址就写在那
只不过,我们在攻击时,已经获取了shell之后,至于原程序,要不要返回,返回到哪,我们不需要管,爱去哪去哪,填入这四个字节的数据,只是为了将我后面/bin/sh的地址放在正确的位置上,而不是放在返回地址上而已
# 示例1:32位 re2libc1 EXP 核心片段(需填充4字节垃圾数据)
from pwn import *
p = process("./re2libc1")
system_addr = 0x08048400 # 示例system地址
binsh_addr = 0x0804A020 # 示例/bin/sh地址
# payload = 垃圾数据(4字节) + system地址 + 垃圾数据(返回地址) + /bin/sh地址
payload = b'A'*4 + p32(system_addr) + b'B'*4 + p32(binsh_addr)
p.sendline(payload)
p.interactive()
刚刚说的是32位的程序,那么64位的程序为什么不用写那串返回地址了呢。是因为64位的程序,system执行时的返回地址,是system_addr下一个栈位置的内容,我们用不到,自然不用写
# 示例2:64位程序 EXP 核心片段(无需填充4字节返回地址)
from pwn import *
p = process("./re2libc1_64")
system_addr = 0x400500 # 示例system地址
binsh_addr = 0x400600 # 示例/bin/sh地址
# 64位通过rdi传参,直接构造rop链,无需填充返回地址
payload = b'A'*0x18 + p64(0x400700) + p64(binsh_addr) + p64(system_addr)
p.sendline(payload)
p.interactive()
Payload 构造对比
理解完这个内容之后,我们会容易理解逆向中的控制流劫持
Re
刚刚了解了程序的返回,那么执行的流程是怎么样的呢?
- 函数执行到末尾,会遇到
ret指令(这是函数返回的触发指令); - 执行
ret指令时,CPU 会做两件事:
- 第一步:从栈顶弹出「返回地址」(这个地址之前被
call指令压入栈); - 第二步:把弹出的返回地址直接写入 RIP 寄存器;
- 此时 RIP 指向原流程的指令地址,程序跳回之前的执行流程,完成 “返回”。
返回地址平时 “待在栈里”,只有返回的瞬间才会临时进入 RIP—— 没有任何寄存器会长期存储它,栈才是它的存储区,这一点 32 位和 64 位程序完全一致。
如果我们能知道这一点,那么我们就可以控制程序要返回的地址
- 在
ret指令前下断点,运行到断点处; - 查看栈顶返回地址:64 位用
rsp、32 位用esp,栈顶(*$rsp)存储的就是 “校验函数执行完后要返回的地址”(比如原返回地址是 0x401000,对应 “校验失败提示” 的代码); - 修改栈顶返回地址:把它改成核心功能代码的起始地址(比如 0x401500,对应 “程序主功能” 的入口);
- 继续执行,程序会跳过原返回流程,直接进入核心功能 —— 相当于绕开了校验。
原理是这样,那么具体要怎么做呢
结合我们之前学过的控制流劫持,我们可以做到控制程序的走向
IDA动态调试–控制流劫持
之前讲过,在我们在控制程序的跳转中,在跳转前将目标地址压入RIP寄存器,这样就能跳转到我们想要的地址。
ret本身也是一种跳转,因此也符合这样原理,只不过,在跳转前,函数的返回地址是先从其他寄存器转来RIP的,64 位是rsp、32 位是esp。我们提前修改这里的地址,就可以达到想要的效果。
那现在,思路打开,我既然可以控制程序走向,是不是可以绕过检测,破解程序了。想要永久破解程序,我们最基础的方法就是改机器码。
控制程序
现在你用 IDA 打开了一个可怜的64位程序,它有一个校验函数,此时我们要绕过这个函数,直接使用程序的功能
控制跳转
我们需要什么?
cmp eax, 0x1(对比校验代码)0x401020: jne 0x401080(失败跳转到退出),其中0x401020是jne的地址,0x401080是 “校验失败” 地址0x401050是 “核心功能” 地址jne到核心功能的偏移量
很显然,我们需要:1. 让跳转变为短地址无条件跳转,2. 让跳转的地址变成 “核心功能” 的地址
这里要补充两个知识点:
1.在 PE 程序里,汇编指令会有对应的机器码,由操作码和操作数组成,在 IDA 中可以看到其 16 进制代码。例如,XOR EAX, EAX对应的机器码省略 0x,就是33 C0,其中33是操作码,表示XOR
2.Windows的小程序正常情况下都是采用小端序规则存储机器码的,简单来说,程序是从高地址向低地址读码的,例如E9 7B 00 00 00,程序是从右边往左边读的,所以E9后面的地址是0x0000007B
现在进入正题:0x401020: jne 0x401080的机器码是这样的:75 5E。拆解一下,75是jne的操作码,跳转指令后面跟的是偏移量,5E就是从0x401020 +2到0x401080的 1 字节偏移量,+2的原因就是因为jne这个指令本身占2字节,从jne后面开始计算偏移量
如果要改成跳转到核心功能0x401050,先算偏移:0x401050 - (0x401020 + 2) = 0x2E。现在只需把0x401020地址处的机器码75 5E替换成EB 2E,(EB是短跳转jmp的操作码,2E是偏移)就能直接跳去核心功能,绕过校验
x86/x64
75 5E
jne 0x401080
EB 2E
jmp 0x401050
目标(0x401050) – (当前(0x401020) + 2) = 0x2E
如果核心功能地址和 jne 地址的偏移超过 ±127(比如要跳0x401100),则改用长跳转:
- 长跳转公式:
偏移量 = 目标地址 - (当前指令地址 + 5)(E9 长跳转占 5 字节); - 机器码格式:
E9 xx xx xx xx(E9是 jmp 长跳转操作码,后 4 字节是小端序补码偏移)。
比如要从0x401020跳0x401100(长跳转):
- 偏移计算:
0x401100 - (0x401020 + 5) = 0x7B; - 机器码:
E9 7B 00 00 00
那么如果是往回跳呢?例如:0x401080 :jne 指令要跳到0x401050,还是先算偏移0x401050 - (0x401080 + 2) = -0x32,然后用0xFF + 1 - 0x32 = 0xCE,机器码为EB CE,+1因为是用高一位0x100来减,也就是0xff+1。
长跳转回跳呢?比如要跳0x400F00,4 字节补码计算规则为0x100000000 - 绝对值(0x100000000 是 4 字节的模),即0x100000000 - 0x185 = 0xFFFFFE7B(小端序7B FE FF FF),因此长跳转机器码为E9 7B FE FF FF
你说算偏移量太麻烦了,有没有简单的方法。那是当然!
控制返回
假如0x401090 处是 ret 指令(机器码C3),原本执行后会跳回0x401000(校验失败地址)。我们可以替换这个ret指令,我们通过push 目标地址 + ret的组合,直接写死目标地址,实现直接跳转到核心功能——最大的好处是无需计算偏移量
32位中,如果要让ret跳转到核心功能地址0x401050,只需把0x401090处的C3替换为机器码:68 50 10 40 00 C3。
- C3 是ret指令的操作码;
- 68 是push imm32的操作码,作用是把后面4字节的地址(小端序50 10 40 00,对应0x00401050)压入栈;
64位有点区别,需要替换为:48 68 50 10 40 00 00 00 00 00 C3
- 48 68是push imm64的操作码,(48是64位的模式前缀,68是推送操作码)将后8字节的地址压入栈
实操演示
下面将展示我破解我自己的64位程序,为了方便演示,我将程序逆向后的可视化优化到最好,几乎IDA看到的就是源码。

可以清晰地看出程序的逻辑,一个对比函数cmp,以及密码正确和错误跳转到的函数ok和err。简单地模拟了一个程序的运行流程,现在我们要破解这个程序,也就是让它永远执行ok函数。

在cmp判断处进入汇编界面,可以看到,call cmp对比后错误就会jnz到call err,我们想执行的ok在jnz下面一行。
接下来先展示用动态调试进行控制流劫持方法(64位程序):在jnz跳转处下断点,并且记住ok函数地址

进入动态调试,随便输入内容,修改RIP寄存器为ok函数地址

修改完后不要忘记,在左上角Edit – Patch program – Apply patches to应用修改的内容。在运行程序,就可以发现我们成功改变了程序运行逻辑。
但是动调调试只能临时改变逻辑,如果想永久破解程序,我们就要对机器码下手。

方法1:直接nop掉jne前两字节的机器码,这样程序就不会触发跳转,直接向下执行ok函数


方法2:计算偏移量,将机器码patch成EB 00,Edit – Patch program – Apply patches to应用修改的内容,这样就无条件跳转到下一条指令call ok


当然,如果有retn指令可用,也是可以用它来控制的
免责声明
本文所涉及的逆向工程技术、代码示例、调试方法等内容,仅用于逆向工程学习研究、软件安全技术交流及合法的程序漏洞分析
作者编写本文的核心目的是分享技术知识,帮助学习者理解程序执行逻辑与安全防护原理。任何个人或组织不得将本文中的技术、方法用于破解商业软件、入侵他人系统
对于擅自利用本文内容从事非法活动所产生的一切法律责任、经济损失及不良后果,均由行为人自行承担,本文作者不承担任何连带责任。
请所有读者遵守法律法规和行业道德,尊重软件著作权,共同维护健康的网络安全环境




