macOS修改MachO文件实现动态库注入(一)

前面已经提到可以通过修改环境变量DYLD_INSERT_LIBRARIES注入动态库,但这种方法具有一定的局限性,在开启SIP的机器上,应用程序可能无法继承该环境变量,导致注入失败。那么有没有局限性更小的注入方法?本文所展示的代码片段来自开源项目FishHook,更多细节可参考该项目。

引入

可以找个Mach-O (Mach Object File Format)文件先分析一下。和Linux系统上的ELF (Extensible Firmware Interface)、Windows系统上的PE (Portable Executable)文件相比,Mach-O文件结构大体与之类似,可分为Header、Segment、Section等部分,使用MachOView工具可以方便查看二进制内容。

Mach-O文件可分为单二进制和胖二进制(Fat Binary),即支持多处理器架构的二进制文件,使用file命令可查看文件支持的架构,如下:

➜  ~ file /usr/bin/python3      
/usr/bin/python3: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64
- Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e
- Mach-O 64-bit executable arm64e]
/usr/bin/python3 (for architecture x86_64): Mach-O 64-bit executable x86_64
/usr/bin/python3 (for architecture arm64e): Mach-O 64-bit executable arm64e

本文不涉及胖二进制的分析修改,先分析单二进制文件如何修改并注入。有关Mach-O文件格式的讲解请自行百度/谷歌。如下图是一个简单的仅支持x86_64架构的Mach-O文件格式。


单架构Mach-O文件格式

需要关注的是Load Commands指令加载部分。其中LC_LOAD_DYLIB指令表示该二进制依赖的动态库信息,如所有二进制均依赖的动态库libSystem.B.dylib就在其中


libSystem.B.dylib加载指令

那么如果注入动态库就需要修改Load Commands,添加一条LC_LOAD_DYLIB的指令,将待添加的动态库名称填入。需要特别注意的是,添加的指令区域必须是该二进制的空白区。因为内存对齐的原因,每个Segment之间会有一段空白区,添加的指令内容需写到Load Commands最后一条指令之后、TEXT Section之前的区域。多数情况下这部分区域是足够容纳要添加的内容的,如果不够则不能写入。

动态库注入分析

在进行注入前首先需要判断文件格式,是否为可执行文件、是否为胖二进制,进一步分析是64bit还是32bit可执行文件。代码如下:

private func signAdhoc() {
        let task = Process()
        task.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
        task.arguments = ["-f", "-s", "-", binaryPath]
        try? task.run()
    }
    
func initWithFile(filePath: String, libPath: String) -> Bool {
        if !FileManager.default.isExecutableFile(atPath: filePath) {
            print("File to be modified is not Executable.")
            return false
        }
        guard let data = FileManager.default.contents(atPath: filePath) else {
            print("Failed to obtain contents for file.")
            return false
        }
        
        binaryPath = filePath
        dylibPath = libPath
        machOData = data
        return true
    }
    
    func repackBinary() -> Bool {
        if machOData.isEmpty {
            return false
        }
        
        return machOData.withUnsafeBytes { pointer in
            guard let header = pointer.bindMemory(to: fat_header.self).baseAddress else {
                print("Failed to get fat header pointer.")
                return false
            }
            
            var result = false
            switch header.pointee.magic {
            case MH_MAGIC_64, MH_CIGAM_64, MH_MAGIC, MH_CIGAM:
                result = processThinMachO(offset: 0)
            default:
                print("Unknown MachO format.")
                return false
            }
            
            signAdhoc()
            return result
        }
    }

这里的signAdhoc是为了给修改后的二进制签名,因为修改后二进制的内容发生了更改,不重新签名校验无法通过,系统会禁止执行。进行二进制注入的关键代码如下:

private func injectDylib(header: mach_header, offset: UInt64, is64bit: Bool) -> Bool {
        guard let fileHandle = FileHandle(forWritingAtPath: binaryPath) else {
            print("Failed to create handler for binary file.")
            return false
        }
        
        let pathSize = (dylibPath.count & ~(pathPadding - 1)) + pathPadding
        let cmdSize = MemoryLayout<dylib_command>.size + pathSize
        var cmdOffset: UInt64 = 0
        var dylibCmd = dylib_command()
        
        if is64bit {
            cmdOffset = offset + UInt64(MemoryLayout<mach_header_64>.size)
        }
        else {
            cmdOffset = offset + UInt64(MemoryLayout<mach_header>.size)
        }
        
        dylibCmd.cmd = UInt32(LC_LOAD_DYLIB)
        dylibCmd.cmdsize = UInt32(cmdSize)
        dylibCmd.dylib.name = lc_str(offset: UInt32(MemoryLayout<dylib_command>.size))
        
        try? fileHandle.seek(toOffset: cmdOffset + UInt64(header.sizeofcmds))
        fileHandle.write(Data(bytes: &dylibCmd, count: MemoryLayout<dylib_command>.size))
        fileHandle.write(dylibPath.data(using: .utf8)!)
        
        var newHeader = header
        newHeader.ncmds = newHeader.ncmds + 1
        newHeader.sizeofcmds = newHeader.sizeofcmds + UInt32(cmdSize)
        try? fileHandle.seek(toOffset: offset)
        fileHandle.write(Data(bytes: &newHeader, count: MemoryLayout<mach_header>.size))
        
        try? fileHandle.close()
        return true
    }
    
    private func processThinMachO(offset: Int) -> Bool {
        let thinData = machOData.advanced(by: offset)
        return thinData.withUnsafeBytes { pointer in
            guard let header = pointer.bindMemory(to: mach_header.self).baseAddress else {
                print("Failed to get mach header pointer.")
                return false
            }
            
            switch header.pointee.magic {
            case MH_MAGIC_64, MH_CIGAM_64:
                return injectDylib(header: header.pointee, offset: UInt64(offset), is64bit: true)
            case MH_MAGIC, MH_CIGAM:
                return injectDylib(header: header.pointee, offset: UInt64(offset), is64bit: false)
            default:
                print("Unknown MachO format.")
                return false
            }
        }
    }

processThinMachO仅分析二进制的格式是64bit还是32bit,injectDylib用于添加依赖库。首先生成了dylibCmd动态库加载指令,指令中的cmdsize对dylib的路径长度进行了8字节对齐取整。指令插入的位置为Header+CmdsSize之后,即Load Commands最后一条指令之后,这里没有判断是否可以插入。注意,还需要修改Header中的指令数量和大小信息,不然新增的指令不会被解析。

修改完成后,修改后的文件直接覆盖原文件,所以测试前请将原文件备份。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容