libprotectClass.so头信息中的陷阱
1. libprotectClass.so头信息中诡异数字
libprotectClass.so是360加固用到的一个共享库。当我尝试用IDA打开这个共享库时,IDA给出如下提示:
提示的意思是说elf文件(动态库so是elf文件的一种)中节区(section)描述符的size有误,SHT意即Section Header Table。为了证实IDA的这个提示,我用readelf查看一下该共享库的elf文件头信息,readelf查看的结果如下:
注意红色线框标记的部分,至少有下面三处异常:
Sizeof section headers:表示节区描述符的size,readelf显示的size为108个byte,而正常的节区描述符的size为40个byte,设置为108当然是个异常值了,难怪IDA要抱怨了。
Numberof section headers:表示节区头部表中节区描述符的数目,这里显示为102个,意味着动态库中有102个节区,这也够诡异的。
Sectionheader string table index:表示节区头部字符串表(本身单独成一个section)对应的节区描述符在节区头部表中的索引。该值肯定不会超过节区头部表中节区描述符的总数,这里却为120,超过了总数102,这明显自相矛盾。
有了以上信息,我们可以得出结论,libprotectClass.so的头信息被人故意做了修改,设置了陷进,使得IDA无法打开。至于节区头部表在elf文件中的偏移(“Start of section headers”字段),是否被篡改,我们现在还不确定,但我想肯定也是有问题的。遇到这种奇葩的修改,readelf也只好投降了,最终提示错误,“无法从节区头部表中读取0x2b08个byte”,0x2b08怎么来的,就是拿size乘以个数即108 * 102。
那么我们可不可以将这些错误的信息修正呢?答案是可以的,不过这需要了解一些elf文件格式相关的信息。
2.共享库的文件头信息以及节区头部表
一般共享库文件的布局如下图所示:
文件头ELF HEADER包含的信息我们在前面已经见到过了,除了共享库对应的CPU平台以及大小端信息外,主要有两部分信息:
程序头部(Program Header)表“描述信息”:程序头部表在文件中的偏移,表中程序描述符的个数,程序描述符的size
节区头部(Section Header)表“描述信息”:节区头部表在文件中的偏移,表中节区描述符的个数,节区描述符的size,节区头部字符串表对应的节区描述符在本表中的索引。
因为共享库链接时,只关心程序头部表,不关心节区头部表,所以对节区头部表“描述信息”的篡改不会影响到共享库的加载,这就是经过360加固后,程序依然可以运行的原因。注意这里修改的是文件头中的节区头部表“描述信息”,而不是节区头部表本身。理论上修改节区头部表也不会影响加载(我推测的,没有实际做过实验),要是360把这个表也修改了,那我们修正起来就更费劲了。
节区头部表就是节区描述符的数组,节区描述符的定义如下:
typedef struct {
/*节区的名字。注意这里不是字符串,而是一个整数,
表示名字在节区头部字符串表中的索引*/
Elf32_Word sh_name;
/*节区的类型:其中
*0表示无效段,
*1表示程序段,
*2表示符号表,
*3表示字符串表
*4表示重定位表
*5表示符号表的哈希表
*6表示动态链接信息
*7表示提示信息
*8表示该段在文件中没内容,比如.bss段
*9表示重定位信息
*10保留
*11动态链接符号表
*/
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
}Elf32_Shdr;
可见节区描述符的size为40个字节,我们重点关注sh_name成员,它的作用注释中已经讲到了,这里要解释下节区头部字符串表。因为节区名称的长度不固定,所以我们无法将名字定义成一个固定长度的字符数组,一种常见的做法是将字符串集中起来,自成一个节区(该节区的名称为“.shstrtab”,通常把这个节区叫节区头部字符串表),然后使用索引来索引字符串表中的字符串。节区头部字符串表中的内容可能是这样的:
例如,如果节区的名称为“.hash”,只需将描述符中的sh_name成员设置为0x1C即可。
对elf文件格式的介绍到此为止,如果想深入了解,识请参考其他专业书籍。
3.问题解决
如果我们能找到共享库文件中节区头部表的位置,并确定节区的数目以及节区头部字符串表的索引,并以此修正文件头部,问题即可解决。
那么怎么去定位节区头部表的位置呢?如下两个经验可能会帮到我们:
1.节区头部表一般在文件末尾的位置,节区头部表中的第一项一般为无效段,意即节区头部表开头的40个字节为0。
2.虽然共享库是个二进制文件,但是节区头部字符串表中的内容是ASCALL字符,而且一般情况下节区头部字符串表在紧挨着节区头部表的上方。
出发吧,祝我们好运。
用UltraEdit打开共享库文件,在文件末尾,发现如下信息:
初步断定:
从地址1e79d开始,图中红色框标记的部分,为节区头部字符串表;
从地址1e868开始的40个字节,图中绿色框标记的部分,为节区头部表的第一项,40个字节全部为0。即节区头部表从地址1e868开始,节区头部字符串表与节区头部表之间补了一个字节的0(即地址1e867处),这是为了对齐的目的。
从地址1e890开始的40个字节,为节区头部表中的第二项,节区名字的索引为0B即11,查节区头部字符串表得到节区的名字为“.dynsym”。
到目前为止,我们的分析结果都很合理。我们继续以40个字节为单位,逐次分析下去,最终会得到完整的节区头部表。这里有一个问题,怎么确定节区头部表在何处结束?答案是,当某40个字节无法表示一个有效的节区时(最简单的一个方法是看节区名称的索引是否超过了节区头部字符串表的范围),即可判断节区头部表结束。本共享库的节区头部表如下:
序号 | 起始地址 | 节区名 |
0 | 1E868 | 无效段 |
1 | 1E890 | .dynsym |
2 | 1E8B8 | .dynstr |
… | … | … |
21 | 1EBB0 | .ARM.attributes |
22 | 1EBD8 | .shstrtab |
可见:
节区头部表的起始位置即偏移为:0x1E868即十进制的125032,这个值360没有修改;
节区的数目为:23;
节区头部字符串表的索引为22。
修正so文件头信息如下:
修改完成之后,readelf可以正确读取头信息了:
IDA也可以正常打开共享库了。