3. 定位并解析LC_SEGMENT_64

任务目标

在 Mach-O 64 位文件中,LC_SEGMENT_64 加载命令描述了可执行文件如何映射到进程地址空间。本案例将编写一个 Python 脚本,解析 Mach-O 文件头,定位所有 LC_SEGMENT_64 命令,并提取每个段的段名(segname)、虚拟地址(vmaddr)、文件偏移(fileoff)和段权限(maxprot/initprot)。

核心概念

Mach-O 文件由文件头(Header)、加载命令(Load Commands)和数据区组成。64 位 Mach-O 文件头以 magic 值 0xfeedfacf(MH_MAGIC_64)标识。加载命令紧跟在文件头之后,每个命令由 cmd(命令类型)和 cmdsize(命令大小)开头。LC_SEGMENT_64 的值为 0x19。解析时需注意:文件头大小为 32 字节,加载命令需按 8 字节对齐。

完整实现

import struct

MH_MAGIC_64 = 0xfeedfacf
LC_SEGMENT_64 = 0x19

# 64位文件头: magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags, reserved
HEADER_FORMAT = '<IIIIIIII'
HEADER_SIZE = 32

# segment_command_64: cmd, cmdsize, segname[16], vmaddr, vmsize, fileoff, filesize, maxprot, initprot, nsects, flags
SEGMENT_FORMAT = '<II16sQQQQiiII'
SEGMENT_SIZE = 72

def parse_segments(path):
    with open(path, 'rb') as f:
        header_data = f.read(HEADER_SIZE)
        if len(header_data) < HEADER_SIZE:
            raise ValueError('文件过小,不是有效的 Mach-O 文件')
        
        magic, _, _, _, ncmds, _, _, _ = struct.unpack(HEADER_FORMAT, header_data)
        if magic != MH_MAGIC_64:
            raise ValueError(f'不是 64 位 Mach-O 文件 (magic=0x{magic:x})')
        
        segments = []
        offset = HEADER_SIZE
        for _ in range(ncmds):
            f.seek(offset)
            cmd_data = f.read(8)
            if len(cmd_data) < 8:
                break
            cmd, cmdsize = struct.unpack('<II', cmd_data)
            
            if cmd == LC_SEGMENT_64:
                f.seek(offset)
                seg_data = f.read(SEGMENT_SIZE)
                if len(seg_data) < SEGMENT_SIZE:
                    break
                _, _, segname_raw, vmaddr, _, fileoff, _, maxprot, initprot, _, _ = \
                    struct.unpack(SEGMENT_FORMAT, seg_data)
                segname = segname_raw.split(b'\x00')[0].decode('utf-8', errors='replace')
                segments.append({
                    'segname': segname,
                    'vmaddr': hex(vmaddr),
                    'fileoff': hex(fileoff),
                    'maxprot': f'0x{maxprot:x}',
                    'initprot': f'0x{initprot:x}'
                })
            offset += cmdsize
    
    return segments

if __name__ == '__main__':
    import sys
    if len(sys.argv) != 2:
        print('用法: python parse_segment.py <mach-o文件路径>')
        sys.exit(1)
    
    try:
        segs = parse_segments(sys.argv[1])
        if not segs:
            print('未找到 LC_SEGMENT_64 命令')
        else:
            print(f'共找到 {len(segs)} 个 LC_SEGMENT_64 段:')
            for s in segs:
                print(f"  段名: {s['segname']:<16} vmaddr: {s['vmaddr']:<12} "
                      f"fileoff: {s['fileoff']:<12} maxprot: {s['maxprot']}  initprot: {s['initprot']}")
    except Exception as e:
        print(f'解析失败: {e}')
        sys.exit(1)

运行与验证

环境要求:Python 3.6+,无需第三方库。

运行方式:

python parse_segment.py /path/to/your/binary

可使用 macOS 系统自带的任意可执行文件(如 /bin/ls)作为测试输入。

验证方法:

  1. 程序输出应包含 __PAGEZERO、__TEXT、__DATA 等典型段名;
  2. 用 otool -l /bin/ls 对比输出,段名、vmaddr、fileoff 应与脚本结果一致;
  3. 权限值中 maxprot 与 initprot 通常相同,常见值为 0x5(r-x)、0x3(rw-)等。

预期输出示例(以 /bin/ls 为例,实际地址因系统版本而异):

共找到 4 个 LC_SEGMENT_64 段:
  段名: __PAGEZERO        vmaddr: 0x0            fileoff: 0x0            maxprot: 0x0  initprot: 0x0
  段名: __TEXT            vmaddr: 0x100000000    fileoff: 0x0            maxprot: 0x5  initprot: 0x5
  段名: __DATA            vmaddr: 0x100003000    fileoff: 0x3000         maxprot: 0x3  initprot: 0x3
  段名: __LINKEDIT        vmaddr: 0x100004000    fileoff: 0x4000         maxprot: 0x1  initprot: 0x1

常见问题

  • magic 不匹配:确认文件是 64 位 Mach-O(fat 通用二进制需先提取单架构,可用 lipo -thin arm64 处理);
  • 段名乱码:段名字段是固定 16 字节的 C 字符串,代码已按 \x00 截断处理;
  • 对齐问题:加载命令之间可能存在填充字节,脚本通过 cmdsize 跳转而非固定步长,可正确处理。