首页
论坛
课程
招聘
《基于linker实现so加壳技术基础》上篇
2021-9-24 18:40 24506

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

2021-9-24 18:40
24506

《基于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层

1
2
3
4
5
6
7
8
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的加载流程。

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

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

1
2
3
4
5
6
7
8
9
10
...
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等一些列工作

1
2
3
4
5
6
7
8
9
10
11
12
bool find_libraries(...) {
 
 
 
...
 if (!find_library_internal(....){
   return false;
 }
...
 
 
                   }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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下一定会使用程序头加载段而节信息可有可无

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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就好

1
2
3
4
5
6
7
int fd;
void *start;
struct stat sb;
fd = open("/data/local/tmp/1.so", O_RDONLY); //打开获得我们插件so的fd
fstat(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指针。

1
2
3
4
5
6
7
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在内存中的地址
那么这里我们就要将这三个方法全部都实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法我们直接从源码里面抄过来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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);;//直接申请空间需要页对其所以好像不能用malloc
 
        load_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占位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
   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写在类的外面也无关紧要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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部分,会写在下篇里面


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞0
打赏
分享
最新回复 (3)
雪    币: 155
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
vavhasdqpk 活跃值 2021-10-13 15:54
2
0
好文,点赞
雪    币: 199
活跃值: 活跃值 (84)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小k_hell 活跃值 2021-11-3 16:37
3
0
好文章
雪    币: 26
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
万里星河 活跃值 2021-11-3 23:07
4
0
支持一下
游客
登录 | 注册 方可回帖
返回