【AICon】探索八个行业创新案例,教你在教育、金融、医疗、法律等领域实践大模型技术! >>> 了解详情
写点什么

Android 动态库压缩壳的实现

  • 2019-08-23
  • 本文字数:3021 字

    阅读完需:约 10 分钟

Android动态库压缩壳的实现

说起壳可能有的同学并不太了解,简单的说,计算机软件领域所说的壳实际上是一种软件加密技术。与自然界中的壳类似,花生用壳保护种子,乌龟用壳保护自己的身体,而我们写的程序为了在一定程度上防止被逆向分析,也可以给它加壳。壳主要分为两大类:加密壳和压缩壳,加密壳侧重于防止软件被篡改,而压缩壳则侧重于减小软件体积。其实,在 Windows 上已经有许多壳了,但 Android(或者可以说 Linux)上的壳相对而言就少了一些。本文就主要讲讲 Android 动态库(so 文件)压缩壳要如何实现。

一、压缩

说到压缩,我们可能首先会想到一些常用的压缩工具,例如 7-zip、WinRAR、tar 等等。使用这些工具可以实现 so 文件的压缩吗?答案是肯定的,但如果我们使用这些工具去压缩 so,在使用上却会有一些不方便,主要体现在以下几个方面。


  1. 程序中需要引入额外的解压代码;

  2. 压缩/解压算法不能随意切换;

  3. 需要先解压成原始文件后才能被调用。


那么,如何才能避免这些麻烦呢?在计算机领域有一句名言“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。这里我们就可以通过加中间层的方式去解决这个问题,请看下图。



图上的 loader 就是我们要增加的中间层。我们知道,so 是 ELF 格式的二进制文件,所以要实现对 so 的压缩,就要自己实现一个 ELF 加载器去加载压缩后的 so。这里的 loader 本质上也是一个 so 文件,只不过它里面被写入了我们压缩后的 so 数据。它的作用主要有三个。


  1. 代替原始 so 被应用程序加载;

  2. 内存中解压出原始 so;

  3. 将原始 so 加载到内存中。


有人可能会说这样每次使用前还要在内存里解压,那不会变慢么?事实上,虽然多了解压的过程,但由于 so 的体积减小,加载 so 时 IO 的耗时也会减小,所以这里速度上并不会慢多少(有兴趣的朋友可以做做实验)。


上面的图示中我们把 so 的压缩过程分成了压缩与合成两个步骤,接下来就分别说说这两个步骤是怎么做的。

a) 压缩

关于压缩算法的选择,因为压缩的过程是在 PC 上进行的,所以压缩时内存占用和压缩的速度并不重要,我们主要需要关注压缩率和解压速度。对于各种压缩算法,其实已经有人做过对比试验了,看下面两张图。




我们的 so 文件属于 Bin(二进制文件)类型,可以看到 lzma 算法的压缩率是非常给力的,解压速度说不上特别快,但也能接受。再结合官网上对其特性的介绍,lzma 算法是非常适合在嵌入式系统中使用的。



虽然在 lzma 的基础上又发展了更高级的 lzma2、xz 等算法,但由于使用这些算法需要引入更多的代码,会导致 loader 体积增加,所以这里我最终还是选择了 lzma 算法。

b) 合成

由于 loader 的本质也是一个 so,要把原始 so 压缩之后的数据嵌入 loader,需要对 ELF 格式有一定的了解。网上有很多分析 ELF 格式的文章,写得都很不错,文末的参考资料中有相关链接。这里主要讲一下我们插入数据会涉及到的一些知识点。这是一张经典的 ELF 文件格式视图。



我们需要把 loader 中嵌入的数据加载到内存中解压并执行,所以这里只需要关注 ELF 的执行视图,执行时是按照段(Segment,各个段的信息定义在程序头部表里)来加载的,所以 ELF 头部中与节区(Section)相关的内容我们就可以随意修改。


此外,为了简化数据插入的过程,我们这里把要嵌入的数据放在最后一个段的末尾,这样做的好处是,不会涉及.text 内各种跳转地址的修正,只需要调整最后一个段的大小,就能够方便的被加载到内存里去。“Talk is cheap, show me the code.”看了半天文字,似乎略显枯燥,我们来看看 ELF Header 和 Program Header 的定义片段就能知道要怎么做了。



定义中标记为斜体的内容就是我们需要修改的地方,可以看到数据插入后,我们需要修改 Program Header 中的文件大小和加载到内存里的大小即可。同时,ELF 头部中与 Section 相关的有 8 个字节,足够让我们存储插入数据的大小和偏移了,这样可以方便在 loader 加载后快速找到我们要解压和执行的数据。


综上,一个 so 的压缩过程就可以用一个简单流程图来描述。


二、加载

Android 中 so 的加载全靠 Linker,所以要理解 so 的加载过程,需要对 Linker 有一定的了解。虽然 Android 各个版本的 Linker 实现都不尽相同,实现的语言也从 C 变成了 C++,不过也是大同小异,乌云上有一篇讲解 Android4.4 Linker 源码的文章,写得挺好,不过乌云上的文档现在貌似访问不了了,文末参考资料放了转载到酷推上的链接。这里我就简单罗列一下 so 加载的过程。


  1. 打开 so 文件;

  2. 解析 ELF 头(获得段的偏移、大小、虚拟地址等等信息);

  3. 根据解出来的信息申请足够的内存;

  4. 将 so 文件中可加载(PT_LOAD)的段依次映射(mmap)到申请的内存上,并找到 PT_DYNAMIC 表数据的起始地址;

  5. 根据 PT_DYNAMIC 表中的数据,找到字符串表、符号表、重定位表、初始化/反初始化函数地址,并执行函数的重定位;

  6. 执行初始化函数;


so 加载完成之后会返回一个 soinfo 结构,所有 so 相关的信息都存在里面,Linker 会把这个 soinfo 结构用一个链表维护起来。


基于 Linker 加载 so 的过程,我们要实现自己的 so 加载器就比较容易了,主要有三步。


  1. 根据 ELF 头部信息,找到我们插入的数据,并解压到内存中;

  2. 参考 Linker 的实现,把读文件的地方,改成从内存取数据,完成 so 的加载;

  3. 最后还需要将我们加载 so 构造出来的 soinfo 的内容拷贝至 loader 的 soinfo。


至此,我们就成功的把原始的 so 加载到内存中去了。至于为什么需要上面的第 3 步,是因为如果我们的 so 被其他程序链接,查找符号时会从 Linker 维护的 soinfo 链表中去搜索,所以原始 so 对应的的 soinfo 必须出现在 Linker 维护的链表中,不然是找不到的。


加载的过程图示如下。


三、一些问题

至此,原理部分就介绍完了,在实现的过程中也遇到了一些问题,在这里总结一下。当然我的解法不一定是最好的,但可以解决问题,希望能给大家一些参考吧。


Q:如何让我们解压和加载的代码自动执行?


A:通过 so 加载的流程,我们知道 so 加载之后会执行初始化函数,所以我们需要自动执行的代码可以放在 constructor function 中。


Q:如何拿到 Linker 里维护的 soinfo 链表?


A:Linker 并没有提供接口让外部拿到这个链表,但我们可以利用 Linker 加载 so 的特性,通过 dlopen 打开一个基础的 so(例如:libc),dlopen 函数返回的内容实际上就是其对应的 soinfo 的节点,我们就可以用这个节点作为链表的“头”节点。


Q:为什么在 Android 5.0 上测试时一跑起来就 crash?


A:我的代码是参考的 Android4.1 的 linker,而 soinfo 的数据结构在 4.3 开始发生变化,记录 so 在内存里基地址的变量跟以前不一样了,需要判断版本将基地址赋值给正确的变量。


事实上,目前还有一些问题需要解决,例如一些奇奇怪怪的兼容性问题、如何让 loader 体积更小等等。本文主要是抛砖引玉,如果各位读者有什么想法和建议,欢迎一起探讨。

四、参考

Lzma SDK:


http://www.7-zip.org/sdk.html


压缩算法对比:


http://bashitout.com/2009/08/30/Linux-Compression-Comparison-GZIP-vs-BZIP2-vs-LZMA-vs-ZIP-vs-Compress.html


ELF 文件格式总结:


http://blog.csdn.net/flydream0/article/details/8719036


Android Linker 学习笔记:


http://www.tuicool.com/articles/AfMZRbZ


作者介绍:


周科,腾讯工程师,QQ 动漫 Android 主力开发,从事过 Rom 开发,参与过手 Q 阅读、手 Q 趣味来电等项目,对 Android 底层原理有深入理解。


本文转载自公众号小时光茶舍(ID:gh_7322a0f167b5)。


原文链接:


https://mp.weixin.qq.com/s/tgK7Fn3lQJ4cznNzKPrIXw


2019-08-23 14:228104

评论

发布
暂无评论
发现更多内容

京东小程序CI工具实践

京东科技开发者

小程序 ci 开发 代码 企业号 3 月 PK 榜

非侵入式入侵 —— Web缓存污染与请求走私

vivo互联网技术

CDN

DockQuery | 基于E-R图的数据建模功能使用实践

BinTools图尔兹

数据建模 信创 #数据库

科技助力金融转型 阿里云联合中国信通院在京举办未来金融思享会

阿里云云效

DevOps 数字化转型 金融 BizDevOps 业技融合

AntDB数据库助力中国移动华南中心计费项目

亚信AntDB数据库

AntDB 国产数据库 aisware antdb AntDB数据库 企业号 3 月 PK 榜

Acrobat Pro DC 2023发布,有哪些新的改进?

Rose

adobe pdf编辑器 Acrobat Pro DC 2023

开源的未来:启动 Open100

开源雨林

社区 开源软件 商业化

企业如何构建内部开发者平台?

SEAL安全

IdP 平台工程 企业号 3 月 PK 榜 内部开发者平台

交易系统之数据库弱依赖解决方案

京东科技开发者

数据库 高并发 灾备 db 企业号 3 月 PK 榜

直播教学!20 分钟开发可视化「智能门铃」丨RTE 开发实战课 • 第一期

声网

最佳实践 直播 RTC 声网

金三突击面试,收获6个Offer,原来面试还能这么简单!

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

一天吃透MySQL锁面试八股文

程序员大彬

MySQL 面试

3D摄影棚布光工具Set A Light 3D Studio

Rose

Mac软件 Set A Light 3D Studio 3D摄影棚布光工具

企业不想走弯路,不如试试低代码开发

引迈信息

低代码 低代码开发 JNPF

从零开始搭建一个通用的业务技术架构,这套架构 有点牛逼!

程序知音

Java 程序员 编程语言 后端

从稀疏表征出发、召回方向的前沿探索

百度Geek说

召回 预训练模型 稀疏矩阵 企业号 3 月 PK 榜

React数字滚动组件 numbers-scroll

观纵科技

JavaScript 前端监控 React

VPN客户端Shimo mac版使用教程:如何创建新的 VPN 帐户?

Rose

vpn mac系统 Shimo下载 Shimo教程

@所有人,优秀前端都应该具备的开发好习惯

引迈信息

前端 低代码 开发

被问了n遍怎么把FB视频无水印下载到手机相册!现在双手奉上教程!

frank

facebook #Facebook

图解Redis,谈谈Redis的持久化,RDB快照与AOF日志

小小怪下士

Java redis 程序员 后端

Polygon马蹄链质押DApp开发合约部署案例

薇電13242772558

智能合约 dapp

Matlab常用图像处理命令108例(四)

timerring

图像处理

Blazor在IoT领域的前端实践 @.NET开发者日

MASA技术团队

.net blazor MASA MAUI

8年服务百万客户,这家SaaS公司是懂云原生的

科技热闻

使用Assembly打包和部署Spring Boot工程

做梦都在改BUG

Java spring Spring Boot assembly 框架

NineData x 阿里云 正式上线

NineData

数据库 阿里云 数据迁移 数据管理 NineData

URule规则引擎

规则引擎 java

可插拔组件设计机制—SPI

京东科技开发者

spi Java】 JavaSPI 企业号 3 月 PK 榜

真的有那么丝滑吗?面试阿里(Java岗)从投简历到面试再到入职

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

Last Week in Milvus

Zilliz

Milvus Zilliz 向量数据库

Android动态库压缩壳的实现_文化 & 方法_周科_InfoQ精选文章