Blutter SIGSEGV Bug 修复报告

使用blutter分析闲鱼的libapp.so时出现段错误(SIGSEGV)。我很早就发现有这个问题了,一直在等作者修复,但发现这个项目的更新频率较低,有一大堆issues和PR都没处理,于是就尝试用claude分析这个问题的原因,还算比较简单,cluade很快发现了问题并修复成功(https://github.com/xjohjrdy/blutter)。以下是我让cluade写的文档:


Blutter SIGSEGV Bug 修复报告

问题概述

Blutter 在分析特定的 ARM64 Flutter 应用 (libapp.so) 时崩溃,错误信息如下:

1
subprocess.CalledProcessError: Command '['.../bin/blutter_dartvm2.19.6_android_arm64', '-i', '.../libapp.so', '-o', '.../out']' died with <Signals.SIGSEGV: 11>.

Bug 定位过程

第一步:添加调试日志

为了定位崩溃位置,在关键代码路径添加了调试日志:

  1. blutter/src/DartLoader.cpp - Dart VM 初始化相关代码
  2. blutter/src/DartApp.cpp - 应用加载和类表处理代码
  3. blutter/src/ElfHelper.cpp - ELF 文件解析代码

第二步:获取崩溃堆栈

使用 gdb 运行程序获取崩溃时的堆栈跟踪:

1
gdb -batch -ex "run -i libapp.so -o out" -ex "bt full" ./blutter_dartvm2.19.6_android_arm64

堆栈跟踪结果:

1
2
3
4
5
6
7
8
9
10
Program received signal SIGSEGV, Segmentation fault.
0x00005555557a6457 in dart::BSS::Initialize(dart::Thread*, unsigned long*, bool) ()
#0 0x00005555557a6457 in dart::BSS::Initialize(dart::Thread*, unsigned long*, bool) ()
#1 0x00005555557a59a2 in dart::FullSnapshotReader::ReadProgramSnapshot() ()
#2 0x00005555557b14a3 in dart::Dart::InitIsolateFromSnapshot(...) ()
#3 0x00005555557b273b in dart::Dart::InitializeIsolate(...) ()
#4 0x000055555578f9ca in dart::CreateIsolate(...) ()
#5 0x000055555578ff0e in Dart_CreateIsolateGroup() ()
#6 0x000055555562be80 in load_isolate(...) ()
...

崩溃发生在 dart::BSS::Initialize 函数中。

第三步:分析 ELF 文件结构

检查测试样本的 ELF 文件结构:

1
readelf -l libapp.so

关键发现:

1
2
3
4
5
6
LOAD           Offset             VirtAddr           PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000003021f80 0x0000000003021f80 R E 0x10000
LOAD 0x0000000003022000 0x0000000003032000 0x0000000003032000
0x00000000000000d0 0x00000000000000e8 RW 0x10000

注意到第二个 LOAD 段:

  • FileSiz (文件大小): 0xd0
  • MemSiz (内存大小): 0xe8

两者的差值 0xe8 - 0xd0 = 0x18 (24字节) 就是 BSS 段的大小。

第四步:分析原始代码

原始的 load_map_file 函数(Linux 版本):

1
2
3
4
5
6
7
8
9
10
11
static void* load_map_file(const char* path)
{
int fd = open(path, O_RDONLY);
struct stat st;

fstat(fd, &st);
void* mem = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);

close(fd);
return mem;
}

问题:只映射了文件大小(st.st_size)的内存,没有为 BSS 段分配额外空间。

Bug 原因分析

ELF 文件中的 BSS 段

BSS(Block Started by Symbol)段是 ELF 文件中用于存储未初始化全局变量和静态变量的区域:

  1. BSS 段在文件中不占用实际空间(类型为 NOBITS
  2. 程序加载时,BSS 段需要分配内存并初始化为零
  3. BSS 段的内存大小(MemSiz)通常大于其在文件中的大小(FileSiz)

崩溃原因

  1. 原始代码只映射了文件大小的内存
  2. Dart VM 初始化时调用 dart::BSS::Initialize 尝试写入 BSS 区域
  3. BSS 区域位于映射内存之外,导致访问非法内存
  4. 触发 SIGSEGV(段错误)

具体来说:

  • 文件映射大小:0x30220d0 字节
  • BSS 段虚拟地址:0x30320d0(相对于映射基址偏移约 0x10000)
  • 访问 BSS 区域时超出映射范围,导致崩溃

修复方案

修改 blutter/src/ElfHelper.cpp

Linux 平台

添加 get_elf_mem_size 函数计算实际需要的内存大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static size_t get_elf_mem_size(int fd, size_t file_size)
{
// 读取 ELF 头
uint8_t ehdr[64];
if (read(fd, ehdr, 64) != 64)
return file_size + 0x1000;

// 检查 ELF 魔数
if (memcmp(ehdr, "\x7f" "ELF", 4) != 0)
return file_size + 0x1000;

// 获取程序头信息
uint64_t phoff = *(uint64_t*)(ehdr + 32); // e_phoff
uint16_t phnum = *(uint16_t*)(ehdr + 56); // e_phnum
uint16_t phentsize = *(uint16_t*)(ehdr + 54); // e_phentsize

// 读取程序头
lseek(fd, phoff, SEEK_SET);
std::vector<uint8_t> phdrs(phnum * phentsize);
if (read(fd, phdrs.data(), phnum * phentsize) != phnum * phentsize)
return file_size + 0x1000;

// 计算最大虚拟地址
uint64_t max_vaddr = 0;
uint64_t min_vaddr = UINT64_MAX;

for (int i = 0; i < phnum; i++) {
uint8_t* phdr = phdrs.data() + i * phentsize;
uint32_t p_type = *(uint32_t*)(phdr + 0);

if (p_type == 1) { // PT_LOAD
uint64_t p_vaddr = *(uint64_t*)(phdr + 16);
uint64_t p_memsz = *(uint64_t*)(phdr + 40);

if (p_vaddr + p_memsz > max_vaddr)
max_vaddr = p_vaddr + p_memsz;
if (p_vaddr < min_vaddr)
min_vaddr = p_vaddr;
}
}

// 计算总内存大小
size_t mem_size = max_vaddr - min_vaddr;
lseek(fd, 0, SEEK_SET);

// 确保至少分配文件大小
if (mem_size < file_size)
mem_size = file_size + 0x1000;

// 页对齐
mem_size = (mem_size + 0xfff) & ~0xfffull;

return mem_size;
}

修改 load_map_file 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void* load_map_file(const char* path)
{
int fd = open(path, O_RDONLY);
struct stat st;

fstat(fd, &st);

// 计算实际需要的内存大小(包括 BSS)
size_t memSize = get_elf_mem_size(fd, st.st_size);

// 分配足够大的内存
void* mem = mmap(NULL, memSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) {
close(fd);
return NULL;
}

// 读取文件内容
ssize_t bytesRead = read(fd, mem, st.st_size);
close(fd);

if (bytesRead != (ssize_t)st.st_size) {
munmap(mem, memSize);
return NULL;
}

// 将 BSS 区域清零
memset((char*)mem + st.st_size, 0, memSize - st.st_size);

return mem;
}

Windows 平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void* load_map_file(const char* path)
{
HANDLE hFile = CreateFileA(path, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("\nCannot find %s\n", path);
return NULL;
}

DWORD fileSize = GetFileSize(hFile, NULL);
const DWORD extraBssSize = 0x1000; // 为 BSS 分配额外空间

HANDLE hMapFile = CreateFileMapping(hFile, NULL, PAGE_READWRITE,
0, fileSize + extraBssSize, NULL);
if (hMapFile == INVALID_HANDLE_VALUE)
return NULL;

void* mem = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 0);
CloseHandle(hMapFile);
CloseHandle(hFile);

return mem;
}

验证结果

修复后重新编译并运行:

1
2
rm -rf build/blutter_dartvm2.19.6_android_arm64
python blutter.py --rebuild test_app/arm64-v8a/ test_app/out

程序成功执行并生成输出文件:

1
2
3
4
5
6
7
8
libapp is loaded at 0x7e2f0b1cd000
Dart heap at 0x7e2e00000000
Analyzing the application
Dumping Object Pool
Generating application assemblies
Generating Frida script
Dart version: 2.19.6, Snapshot: adb4292f3ec25074ca70abcd2d5c7251, Target: android arm64
flags: product no-code_comments dwarf_stack_traces_mode no-lazy_dispatchers dedup_instructions no-asserts arm64 android compressed-pointers no-null-safety

输出文件:

1
2
3
4
5
6
test_app/out/
├── asm/ # 反汇编输出目录
├── blutter_frida.js
├── ida_script/
├── objs.txt
└── pp.txt

经验总结

  1. ELF 文件映射不仅要考虑文件大小,还要考虑内存大小:BSS 段在文件中不占空间,但在内存中需要分配。

  2. 调试技巧:通过添加日志和使用 gdb 可以快速定位崩溃位置。

  3. 程序头分析readelf -l 命令可以查看 ELF 程序头,其中 FileSizMemSiz 的差异揭示了 BSS 段的存在。

  4. 正确的内存映射方式:对于需要写入 BSS 区域的程序,应使用匿名映射 + 文件读取的方式,而不是直接映射文件。