一步一步教你写classdump

最近论文要搞静态检测,设计到了一些对macho文件的分析,恰巧刚读完《Mac OS X and iOS Internals**》的前几章,也想练练手,写tweak,app也烦了,基于上述原因就试着写了这个demo,虽然功能都可以通过otool和classdump去实现,但也算是对macho文件格式的一种学习吧,很多地方可能会有错误,也存在很多我本身就还没弄明白的地方,希望大神们能不吝赐教。程序是用c语言写的,没用OC,不只是想到移植问题,更多还是想借此机会学习一下c语言,代码一定惨不忍睹,望各位看官多提宝贵意见。**
**/----------------------------------转载请注明出处啦,我还得过明年论文查重这一关呢--------------------------------------/
**先来看看效果:mac:


Ubuntu:



1. 准备工作(1) otool
(2) UltraEdit
(3) Classdump源码

2. 基础知识a. mach-o文件
mach-o是Mach Object的缩写,是OS X/iOS系统上可执行文件,目标文件,动态库,coredump的文件格式。

b. mach-o文件结构
直接上图:

如图所示:
第一部分是mach-o头,里面存储了该mach-o文件的一些基本信息,如:cpu架构类型,文件类型(可执行文件,动态库还是coredump),加载命令的数量等等。具体信息见下表,我们自己可以通过otool –h的输出来获得:


/--------------------------------------------------------------------------------------------------------------------------------------------------以下内容可以留着以后看-----------------------------------------------------------------------------------------------------------------------------------------/
(1) magic:“魔数”,用来标示该文件是32bit还是64bit,其中0xFEEDFACE表示32bit,0xFEEDFACF表示64bit。占4个字节。

 (2) cputype:顾名思义,表示该文件运行在哪种cpu架构上。对我们来说,常用也就下面这个表格的这些,占4个字节        (3) cpusubsyte:也就是我们经常说的,armv7,armv7s后面的v7,v7s这些版本号,常用的见下表,占3个字节
          <img src="/uploads/default/521/e8f8fe4e463c1b2c.png" width="556" height="47"><br/>
(4) filetype:文件类型。占4个字节 
          <img src="/uploads/default/522/4b013adb5e518001.png" width="465" height="71"><br/>
(5) ncmds:加载命令的数量。加载命令的作用是告诉系统如何去加载该mach-o文件。常用的有8个段

      <img src="/uploads/default/523/a1069bc9d58516f5.png" width="549" height="181"><br/>
         其中这次需要用到的是前两个加载命令,LC_SEGMENT,LC_SEGMENT_64,里面包含了该macho文件的段信息。          (6) sizeofcmds:加载命令的大小
 (7) flags:这个flags是给动态链接器dyld看的,先不管它
 (8) reserverd:保留字段,只有64bit的macho文件才有这个字段。

/---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/
第二部分是加载指令(load Command),这些指令告诉了系统怎么去加载该二进制文件,这部分还没仔细研究,希望有大神能给一些详细的说明
第三部分则是段(segment)跟节(section),这些段和节中存放了大量信息,包括所使用类名,方法名,常量字符串,程序的机器码等等,可以说是macho文件的核心部分。

c. fat and thin
单个的macho文件我们称为thin二进制文件,这种类型的文件只能运行在某一种cpu架构下,为了扩展其兼容性,我们可以把多个cpu结构的macho文件组合成一个多重架构的二进制文件,称为fat二进制文件。需要指出的是,fat二进制文件在文件开始有一个fat header和fat arch,最关键的是里面存放了每个架构在文件中的偏移offset。(没找到太多资料,最后通过UE和classdump源码中知道的).

d. 常用的段(segment)和节(section)

其中,我们需要关注的就是classlist ,classname,methname,methtype等几个节,因为这些节里面包含着我们最后需要导出的信息。通过otool –l 命令我们可以得到segment和section的一些属性。(1) segment


通过该输出和UE配合,或者直接看classdump的源码,我们可以得知segment的属性如下表所示:

由以上信息,我们可以创建一个segment的结构体,用来保存从文件中读取到的信息。

(2) section.同样分析,我们可以得到section的相关属性

  <img src="/uploads/default/530/ec0e99121436c801.png" width="448" height="448"><br/>

3. 实现写到这儿,我们已经可以开始进行代码的编写了。首先我们要确定该程序的目的,即classdump的功能,把一个程序中的类名和方法名提取出来(协议和成员变量就留给大家自己去实现了)。
我们一个一个来解决,先列出所包含类的名字。
一个应用的大部分信息在二进制文件中都是保存在段和节中的,类名同样也不例外,通过otool –l指令,我们可以发现一个很可疑的节__objc_classname。

打开ue,找到该节在文件中的位置(1347424+0x4000)=0x14cf60,注意前面说过对于fat文件而言section的offset不是相对于文件开始的偏移而是相对于arch开始的偏移,因此需要加上arch的偏移量,这里我们的第一个arch开始于0x4000的位置。


可以看到我们所有的类名信息都保存在了从0x14cf60开始的地方。接下来呢,就是想办法把这些类名提取出来了,最简单的方法就是挨个字节读出来,遇到00则结束,开始新一个类的读取。
同理,我们可以在__objc_methodname节中找到方法名的信息。


由上图可以看见所有方法名的信息都保存在这儿了,我们可以用同样的方法将他们提取出来,到这里,表面上看去我们已经完成了我们的任务,提取出了所有的类名和方法名,但是存在一个问题,这些方法名和类名并没有一一对应上,换句话说,我们并不知道哪个方法属于哪个类。

这里纠结了很久,也没找到相关的资料,在这种毫无思路的情况下,只能上神器了——看classdump源码。
/--------------------------------------------------------------------------------------------一段漫长的时间过去了-----------------------------------------------------------------------------------------------------------------------------------/
看完classdump明白了以下几点:
a. section节中的VM地址与文件偏移offset的转换;
Offset = archOffset + section.offset + (VM - section.add);
b. 每一个类基本信息的VM地址都保存在DATA段的__objc_classlist节中,
根据前面的方法,在UE中找到该节的位置:


我们这里拿第一个类作分析,可以看到第一个类的VM为0x15FC04,接下来需要判断该VM在哪个段哪个节中,由上文可知,每一个section都有addr和size属性,因此只需要遍历所有的section,判断(0x15FC04 >= addr && 0x15FC04 <= addr +size)即可以找到包含0x15FC04的段跟节,这里,__DATA段中的__objc_data节中包含了该VM。

c. 接下来需要在__objc_data中找到0x15FC04所对应的部分,根据a可知这部分相对于文件开始的偏移=archOffset + __objc_data.offset + (0x15FC04 - __objc_data addr) = 0x15FC04(额…….怎么和VM一样,这里我看classdump源码是这样去找的,但是我这儿每次都是和VM相等,不知道是巧合还是另有原因,希望大神们能够为我解答一下),在UE中找到这个地方:

我们通过查看classdump的源码知道,此处的信息对应着一个结构体:

其中,最关键的信息存储在data中,由图可知,data的VM=0x155E58,同理找到包含该VM的段和节(__DATA段,__objc_const节),并找到该位置在文件中所对应的位置(fileoffset =0x155e58 ):

该位置同样对应着一个结构体:

其中成员name和baseMethod便是我们的目标。

d. 由上图可知,name的VM为0x14CF60,同样找到该位置所在的段跟节(__TEXT:ustring,fileoffset = 0x14CF60),在UE中定位到该位置:

可算找到它了!这就是我们的第一个类的类名App_SocialTask。接下来只需要拿一个char *把它保存下来就ok啦。

e. 让我们回到第c步,继续解析baseMethod这个成员。由图可知,baseMethod的VM为0x155D68,所在的段跟节为__DATA:_const,fileoffset为0x155D68,在UE中找到该位置。

首先的8个字节,前4个是entsize,后4个是该类拥有方法的个数,接着以12个字节为单位(前4个字节是方法名,中间4个是方法类型,后4个字节是imp)分别是每一个方法的信息。这里我们只需要拿出name即可,其他的留给大家自己去做。找到第一个方法的name在文件中对应的位置:0x1439B6


这样我们便得到了App_SocialTask类的第一个方法iniTaskOf:

f. 根据上面的分析也就很容易得出所有类的类名跟方法名了。
4. 代码

想到以后的移植以及作为一次对自己的练习,所有代码均采用c语言编写,没有使用OC。本人c语言基础渣出翔,本科毕业了都还是停留在只会用for,if写代码的水平,所以这里把代码贴出来,除了交流,也是为了能够有大神能帮忙勘正一下代码,或者说直接从思维上去纠正那是最好的了。代码在Mac OS X 10.10和Ubuntu14.04上跑起来没问题。代码见附件。lwClassdump_DEMO.zip (9.59 KB)

可算弄完了,贴图累死了,比写代码工作量还大。

2 个赞

这里我们的第一个arch开始于0x4000的位置。
这个0x4000是从哪里分析的?
而且我的好像不用加?

你的是fat文件吗?如果只是单个arch就不用加

怎么判断是不是fat文件呢?

看了楼主的帖子,我只想说一句好好好!!!!好的一塌糊涂,好好这是我在这个论坛看见的最最最给力的一篇帖子,多谢分享,给力,好好。手动点击1万个赞!!!

1 个赞

用file指令看看