字符串、交叉引用与控制流
字符串、交叉引用与控制流
本文适合
刚开始用 IDA/Ghidra 做逆向、容易在反编译代码里迷路的新手。学完你能:从字符串定位关键函数,用交叉引用找到成功失败分支,并把控制流还原成可验证的判断链。
逆向入门不是一上来就读完整汇编。更现实的入口是先找程序里已经存在的文本,再顺着这些文本找到代码位置,最后理解程序如何决定成功或失败。
一句话判断
当程序里能找到成功提示、失败提示、输入提示、关键常量或可疑字符串时,先查这些字符串的交叉引用,再顺着控制流回到真正的校验逻辑。
逆向的入口常常不是 main,而是离 Correct/Wrong/flag/password 最近的分支。
题目中常见信号
strings或 Strings 窗口出现Correct、Wrong、success、flag、password、serial。- 伪代码里有多个提示字符串,但不知道哪个函数负责校验。
- 程序没有明显源码符号,只剩一堆
sub_401000。 - 成功和失败文本出现在同一个函数或相邻基本块。
- 搜不到明文 flag,但能搜到输入提示或错误提示。
- 动态调试不知道断哪里,需要静态定位断点。
核心概念
字符串是静态线索,交叉引用是从线索回到代码的路标,控制流是代码如何走向成功或失败的地图。
做 CTF 逆向时要按这三层推进:
- 找线索:字符串、导入函数、错误提示、菜单项、协议字段。
- 找引用:谁读取了这个字符串,在哪个函数里使用。
- 找分支:字符串附近的
if/else、跳转、返回值和循环比较。
真正要还原的往往不是提示字符串本身,而是它前面的条件表达式。
最小分析流程
- 用
file和strings -a做第一轮观察。 - 在 IDA/Ghidra 打开 Strings,筛选成功、失败、输入、flag、key、secret 等关键词。
- 对关键字符串查看 xref,跳到引用点所在函数。
- 从字符串引用点向上找最近的条件判断、函数调用和返回值检查。
- 标注成功分支、失败分支和校验函数,重命名关键函数。
- 动态调试时在校验函数入口、比较函数、条件跳转处下断点验证路径。
最小验证示例
静态定位:
strings -a ./challenge | grep -Ei "correct|wrong|flag|password|success"假设看到 Correct!。在 IDA 中:
Shift+F12 打开 Strings
双击 Correct!
按 X 查看交叉引用
跳到引用函数
向上找 if (check(input)) 或 call sub_xxx动态验证:
gdb ./challenge
break *0x4012ab # 条件跳转或 check 调用地址
run
x/10i $rip如果换不同输入会让同一个条件跳转走不同方向,说明这里就是关键控制流点。
常见利用 / 解题路线
路线总览:
- 成功字符串回溯路线:从
Correct/success的 xref 向上找判断条件。 - 失败字符串回溯路线:从
Wrong/fail的 xref 找到失败跳转,再取反得到成功条件。 - 输入函数前推路线:从
scanf/read/fgets的 xref 追输入缓冲区后续使用点。 - 比较函数路线:从
strcmp/memcmp调用点看参数,确认目标字节或目标字符串。 - 控制流重命名路线:把
sub_401000重命名为check_input、decode_table,降低后续阅读成本。 - 动静结合路线:静态找断点,动态用两组输入验证跳转和数据流。
字符串是什么
程序中经常会保存一些固定文本,例如:
Input password:
Wrong
Correct
flag这些文本可能出现在 .rodata、资源段、字符串池或运行时构造逻辑里。
字符串能告诉你程序大概有哪些功能,也能告诉你哪里可能是校验入口。
交叉引用是什么
交叉引用通常叫 xref。它回答一个问题:这个字符串、函数或变量在哪里被使用。
例如程序里有字符串 Correct,交叉引用可能指向一段代码:
if (check(input)) {
puts("Correct");
} else {
puts("Wrong");
}这时真正重要的不是 Correct 这个字符串,而是它前面的 check(input)。
控制流是什么
控制流表示程序执行路径如何变化。常见控制流包括:
- 顺序执行。
- 条件分支。
- 循环。
- 函数调用。
- 异常跳转。
逆向题里最常见的结构是“输入 -> 处理 -> 比较 -> 成功或失败”。
你要找到的通常不是 flag 字符串本身,而是把输入变成判断结果的那段逻辑。
成功分支和失败分支
看到 Wrong 和 Correct 时,先判断哪个分支是成功路径。
如果成功路径里有打印 flag、解密数据或进入下一关,那它就是重点。
如果成功路径只打印一句话,而真正 flag 在前面已经被计算出来,就要回到分支条件前继续追。
静态和动态要配合
静态分析负责看结构:字符串、函数、分支、常量、算法。
动态分析负责验证猜想:输入不同内容时,程序走到哪里,比较了什么,寄存器和内存里有什么。
只看静态容易误读伪代码。只看动态又可能不知道该断在哪里。
新手最该掌握的顺序
先看文件类型和架构。
再看字符串。
然后找关键字符串的交叉引用。
接着找到成功失败分支。
最后把校验逻辑翻译成可复现的计算过程。
IDA 交叉引用类型
IDA 中有多种交叉引用类型:
代码交叉引用(Code xref):
- Call(调用):函数调用时产生
- Jump(跳转):条件/无条件跳转
- Fall through(顺序执行):代码顺序流
数据交叉引用(Data xref):
- Read(读取):数据被读取
- Write(写入):数据被修改
- Offset(偏移):地址引用查看交叉引用
# IDA 快捷键:
# X:查看当前项的交叉引用
# Ctrl+X:在 IDA View 中查看
# IDAPython 脚本获取交叉引用
import idautils
import idc
def get_xrefs(addr):
"""获取地址的所有交叉引用"""
xrefs = []
for xref in idautils.XrefsTo(addr):
xrefs.append({
'from': xref.frm,
'type': xref.type,
'iscode': xref.iscode
})
return xrefs
# 查看字符串的交叉引用
def find_string_refs(string_addr):
"""查找引用特定字符串的代码"""
refs = []
for xref in idautils.XrefsTo(string_addr):
func_addr = idc.get_func_attr(xref.frm, idc.FUNCATTR_START)
refs.append({
'addr': xref.frm,
'func': func_addr
})
return refs控制流图恢复
# 使用 IDAPython 恢复控制流图
import idaapi
import idautils
def build_cfg(func_addr):
"""构建函数的控制流图"""
func = idaapi.get_func(func_addr)
if not func:
return None
cfg = {}
flowchart = idaapi.FlowChart(func)
for block in flowchart:
cfg[block.start_ea] = {
'end': block.end_ea,
'succs': [succ.start_ea for succ in block.succs()],
'preds': [pred.start_ea for pred in block.preds()]
}
return cfg
# 使用示例
cfg = build_cfg(0x401000)
for addr, info in cfg.items():
print(f"Block {addr:#x}:")
print(f" Successors: {[hex(s) for s in info['succs']]}")
print(f" Predecessors: {[hex(p) for p in info['preds']]}")Switch 表识别
# IDA 中的 switch 表识别
# IDA 通常会自动识别 switch 语句
# 但有时需要手动确认
def analyze_switch(addr):
"""分析 switch 语句"""
# 获取 switch 信息
switch_info = idaapi.get_switch_info(addr)
if switch_info:
print(f"Switch at {addr:#x}")
print(f" Cases: {switch_info.ncases}")
print(f" Default: {switch_info.defjump:#x}")
# 获取 case 表
cases = []
for i in range(switch_info.ncases):
case_addr = switch_info.jumps[i]
cases.append(case_addr)
return cases
return None
# Ghidra 中分析 switch
# 使用 SwitchAnalysisCmd 或手动查看跳转表数据流分析
# 追踪数据在程序中的流动
# 1. 正向分析:从输入到输出
# 2. 反向分析:从输出到输入
def forward_taint_analysis(start_addr, reg):
"""
正向污点分析
追踪寄存器/变量的传播
"""
visited = set()
queue = [(start_addr, reg)]
while queue:
addr, reg = queue.pop(0)
if addr in visited:
continue
visited.add(addr)
# 分析当前指令
# 看 reg 是否被使用、修改、传播
# 如果 reg 被写入内存,追踪内存位置
# 如果 reg 被用于计算,追踪结果寄存器
# 如果 reg 被用于条件跳转,标记分支
return visited
def backward_taint_analysis(target_addr, reg):
"""
反向污点分析
追踪影响目标值的来源
"""
visited = set()
queue = [(target_addr, reg)]
while queue:
addr, reg = queue.pop(0)
if addr in visited:
continue
visited.add(addr)
# 反向分析
# 找到谁最后修改了 reg
# 追踪来源(输入、计算、内存等)
return visitedIDAPython 实用脚本
import idaapi
import idc
import idautils
def find_all_strings():
"""查找所有字符串"""
strings = []
for s in idautils.Strings():
strings.append({
'addr': s.ea,
'string': str(s),
'length': s.length
})
return strings
def find_interesting_strings():
"""查找可能有趣的字符串"""
interesting = []
keywords = ['flag', 'password', 'key', 'secret', 'correct', 'wrong',
'success', 'fail', 'admin', 'login', 'auth']
for s in idautils.Strings():
s_str = str(s).lower()
for kw in keywords:
if kw in s_str:
interesting.append({
'addr': s.ea,
'string': str(s),
'keyword': kw
})
break
return interesting
def rename_functions():
"""根据字符串重命名函数"""
# 找到引用特定字符串的函数
for s in idautils.Strings():
s_str = str(s)
if 'flag' in s_str.lower():
xrefs = list(idautils.XrefsTo(s.ea))
for xref in xrefs:
func = idaapi.get_func(xref.frm)
if func:
idc.set_name(func.start_ea, "check_flag",
idc.SN_CHECK)Ghidra 脚本辅助
# Ghidra Python 脚本
# 在 Ghidra Script Manager 中运行
from ghidra.program.model.listing import CodeUnit
from ghidra.program.model.symbol import SymbolType
def find_strings_ghidra():
"""在 Ghidra 中查找字符串"""
listing = currentProgram.getListing()
strings = []
data = listing.getDefinedData(True)
for d in data:
if d.getDataType().getName().startswith("string"):
addr = d.getAddress()
value = d.getValue()
strings.append((addr, value))
return strings
def trace_user_input(start_addr):
"""追踪用户输入的流向"""
import idautils
import idc
# 找到 scanf/getchar/gets 等输入函数的调用
input_funcs = ['scanf', 'gets', 'getchar', 'fgets', 'read']
call_addrs = []
for func_name in input_funcs:
func_addr = idc.get_name_ea_simple(func_name)
if func_addr != idc.BADADDR:
for xref in idautils.XrefsTo(func_addr):
call_addrs.append(xref.frm)
# 从每个输入调用点追踪数据流
for call_addr in call_addrs:
# 获取输入存储的目标地址(通常是第一个参数之后的参数)
# 在调用之后追踪对目标地址的读写
next_addr = idc.next_head(call_addr)
while next_addr != idc.BADADDR:
# 检查是否使用了输入缓冲区
disasm = idc.GetDisasm(next_addr)
if 'cmp' in disasm or 'test' in disasm:
print(f"[+] Input compared at {next_addr:#x}: {disasm}")
next_addr = idc.next_head(next_addr)
# 限制追踪深度
if next_addr - call_addr > 0x100:
break静态与动态分析配合
# 静态分析提供结构,动态分析验证猜想
# 步骤 1:静态分析找关键点
# - 字符串位置
# - 函数调用关系
# - 可疑代码段
# 步骤 2:动态分析验证
# - 设置断点
# - 观察寄存器/内存
# - 追踪执行路径
# 步骤 3:迭代分析
# - 根据动态结果修正静态分析
# - 发现新的分析点
# GDB + pwndbg 配合 IDA
# 在 IDA 中确定断点地址
# 在 GDB 中设置断点并运行
# 观察实际行为常见失败原因
- 看到
flag{...}就认为一定是真 flag:CTF 逆向里常有假 flag,要看它是否在成功路径中被使用。 - 只看反编译代码:优化、混淆和类型错误会让伪代码误导,关键分支要回到汇编确认。
- 找到
Wrong就停住:Wrong前面的条件跳转才是重点,失败路径可以反推出成功条件。 - 把所有变换都叫“加密”:区分编码、异或、移位、哈希、查表和真正密码算法,逆运算不同。
- 没有用不同输入验证:至少用
aaaa和接近目标的输入跑两次,看控制流是否按理解变化。 - 忽略运行时构造字符串:静态搜不到关键字时,断在输出函数或内存写入点观察运行时内容。
迷你案例
程序输出 try again。strings 搜到:
Input:
Correct!
try again在 IDA 对 Correct! 查看 xref,跳到函数 sub_401180。伪代码显示:
if (sub_401050(buf))
puts("Correct!");
else
puts("try again");把 sub_401050 重命名为 check_input,再进入该函数,发现循环对 buf[i] ^ 0x23 与常量数组比较。写脚本把常量数组再异或 0x23,得到正确输入。这个案例的闭环是:字符串 -> xref -> 成功分支 -> 校验函数 -> 逆运算。