应用安全 --- 逆向技巧 之 识别未知函数
为 Android NDK libc++ 生成 IDA Pro 签名文件完整指南
前置知识:为什么这个任务比普通库更复杂
text
┌─────────────────────────────────────────────────────────────┐
│ std::string 的本质 │
│ │
│ typedef basic_string<char> string; │
│ typedef basic_string<wchar_t> wstring; │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 函数分布位置 │ │
│ │ │ │
│ │ 头文件 string (inline/template) ──► 90% 的函数 │ │
│ │ libc++_static.a ──► 8% 支撑函数 │ │
│ │ libc++_shared.so ──► 2% 导出符号 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
第一阶段:环境准备
1.1 确认 NDK 目录结构
Bash
# 典型的 NDK 目录布局
NDK_ROOT/
├── toolchains/
│ └── llvm/
│ └── prebuilt/
│ └── linux-x86_64/
│ ├── bin/
│ │ ├── clang++ # 编译器
│ │ ├── llvm-ar # 打包工具
│ │ └── llvm-nm # 符号查看
│ └── sysroot/
│ └── usr/
│ ├── include/
│ │ └── c++/v1/
│ │ └── string # ← 核心头文件(无后缀)
│ └── lib/
│ └── aarch64-linux-android/
│ └── 21/
│ └── libc++_static.a # ← 目标
Bash
# 设置环境变量
export NDK_ROOT=/path/to/your/android-ndk-r25c
export NDK_TOOLCHAIN=$NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64
export NDK_BIN=$NDK_TOOLCHAIN/bin
export NDK_SYSROOT=$NDK_TOOLCHAIN/sysroot
# 验证关键文件存在
ls $NDK_SYSROOT/usr/include/c++/v1/string
ls $NDK_SYSROOT/usr/lib/aarch64-linux-android/21/libc++_static.a
1.2 安装 FLAIR 工具链
Bash
# 下载 FLAIR(IDA Pro 官方签名生成工具)
# 来源:https://hex-rays.com/products/ida/support/ida/flair.zip
mkdir -p ~/flair && cd ~/flair
unzip flair.zip
# FLAIR 工具清单
ls -la ~/flair/bin/linux/
# 你需要的文件:
# pelf - 解析 ELF/AR 格式,生成 .pat 模式文件
# pcf - 解析 COFF 格式
# sigmake - 将 .pat 转换为 .sig 签名文件
# zipsig - 压缩 .sig 文件
chmod +x ~/flair/bin/linux/{pelf,sigmake,zipsig}
# 添加到 PATH
export PATH=$PATH:~/flair/bin/linux
第二阶段:理解目标函数的分布
2.1 检查 libc++_static.a 中实际存在的符号
Bash
# ============================================================
# 以 ARM64 为例(最常见的目标架构)
# ============================================================
TARGET_ARCH="aarch64-linux-android"
API_LEVEL="21"
LIBCXX_STATIC="$NDK_SYSROOT/usr/lib/$TARGET_ARCH/$API_LEVEL/libc++_static.a"
# 查看库中所有 .o 文件
$NDK_BIN/llvm-ar t $LIBCXX_STATIC | head -50
# 预期输出(与 string 相关的):
# string.o ← 显式实例化后的 basic_string 函数
# locale.o
# ios.o
# stdexcept.o ← __throw_length_error 等异常支撑
# 查看 string.o 中的所有符号
$NDK_BIN/llvm-ar p $LIBCXX_STATIC string.o | \
$NDK_BIN/llvm-nm -C -D - 2>/dev/null | head -80
2.2 建立函数到 Mangled Name 的映射表
Bash
# 创建映射表脚本
cat > ~/analyze_string_symbols.sh << 'EOF'
#!/bin/bash
NDK_BIN=$1
LIBCXX=$2
echo "=========================================="
echo " std::string / std::wstring 符号分析报告"
echo "=========================================="
# 提取所有 basic_string 相关符号
$NDK_BIN/llvm-nm --demangle --defined-only $LIBCXX 2>/dev/null | \
grep -E "basic_string|__throw_length|__throw_out_of_range" | \
sort | uniq | \
awk '{
# 分类输出
if ($3 ~ /find/) print "[FIND] " $3
else if ($3 ~ /compare/) print "[COMPARE] " $3
else if ($3 ~ /assign/) print "[ASSIGN] " $3
else if ($3 ~ /append/) print "[APPEND] " $3
else if ($3 ~ /insert/) print "[INSERT] " $3
else if ($3 ~ /erase/) print "[ERASE] " $3
else if ($3 ~ /replace/) print "[REPLACE] " $3
else if ($3 ~ /substr/) print "[SUBSTR] " $3
else if ($3 ~ /__/) print "[PRIVATE] " $3
else print "[OTHER] " $3
}'
echo ""
echo "=========================================="
echo " to_string / to_wstring 符号"
echo "=========================================="
$NDK_BIN/llvm-nm --demangle --defined-only $LIBCXX 2>/dev/null | \
grep -E "to_string|to_wstring" | sort | uniq
EOF
chmod +x ~/analyze_string_symbols.sh
~/analyze_string_symbols.sh $NDK_BIN $LIBCXX_STATIC
第三阶段:核心签名生成流程
3.1 方法一:直接从 libc++_static.a 生成(推荐)
Bash
# ============================================================
# 创建工作目录
# ============================================================
mkdir -p ~/sig_workspace/{arm64,arm32,x86,x86_64}
cd ~/sig_workspace
# ============================================================
# ARM64 (aarch64) 签名生成
# ============================================================
ARCH="aarch64-linux-android"
API="21"
LIBCXX="$NDK_SYSROOT/usr/lib/$ARCH/$API/libc++_static.a"
# 步骤1:生成 .pat 模式文件
pelf \
-p64 \
$LIBCXX \
arm64/libc++_string.pat
# pelf 参数说明:
# -p64 = 64位模式(ARM64用这个)
# -p32 = 32位模式(ARM32用这个)
# 检查 .pat 文件内容
head -20 arm64/libc++_string.pat
# 每行格式:
# [字节模式] [CRC16] [长度] [偏移:引用名称] ... :函数名
# 例:
# 5500000000000000.... 1234 0020 :0000 __ZNSt3__112basic_stringIcNS_11...
# 步骤2:生成 .sig 签名文件
sigmake \
-n "NDK_r25c_ARM64_libc++_string" \
arm64/libc++_string.pat \
arm64/libc++_string.sig
# 处理冲突(sigmake 会生成 .exc 文件列出冲突的函数)
3.2 处理 sigmake 冲突(重要!)
Bash
# ============================================================
# 冲突处理脚本
# ============================================================
cat > ~/resolve_conflicts.sh << 'SCRIPT'
#!/bin/bash
PAT_FILE=$1
SIG_NAME=$2
OUTPUT_DIR=$(dirname $PAT_FILE)
BASE_NAME=$(basename $PAT_FILE .pat)
EXC_FILE="$OUTPUT_DIR/$BASE_NAME.exc"
echo "[1] 首次运行 sigmake,生成冲突列表..."
sigmake -n "$SIG_NAME" $PAT_FILE $OUTPUT_DIR/$BASE_NAME.sig 2>&1
if [ -f "$EXC_FILE" ]; then
echo "[2] 发现冲突文件: $EXC_FILE"
echo " 冲突数量: $(grep -c "^[^;]" $EXC_FILE) 个"
# 策略:自动在冲突行前添加 '+' 号(选择第一个匹配)
# 格式说明:
# ;function_name ← 注释行(冲突的函数名标题)
# [模式行1] ← 第一个候选(添加+号选择它)
# [模式行2] ← 第二个候选(忽略)
# 自动解决:选择每组冲突中的第一个
python3 << 'PYEOF'
import sys
exc_file = sys.argv[1] if len(sys.argv) > 1 else "$EXC_FILE"
with open(exc_file, 'r') as f:
lines = f.readlines()
output = []
i = 0
resolved = 0
while i < len(lines):
line = lines[i]
# 注释行(以 ; 开头)- 直接保留
if line.startswith(';'):
output.append(line)
i += 1
continue
# 空行 - 直接保留
if line.strip() == '':
output.append(line)
i += 1
continue
# 这是一个冲突的模式行
# 在第一个冲突项前加 '+' 来选择它
if not line.startswith('+'):
output.append('+' + line)
resolved += 1
else:
output.append(line)
i += 1
with open(exc_file, 'w') as f:
f.writelines(output)
print(f"[3] 已自动解决 {resolved} 个冲突")
PYEOF
echo "[4] 重新运行 sigmake..."
sigmake -n "$SIG_NAME" $PAT_FILE $OUTPUT_DIR/$BASE_NAME.sig 2>&1
fi
if [ -f "$OUTPUT_DIR/$BASE_NAME.sig" ]; then
echo "[✓] 签名文件生成成功: $OUTPUT_DIR/$BASE_NAME.sig"
ls -lh "$OUTPUT_DIR/$BASE_NAME.sig"
else
echo "[✗] 签名文件生成失败"
fi
SCRIPT
chmod +x ~/resolve_conflicts.sh
~/resolve_conflicts.sh arm64/libc++_string.pat "NDK_r25c_ARM64_libc++"
3.3 方法二:编译专用目标文件(针对内联函数)
Bash
# ============================================================
# 这是处理"内联消失"问题的关键技术
# 强制编译器实例化所有 std::string 函数,禁止内联
# ============================================================
cat > ~/force_instantiate_string.cpp << 'CPP'
// ============================================================
// 强制实例化 std::string 和 std::wstring 的所有成员函数
// 目的:让内联函数变成可见的符号,供 pelf 提取
// ============================================================
// 关键编译指令:禁止内联
#pragma clang optimize off
#include <string>
#include <stdexcept>
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 显式实例化所有需要的特化版本
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// std::string = basic_string<char>
template class std::__ndk1::basic_string<
char,
std::__ndk1::char_traits<char>,
std::__ndk1::allocator<char>
>;
// std::wstring = basic_string<wchar_t>
template class std::__ndk1::basic_string<
wchar_t,
std::__ndk1::char_traits<wchar_t>,
std::__ndk1::allocator<wchar_t>
>;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 包装函数:强制让每个函数都出现在符号表中
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
using Str = std::__ndk1::basic_string<char>;
using WStr = std::__ndk1::basic_string<wchar_t>;
// --- 构造与析构 ---
void __force_string_ctor_default() { Str s; }
void __force_string_ctor_cstr() { Str s("hello"); }
void __force_string_ctor_copy(Str& o) { Str s(o); }
void __force_string_ctor_move(Str&& o) { Str s(std::move(o)); }
void __force_string_ctor_count() { Str s(10, 'x'); }
void __force_string_ctor_substr(Str& o){ Str s(o, 0, 5); }
// --- 赋值操作 ---
void __force_string_assign_copy(Str& s, const Str& o) { s = o; }
void __force_string_assign_move(Str& s, Str&& o) { s = std::move(o); }
void __force_string_assign_cstr(Str& s, const char* p) { s = p; }
void __force_string_assign_char(Str& s, char c) { s = c; }
// --- 元素访问 ---
char& __force_string_at(Str& s, size_t i) { return s.at(i); }
char& __force_string_bracket(Str& s, size_t i) { return s[i]; }
char& __force_string_front(Str& s) { return s.front(); }
char& __force_string_back(Str& s) { return s.back(); }
const char* __force_string_data(const Str& s) { return s.data(); }
const char* __force_string_cstr(const Str& s) { return s.c_str(); }
// --- 容量操作 ---
size_t __force_string_size(const Str& s) { return s.size(); }
size_t __force_string_length(const Str& s) { return s.length(); }
size_t __force_string_capacity(const Str& s){ return s.capacity(); }
bool __force_string_empty(const Str& s) { return s.empty(); }
void __force_string_reserve(Str& s) { s.reserve(64); }
void __force_string_shrink(Str& s) { s.shrink_to_fit(); }
void __force_string_clear(Str& s) { s.clear(); }
size_t __force_string_max_size(const Str& s){ return s.max_size(); }
void __force_string_resize(Str& s) { s.resize(10); }
void __force_string_resize_c(Str& s) { s.resize(10, 'x'); }
// --- 追加操作 ---
void __force_string_push_back(Str& s) { s.push_back('x'); }
void __force_string_pop_back(Str& s) { s.pop_back(); }
Str& __force_string_append_str(Str& s, const Str& o) { return s.append(o); }
Str& __force_string_append_cstr(Str& s, const char* p) { return s.append(p); }
Str& __force_string_append_count(Str& s) { return s.append(5,'x'); }
Str& __force_string_append_substr(Str& s, const Str& o) { return s.append(o,0,3); }
Str& __force_string_op_plus_str(Str& s, const Str& o) { return s += o; }
Str& __force_string_op_plus_cstr(Str& s, const char* p){ return s += p; }
Str& __force_string_op_plus_char(Str& s, char c) { return s += c; }
// --- 插入操作 ---
Str& __force_string_insert_pos_str(Str& s, const Str& o) { return s.insert(0, o); }
Str& __force_string_insert_pos_cstr(Str& s, const char* p){ return s.insert(0, p); }
Str& __force_string_insert_pos_count(Str& s) { return s.insert(0, 3, 'x'); }
// --- 删除操作 ---
Str& __force_string_erase_pos(Str& s) { return s.erase(0); }
Str& __force_string_erase_range(Str& s) { return s.erase(0, 3); }
// --- 替换操作 ---
Str& __force_string_replace_str(Str& s, const Str& o) { return s.replace(0, 2, o); }
Str& __force_string_replace_cstr(Str& s, const char* p){ return s.replace(0, 2, p); }
// --- 查找操作 ---
size_t __force_string_find_str(const Str& s, const Str& o) { return s.find(o); }
size_t __force_string_find_cstr(const Str& s, const char* p) { return s.find(p); }
size_t __force_string_find_char(const Str& s, char c) { return s.find(c); }
size_t __force_string_rfind_str(const Str& s, const Str& o) { return s.rfind(o); }
size_t __force_string_rfind_char(const Str& s, char c) { return s.rfind(c); }
size_t __force_string_find_first_of(const Str& s, const Str& o) { return s.find_first_of(o); }
size_t __force_string_find_last_of(const Str& s, const Str& o) { return s.find_last_of(o); }
size_t __force_string_find_first_not_of(const Str& s, const Str& o) { return s.find_first_not_of(o); }
size_t __force_string_find_last_not_of(const Str& s, const Str& o) { return s.find_last_not_of(o); }
// --- 比较操作 ---
int __force_string_compare_str(const Str& s, const Str& o) { return s.compare(o); }
int __force_string_compare_cstr(const Str& s, const char* p) { return s.compare(p); }
bool __force_string_op_eq(const Str& a, const Str& b) { return a == b; }
bool __force_string_op_lt(const Str& a, const Str& b) { return a < b; }
// --- 子串操作 ---
Str __force_string_substr(const Str& s) { return s.substr(0, 5); }
// --- 交换操作 ---
void __force_string_swap(Str& a, Str& b) { a.swap(b); }
// --- 迭代器 ---
Str::iterator __force_string_begin(Str& s) { return s.begin(); }
Str::iterator __force_string_end(Str& s) { return s.end(); }
Str::const_iterator __force_string_cbegin(const Str& s){ return s.cbegin(); }
Str::const_iterator __force_string_cend(const Str& s) { return s.cend(); }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// to_string / to_wstring
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Str __force_to_string_int(int v) { return std::to_string(v); }
Str __force_to_string_long(long v) { return std::to_string(v); }
Str __force_to_string_ll(long long v) { return std::to_string(v); }
Str __force_to_string_uint(unsigned v) { return std::to_string(v); }
Str __force_to_string_float(float v) { return std::to_string(v); }
Str __force_to_string_double(double v) { return std::to_string(v); }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// wstring 对应版本
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const wchar_t* __force_wstring_cstr(const WStr& s) { return s.c_str(); }
size_t __force_wstring_size(const WStr& s) { return s.size(); }
WStr& __force_wstring_append(WStr& s, const WStr& o){ return s.append(o); }
size_t __force_wstring_find(const WStr& s, wchar_t c){ return s.find(c); }
int __force_wstring_compare(const WStr& a, const WStr& b){ return a.compare(b); }
WStr __force_wstring_substr(const WStr& s) { return s.substr(0, 5); }
WStr __force_to_wstring_int(int v) { return std::to_wstring(v); }
WStr __force_to_wstring_double(double v) { return std::to_wstring(v); }
CPP
echo "C++ 源文件已创建"
3.4 为所有目标架构编译
Bash
# ============================================================
# 编译脚本:为4个架构分别生成目标文件
# ============================================================
cat > ~/compile_all_archs.sh << 'SCRIPT'
#!/bin/bash
NDK_BIN=$1
NDK_SYSROOT=$2
SOURCE_FILE=$3
OUTPUT_BASE=$4
# 架构配置表
declare -A ARCH_TARGETS=(
["arm64"]="aarch64-linux-android21"
["arm32"]="armv7a-linux-androideabi21"
["x86"]="i686-linux-android21"
["x86_64"]="x86_64-linux-android21"
)
declare -A PELF_FLAGS=(
["arm64"]="-p64"
["arm32"]="-p32"
["x86"]="-p32"
["x86_64"]="-p64"
)
CLANGPP="$NDK_BIN/clang++"
for ARCH in "${!ARCH_TARGETS[@]}"; do
TARGET="${ARCH_TARGETS[$ARCH]}"
PELF_FLAG="${PELF_FLAGS[$ARCH]}"
echo "========================================"
echo " 正在处理架构: $ARCH ($TARGET)"
echo "========================================"
OUTPUT_DIR="$OUTPUT_BASE/$ARCH"
mkdir -p $OUTPUT_DIR
OBJ_FILE="$OUTPUT_DIR/string_force.o"
PAT_FILE="$OUTPUT_DIR/string_force.pat"
SIG_FILE="$OUTPUT_DIR/string_force.sig"
# Step 1: 编译为目标文件
$CLANGPP \
--target=$TARGET \
--sysroot=$NDK_SYSROOT \
-std=c++17 \
-O0 \
-fno-inline \
-fno-inline-functions \
-fno-optimize-sibling-calls \
-g \
-c \
$SOURCE_FILE \
-o $OBJ_FILE
if [ $? -ne 0 ]; then
echo " [✗] 编译失败: $ARCH"
continue
fi
echo " [✓] 编译成功: $OBJ_FILE"
# Step 2: 查看符号数量
SYM_COUNT=$($NDK_BIN/llvm-nm --defined-only $OBJ_FILE | wc -l)
echo " [i] 符号数量: $SYM_COUNT"
# Step 3: 生成 .pat 文件
pelf $PELF_FLAG $OBJ_FILE $PAT_FILE
if [ $? -ne 0 ]; then
echo " [✗] pelf 失败: $ARCH"
continue
fi
echo " [✓] .pat 生成成功: $PAT_FILE"
# Step 4: 同时处理 libc++_static.a
# 找到对应的静态库
ARCH_LIB_MAP_arm64="aarch64-linux-android"
ARCH_LIB_MAP_arm32="arm-linux-androideabi"
ARCH_LIB_MAP_x86="i686-linux-android"
ARCH_LIB_MAP_x86_64="x86_64-linux-android"
LIB_ARCH_VAR="ARCH_LIB_MAP_$ARCH"
LIB_ARCH="${!LIB_ARCH_VAR}"
LIBCXX="$NDK_SYSROOT/usr/lib/$LIB_ARCH/21/libc++_static.a"
if [ -f "$LIBCXX" ]; then
LIB_PAT="$OUTPUT_DIR/libc++_static.pat"
pelf $PELF_FLAG $LIBCXX $LIB_PAT
echo " [✓] libc++_static.pat 生成成功"
# Step 5: 合并两个 .pat 文件
MERGED_PAT="$OUTPUT_DIR/merged.pat"
cat $PAT_FILE $LIB_PAT > $MERGED_PAT
echo " [✓] 合并 .pat 完成: $MERGED_PAT"
# Step 6: 生成最终 .sig
sigmake \
-n "NDK_r25c_${ARCH}_libc++_string" \
$MERGED_PAT \
$SIG_FILE 2>&1
else
echo " [!] 找不到 $LIBCXX,仅使用强制实例化"
sigmake \
-n "NDK_r25c_${ARCH}_libc++_string" \
$PAT_FILE \
$SIG_FILE 2>&1
fi
# Step 7: 处理冲突
EXC_FILE="${SIG_FILE%.sig}.exc"
if [ -f "$EXC_FILE" ]; then
echo " [!] 发现冲突,自动解决..."
# 在第一个有效模式行前添加 '+'
sed -i '/^[^;+]/{ s/^/+/; }' $EXC_FILE
# 重新生成
sigmake \
-n "NDK_r25c_${ARCH}_libc++_string" \
${MERGED_PAT:-$PAT_FILE} \
$SIG_FILE 2>&1
fi
if [ -f "$SIG_FILE" ]; then
echo " [✓] 签名生成完成: $SIG_FILE ($(ls -lh $SIG_FILE | awk '{print $5}'))"
fi
echo ""
done
SCRIPT
chmod +x ~/compile_all_archs.sh
~/compile_all_archs.sh \
$NDK_BIN \
$NDK_SYSROOT \
~/force_instantiate_string.cpp \
~/sig_workspace
第四阶段:验证签名质量
4.1 创建验证脚本
Bash
cat > ~/verify_sig.sh << 'SCRIPT'
#!/bin/bash
# ============================================================
# 签名质量验证脚本
# ============================================================
SIG_FILE=$1
NDK_BIN=$2
echo "=========================================="
echo " 签名文件验证报告"
echo " 文件: $SIG_FILE"
echo "=========================================="
# 检查文件是否存在
if [ ! -f "$SIG_FILE" ]; then
echo "[✗] 文件不存在: $SIG_FILE"
exit 1
fi
# 检查文件大小
SIZE=$(ls -lh "$SIG_FILE" | awk '{print $5}')
echo "[i] 文件大小: $SIZE"
# 使用 sigmake -d 查看签名内容(如果支持)
# 注意:不同版本的 FLAIR 支持不同的选项
# 统计签名中的函数数量
# .sig 是二进制格式,无法直接统计,但可以通过 .pat 文件来统计
PAT_FILE="${SIG_FILE%.sig}.pat"
if [ -f "$PAT_FILE" ]; then
FUNC_COUNT=$(wc -l < "$PAT_FILE")
echo "[i] 模式数量 (.pat): $FUNC_COUNT 行"
fi
echo ""
echo "=========================================="
echo " 检查关键函数覆盖率"
echo "=========================================="
# 要检查的关键 mangled name 列表
CRITICAL_FUNCTIONS=(
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4findEcm"
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE4findERKS5_m"
"_ZNKSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE7compareERKS5_"
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6appendERKS5_"
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6insertEmRKS5_"
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5eraseEmm"
"_ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE6substrEmm"
)
FOUND=0
MISSING=0
for FUNC in "${CRITICAL_FUNCTIONS[@]}"; do
# 在 .pat 文件中检查符号名
if [ -f "$PAT_FILE" ] && grep -q "$FUNC" "$PAT_FILE" 2>/dev/null; then
echo " [✓] 找到: $(echo $FUNC | c++filt 2>/dev/null || echo $FUNC)"
((FOUND++))
else
echo " [✗] 缺失: $(echo $FUNC | c++filt 2>/dev/null || echo $FUNC)"
((MISSING++))
fi
done
echo ""
echo " 覆盖率: $FOUND / $((FOUND + MISSING))"
SCRIPT
chmod +x ~/verify_sig.sh
~/verify_sig.sh ~/sig_workspace/arm64/string_force.sig $NDK_BIN
4.2 使用 IDA Python 验证签名匹配率
Python
# ============================================================
# IDA Pro Python 脚本
# 在 IDA 中运行此脚本来验证签名匹配效果
# File > Script File > 选择此文件
# ============================================================
import idaapi
import idc
import idautils
def check_string_sig_coverage():
"""
检查 std::string 签名的匹配覆盖率
"""
# 需要检查的函数模式(Demangled 形式的关键词)
target_patterns = [
# 构造和析构
"basic_string::basic_string",
"basic_string::~basic_string",
# 查找函数
"basic_string::find",
"basic_string::rfind",
"basic_string::find_first_of",
"basic_string::find_last_of",
"basic_string::find_first_not_of",
"basic_string::find_last_not_of",
# 修改函数
"basic_string::append",
"basic_string::insert",
"basic_string::erase",
"basic_string::replace",
"basic_string::assign",
# 比较
"basic_string::compare",
# 其他
"basic_string::substr",
"basic_string::resize",
"basic_string::reserve",
"to_string",
"to_wstring",
# 私有支撑函数
"__grow_by",
"__init",
"__assign_no_alias",
]
print("=" * 60)
print(" std::string 签名匹配报告")
print("=" * 60)
matched = {}
unmatched = list(target_patterns)
# 遍历所有命名函数
for ea in idautils.Functions():
name = idc.get_func_name(ea)
demangled = idc.demangle_name(name, idc.get_inf_attr(idc.INF_SHORT_DN))
display_name = demangled if demangled else name
for pattern in target_patterns:
if pattern in display_name:
if pattern not in matched:
matched[pattern] = []
matched[pattern].append({
'ea': ea,
'name': display_name
})
if pattern in unmatched:
unmatched.remove(pattern)
# 报告已匹配
print(f"\n[✓] 已匹配的函数类别 ({len(matched)}/{len(target_patterns)}):")
for pattern, funcs in sorted(matched.items()):
print(f" {pattern}: {len(funcs)} 个匹配")
for f in funcs[:3]: # 最多显示3个
print(f" 0x{f['ea']:08X}: {f['name'][:80]}")
# 报告未匹配
if unmatched:
print(f"\n[✗] 未匹配的函数类别 ({len(unmatched)}):")
for pattern in unmatched:
print(f" 缺失: {pattern}")
# 总体统计
total = len(target_patterns)
found = len(matched)
coverage = found / total * 100
print(f"\n{'=' * 60}")
print(f" 总体覆盖率: {found}/{total} = {coverage:.1f}%")
if coverage >= 90:
print(" [★★★] 优秀:签名质量很高")
elif coverage >= 70:
print(" [★★☆] 良好:签名基本可用")
elif coverage >= 50:
print(" [★☆☆] 一般:建议补充更多函数")
else:
print(" [☆☆☆] 较差:签名需要重新生成")
print("=" * 60)
return matched, unmatched
# 运行检查
matched, unmatched = check_string_sig_coverage()
第五阶段:安装和使用签名
5.1 安装到 IDA Pro
Bash
# ============================================================
# 将生成的 .sig 文件安装到 IDA
# ============================================================
# 找到 IDA 的签名目录
# Windows: C:\Program Files\IDA Pro\sig\
# Linux: ~/idapro/sig/
# macOS: /Applications/IDA Pro.app/Contents/MacOS/sig/
# 在 sig 目录下按平台分类
IDA_SIG_DIR=~/idapro/sig
mkdir -p $IDA_SIG_DIR/{arm64,arm32,x86,x86_64}
# 复制签名文件
cp ~/sig_workspace/arm64/*.sig $IDA_SIG_DIR/arm
cp ~/sig_workspace/arm32/*.sig $IDA_SIG_DIR/arm
cp ~/sig_workspace/x86/*.sig $IDA_SIG_DIR/pc
cp ~/sig_workspace/x86_64/*.sig $IDA_SIG_DIR/pc
echo "签名文件已安装到 IDA"
5.2 在 IDA Pro 中应用签名
text
# ============================================================
# IDA Pro 操作步骤
# ============================================================
1. 打开目标 .so 文件
2. 等待 IDA 完成初始分析
3. 应用签名:
菜单 → File → Load File → FLIRT Signature File
或者快捷键方式:
View → Open subviews → Signatures (Shift+F5)
右键 → Apply new signature
4. 选择你生成的 .sig 文件
5. 查看匹配结果:
View → Open subviews → Signatures
可以看到每个签名匹配了多少函数
# ============================================================
# IDA Python 自动应用签名
# ============================================================
Python
# IDA Python:自动应用签名脚本
import idaapi
import os
def apply_string_signatures(sig_dir):
"""自动应用目录中所有的 .sig 文件"""
sig_files = [f for f in os.listdir(sig_dir) if f.endswith('.sig')]
print(f"找到 {len(sig_files)} 个签名文件")
results = {}
for sig_file in sig_files:
full_path = os.path.join(sig_dir, sig_file)
print(f"正在应用: {sig_file}")
# 应用签名
# IDA API: idc.plan_and_wait() + apply_sig()
ret = idc.apply_idasgn(full_path)
if ret:
print(f" [✓] 成功应用: {sig_file}")
else:
print(f" [✗] 应用失败: {sig_file}")
results[sig_file] = ret
return results
# 调用示例
# apply_string_signatures("/path/to/your/sig/files")
第六阶段:针对特殊情况的高级处理
6.1 处理动态链接(libc++_shared.so)
Bash
# ============================================================
# 如果目标 .so 是动态链接 libc++,
# 应该对 libc++_shared.so 生成签名
# ============================================================
# 找到 libc++_shared.so
LIBCXX_SHARED=$(find $NDK_SYSROOT -name "libc++_shared.so" | head -1)
echo "找到: $LIBCXX_SHARED"
# 对 .so 文件使用 pelf
pelf -p64 $LIBCXX_SHARED ~/sig_workspace/arm64/libc++_shared.pat
# 注意:.so 文件中的符号可能已经是导出符号,更容易匹配
# 检查导出符号
$NDK_BIN/llvm-nm -D --demangle $LIBCXX_SHARED | \
grep -E "basic_string|to_string" | head -30
6.2 处理 LTO(链接时优化)编译的库
Bash
# ============================================================
# 如果目标 APK 用了 LTO 编译,内联情况更严重
# 需要用更激进的策略
# ============================================================
cat > ~/force_instantiate_lto_workaround.cpp << 'CPP'
// 在 LTO 场景下,增加 __attribute__((noinline)) 强制阻止内联
#include <string>
#define FORCE_NOINLINE __attribute__((noinline, used, visibility("default")))
using Str = std::__ndk1::basic_string<char>;
// 为最常用的函数添加强制导出版本
extern "C" {
FORCE_NOINLINE
size_t __ida_sig_string_find_char(const Str* s, char c, size_t pos) {
return s->find(c, pos);
}
FORCE_NOINLINE
size_t __ida_sig_string_find_str(const Str* s, const Str* o, size_t pos) {
return s->find(*o, pos);
}
FORCE_NOINLINE
int __ida_sig_string_compare(const Str* a, const Str* b) {
return a->compare(*b);
}
FORCE_NOINLINE
void __ida_sig_string_append(Str* s, const Str* o) {
s->append(*o);
}
FORCE_NOINLINE
Str* __ida_sig_string_substr(const Str* s, size_t pos, size_t len, Str* result) {
*result = s->substr(pos, len);
return result;
}
}
CPP
# 编译 LTO 版本的签名源
$NDK_BIN/clang++ \
--target=aarch64-linux-android21 \
--sysroot=$NDK_SYSROOT \
-std=c++17 \
-O0 \
-fno-inline \
-c \
~/force_instantiate_lto_workaround.cpp \
-o ~/sig_workspace/arm64/string_lto_workaround.o
6.3 一键完整流程脚本
Bash
# ============================================================
# 一键生成所有架构签名的完整脚本
# ============================================================
cat > ~/generate_all_sigs.sh << 'MASTER_SCRIPT'
#!/bin/bash
set -e # 遇到错误立即退出
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Android NDK libc++ string 签名生成工具 ║"
echo "║ 支持架构: arm64, arm32, x86, x86_64 ║"
echo "╚══════════════════════════════════════════════════════╝"
# ─── 配置区域 ───────────────────────────────────────────────
NDK_ROOT=${1:-/path/to/ndk}
FLAIR_DIR=${2:-~/flair/bin/linux}
OUTPUT_DIR=${3:-~/ndk_string_sigs}
# ────────────────────────────────────────────────────────────
NDK_BIN="$NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin"
NDK_SYSROOT="$NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
CLANGPP="$NDK_BIN/clang++"
PELF="$FLAIR_DIR/pelf"
SIGMAKE="$FLAIR_DIR/sigmake"
# 验证工具存在
for TOOL in $CLANGPP $PELF $SIGMAKE; do
if [ ! -f "$TOOL" ]; then
echo "[✗] 工具不存在: $TOOL"
exit 1
fi
done
mkdir -p $OUTPUT_DIR
# 创建源文件
SOURCE="$OUTPUT_DIR/string_instantiate.cpp"
# ... (这里包含上文的 C++ 源码)
cp ~/force_instantiate_string.cpp $SOURCE
# 处理每个架构
declare -A ARCHS=(
["arm64"]="aarch64-linux-android21|-p64|aarch64-linux-android"
["arm32"]="armv7a-linux-androideabi21|-p32|arm-linux-androideabi"
["x86"]="i686-linux-android21|-p32|i686-linux-android"
["x86_64"]="x86_64-linux-android21|-p64|x86_64-linux-android"
)
TOTAL_SUCCESS=0
TOTAL_FAIL=0
for ARCH in "${!ARCHS[@]}"; do
IFS='|' read -r TARGET PELF_FLAG LIB_ARCH <<< "${ARCHS[$ARCH]}"
echo ""
echo "── 处理 $ARCH ──────────────────────────────────────"
ARCH_DIR="$OUTPUT_DIR/$ARCH"
mkdir -p $ARCH_DIR
OBJ="$ARCH_DIR/string.o"
PAT_OBJ="$ARCH_DIR/string_obj.pat"
PAT_LIB="$ARCH_DIR/string_lib.pat"
PAT_MERGED="$ARCH_DIR/merged.pat"
SIG="$ARCH_DIR/ndk_libc++_${ARCH}.sig"
# 1. 编译
if $CLANGPP --target=$TARGET --sysroot=$NDK_SYSROOT \
-std=c++17 -O0 -fno-inline -c $SOURCE -o $OBJ 2>/dev/null; then
echo " ✓ 编译完成"
else
echo " ✗ 编译失败"
((TOTAL_FAIL++))
continue
fi
# 2. pelf 处理目标文件
$PELF $PELF_FLAG $OBJ $PAT_OBJ 2>/dev/null || true
# 3. pelf 处理静态库
LIBCXX="$NDK_SYSROOT/usr/lib/$LIB_ARCH/21/libc++_static.a"
if [ -f "$LIBCXX" ]; then
$PELF $PELF_FLAG $LIBCXX $PAT_LIB 2>/dev/null || true
cat $PAT_OBJ $PAT_LIB > $PAT_MERGED 2>/dev/null || cp $PAT_OBJ $PAT_MERGED
echo " ✓ 合并静态库完成"
else
cp $PAT_OBJ $PAT_MERGED
echo " ! 静态库未找到,仅使用编译输出"
fi
# 4. 生成签名
$SIGMAKE -n "NDK_libc++_${ARCH}" $PAT_MERGED $SIG 2>/dev/null || true
# 5. 处理冲突
EXC="${SIG%.sig}.exc"
if [ -f "$EXC" ]; then
# 自动解决冲突:选择第一个匹配项
perl -i -pe 'if (/^[^;+\r\n]/) { s/^/+/ unless $seen }; $seen = /^;/' $EXC
$SIGMAKE -n "NDK_libc++_${ARCH}" $PAT_MERGED $SIG 2>/dev/null || true
fi
# 6. 报告结果
if [ -f "$SIG" ]; then
SIZE=$(ls -lh $SIG | awk '{print $5}')
echo " ✓ 签名生成完成: $(basename $SIG) [$SIZE]"
((TOTAL_SUCCESS++))
else
echo " ✗ 签名生成失败"
((TOTAL_FAIL++))
fi
done
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ 生成完成 成功: $TOTAL_SUCCESS 失败: $TOTAL_FAIL"
echo "║ 输出目录: $OUTPUT_DIR"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
echo "签名文件列表:"
find $OUTPUT_DIR -name "*.sig" -exec ls -lh {} \;
MASTER_SCRIPT
chmod +x ~/generate_all_sigs.sh
# 执行
~/generate_all_sigs.sh \
/path/to/android-ndk-r25c \
~/flair/bin/linux \
~/ndk_string_sigs
完整流程总结
text
┌─────────────────────────────────────────────────────────────────┐
│ 签名生成完整流程图 │
│ │
│ NDK libc++_static.a │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────────────────────────┐ │
│ │ pelf -p64 │ │ 强制实例化 .cpp │ │
│ │ (直接解析 │ │ clang++ -O0 -fno-inline │ │
│ │ 静态库) │ │ → string_force.o │ │
│ └──────┬──────┘ └───────────────┬──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ libc++_static.pat string_force.pat │
│ │ │ │
│ └────────────┬──────────────┘ │
│ │ cat (合并) │
│ ▼ │
│ merged.pat │
│ │ │
│ ▼ │
│ sigmake -n "名称" │
│ │ │
│ ┌─────────┴──────────┐ │
│ │ 有冲突? │ │
│ ▼ 是 ▼ 否 │
│ .exc 文件 .sig 文件 ✓ │
│ 添加 '+' 号 │
│ 重新 sigmake │
│ │ │
│ ▼ │
│ .sig 文件 ✓ │
│ │ │
│ ▼ │
│ 复制到 IDA/sig/ 目录 │
│ │ │
│ ▼ │
│ IDA: File → Load → FLIRT Sig │
│ │ │
│ ▼ │
│ 自动识别 std::string 函数 ✓ │
└─────────────────────────────────────────────────────────────────┘
生成 ARM64 FLIRT SIG 文件完整流程
环境说明
text
工具: IDA Professional 9.3 (Windows)
目标: libc++_shared.so (ARM64)
问题: pelf.exe 不支持ARM64
解决: 用IDA打开so后用idb2pat.py导出pat
第一步:准备工作目录
PowerShell
# 创建工作目录
New-Item -ItemType Directory -Force -Path C:\sig_work
cd C:\sig_work
# 复制sigmake工具
Copy-Item "C:\Program Files\IDA Professional 9.3\tools\flair\sigmake.exe" C:\sig_work\
第二步:获取目标so文件
PowerShell
# 方法1: 从NDK目录获取
$ndkBase = "$env:LOCALAPPDATA\Android\Sdk\ndk"
$ndkVer = (ls $ndkBase | Select-Object -Last 1).FullName
$soPath = "$ndkVer\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\lib\aarch64-linux-android\libc++_shared.so"
Copy-Item $soPath C:\sig_work\
# 方法2: 从APK提取
# 把APK改名为zip解压
# 取 lib\arm64-v8a\libc++_shared.so
# 方法3: adb从设备提取
# adb pull /system/lib64/libc++_shared.so C:\sig_work\
PowerShell
# 验证so文件有效性
$bytes = [System.IO.File]::ReadAllBytes("C:\sig_work\libc++_shared.so")
$bytes[0..19] | ForEach-Object { "{0:X2}" -f $_ }
# 正确输出:
# 7F 45 4C 46 → ELF魔数 ✓
# 02 → 64位 ✓
# B7 → ARM64 ✓
第三步:用IDA打开so生成IDB
text
1. IDA打开 libc++_shared.so
2. 等待自动分析完成(进度条消失)
3. File → Save 保存IDB文件
第四步:下载安装 idb2pat.py
text
浏览器打开:
https://github.com/fireeye/flare-ida/blob/master/python/flare/idb2pat.py
下载后复制到:
C:\Program Files\IDA Professional 9.3\plugins\idb2pat.py
第五步:导出PAT文件
text
IDA中操作:
1. File → Script file
2. 选择 idb2pat.py
3. 弹出对话框选择保存位置:
C:\sig_work\libcxx.pat
4. 等待导出完成
第六步:生成SIG文件
PowerShell
cd C:\sig_work
# 注意: 文件名不能含++号
# libc++_shared.pat → 重命名为 libcxx.pat
.\sigmake.exe libcxx.pat libcxx.sig
text
第一次运行输出:
libcxx.sig: modules/leaves: 1670/3039, COLLISIONS: 129
同时生成 libcxx.exc 冲突文件
第七步:处理EXC冲突文件
PowerShell
# 正确处理方式: 只删除开头4行注释
$content = Get-Content "C:\sig_work\libcxx.exc" -Encoding UTF8 -Raw
# 删除开头注释行
$newContent = $content -replace '(?s)^(;[^\r\n]*[\r\n]+){1,4}', ''
# 保存
[System.IO.File]::WriteAllText("C:\sig_work\libcxx.exc", $newContent)
# 验证格式(前5行应该是空行或函数名)
Get-Content "C:\sig_work\libcxx.exc" | Select-Object -First 5
text
正确的EXC文件格式:
(空行)
函数名A 00 0000 AABBCC...
函数名B 00 0000 AABBCC...
(空行)
函数名C 08 FC78 DDEEFF...
函数名D 08 FC78 DDEEFF...
第八步:重新生成SIG
PowerShell
# 删除旧sig
Remove-Item "C:\sig_work\libcxx.sig" -ErrorAction SilentlyContinue
# 重新生成
.\sigmake.exe libcxx.pat libcxx.sig
# 成功输出(无COLLISIONS):
# libcxx.sig: modules/leaves: XXXX/XXXX
第九步:安装SIG到IDA
PowerShell
# 复制到IDA sig目录
Copy-Item "C:\sig_work\libcxx.sig" `
"C:\Program Files\IDA Professional 9.3\sig\arm\libcxx.sig"
Write-Host "安装完成!"
第十步:IDA中应用签名
text
1. 用IDA打开目标so文件
2. Shift+F5 打开Signatures窗口
3. 右键 → Apply new signature(Ins)
4. 找到并选择 libcxx
5. 点击OK等待识别
识别结果:
sub_1EE5C → std::string::c_str ✓
sub_3F890 → _Unwind_Resume ✓
完整流程图
text
libc++_shared.so (ARM64)
│
▼
IDA打开分析
│
▼
idb2pat.py导出
│
▼
libcxx.pat
│
▼
sigmake.exe
│
┌────┴────┐
│ │
成功 有冲突
│ │
│ 处理libcxx.exc
│ 删除前4行注释
│ │
│ 重新sigmake
│ │
└────┬────┘
│
libcxx.sig
│
▼
复制到sig\arm\
│
▼
IDA应用签名
│
▼
sub_xxx → 真实函数名 ✓
关键注意事项
text
1. pelf.exe 不支持ARM64
→ 必须用IDA+idb2pat.py方案
2. 文件名不能含++
→ libc++_shared.pat 改名为 libcxx.pat
3. 处理EXC文件
→ 只删除前4行注释
→ 保留所有空行和函数名行
→ 用Raw模式读写避免格式损坏
4. so版本要匹配
→ 目标APK用哪个NDK版本
→ 就用对应版本的libc++_shared.so
5. 签名放置位置
→ ARM64 放 sig\arm\ 目录
我们如何flart分析一个安卓so,用ida打开后发现依赖的so文件和ndk版本号,找到对应版本号的这些so文件,用ph制作sig文件
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
浙公网安备 33010602011771号