iOS开发技术逆向工程iOS基础知识

Mach-O可执行文件

2016-07-27  本文已影响6487人  青花瓷的平方

Mach-O 概述 和 部分命令介绍

我们知道Windows下的文件都是PE文件,同样在OS X和iOS中可执行文件是Mach-o格式的。
Mach-O通常有三部分组成
*头部 (Header): Mach-O文件的架构 比如Mac的 PPC, PPC64, IA-32, x86-64,ios的arm系列.
*加载命令(Load commands): .
*原始段数据(Raw segment data):可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。
官方给的图如下:

mach_o_segments

Xcode本身包含了command-line tools,命令行工具本身包含了分析和编译Mach-O,相关如下:

lipo -info lib1.a

Mach-O 分析

通常一个iOS App应用会安装在/var/mobile/Applications,系统的原生App会安装在/Applications目录下,大部分情况下,xxx.app/xxx文件并不是Mach-O格式文件,由于现在需要支持不同CPU架构的iOS设备,所以我们编译打包出来的执行文件是一个Universal Binary格式文件(通用二进制文件,也称胖二进制文件),实际上Universal Binary只不过将支持不同架构的Mach-O打包在一起,再在文件起始位置加上Fat Header来说明所包含的Mach-O文件支持的架构和偏移地址信息。
例如:

file CTRIP_WIRELESS
CTRIP_WIRELESS: Mach-O universal binary with 2 architectures
CTRIP_WIRELESS (for architecture i386): Mach-O executable i386
CTRIP_WIRELESS (for architecture x86_64):   Mach-O 64-bit executable x86_64

上面显示程序支持i386和x86_64架构,胖二进制文件定义在 /usr/include/mach-o/fat.h,我们查看一下源码

#include <stdint.h>
#include <mach/machine.h>
#include <architecture/byte_order.h>

#define FAT_MAGIC   0xcafebabe
#define FAT_CIGAM   0xbebafeca  /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

struct fat_arch {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint32_t    offset;     /* file offset to this object file */
    uint32_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
};

结构体struct fat_header:

(1).头部Header

我们可以使用 otool(1) 来观察可执行文件的头部 -- 规定了这个文件是什么,以及文件是如何被加载的。通过 -h 可以打印出头信息:
例如使用otool命令可以查看Mach-O文件的头信息,头信息就是Mach-O文件的第一部分,我们在第一部分介绍Mach-o概述已经介绍


mach_o_header

头信息的结构可以在 /usr/include/mach-o/loader.h

/*
 * The 32-bit mach header appears at the very beginning of the object file for
 * 32-bit architectures.
 */
struct mach_header {
    uint32_t    magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t    filetype;   /* type of file */
    uint32_t    ncmds;      /* number of load commands */
    uint32_t    sizeofcmds; /* the size of all the load commands */
    uint32_t    flags;      /* flags */
};

我们依次介绍这个头信息

1.magic,可以看到文件中的内容最开始部分,是以 cafe babe开头的
对于一个 二进制文件 来讲,每个类型都可以在文件最初几个字节来标识出来,即“魔数”。不同类型的 二进制文件,都有自己独特的"魔数"。
OS X上,可执行文件的标识有这样几个魔数(不同的魔数代表不同的可执行文件类型)
是mach-o文件的魔数,0xfeedface代表的是32位,0xfeedfacf代表64位,cafebabe是跨处理器架构的通用格式,#!代表的是脚本文件。
2.cputype和cupsubtype代表的是cpu的类型和其子类型,图上的例子是模拟器程序,cpu结构是x86_64,如果直接查看ipa,可以看到cpu是arm,subtype是armv7,arm64等
3.接着是filetype,2,代表可执行的文件 #define MH_EXECUTE 0×2
4.ncmds 指的是加载命令(load commands)的数量,例子中一共65个,编号0-64
5.sizeofcmds 表示23个load commands的总字节大小, load commands区域是紧接着header区域的。
6.最后个flags,例子中是0×00200085,可以按文档分析之。
也可以借助UE程序MachOView,MachOView是Mac上查看Mach-O结构的工具,如下图

mach_o_h_view

(2).加载命令(Load commands)

load commmand直接跟在 header 部分的后面,结构定义如下

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

这些加载命令在Mach-O文件加载解析时,被内核加载器或者动态链接器调用,指导如何设置加载对应的二进制数据段,加载命令的种类有很多种,在<mach-o/loader.h>头文件有简单的注释。
具体可以使用命令

otool -v -l CTRIP_WIRELESS | open -f

查看。

(3)段数据(Segments)

Segments包含了很多segment,每一个segment定义了一些Mach-O文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。每个段都有不同的功能,一般包括:

一般的段又会按不同的功能划分为几个区(section),即段所有字母大小,加两个下横线作为前缀,而区则为小写,同样加两个下横线作为前缀

下面列出段中可能包含的section:

__TEXT段:
__text, __cstring, __picsymbol_stub, __symbol_stub, __const, __litera14, __litera18;

__DATA段:
__data, __la_symbol_ptr, __nl_symbol_ptr, __dyld, __const, __mod_init_func, __mod_term_func, __bss, __commom;

__IMPORT段
__jump_table, __pointers;

其中__TEXT段中的__text是实际上的代码部分;__DATA段的__data是实际的初始数据,更加详细的说明见这里

可以通过otool –s查看某segment的某个section。

otool -s __TEXT __text a.out 
a.out:
(__TEXT,__text) section
0000000100000e80 55 48 89 e5 48 83 ec 20 c7 45 fc 00 00 00 00 89 
0000000100000e90 7d f8 48 89 75 f0 e8 a7 00 00 00 48 8b 35 8e 02 
0000000100000ea0 00 00 48 8b 0d 6f 02 00 00 48 89 f7 48 89 ce 48 
0000000100000eb0 89 45 e0 e8 90 00 00 00 48 8b 35 61 02 00 00 48 
0000000100000ec0 89 c7 e8 81 00 00 00 48 89 45 e8 48 8b 45 e8 48 
0000000100000ed0 8b 35 52 02 00 00 48 89 c7 e8 6a 00 00 00 c7 45 
0000000100000ee0 fc 00 00 00 00 48 8b 7d e0 e8 4e 00 00 00 8b 45 
0000000100000ef0 fc 48 83 c4 20 5d c3 90 90 90 90 90 90 90 90 90 
0000000100000f00 55 48 89 e5 48 83 ec 10 48 89 7d f8 48 89 75 f0 
0000000100000f10 e8 1b 00 00 00 48 8d 35 1c 01 00 00 48 89 f7 48 
0000000100000f20 89 c6 b0 00 e8 0d 00 00 00 48 83 c4 10 5d c3 

由于 -s __TEXT __text 很常见,otool 对其设置了一个缩写 -t 。我们还可以通过添加 -v 来查看反汇编代码:

otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000e80    pushq   %rbp
0000000100000e81    movq    %rsp, %rbp
0000000100000e84    subq    $0x20, %rsp
0000000100000e88    movl    $0x0, -0x4(%rbp)
0000000100000e8f    movl    %edi, -0x8(%rbp)
0000000100000e92    movq    %rsi, -0x10(%rbp)
0000000100000e96    callq   0x100000f42
0000000100000e9b    movq    0x28e(%rip), %rsi
0000000100000ea2    movq    0x26f(%rip), %rcx
0000000100000ea9    movq    %rsi, %rdi
0000000100000eac    movq    %rcx, %rsi
...

了解Mach-O的作用

(1)Xcode中配置LinkMap

LinkMap文件是Xcode产生可执行文件(Mach-O)的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置,如下图


linkmap

编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File
位于~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

LinkMap里展示了整个可执行文件的全貌,分为三段,分别是:

# Symbols:
# Address   Size        File  Name
0x100002070 0x000000B0  [  1] -[PackageManager init]
0x100002120 0x00000080  [  1] +[PackageManager share]
0x1000021A0 0x00000050  [  1] ___23+[PackageManager share]_block_invoke
0x1000021F0 0x00000080  [  1] +[PackageManager getPackageType]
0x100002270 0x000000E0  [  1] +[PackageManager isProductionEnv]
0x100002350 0x000000E0  [  1] +[PackageManager isSpecialPackageForTest]
0x100002430 0x000000E0  [  1] +[PackageManager isDevEnv]
0x100002510 0x00000020  [  1] +[PackageManager isAUTOMATIC_TEST_ENV]
0x100002530 0x00000020  [  1] +[PackageManager isPRO_PACKAGE]
0x100002550 0x00000020  [  1] +[PackageManager ApplicationVersion]
0x100002570 0x00000020  [  1] -[PackageManager packageType]
0x100002590 0x00000040  [  1] -[PackageManager setPackageType:]
0x1000025D0 0x00000033  [  1] -[PackageManager .cxx_destruct]
0x100002610 0x0000005B  [  2] _main
0x10000266B 0x00000255  [  3] -[CTMyCtripOROrderAction isEqual:]

同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。
例如第二行代表了文件序号为2(反查上面就是PackageManage.o)的share方法占用了8*16=128byte大小。

根据上面符号文件的分析,我们可以写一个脚本统计我们程序的每一个静态库和framwork以及每一个实现文件的大小,有便于我们分析程序文件大小,为代码优化,减少二进制包大小提供了优化方向。我写了一个脚本,代码如下:

#!usr/bin/python
## -*- coding: UTF-8 -*-
#
#使用简介:python linkmap.py XXX-LinkMap-normal-xxxarch.txt 或者 python linkmap.py XXX-LinkMap-normal-xxxarch.txt -g
#使用参数-g会统计每个模块.o的统计大小
#
__author__ = "zmjios"
__date__ = "2016-07-27"

import os
import re
import shutil
import sys

class SymbolModel:
    file = ""
    size = 0

def verify_linkmapfile(args):
    if len(sys.argv) < 2:
        print("请输入linkMap文件")
        return False
    
    path = args[1]

    if not os.path.isfile(path):
        print("请输入文件")
        return False

    file = open(path)
    content = file.read()
    file.close()

    #查找是否存在# Object files:
    if content.find("# Object files:") == -1:
        print("输入linkmap文件非法")
        return False
    #查找是否存在# Sections:
    if content.find("# Sections:") == -1:
        print("输入linkmap文件非法")
        return False
    #查找是否存在# Symbols:
    if content.find("# Symbols:") == -1:
        print("输入linkmap文件非法")
        return False

    return True 

def symbolMapFromContent():
    symbolMap = {}
    reachFiles = False
    reachSections = False
    reachSymblos = False
    file = open(sys.argv[1])
    for line in file.readlines():
        if line.startswith("#"):
            if line.startswith("# Object files:"):
                reachFiles = True
            if line.startswith("# Sections:"):
                reachSections = True
            if line.startswith("# Symbols:"):
                reachSymblos = True
        else:
            if reachFiles == True and reachSections == False and reachSymblos == False:
                #查找 files 列表,找到所有.o文件
                location = line.find("]")
                if location != -1:
                    key = line[:location+1]
                    if  symbolMap.get(key) is not None:
                        continue
                    symbol = SymbolModel()
                    symbol.file = line[location + 1:]
                    symbolMap[key] = symbol
            elif reachFiles == True and reachSections == True and reachSymblos == True:
                #'\t'分割成三部分,分别对应的是Address,Size和 File  Name
                symbolsArray = line.split('\t')
                if len(symbolsArray) == 3:
                    fileKeyAndName = symbolsArray[2]
                    #16进制转10进制
                    size = int(symbolsArray[1],16)
                    location = fileKeyAndName.find(']')
                    if location != -1:
                        key = fileKeyAndName[:location + 1]
                        symbol = symbolMap.get(key)
                        if symbol is not None:
                            symbol.size = symbol.size + size
    file.close()
                            
    return symbolMap
    
def sortSymbol(symbolList):
     return sorted(symbolList, key=lambda s: s.size,reverse = True)

def buildResultWithSymbols(symbols):
    results = ["文件大小\t文件名称\r\n"]
    totalSize = 0
    for symbol in symbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("总大小: %.2fM" % (totalSize/1024.0/1024.0))
    return results

def buildCombinationResultWithSymbols(symbols):
    #统计不同模块大小
    results = ["库大小\t库名称\r\n"]
    totalSize = 0
    combinationMap = {}
    
    for symbol in symbols:
        names = symbol.file.split('/')
        name = names[len(names) - 1].strip('\n')
        location = name.find("(")
        if name.endswith(")") and location != -1:
            component = name[:location]
            combinationSymbol = combinationMap.get(component)
            if combinationSymbol is None:
                combinationSymbol = SymbolModel()
                combinationMap[component] = combinationSymbol

            combinationSymbol.file = component
            combinationSymbol.size = combinationSymbol.size + symbol.size
        else:
            #symbol可能来自app本身的目标文件或者系统的动态库
            combinationMap[symbol.file] = symbol
    sortedSymbols = sortSymbol(combinationMap.values())

    for symbol in sortedSymbols:
        results.append(calSymbol(symbol))
        totalSize += symbol.size
    results.append("总大小: %.2fM" % (totalSize/1024.0/1024.0))

    return results

def calSymbol(symbol):
    size = ""
    if symbol.size / 1024.0 / 1024.0 > 1:
        size = "%.2fM" % (symbol.size / 1024.0 / 1024.0)
    else:
        size = "%.2fK" % (symbol.size / 1024.0)
    names = symbol.file.split('/')
    if len(names) > 0:
        size = "%s\t%s" % (size,names[len(names) - 1])
    return size

def analyzeLinkMap():
    if verify_linkmapfile(sys.argv) == True:
        print("**********正在开始解析*********")
        symbolDic = symbolMapFromContent()
        symbolList = sortSymbol(symbolDic.values())
        if len(sys.argv) >= 3 and sys.argv[2] == "-g":
            results = buildCombinationResultWithSymbols(symbolList)
        else:
            results = buildResultWithSymbols(symbolList)
        for result in results:
            print(result)
        print("***********解析结束***********")


if __name__ == "__main__":
    analyzeLinkMap()

(2).查找无用selector和无用class

WeMobileDev公众号之前介绍了iOS微信安装包瘦身也做了相关介绍。无论是Mach-O或者是linkMap文件,都能做相关操作。具体原理是过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。
万能的github(https://github.com/nst/objc_cover)已经有人写了相关脚本,有需要可以参考。

(3)class-dump和越狱相关

class-dump正式利用Mach-O文件导出出Mac或者iOS app的头文件的命令行工具。

上一篇 下一篇

猜你喜欢

热点阅读