前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Linker加载so失败问题分析

Linker加载so失败问题分析

作者头像
WeTest质量开放平台团队
发布于 2018-12-11 07:25:45
发布于 2018-12-11 07:25:45
1.8K00
代码可运行
举报
运行总次数:0
代码可运行

作者

段聪,腾讯社交平台部高级工程师

商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。

WeTest 导读

近期测试反馈一个问题,在旧版本微视基础上覆盖安装新版本的微视APP,首次打开拍摄页录制视频合成时高概率出现crash。

那么我们直奔主题,看看日志:

另外复现的日志中还出现如下信息:

'/data/data/com.tencent.weishi/appresArchiveExtra/res1bodydetect/bodydetect/libxnet.so: strtab out of bounds error

后经过测试,发现覆盖安装后首次使用美体功能也会出现crash,日志如下:

由于出现问题的场景都是覆盖安装首次使用,并且涉及到人体检测相关的so,似乎存在某种共同的原因。

因此Abort异常比起fault addr类问题更容易分析,先从前面Linker出现Abort异常的位置开始着手。

Linker是so链接和加载的关键,属于系统可执行文件,因此分析起来比较棘手。好在手上正好有一台刚刷完自己编译的Android AOSP的Pixel,做一些实验变得更轻松了。

出现异常的Linker代码linker_soinfo.cpp如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const char* soinfo::get_string(ElfW(Word) index) const {
  if (has_min_version(1) && (index >= strtab_size_)) {
    async_safe_fatal("%s: strtab out of bounds error; STRSZ=%zd, name=%d",
        get_realpath(), strtab_size_, index);
  }

  return strtab_ + index;
}

bool soinfo::elf_lookup(SymbolName& symbol_name,
                        const version_info* vi,
                        uint32_t* symbol_index) const {
  uint32_t hash = symbol_name.elf_hash();

  TRACE_TYPE(LOOKUP, "SEARCH %s in %s@%p h=%x(elf) %zd",
             symbol_name.get_name(), get_realpath(),
             reinterpret_cast<void*>(base), hash, hash % nbucket_);

  ElfW(Versym) verneed = 0;
  if (!find_verdef_version_index(this, vi, &verneed)) {
    return false;
  }

  for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) {
    ElfW(Sym)* s = symtab_ + n;
    const ElfW(Versym)* verdef = get_versym(n);

    // skip hidden versions when verneed == 0
    if (verneed == kVersymNotNeeded && is_versym_hidden(verdef)) {
        continue;
    }

    if (check_symbol_version(verneed, verdef) &&
        strcmp(get_string(s->st_name), symbol_name.get_name()) == 0 &&
        is_symbol_global_and_defined(this, s)) {
      TRACE_TYPE(LOOKUP, "FOUND %s in %s (%p) %zd",
                 symbol_name.get_name(), get_realpath(),
                 reinterpret_cast<void*>(s->st_value),
                 static_cast<size_t>(s->st_size));
      *symbol_index = n;
      return true;
    }
  }

  TRACE_TYPE(LOOKUP, "NOT FOUND %s in %s@%p %x %zd",
             symbol_name.get_name(), get_realpath(),
             reinterpret_cast<void*>(base), hash, hash % nbucket_);

  *symbol_index = 0;
  return true;
}

从代码上看,是在so的symtab中查找某个符号时ElfW(Sym)* s的地址出现异常,导致s->st_name获取到错误的数据。

通过复现问题,可以抓到更完整的 /data/tombstone日志,得到如下完整的信息:

尽管从tombstone中我们可以看到一些寄存器数据及寄存处地址附近内存数据,同时也可以看到crash时的虚拟内存映射表,仍然无法获取有价值的信息。另外通过几次复现,发现并不是每次Crash都是SIGABRT,也出现不少SIGSEGV信号,而调用栈和之前都是一样的,比如这个:

这基本上可以说明,并不是so本身的代码存在异常,只可能是加载的so出现了文件异常。

另外通过在linker中增加日志,并重新编译linker替换到/system/lib/linker中:

可以获取到如下的地址信息:

通过根据tombstone中的/proc/<poc>/maps的虚拟内存地址与日志打印的地址进行对比,可以发现最为符号表地址的s并没有指向so文件在虚拟内存中的地址段,因此可以怀疑,so加载确实出现了异常。

因为手机root,可以直接获取到crash时的so文件(adb pull /data/data/com.tencent.weishi/appresArchiveExtra/res1bodydetect/bodydetect/libxnet.so),导出来对比md5,然而发现与正常情况下的so是一模一样的:

既然前面的这些实验都没有得出什么有意义的结论,那么我回过头来分析一下,与问题关联的so加载到底有什么特殊性。

实际上,微视为了减包,将一部分so文件进行下发,由于so也处于不断迭代的过程中,新版本的微视可能会在后台更新so文件,那么客户端一旦发现新的版本有新的so,就会去下载so并进行本地替换。

那么这个过程有什么问题呢?唯一可能的问题,就是先加载了旧的so,之后下载新的so进行了热更新。

我们先看下微视中是否有这种现象。要观察这种现象,我们可以打开linker自身的调试开关,开启so加载的日志。通过设置系统属性,我们可以很容易地进行开启LD_LOG日志:

adb shell setprop debug.ld.all dlerror,dlopen

当然我们也可以只针对某个应用开启这个日志(设置系统属性debug.ld.app.)。另外,为了开启linker中更多的日志,比如DEBUG打印的信息等,我们只需要在adb shell中设置环境变量:

export LD_DEBUG=10

那么,我们重新复现问题,可以看到如下so加载过程:

这个过程表明:旧的so先被加载了,然后下载了新版本的so,并进行了替换。

这个过程有什么问题呢?根据《理解inode》一文我们可以得知,linux文件系统使用的inode机制支持了so文件的热更新(动态更新),即每个文件都有一个唯一的inode号,打开文件后使用inode号区分文件而不是文件名:

八、inode的特殊作用 由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。 1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。 2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。 3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。 第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。

但是问题就出在这里,如果替换文件使用的是cp这样的操作,会导致原来的so文件截断,然后重新写入数据,但是inode并没有更新号,磁盘与内存中的信息出现不一致,这种情况在linux中很常见,比如这篇文章就进行了分析:

1. cp new.so old.so,文件的inode号没有改变,dentry找到是新的so,但是cp过程中会把老的so截断为0,这时程序再次进行加载的时候,如果需要的文件偏移大于新的so的地址范围会生成buserror导致程序core掉,或者由于全局符号表没有更新,动态库依赖的外部函数无法解析,会产生sigsegv从而导致程序core掉,当然也有一定的可能性程序继续执行,但是十分危险。 2. mv new.so old.so,文件的inode号会发生改变,但老的so的inode号依旧存在,这时程序必须停止重启服务才能继续使用新的so,否则程序继续执行,使用的还是老的so,所以程序不会core掉,就像我们在第二部分删除掉log文件,而依然能用lsof命令看到一样。

还有更深入的解释:

Linux由于Demand Paging机制的关系,必须确保正在运行中的程序镜像(注意,并非文件本身)不被意外修改,因此内核在启动程序后会绑定 内存页 到这个so的inode,而一旦此inode文件被open函数O_TRUNC掉,则kernel会把so文件对应在虚存的页清空,这样当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。Kernel从so文件中copy一份到内存中去,a)但是这时的全局符号表并没有经过解析,当调用到时就产生segment fault , b)如果需要的文件偏移大于新的so的地址范围,就会产生bus error。

那么问题基本清晰了。我们在回去看看微视的代码,这里下载了so之后直接unzip到原来的路径,并没有先进行rm操作。

更近一步,我们自己写个demo测试下刚才的问题(2个按钮,一个加载指定so,一个调用so中的native方法):

代码不能再简单了:

正常加载so然后执行native方法都是ok的,使用rm+mv替换或者adb push替换也都是ok的,最后再按照错误的方法操作,步骤为:

1. 启动app,点击加载so;

2. 通过cp命令替换so;

3. 点击执行native方法;

结果确实是crash了:

日志如下,是不是很最开始的日志信息一样呢:

到此,我们有两种解决办法:

1. 如果so有升级,先不加载旧的so,等新的so下载完成之后再加载;

2. 可以先加载旧的so,但是下载了新的so之后,要删除旧的so,再进行替换。

引文参考:

https://www.cnblogs.com/cnland/archive/2013/03/19/2969337.html

https://www.cnblogs.com/cnland/archive/2013/03/20/2970537.html

http://www.nginx.cn/1329.html

http://www.ruanyifeng.com/blog/2011/12/inode.html

https://www.bo56.com/linux%E4%B8%8Bcp%EF%BC%8Cmv%E8%BF%9B%E8%A1%8C%E5%8A%A8%E6%80%81%E5%BA%93%E8%A6%86%E7%9B%96%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-11-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯WeTest 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Linker加载so失败问题分析
原文链接:https://wetest.qq.com/lab/view/421.html
WeTest质量开放平台团队
2018/11/14
1.6K0
Linker加载so失败问题分析
mold源码阅读十二 创建一些输出段
这里对verdef和verneed段进行构造,实际写入内容。其中包含了字符串信息,因此还会将字符串写入dynstr中。
AkemiHomura
2023/10/22
2150
Android Linker学习笔记[转]
Linker是Android系统动态库so的加载器/链接器,要想轻松地理解Android linker的运行机制,我们需要先熟悉ELF的文件结构,再了解ELF文件的装入/启动,最后学习Linker的加载和启动原理。 鉴于ELF文件结构网上有很多资料,这里就不做累述了。
用户2930595
2018/08/23
2.7K0
Android Linker学习笔记[转]
腾讯Matrix分析--ELFHook原理
通过Hook系统在本进程中的open和close、read、write这些系统函数,来了解打开的文件以及其是否被释放。由于只是Hook本App的系统调用,所以不需要Root权限也可以完成。
None_Ling
2019/04/09
2.2K0
Java反射原理
对于Java反射而言 , 会非常耗性能 , 尤其是通过Class.forName来找到的Class对象. 主要的原理如下 :
None_Ling
2020/09/10
1.2K0
Apple 操作系统可执行文件 Mach-O
Mach-O 的全称是 Mach Object File Format。可以是可执行文件,目标代码或共享库,动态库。Mach 内核的操作系统比如 macOS,iPadOS 和 iOS 都是用的 Mach-O。Mach-O 包含程序的核心逻辑,以及入口点主要功能。
用户7451029
2020/06/16
3K0
OpenHarmony 内核源码分析(ELF格式篇) | 应用程序入口并不是main
先说明,本篇很长,也很枯燥,若不是绝对的技术偏执狂是看不下去的.将通过一段简单代码去跟踪编译成ELF格式后的内容.看看ELF究竟长了怎样的一副花花肠子,用readelf命令去窥视ELF的全貌,最后用objdump命令反汇编ELF.找到了大家熟悉main函数.
小帅聊鸿蒙
2025/03/23
600
OpenHarmony 内核源码分析(ELF格式篇) | 应用程序入口并不是main
Android Linker 与 SO 加壳技术
1. 前言 Android 系统安全愈发重要,像传统pc安全的可执行文件加固一样,应用加固是Android系统安全中非常重要的一环。目前Android 应用加固可以分为dex加固和Native加固,Native 加固的保护对象为 Native 层的 SO 文件,使用加壳、反调试、混淆、VM 等手段增加SO文件的反编译难度。目前最主流的 SO 文件保护方案还是加壳技术, 在SO文件加壳和脱壳的攻防技术领域,最重要的基础的便是对于 Linker 即装载链接机制的理解。对于非安全方向开发者,深刻理解系统的装载与链
腾讯Bugly
2018/03/23
3.3K1
webview接入HttpDNS实践
本文是对去年做的webview接入HttpDNS工作的一个总结,拖的时间有点久了。主要分享了GOT Hook webview中域名解析函数的方法。 HttpDNS简介 首先简单介绍下移动App接入HttpDNS后有什么好处,这里直接引用腾讯云文档中的说明: HttpDNS是通过将移动APP及桌面应用的默认域名解析方式,替换为通过Http协议进行域名解析,以规避由运营商Local DNS服务异常所导致的用户网络接入异常。减少用户跨网访问,降低用户解析域名时延。 更详细的内容可以参考这篇文章:【鹅厂网事】
felix
2018/06/08
3.8K0
深入了解GOT,PLT和动态链接
之前几篇介绍exploit的文章, 有提到return-to-plt的技术. 当时只简单介绍了 GOT和PLT表的基本作用和他们之间的关系, 所以今天就来详细分析下其具体的工作过程.
evilpan
2023/02/12
1.6K0
深入了解GOT,PLT和动态链接
mold源码阅读 其二 读取SharedFile
这期的内容主要是讲完读取输入的部分,有一些之前遗漏的信息,以及之前未讲完的初始化ehframe以及shared object读取的部分。有许多地方默认读者读过上期内容,建议先阅读上期内容后再来查看本期。
AkemiHomura
2023/04/07
4170
mold源码阅读 其二 读取SharedFile
Ret2dl_resolve漏洞利用分析
ret2dlresolve是linux下一种利用linux系统延时绑定(Lazy Binding)机制的一种漏洞利用方法,其主要思想是利用dlruntimeresolve()函数写GOT表的操作,改写写入GOT的内容,使其成为getshell的函数值。
FB客服
2020/05/14
8780
Ret2dl_resolve漏洞利用分析
mold源码阅读七 创建输出段之前
上期的内容主要是section size相关的优化,这期内容是创建输出段前的最后一些处理
AkemiHomura
2023/05/21
3240
【linux命令讲解大全】054.readelf:展示ELF格式文件信息的工具
readelf命令用来显示一个或者多个elf格式的目标文件的信息,可以通过它的选项来控制显示哪些信息。这里的elf-file(s)就表示那些被检查的文件。可以支持32位,64位的elf格式文件,也支持包含elf文件的文档(这里一般指的是使用ar命令将一些elf文件打包之后生成的例如lib*.a之类的“静态库”文件)。
全栈若城
2024/03/02
7170
OpenHarmony 内核源码分析(ELF解析篇) | 内核加载
ELF,它实在是太重要了,内核加载的就是它,不说清楚它怎么去说清楚应用程序运行的过程呢.看到下面这一坨一坨的,除了.text,.bss,.data听过见过外,其他的咱也没啥交情。
小帅聊鸿蒙
2025/03/23
400
Android JNI Crash定位步骤
今天讲的是纯干货,目的就是为了指导Android开发者如何根据JNI Crash日志顺藤摸瓜,最后直捣黄龙定位磨人的JNI Crash。所以废话不多,直接开干吧。
音视频开发进阶
2019/11/18
2.9K0
原创 Paper | 从 XZ 后门学奇技淫巧
对CVE-2024-3094漏洞的分析文章网上已经有好几篇了,这里来学习一下在该事件中后门隐藏的奇技淫巧。
Seebug漏洞平台
2024/05/11
2830
原创 Paper | 从 XZ 后门学奇技淫巧
干货 | 突破disable_functions限制执行命令·上
disable_functions是php.ini中的一个设置选项。相当一个黑名单,可以用来设置PHP环境禁止使用某些函数,通常是网站管理员为了安全起见,用来禁用某些危险的命令执行函数等。
HACK学习
2022/02/17
5.4K0
干货 | 突破disable_functions限制执行命令·上
学习tombstone,signal
电脑上,从HDD 到SSD,从SATA SSD到PCIe SSD,硬盘是越来越快;
天天Lotay
2022/11/11
1.8K0
学习tombstone,signal
黑客级别的文章:把动态库的内存操作玩出了新花样!
在上周的一篇转载文章中,介绍了一种如何把一个动态库中的调用函数进行“掉包”的技术,从而达到一些特殊的目的。
IOT物联网小镇
2021/11/02
1.3K0
相关推荐
Linker加载so失败问题分析
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文