《基于linker实现so加壳技术基础》上篇

前言

本篇是一个追随技术的人照着网上为数不多的资料,在探索过程中发现了许多意想不到的问题,搜了好多文章发现对于这方面的记载很少,甚至连一个实现的蓝本都没找到,写下这篇文章希望能帮到像我一样对so壳感兴趣的伙伴在做实践的时候能有一个抓手,只有了解了加壳原理才能更好的脱壳,其实so文件在系统当中都对应了一个soinfo指针如果能找到soinfo指针那么脱掉so壳也不是不可能,如果有机会的话后面会出so脱壳相关的知识,其实最难的点在于如何找到当前so的soinfo指针这个,源自aosp8.1.0_r33.

思路

回想dex可以通过动态加载插件的方式完成加壳,例如一代壳,那么so是否也有这种技术呢,答案是肯定的,只是用c实现的so没有办法使用java中的classloader这种,快速便捷的类加载方式,那么我们现在就遇到了2个问题就是如何将so加载到内存中,如何使用so中的函数,那么其实我们现在就可以从安卓源码入手,看一看so是如何加载到系统中的

装载

从System.loadLibrary入手(ps:我们平常加载so就是用到这两个函数一个是System.loadLibrary传入Classloader所在的相对路径,一个是System.load传入绝对路径,最后发现他们在native层都会汇聚到一点),一直跟下去就会发现最后Runtime下的私有函数doLoad中调用了nativeLoad来进入native层

public static void loadLibrary(String libname) {Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);}
.....private String doLoad(String name, ClassLoader loader) {.....
return nativeLoad(name, loader, librarySearchPath);}

那么顺利进入so层,进入了libcore/ojluni/src/main/native/Runtime.c中的Runtime_nativeLoad方法,一直跟下去最终在system/core/libnativeloader/native_loader.cpp中找到了OpenNativeLibrary方法找到了dlopen和android_dlopen_ext(ps:通过分析发现这两个函数最终也汇聚于linker下的do_dlopen)

Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,jobject javaLoader, jstring javaLibrarySearchPath)
{return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}
......c
void* OpenNativeLibrary(...) {#if defined(__ANDROID__)UNUSED(target_sdk_version);if (class_loader == nullptr) {*needs_native_bridge = false;return dlopen(path, RTLD_NOW);}
.....void* handle = android_dlopen_ext(path, RTLD_NOW, &extinfo);//当classloader不为空的情况下调用android_dlopen_ext继续跟下去其实也可以看到它的实现最终也是do_dlopen

那么其实现在就清楚了,其实java层的loadLibrary也是通过linker中的do_dlopen来j将so文件加载到内存中的,那么下面开始分析do_dlopen中的so的加载流程。

void* do_dlopen(......) {...
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller)//通过find_library函数拿到当前handle对应的soinfo
...}

那么继续跟进find_library函数,发现如下关键点代码

...
static soinfo* find_library(android_namespace_t* ns,const char* name, int rtld_flags,const android_dlextinfo* extinfo,soinfo* needed_by) {...
if (!find_libraries(const_cast<android_namespace_t*>(task->get_start_from()),task,&zip_archive_cache,&load_tasks,rtld_flags,search_linked_namespaces || is_dt_needed))//关键函数详细分析
...}

在find_libraries函数中,通过find_library_internal,来搞定所有需要的so和初始化我们的so等一些列工作

bool find_libraries(...) {...if (!find_library_internal(....){return false;}
...}

一直跟下去,最终在load_library函数中发现了调用了LoadTask对象的read函数,之后又调用了ElfReader对象的read函数来初始化LoadTask

static bool load_library(...) {...
if (!task->read(realpath.c_str(), file_stat.st_size))//通过ElfReader对象的read函数初始化我们的LoadTask对象
...for (const ElfW(Dyn)* d = elf_reader.dynamic(); d->d_tag != DT_NULL; ++d) {if (d->d_tag == DT_RUNPATH) {si->set_dt_runpath(elf_reader.get_string(d->d_un.d_val));}if (d->d_tag == DT_SONAME) {si->set_soname(elf_reader.get_string(d->d_un.d_val));}}for_each_dt_needed(task->get_elf_reader(), [&](const char* name) {load_tasks->push_back(LoadTask::create(name, si, ns, task->get_readers_map()));});}

LoadTask对象的read方法十分简洁这里就不贴代码了。在/bionic/linker/linker_phdr.cpp目录中找到了ElfReader类中read函数的实现,这个函数很重要所以就全贴上了,可以看到,这里将我们打开so文件的fd以及file_size等一系列信息复制给了ElfReader对象然后做了5件事
1:读取elf头
2:验证elf头
3:读取程序头
4:读取节头
5:读取Dynamic节
但是我们是做一个简单的壳肯定要去掉这些对于加载用不到的地方,所以这里可以只实现读取程序头和elf头,因为Execution View下一定会使用程序头加载段而节信息可有可无

bool ElfReader::Read(const char* name, int fd, off64_t file_offset, off64_t file_size) {if (did_read_) {return true;}name_ = name;fd_ = fd;file_offset_ = file_offset;file_size_ = file_size;if (ReadElfHeader() &&VerifyElfHeader() &&ReadProgramHeaders() &&ReadSectionHeaders() &&ReadDynamicSection()) {did_read_ = true;}return did_read_;
}

下面就是我模仿安卓源码写的一个ReadProgramHeader的实现,首先要实现一个loader类,模仿源码中的ElfReader类给他填上属性。,然后实现ReadProgramHeader方法.

class load {public:const char *name_;int fd_;ElfW(Ehdr) header_;size_t phdr_num_;void *phdr_mmap_;ElfW(Phdr) *phdr_table_;ElfW(Addr) phdr_size_;void *load_start_;size_t load_size_;ElfW(Addr) load_bias_;const ElfW(Phdr) *loaded_phdr_;void *st;
public:load(void *sta): phdr_num_(0), phdr_mmap_(NULL), phdr_table_(NULL), phdr_size_(0),load_start_(NULL), load_size_(0), load_bias_(0),loaded_phdr_(NULL), st(sta) {}bool loadhead(){return   memcpy(&(header_),st,sizeof(header_));//赋值elf头};bool ReadProgramHeader() {phdr_num_ = header_.e_phnum;//由于我们没有执行ReadElfHeader函数所以一会要手动给这个header赋值ElfW(Addr) page_min = PAGE_START(header_.e_phoff);//页对齐ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);//获得程序头的偏移void **c = reinterpret_cast<void **>((char *) (st) + page_min);phdr_table_ = reinterpret_cast<ElfW(Phdr) *>(reinterpret_cast<char *>(c) + page_offset);//获得程序头实际地址return true;};}

需要将我们的真正的so加载到内存中(如果是壳的话可以是加密文件或者直接贴在dex文件最后有很多方法这里不再讨论,这里只讨论最基础的动态加载),使用mmap就好

    int fd;void *start;struct stat sb;fd = open("/data/local/tmp/1.so", O_RDONLY); //打开获得我们插件so的fdfstat(fd, &sb); start = static_cast<void **>(mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0));//用mmap把文件加载到内存中load a(start);//构造load对象

然后read函数就完成了其他部分我们不需要看。结束后load_library函数中第二个关键的部分就是对依赖so的加载,这里重点看for_each_dt_needed函数,逻辑十分的简单,就是从dynamic段中寻找类型为0x1(在elf.h中#define DT_NEEDED 1)的段然后将这个so放入到LoadTask对象的加载队列中,最终通过read函数将每一个未被加载的so加载到内存中,并且返回所有的soinfo指针。

static void for_each_dt_needed(const ElfReader& elf_reader, F action) {for (const ElfW(Dyn)* d = elf_reader.dynamic(); d->d_tag != DT_NULL; ++d) {if (d->d_tag == DT_NEEDED) {action(fix_dt_needed(elf_reader.get_string(d->d_un.d_val), elf_reader.name()));}}
}

接着继续find_library函数,发现之后又调用了task的load函数。跟踪进去load函数中发现分2块内容,第一块内容就是调用ElfReader类的load方法装载so,第二部分就是对于有soinfo的修正,代码十分简洁这里就不贴了,那么现在的任务就是看看ElfReader类的load方法是如何实现的。load函数就少了一点只做了3件事
1:申请段加载空间
2:装载段
3:找到Phdr在内存中的地址
那么这里我们就要将这三个方法全部都实现了

bool ElfReader::Load(const android_dlextinfo* extinfo) {CHECK(did_read_);if (did_load_) {return true;}if (ReserveAddressSpace(extinfo) &&LoadSegments() &&FindPhdr()) {did_load_ = true;}return did_load_;
}

首先是申请加载空间,期间有一个phdr_table_get_load_size方法我们直接从源码里面抄过来即可

size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count,ElfW(Addr)* out_min_vaddr) {ElfW(Addr) min_vaddr = UINTPTR_MAX;ElfW(Addr) max_vaddr = 0;bool found_pt_load = false;for (size_t i = 0; i < phdr_count; ++i) {const ElfW(Phdr)* phdr = &phdr_table[i];if (phdr->p_type != PT_LOAD) {continue;}found_pt_load = true;if (phdr->p_vaddr < min_vaddr) {min_vaddr = phdr->p_vaddr;}if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {max_vaddr = phdr->p_vaddr + phdr->p_memsz;}}if (!found_pt_load) {min_vaddr = 0;}min_vaddr = PAGE_START(min_vaddr);max_vaddr = PAGE_END(max_vaddr);if (out_min_vaddr != nullptr) {*out_min_vaddr = min_vaddr;}return max_vaddr - min_vaddr;
}bool ReserveAddressSpace() {ElfW(Addr) min_vaddr;load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);//获得段总大小uint8_t *addr = reinterpret_cast<uint8_t *>(min_vaddr);void *start;int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;start = start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);;//直接申请空间需要页对其所以好像不能用mallocload_start_ = start;load_bias_ = reinterpret_cast<uint8_t *>(load_start_) - addr;__android_log_print(6,"r0ysue","%p 111111  %x",load_bias_,load_size_);return true;};

接下来就是装载段信息,这部分也是完全仿照安卓源码的写法,将段头中所有类型为0x1的段加载到内存当中( #define PT_LOAD 1)我这里直接用memcpy了主要是不想从文件中加载so想的是我这种方案直接内存加载so,并且用mprotect申请权限,最后填0占位

   bool LoadSegments() {for (size_t i = 0; i < phdr_num_; ++i) {const ElfW(Phdr) *phdr = &phdr_table_[i];if (phdr->p_type != PT_LOAD) {continue;}// Segment addresses in memory.ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;ElfW(Addr) seg_end = seg_start + phdr->p_memsz;ElfW(Addr) seg_page_start = PAGE_START(seg_start);ElfW(Addr) seg_page_end = PAGE_END(seg_end);ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz;// File offsets.ElfW(Addr) file_start = phdr->p_offset;ElfW(Addr) file_end = file_start + phdr->p_filesz;ElfW(Addr) file_page_start = PAGE_START(file_start);ElfW(Addr) file_length = file_end - file_page_start;long* pp= reinterpret_cast<long *>(seg_page_start);__android_log_print(6,"r0ysue","%p 111111",load_bias_);__android_log_print(6,"r0ysue","%p 111111",seg_page_end);mprotect(reinterpret_cast<void *>(seg_page_start), seg_page_end-seg_page_start, PROT_WRITE);//申请访问权限if (file_length != 0) {void* c=(char*)st+file_page_start;memcpy(reinterpret_cast<void *>(seg_page_start), c, file_length);//我把mmap改成了memcpy因为安卓源码中用了fd我期望全使用内存加载的方式所以有fd的地方我都改了}if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));}seg_file_end = PAGE_END(seg_file_end);if (seg_page_end > seg_file_end) {void* zeromap = mmap(reinterpret_cast<void*>(seg_file_end),seg_page_end - seg_file_end,PFLAGS_TO_PROT(phdr->p_flags),MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,-1,0);__android_log_print(6,"r0ysue","duiqi %p ",zeromap);}
//            __android_log_print(6,"r0ysue","%p 111111",seg_file_end);}return true;};

最后实现FindPhdr方法,其中引用了CheckPhdr方法,这里FindPHPtr函数主要分2类如果直接就有程序头段那么直接用就好否则就找虚拟地址为0的段从文件头解析它,全部也是直接超过来就好,但是注意这个要抄在load类里面,上面的phdr_table_get_load_size写在类的外面也无关紧要。

  bool CheckPhdr(ElfW(Addr) loaded) {const ElfW(Phdr) *phdr_limit = phdr_table_ + phdr_num_;ElfW(Addr) loaded_end = loaded + (phdr_num_ * sizeof(ElfW(Phdr)));for (ElfW(Phdr) *phdr = phdr_table_; phdr < phdr_limit; ++phdr) {if (phdr->p_type != PT_LOAD) {continue;}ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;ElfW(Addr) seg_end = phdr->p_filesz + seg_start;if (seg_start <= loaded && loaded_end <= seg_end) {loaded_phdr_ = reinterpret_cast<const ElfW(Phdr) *>(loaded);return true;}}return false;};bool FindPHPtr(){const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_;for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {if (phdr->p_type == PT_PHDR) {return CheckPhdr(load_bias_ + phdr->p_vaddr);//主要检测这个段是否越界}}for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {if (phdr->p_type == PT_LOAD) {if (phdr->p_offset == 0) {ElfW(Addr)  elf_addr = load_bias_ + phdr->p_vaddr;const ElfW(Ehdr)* ehdr = reinterpret_cast<const ElfW(Ehdr)*>(elf_addr);ElfW(Addr)  offset = ehdr->e_phoff;return CheckPhdr((ElfW(Addr))ehdr + offset);}break;}}return false;};

至此我们已经写完了装载过程,至于引用的其他so我们写一个循环直接重走一边即可,由于我这里没有引用所以先不展示这一方面的内容了,这里还有一段Load函数结尾需要将我们上面读到的内容存储到link维护的本so的soinfo中,而由于我们是加壳所以要做的就是将刚才得到的段信息替换掉本so的soinfo,类似于dex整体加壳(一代壳)的classloader的修正,接下来进入获得soinfo部分,会写在下篇里面

基于linker实现so加壳技术上相关推荐

  1. 基于linker实现so加壳技术下

    <基于linker实现so加壳技术基础>下篇 获得linker维护的本so的soinfo 但是问题又来了如何获得当前so的soinfo指针的基址呢?翻阅网上的资料说可以dlopen打开se ...

  2. 【腾讯Bugly干货分享】Android Linker 与 SO 加壳技术

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57e3a3bc42eb88da6d4be143 作者:王赛 1. 前言 Andr ...

  3. 【Android 逆向】加壳技术简介 ( 动态加载 | 第一代加壳技术 - DEX 整体加固 | 第二代加壳技术 - 函数抽取 | 第三代加壳技术 - VMP / Dex2C | 动态库加壳技术 )

    文章目录 一.动态加载 二.第一代加壳技术 ( DEX 整体加固 ) 三.第二代加壳技术 ( 函数抽取 ) 四.第三代加壳技术 ( Java 函数 -> Native 函数 ) 五.so 动态库 ...

  4. AndroidLinker与SO加壳技术之下篇

    2.4 链接 链接过程由 soinfo_link_image 函数完成,主要可以分为四个主要步骤: 1. 定位 dynamic section, 由函数 phdr_table_get_dynamic_ ...

  5. AndroidLinker与SO加壳技术之上篇

    1. 前言 Android 系统安全愈发重要,像传统pc安全的可执行文件加固一样,应用加固是Android系统安全中非常重要的一环.目前Android 应用加固可以分为dex加固和Native加固,N ...

  6. 病毒加壳技术与脱壳杀毒方法解析

    壳是什么?脱壳又是什么?这是很多经常感到迷惑和经常提出的问题,其实这个问题一点也不幼稚.当你想听说脱壳这个名词并试着去了解的时候,说明你已经在各个安全站点很有了一段日子了.下面,我们进入"壳 ...

  7. AndroidLinker与SO加壳技术之下篇 1

    主页内可搜索查看<AndroidLinker与SO加壳技术之上篇> 2.4 链接 链接过程由 soinfo_link_image 函数完成,主要可以分为四个主要步骤: 1.定位 dynam ...

  8. 一种NET软件加壳技术的设计与实现

    1 引言     为了保护自己的软件的技术内核不被他人轻易盗用,软件开发人员使用了各种加密技术来保障软件的版权不被侵犯,壳便是我们常用的一种软件保护手段.对于Win32 中软件加壳技术已经有非常成熟的 ...

  9. linux so 加壳,AndroidLinker与SO加壳技术之下篇

    2.4 链接 链接过程由 soinfo_link_image 函数完成,主要可以分为四个主要步骤: 1. 定位 dynamic section, 由函数 phdr_table_get_dynamic_ ...

最新文章

  1. 【Scratch】青少年蓝桥杯_每日一题_2.13_碰苹果
  2. Touch UI:高质量的移动端UI框架介绍
  3. linux id高 负载高,linux下的rsync连接数突然增高,负载增高导致服务登录失败
  4. PostgreSQL 10.1 手册_部分 II. SQL 语言_第 11 章 索引_11.5. 组合多个索引
  5. BJUT算法设计与分析考试真题 无答案
  6. java安装pydev找不到_为什么安装成功也重启了,但是在window-preferences里找不到PyDev...
  7. 用计算机制作动画,如何使用制作工具制作一个简单的Flash动画-电脑自学网
  8. c++调试窗口不见了_Sublime Text配置GDB调试环境
  9. 网络工程师Day1--实验1-4 配置三层交换
  10. ghost mysql_Ghost - 博客搭建
  11. 安装旧版本Xcode——MACOS
  12. chrome 浏览手机网站
  13. 登陆失败:用户账户限制。可能的原因包括不允许空密码.........解决方案
  14. 码流 | 码率 | 比特率 | 帧速率 | 分辨率 | 高清的区别
  15. 安全日记—零基础开始学安全(3)
  16. 有哪些一般人不知道的数据获取方式
  17. 遗传算法中常用的选择策略
  18. 3dsmax中计算机快捷键大全,【1人回答】3DMax打开计算器的快捷键是什么?-3D溜溜网...
  19. 安全无界·成长无限—2023年网络安全“攻防”技能大赛报名启动
  20. scala详细笔记(七)scala集合练习题 [函数练习题][scala案例][scala练习]

热门文章

  1. OpenXDS 配置指南
  2. mysql数据库中只能插入数字,不能插入中英文
  3. 万豪发布后疫情时代餐饮业十大新兴趋势;凯悦旗下中高端酒店品牌逸扉在上海亮相 | 美通企业日报...
  4. PDF编辑:Adobe Acrobat X Pro 官方原版下载+中文汉化补丁
  5. 关于微信服务器ngrok 配置失败解决方案
  6. 顶级配置华硕笔记本电脑900元清仓咯
  7. 关于最近淘宝商品id换成动态id的解决方式
  8. java常用小技巧:String转List
  9. Python数据分析期中测试--百货商场案例
  10. 实例域与局部变java量_Java核心技术——第4章