iOS 恢复调用栈(适配iOS14)

0x00 前言

之前杨君大佬写过这个工具:iOS符号表恢复,恢复后堆栈可显示函数信息。

不知道为何我使用后,堆栈依旧没有显示出OC函数来。所以整理下资料,在restore-symbol工程上再适配 iOS14。

不同场景下,也可以用xia0大佬写过的 Frida调用栈符号恢复,利用 runtime 机制动态获取函数信息,近似匹配来恢复调用栈。

0x01 符号与调用栈

1. 调用栈

在分析程序 crash 时,一般会查看调用栈来定位问题。如果可执行文件没有经过 strip,符号表还保存着符号信息,调用栈是可以显示函数名称的。

  • 无符号调用栈

          "0   AlipayWallet                        0x0000000107545f18 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 29852464",
          "1   AlipayWallet                        0x0000000107567f94 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 29991852",
          "2   AlipayWallet                        0x0000000107571b98 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30031792",
          "3   AlipayWallet                        0x00000001075a82d8 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30254832",
          "4   AlipayWallet                        0x000000010758f778 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30153616",
          "5   AlipayWallet                        0x00000001075745b4 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30042572",
          "6   AlipayWallet                        0x0000000107557770 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 29924232",
          "7   libsystem_pthread.dylib             0x0000000183459d8c _pthread_start + 156",
          "8   libsystem_pthread.dylib             0x000000018345d76c thread_start + 8"
    

没有符号的调用栈无法给出有效函数信息,导致无法定位问题所在。

2. 符号

  • 符号(Symbol:类名、函数名或变量名称为符号名 (Symbol Name);

  • 按类型分,符号可以分三类:

    • 全局符号:目标文件外可见的符号,可以被其他目标文件引用,或者需要其他目标文件定义;
    • 局部符号:只在目标文件内可见的符号,指只在目标文件内可见的函数和变量;
    • 调试符号:包括 行号 信息的调试符号信息,行号信息中记录了函数和变量所对应的 文件和文件行号
  • 符号表(Symbol Table:符号表是 内存地址与函数名、文件名、行号 的映射表;每个定义的符号有一个对应的值,叫做 符号值(Symbol Value),对于变量和函数来说,符号值就是他们的地址;符号表元素如下所示:

    < 起始地址 > < 结束地址 > < 函数 > [< 文件名: 行号 >]
    
    
  • dSYM(debug symbols):是 iOS 的 符号表文件,存储 16 进制 地址信息和符号的映射文件;文件名通常为:xxx.app.dSYM,类似 Android 构建 release 产生的 mapping 文件;利用 dSYM 文件文件,可以将堆栈信息中地址信息还原成对应的符号,帮助问题排查;

App relaese 包一般会 strip 掉局部符号和调试符号以减小包体积,去掉这些符号不影响 App 正常运行,也可以一定程度上保护 App。

App 被 strip 掉符号后,调用栈无法显示函数名称。如果得到函数名称与地址,恢复符号表,将符号表 patch 到可执行文件后,理论上调用栈就可以显示出信息。

0x02 获取函数信息

1. objective-c 函数

一般情况下 App strip 掉符号后,如果不借助外部 dSYM 文件,是无法恢复的函数信息,比如 C 函数在可执行文件仅剩地址。

objective-c 函数信息除了保存在符号表中,还保存在其他段中

  • __TEXT.__objc_methname - Method names for locally implemented methods
  • __TEXT.__objc_classname - Names for locally implemented classes
  • __TEXT.__objc_methtype - Types for locally implemented method types
  • __DATA.__objc_classlist - An array of pointers to ObjC classes
  • __DATA.__objc_nlclslist - An array of pointers to classes who implement +load
  • __DATA.__objc_catlist - List of ObjC categories
  • __DATA.__objc_protolist - List of ObjC protocols
  • __DATA.__objc_imageinfo - Version info, not really useful
  • __DATA.__objc_const - Constant data, i.e. class_ro_t data
  • __DATA.__objc_selrefs - External references to selectors
  • __DATA.__objc_protorefs - External references to protocols
  • __DATA.__objc_classrefs - External references to other classes
  • __DATA.__objc_superrefs - External references to super classes
  • __DATA.__objc_ivar - Offsets to ObjC properties
  • __DATA.__objc_data - Misc ObjC storage, notably ObjC classes

strip 仅删除符号表中的数据,所以 OC 符号可以恢复。

2. 解析 __objc_* section

从 objc 的开源代码来看如何解析 __objc_* section

i. 现用 class 结构

typedef struct objc_class *Class;

struct objc_object {
private:
    isa_t isa; // 一个指针大小
    ...
}
struct objc_class : objc_object {
    // Class ISA;
    Class superclass; // 一个指针大小
    cache_t cache;     // 两个指针大小        // formerly cache pointer and vtable
    class_data_bits_t bits; // 一个指针大小   // class_rw_t * plus custom rr/alloc flags
    ...
}

class_data_bits_t 是一个指针大小的结构体,保存着类的方法、属性、遵循的协议等信息。

struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
    ...

  class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);
  }

  const class_ro_t *safe_ro() const {class_rw_t *maybe_rw = data();
    if (maybe_rw->flags & RW_REALIZED) {
      // maybe_rw is rw
      return maybe_rw->ro();} else {
      // maybe_rw is actually ro
      return (class_ro_t *)maybe_rw;
    }
  }
  
} 

在编译期间,class_ro_t 结构体就已经确定,objc_class 中的 bits 的 data 部分存放着该结构体的地址。

运行 runtime 的 realizeClass 方法时,会生成 class_rw_t 结构体,该结构体包含了 class_ro_t,并且更新 data 部分,换成 class_rw_t 结构体的地址。

所以保存在 Mach-O 可执行文件中的结构是 class_ro_t

在 iOS14 后,class_rw_t 结构体发生较大变化,class_ro_t 结构体未变化。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

ii、方法、属性、协议

class_ro_t 里面的 method_list_tivar_list_t property_list_t 都使用 entsize_list_tt 的结构体模板;iOS 14 method_list_t 有较大改变

// iOS 14 以下
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
      // other code
    uint32_t entsize() const {return entsizeAndFlags & ~FlagMask;}
}

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {...};

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {...};

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};


// iOS14 以下
struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;
  ...
};

  • 方法
// iOS 14
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    uint32_t entsize() const {return entsizeAndFlags & ~FlagMask;}
    uint32_t flags() const {return entsizeAndFlags & FlagMask;}
};

struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this)/ entsize());
        ASSERT(i < count);
        return i;
    }

    bool isSmallList() const {return flags() & method_t::smallMethodListFlag;
    }
};


struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

private:
    bool isSmall() const {return ((uintptr_t)this & 1)== 1;}

    // The representation of a "small" method. This stores three
    // relative offsets to the name, types, and implementation.
    struct small {
        // The name field either refers to a selector (in the shared
        // cache)or a selref (everywhere else).
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP> imp;

        bool inSharedCache() const {
            return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS &&
                    objc::inSharedCache((uintptr_t)this));}
    };

    small &small() const {ASSERT(isSmall());
        return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);}
};

方法以 method_t 结构保存,iOS14 以上该结构发生巨大变化。struct big 在 64 位的系统上会占用 24 字节,name、types、imp 分别占用 64 bit 大小,与之前一样。但是 struct small 占用 12 字节,name、types、imp 分别占用 32 bit 大小。

name、types、imp 分别指向方法的 名称、参数数据、函数指针,苹果考虑到镜像中的方法都是固定的,不会跑到其他镜像中去。其实不需要 64 位寻址的指针,只需要 32 位即可 (多余 32 位寻址,可执行文件在内存中要超过 4G)。small 结构里面的数据,都是相对地址偏移,不是内存中的具体位置。如果要还原,需要进行计算。

template <typename T>
struct RelativePointer: nocopy_t {
    int32_t offset;

    T get() const {if (offset == 0)
            return nullptr;
        uintptr_t base = (uintptr_t)&offset;
        uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
        uintptr_t pointer = base + signExtendedOffset;
        return (T)pointer;
    }
};

3. 手动解析

现在从 mach-o 文件来看怎么一步步找到 method 信息。

__objc_classlist 节保存所有类的地址,以第一个 class 为例,地址为:0x010001a340

跳转到 0x010001a340 地址处,红框所包即 class 结构所保存的值。方法等信息保存在 class_data_bits_t 处,class_data_bits_t 的值为 0x01000187a8。

跳转到 0x01000187a8 地址处,里面保存的为 class_ro_t 结构。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

对照结构体内容,可知 baseMethodList 的地址为 0x0100010c78。

前 64 位 分别保存 baseMethodList 的 flag 和 count,flag 判断后续保存的 method_t 结构是 struct big 还是 struct small,

如果是 big 的话,以此取三个 64 位值,就可以得到 函数名称、类型、函数地址。

struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    

smallMethodListFlag 值为 0x80000000, flag 值为 0x800000c0,进行 位与 运算,说明以下的 method_t 结构是 small 结构。

struct small {
        // The name field either refers to a selector (in the shared
        // cache)or a selref (everywhere else).
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP> imp;

        bool inSharedCache() const {
            return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS &&
                    objc::inSharedCache((uintptr_t)this));}
    };

template <typename T>
struct RelativePointer: nocopy_t {
    int32_t offset;

    T get() const {if (offset == 0)
            return nullptr;
        uintptr_t base = (uintptr_t)&offset;
        uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
        uintptr_t pointer = base + signExtendedOffset;
        return (T)pointer;
    }
};
  • name

    name 的值为 0x9398,对应到 RelativePointer 的源代码,offset 值为 0x9398。

    源码需要取 offset 的地址,才能计算出真正 name 的地址。offset 的地址可以计算出为 0x0100010c80,baseMethodList 的地址 + sizeof(flag) + sizeof(count) = 0x0100010c78 + 0x4 + 0x4。

    name 真正地址即为 (int)0x9398 + 0x0100010c80 = 0x10001A018

  • type

    Type 保存函数的参数和返回值类型,同理可计算出 type 的真正地址:

    (int)0x1ffa + 0x0100010c84 = 0x0100012c7e

  • imp

    imp 是函数地址,同理可以用计算出:

    (int)0xffff5fd8 + 0x0100010c88 = 0x0100006c60 (这里 0xffff5fd8 是负数)

0x03 恢复符号表

符号在符号表中用 nlist 结构体保存。

1. nlist 结构体

// Describes an entry in the symbol table. It’s declared in /usr/include/mach-o/nlist.h.
struct nlist
{
    union {
#ifndef __LP64__
				char *n_name;	/* for use when in-core */
#endif
        long  n_strx;
    } n_un;
    unsigned char n_type;
    unsigned char n_sect;
    short n_desc;
    
#ifdef __LP32__
      /*32 位中 4byte*/
    unsigned long n_value;
#else
      /*64 位中 8byte*/
    unsigned long long n_value; 
#endif
  
};

i. u_un(n_strx)

u_un 的 n_strx 代表该符号在字符串表中偏移。

假如 n_strx 值为 0x22,那么该符号名称字符串相当于文件的偏移为:stroff + 0x22,空串为偏移为 0,对应着 0x00。

ii. n_type

n_type 字段主要用来标识符号的种类。n_type 拥有 8 个 bit,分配如下。

/*
 * Symbols with a index into the string table of zero (n_un.n_strx == 0) are
 * defined to have a null, "", name.  Therefore all string indexes to non null
 * names must not have a zero string index.  This is bit historical information
 * that has never been well documented.
 */

/*
 * The n_type field really contains four fields:
 *	unsigned char N_STAB:3,
 *		      N_PEXT:1,
 *		      N_TYPE:3,
 *		      N_EXT:1;
 * which are used via the following masks.
 */
#define	N_STAB	0xe0  /* if any of these bits set, a symbolic debugging entry */
#define	N_PEXT	0x10  /* private external symbol bit */
#define	N_TYPE	0x0e  /* mask for the type bits */
#define	N_EXT	0x01  /* external symbol bit, set for external symbols */

/*
 * Only symbolic debugging entries have some of the N_STAB bits set and if any
 * of these bits are set then it is a symbolic debugging entry (a stab).  In
 * which case then the values of the n_type field (the entire field) are given
 * in <mach-o/stab.h>
 */

/*
 * Values for N_TYPE bits of the n_type field.
 */
#define	N_UNDF	0x0		/* undefined, n_sect == NO_SECT */
#define	N_ABS	0x2		/* absolute, n_sect == NO_SECT */
#define	N_SECT	0xe		/* defined in section number n_sect */
#define	N_PBUD	0xc		/* prebound undefined (defined in a dylib) */
#define N_INDR	0xa		/* indirect */
  • bit[0:1] 是 N_EXT,表示是外部符号。

  • bit[1:4] 是 N_TYPE,表示符号类型。

    分 N_UNDF 未定义、N_ABS 绝对地址、N_SECT 本地符号、N_PBUD 预绑定符号、N_INDR 同名符号几种类型。

  • bit[4:5] 是 N_PEXT,表示私有外部符号。

  • bit[5:8] 是 N_STAB,表示调试符号,具体定义在 /usr/include/mach-o/stab.h

    nlist_64 fields: n_value         n_type n_sect  n_desc  n_strx  
                    0000000100006944 0e     01      0000    00001bc4 -[ViewController locationManager:didUpdateLocations:]
                    00000001000077ac 0e     01      0000    00001bfa -[ViewController didReceiveMemoryWarning]
                    00000001000077f8 0e     01      0000    00001c24 -[ViewController locationManager:didUpdateHeading:]
                    0000000100007950 0e     01      0000    00001c58 -[ViewController writePermissionToFile:]
stab 类型
/*
 * Symbolic debugger symbols.  The comments give the conventional use for
 * 
 * 	.stabs "n_name", n_type, n_sect, n_desc, n_value
 *
 * where n_type is the defined constant and not listed in the comment.  Other
 * fields not listed are zero. n_sect is the section ordinal the entry is
 * refering to.
 */
#define	N_GSYM	0x20	/* global symbol: name,,NO_SECT,type,0 */
#define	N_FNAME	0x22	/* procedure name (f77 kludge): name,,NO_SECT,0,0 */
#define	N_FUN	0x24	/* procedure: name,,n_sect,linenumber,address */
#define	N_STSYM	0x26	/* static symbol: name,,n_sect,type,address */
#define	N_LCSYM	0x28	/* .lcomm symbol: name,,n_sect,type,address */
#define N_BNSYM 0x2e	/* begin nsect sym: 0,,n_sect,0,address */
#define N_AST	0x32	/* AST file path: name,,NO_SECT,0,0 */
#define N_OPT	0x3c	/* emitted with gcc2_compiled and in gcc source */
#define	N_RSYM	0x40	/* register sym: name,,NO_SECT,type,register */
#define	N_SLINE	0x44	/* src line: 0,,n_sect,linenumber,address */
#define N_ENSYM 0x4e	/* end nsect sym: 0,,n_sect,0,address */
#define	N_SSYM	0x60	/* structure elt: name,,NO_SECT,type,struct_offset */
#define	N_SO	0x64	/* source file name: name,,n_sect,0,address */
#define	N_OSO	0x66	/* object file name: name,,0,0,st_mtime */
#define	N_LSYM	0x80	/* local sym: name,,NO_SECT,type,offset */
#define N_BINCL	0x82	/* include file beginning: name,,NO_SECT,0,sum */
#define	N_SOL	0x84	/* #included file name: name,,n_sect,0,address */
#define	N_PARAMS  0x86	/* compiler parameters: name,,NO_SECT,0,0 */
#define	N_VERSION 0x88	/* compiler version: name,,NO_SECT,0,0 */
#define	N_OLEVEL  0x8A	/* compiler -O level: name,,NO_SECT,0,0 */
#define	N_PSYM	0xa0	/* parameter: name,,NO_SECT,type,offset */
#define N_EINCL	0xa2	/* include file end: name,,NO_SECT,0,0 */
#define	N_ENTRY	0xa4	/* alternate entry: name,,n_sect,linenumber,address */
#define	N_LBRAC	0xc0	/* left bracket: 0,,NO_SECT,nesting level,address */
#define N_EXCL	0xc2	/* deleted include file: name,,NO_SECT,0,sum */
#define	N_RBRAC	0xe0	/* right bracket: 0,,NO_SECT,nesting level,address */
#define	N_BCOMM	0xe2	/* begin common: name,,NO_SECT,0,0 */
#define	N_ECOMM	0xe4	/* end common: name,,n_sect,0,0 */
#define	N_ECOML	0xe8	/* end common (local name): 0,,n_sect,0,address */
#define	N_LENG	0xfe	/* second stab entry with length information */

/*
 * for the berkeley pascal compiler, pc(1):
 */
#define	N_PC	0x30	/* global pascal symbol: name,,NO_SECT,subtype,line */

可以用 nm -a 查看所有符号,显示 stab 类型。

0000000100008414 - 01 0000   FUN -[ViewController .cxx_destruct]
0000000100008414 t -[ViewController .cxx_destruct]
0000000100006e64 t -[ViewController viewDidLoad]
0000000100006e64 - 01 0000   FUN -[ViewController viewDidLoad]
0000000100007cd4 t -[ViewController viewWillAppear:]
0000000100007cd4 - 01 0000   FUN -[ViewController viewWillAppear:]
0000000000000000 - 00 0000  GSYM _OBJC_CLASS_$_ViewController
000000010001a628 S _OBJC_CLASS_$_ViewController
0000000000000000 - 00 0000  GSYM _OBJC_METACLASS_$_ViewController
000000010001a650 S _OBJC_METACLASS_$_ViewController
0000000100018718 - 13 0000 STSYM __OBJC_$_INSTANCE_METHODS_ViewController
0000000100018718 s __OBJC_$_INSTANCE_METHODS_ViewController
0000000100018828 s __OBJC_$_INSTANCE_VARIABLES_ViewController
0000000100018828 - 13 0000 STSYM __OBJC_$_INSTANCE_VARIABLES_ViewController
0000000100018850 s __OBJC_$_PROP_LIST_ViewController
0000000100018850 - 13 0000 STSYM __OBJC_$_PROP_LIST_ViewController
00000001000186b0 s __OBJC_CLASS_PROTOCOLS_$_ViewController
00000001000186b0 - 13 0000 STSYM __OBJC_CLASS_PROTOCOLS_$_ViewController
00000001000188a8 - 13 0000 STSYM __OBJC_CLASS_RO_$_ViewController
00000001000188a8 s __OBJC_CLASS_RO_$_ViewController
00000001000186d0 s __OBJC_METACLASS_RO_$_ViewController
00000001000186d0 - 13 0000 STSYM __OBJC_METACLASS_RO_$_ViewController

iii. n_sect

section 索引,说明符号保存在哪一个 section 中,比如 -[ViewController .cxx_destruct] 保存在 __TEXT__text 节中,__text 节的索引为 1。

iv. n_desc

未定义符号和 weak 符号的类型等。链接相关。

v. n_value

随着符号的种类,也就是 n_type 值的不同,n_value 也有不一样的含义。
如果是 N_SECT 符号,n_value 是符号所在的地址。

0x04 patch 可执行文件

1. Mach-O 简介

Mach 则是一种操作系统内核,Mach 内核被 NeXT 公司的 NeXTSTEP 操作系统使用。在 Mach 上,一种可执行的文件格是就是 Mach-O(Mach Object file format)。

Mach-O 文件的格式如下图所示:

15084241951708

  • Header:保存了 Mach-O 的一些基本信息,包括了平台、文件类型、LoadCommands 的个数等等。
  • LoadCommands:这一段紧跟 Header,加载 Mach-O 文件时会使用这里的数据来确定内存的分布。
  • Data:每一个 segment 的具体数据都保存在这里,这里包含了具体的代码、数据等等。

2. patch 加载命令

所有 load_command 都有的数据结构为:

struct load_command {
	uint32_t cmd;		/* type of load command */
	uint32_t cmdsize;	/* total size of command in bytes */
};

cmd 字段代表当前加载命令的类型,cmdsize 字段代表当前加载命令的大小。

Load Commands 直接就跟在 Header 后面,所有 command 占用内存的总和在 Mach-O Header 里面已经给出了。

在加载过 Header 之后就是通过解析 LoadCommand 来加载接下来的数据了。

i. LC_SYMTAB

LC_SYMTAB 数据结构如下,保存着符号表以及字符串表在 dylib 文件中的偏移和大小。

struct symtab_command
{
     unsigned long cmd;
     unsigned long cmdsize;
     unsigned long symoff;
     unsigned long nsyms;
     unsigned long stroff;
     unsigned long strsize;
};

/*
cmd 以及 cmdsize 如上文所说:cmd 字段代表当前加载命令的类型,cmdsize 字段代表当前加载命令的大小

symoff: image 文件开头到符号表位置的字节偏移,符号表是 **nlist 结构体** 数组
nsyms: 符号个数
stroff: image 文件开头到字符串表位置的字节偏移
strsize: 字符串表所占大小
*/

获取符号表和字符表的偏移后,将恢复的符号信息添加到符号表里,将函数名的字符串添加到字符串表。

并且修改 LC_SYMTAB 结构中数据:

  • 符号的个数

    一共新增 152 个符号

  • 字符串表偏移

    152 * 16 + 126024 = 128456

    increase_sym_num * sizeof(nlist) + orig_str_off = new_str_off

  • 字符串表的大小。

    新增 152 个符号的名称,4696 + 5456 = 10152

    orig_str_size + increase_str_size = new_str_size

ii. LC_DYSYMTAB

LC_DYSYMTAB 记录各种符号在符号表索引和个数。

struct dysymtab_command {
    uint32_t cmd;	/* LC_DYSYMTAB */
    uint32_t cmdsize;	/* sizeof(struct dysymtab_command) */

    uint32_t ilocalsym;	/* index to local symbols */
    uint32_t nlocalsym;	/* number of local symbols */

    uint32_t iextdefsym;/* index to externally defined symbols */
    uint32_t nextdefsym;/* number of externally defined symbols */

    uint32_t iundefsym;	/* index to undefined symbols */
    uint32_t nundefsym;	/* number of undefined symbols */

    uint32_t tocoff;	/* file offset to table of contents */
    uint32_t ntoc;	/* number of entries in table of contents */

    uint32_t modtaboff;	/* file offset to module table */
    uint32_t nmodtab;	/* number of module table entries */

    uint32_t extrefsymoff;	/* offset to referenced symbol table */
    uint32_t nextrefsyms;	/* number of referenced symbol table entries */

    uint32_t indirectsymoff; /* file offset to the indirect symbol table */
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */

    uint32_t extreloff;	/* offset to external relocation entries */
    uint32_t nextrel;	/* number of external relocation entries */

    uint32_t locreloff;	/* offset to local relocation entries */
    uint32_t nlocrel;	/* number of local relocation entries */

};	

新增本地符号 140 个,外部符号 12 个,一共 152 个。修改相应符号数据。

重定位项表偏移(indSym table offset)因为符号表和字符串表增大,所以也会增大。

125296 + 152 * 16 = 127728

Orig_indSym_offset + Increase_sym_num * sizeof(nlist) = new_indSym_offset

iii. LC_SEGMENT

表示一个段加载命令,需要将它加载到对应的进程空间中。

 /*
 * The 64-bit segment load command indicates that a part of this file is to be
 * mapped into a 64-bit task's address space.  If the 64-bit segment has
 * sections then section_64 structures directly follow the 64-bit segment
 * command and their size is reflected in cmdsize.
 */
struct segment_command_64 { /* for 64-bit architectures */
	uint32_t	cmd;		/* LC_SEGMENT_64 */
	uint32_t	cmdsize;	/* includes sizeof section_64 structs */
	char		segname[16];	/* segment name */
	uint64_t	vmaddr;		/* memory address of this segment */
	uint64_t	vmsize;		/* memory size of this segment */
	uint64_t	fileoff;	/* file offset of this segment */
	uint64_t	filesize;	/* amount to map from the file */
	vm_prot_t	maxprot;	/* maximum VM protection */
	vm_prot_t	initprot;	/* initial VM protection */
	uint32_t	nsects;		/* number of sections in segment */
	uint32_t	flags;		/* flags */
};
  • segname 16 字节大小,用来存储段的名称
  • vmaddr 段要加载的虚拟内存的地址
  • vmsize 段所占的虚拟内存的大小
  • fileoff 段数据所在文件中的偏移位置
  • filesize 段数据实际的大小
  • maxprot 页面所需要的最高内存保护
  • initprot 页面初始的内存保护
  • nsects 段所包含的节区
  • flags 段的标志信息

__LINKEDIT 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。

__LINKEDIT 的 LC_SEGMENT 只需要修改段的大小:

36048 + 152 * 16 + 5456 = 43936

file_size + Increase_sym_num * sizeof(nlist) + increase_str_size = new_file_size

0x05 效果

恢复支付宝符号并打印调用栈:

      "0   AlipayWallet                        0x000000010a5e0660 +[AUNetworkInfo bssid] + 232",
      "1   AlipayWallet                        0x000000010a5dfe90 +[AUNetworkInfo networkInfo] + 160",
      "2   AlipayWallet                        0x000000010a5d99e0 +[AUDeviceInfo deviceInfo] + 132",
      "3   AlipayWallet                        0x000000010a5d8c10 +[AUDeviceInfo deviceInfoWithoutAsyncData] + 84",
      "4   AlipayWallet                        0x000000010a5d8934 +[AUDeviceInfo deviceInfoWithBlock:] + 224",
      "5   libdispatch.dylib                   0x000000018340a610 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 374288",
      "6   libdispatch.dylib                   0x000000018340b184 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 377220",
      "7   libdispatch.dylib                   0x00000001833e4b50 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 219984",
      "8   libdispatch.dylib                   0x00000001833f1110 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 270608",
      "9   libdispatch.dylib                   0x00000001833f18b0 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 272560",
      "10  libsystem_pthread.dylib             0x000000018345ab48 _pthread_wqthread + 212",
      "11  libsystem_pthread.dylib             0x000000018345d760 start_wqthread + 8"

代码:GitHub - HeiTanBc/restore-symbol: A reverse engineering tool to restore stripped symbol table for iOS app.

参考资料

https://opensource.apple.com/source/objc4/objc4-781/

https://opensource.apple.com/source/objc4/objc4-818.2/

21 个赞

:+1: 紫薯布丁


紫薯布丁

原因是?紫薯布丁

:call_me_hand:t6: :call_me_hand:t6: :call_me_hand:t6: :call_me_hand:t6: :call_me_hand:t6: :call_me_hand:t6:

使用这个方法依旧显示 对swift源码无效么?


bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001ae089644 UIKitCore`-[UIActivityIndicatorView startAnimating]
    frame #1: 0x0000000100d527c4 ***`___lldb_unnamed_symbol11503$$*** + 1804
    frame #2: 0x0000000100b153bc ***`___lldb_unnamed_symbol3013$$*** + 3520
    frame #3: 0x0000000100b15d08 ***`___lldb_unnamed_symbol3016$$*** + 148

之前的 restore-symbol 项目直接将新增的符号表内容添加到旧符号表后面,也没有修复 LC_DYSYMTAB。所以猜测加载的时候,没有把添加的符号成功绑定吧。

测试过就算不修改 LC_DYSYMTAB 内容,把新增的符号(都是本地符号)都插入到符号表里面本地符号后(不是直接把新增内容添加到符号表后边),可以成功恢复堆栈的。

本来这个方法只能用在 OC 函数的,swift 的情况类似 C

1 个赞

:+1: :+1: :+1: :+1: :+1: :+1:
Mark,后续用来学习

mark,mach-o 楼主很溜

膜膜膜

膜拜大佬