GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 逆向技巧 之 识别未知函数

为 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文件

posted on 2026-04-11 05:37  GKLBB  阅读(20)  评论(0)    收藏  举报