首页
社区
课程
招聘
[原创]安卓so加固初探
2023-4-23 16:24 14317

[原创]安卓so加固初探

2023-4-23 16:24
14317

图片描述

 

安卓的so加固最简单的思路,就是soinfo实例的替换,和Dex整体加固思路类似,学习逆向过程中不能绕过的基础必备之一,包括so文件的装载&链接,以及真正执行和art虚拟机的交互涉及到的流程还是相当复杂的。那么涉及安卓中so是怎么加载并映射到内存的?最简单的so加固如何实现?在这篇文章里,我们一起来看看。

 

学习目录

  1. so加载流程

  2. so的装载&链接

  3. soinfo的替换

  4. 参考文章

  5. 附录

so加载流程
网上文章还是很多了,有兴趣的话,可以看my bro文章 安卓11linker加载so流程 我这里简单的总结下流程,System.loadLibrary-> soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);-> si->call_constructors();好了到这里 call_function("DT_INIT", initfunc, get_realpath());打个断点,这样的话每次加载so开始的时候就会断在这里,方便我们观察有没有反调试或者其他准备解密的函数。
图片描述
图片描述
这里随便抄一个hook加载so的代码

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
function hook_dlopen(){
    var dlopen = Module.findExportByName(null, "dlopen");
    if(dlopen != null){
        Interceptor.attach(dlopen,{
            onEnter: function(args){
                var soName = args[0].readCString();
 
                if(soName.indexOf("libnative.so") != -1){
                    this.hook = true;
                }
            },
            onLeave: function(retval){
                if(this.hook) { hook_so() };
            }
        });
    }
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
    if(android_dlopen_ext != null){
        Interceptor.attach(android_dlopen_ext,{
            onEnter: function(args){
                var soName = args[0].readCString();
                if(soName.indexOf("libnative.so") != -1){
                    this.hook = true;
                }
            },
            onLeave: function(retval){
                if(this.hook) {
                    hook_so()
                };
            }
        });
    }
}
var isopen = false;
function hook_so(){
    if(!isopen){
        var soAddr = Module.findBaseAddress("libnative.so");
        console.log(soAddr)
        isopen = true;
    }
}
function main(){
    hook_dlopen();
}
setImmediate(main);

so的装载&链接&重定位
在上一节中,已经开始将要执行so的初始化函数了,这里边已经经过了so文件的装载和链接等过程,实际的装载步骤都在 soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);这里以安卓11的linker开始分析

 

//1.在指定命名空间中查找指定名称的共享库,并返回找到的共享库对应的 soinfo 结构体。

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
static soinfo* find_library(android_namespace_t* ns,
                            const char* name, int rtld_flags,
                            const android_dlextinfo* extinfo,
                            soinfo* needed_by) {
  soinfo* si = nullptr;
 
  if (name == nullptr) {
    si = solist_get_somain();
  } else if (!find_libraries(ns,
                             needed_by,
                             &name,
                             1,
                             &si,
                             nullptr,
                             0,
                             rtld_flags,
                             extinfo,
                             false /* add_as_children */,
                             true /* search_linked_namespaces */)) {
    if (si != nullptr) {
      soinfo_unload(si);
    }
    return nullptr;
  }
 
  si->increment_ref_count();
 
  return si;
}
2.搜索指定的共享库及其依赖项,并将查找结果添加到 load_tasks 列表中
   if (!find_library_internal(const_cast<android_namespace_t*>(task->get_start_from()),
                               task,
                               &zip_archive_cache,
                               &load_tasks,
                               rtld_flags,
                               search_linked_namespaces || is_dt_needed)) {
      return false;
    }
//如果查找成功,则从 LoadTask 对象中获取其关联的 soinfo 对象。
    soinfo* si = task->get_soinfo();
 
    if (is_dt_needed) {
      needed_by->add_child(si);
    }
 
3.源码一大堆条件直接省略一部分
// soinfo_alloc 函数来为共享库分配一个 soinfo 结构体,并设置其属性
  soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
  if (si == nullptr) {
    return false;
  }
//将共享库对应的 soinfo 结构体添加到 LoadTask 对象中。
  task->set_soinfo(si);
 
  // 读取共享库文件的 ELF 头和一些段数据到内存中。
  if (!task->read(realpath.c_str(), file_stat.st_size)) {
    soinfo_free(si);
    task->set_soinfo(nullptr);
    return false;
  }
//读取 ELF 文件的头部、程序头、节头和动态节等重要数据到内存中
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_;
}
 
//遍历 load_list 中所有的 LoadTask 对象,并调用它们的 load 函数来将对应的共享库加载到指定地址空间中
 for (auto&& task : load_list) {
    address_space_params* address_space =
        (reserved_address_recursive || !task->is_dt_needed()) ? &extinfo_params : &default_params;
    if (!task->load(address_space)) {
      return false;
    }
  }
//它用于将读取到内存中的 ELF 文件加载到指定的地址空间中。
bool ElfReader::Load(address_space_params* address_space) {
  CHECK(did_read_);
  if (did_load_) {
    return true;
  }
  if (ReserveAddressSpace(address_space) && LoadSegments() && FindPhdr()) {
    did_load_ = true;
  }
 
  return did_load_;
}
//初始化共享库的一些属性基址、大小、偏移量、程序头表等
  bool load(address_space_params* address_space) {
    ElfReader& elf_reader = get_elf_reader();
    if (!elf_reader.Load(address_space)) {
      return false;
    }
 
    si_->base = elf_reader.load_start();
    si_->size = elf_reader.load_size();
    si_->set_mapped_by_caller(elf_reader.is_mapped_by_caller());
    si_->load_bias = elf_reader.load_bias();
    si_->phnum = elf_reader.phdr_count();
    si_->phdr = elf_reader.loaded_phdr();
 
    return true;
  }
 
  // Step 3: pre-link all DT_NEEDED libraries in breadth first order.
  for (auto&& task : load_tasks) {
    soinfo* si = task->get_soinfo();
    if (!si->is_linked() && !si->prelink_image()) {
      return false;
    }
    register_soinfo_tls(si);
  }
 
 
//预链接操作提取出其中的相关信息,并将其填充到 soinfo 结构体中。这些信息包括:符号表、重定位表、动态链接库名称、TLS 段、哈希表等。
bool soinfo::prelink_image() {
  uint32_t needed_count = 0;
  for (ElfW(Dyn)* d = dynamic; d->d_tag != DT_NULL; ++d) {
    DEBUG("d = %p, d[0](tag) = %p d[1](val) = %p",
          d, reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
    switch (d->d_tag) {
      case DT_SONAME:
        // this is parsed after we have strtab initialized (see below).
        break;
 
      case DT_HASH:
        nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
        nchain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
        bucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8);
        chain_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr + 8 + nbucket_ * 4);
        break;
 
      case DT_GNU_HASH:
        gnu_nbucket_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[0];
        // skip symndx
        gnu_maskwords_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[2];
        gnu_shift2_ = reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[3];
 
        gnu_bloom_filter_ = reinterpret_cast<ElfW(Addr)*>(load_bias + d->d_un.d_ptr + 16);
        gnu_bucket_ = reinterpret_cast<uint32_t*>(gnu_bloom_filter_ + gnu_maskwords_);
        // amend chain for symndx = header[1]
        gnu_chain_ = gnu_bucket_ + gnu_nbucket_ -
            reinterpret_cast<uint32_t*>(load_bias + d->d_un.d_ptr)[1];
 
        if (!powerof2(gnu_maskwords_)) {
          DL_ERR("invalid maskwords for gnu_hash = 0x%x, in \"%s\" expecting power to two",
              gnu_maskwords_, get_realpath());
          return false;
        }
        --gnu_maskwords_;
 
        flags_ |= FLAG_GNU_HASH;
        break;
 
      case DT_STRTAB:
        strtab_ = reinterpret_cast<const char*>(load_bias + d->d_un.d_ptr);
        break;
 
      case DT_STRSZ:
        strtab_size_ = d->d_un.d_val;
        break;
 
      case DT_SYMTAB:
        symtab_ = reinterpret_cast<ElfW(Sym)*>(load_bias + d->d_un.d_ptr);
        break;
 
      case DT_SYMENT:
        if (d->d_un.d_val != sizeof(ElfW(Sym))) {
          DL_ERR("invalid DT_SYMENT: %zd in \"%s\"",
              static_cast<size_t>(d->d_un.d_val), get_realpath());
          return false;
        }
        break;
 
      case DT_PLTREL:
#if defined(USE_RELA)
        if (d->d_un.d_val != DT_RELA) {
          DL_ERR("unsupported DT_PLTREL in \"%s\"; expected DT_RELA", get_realpath());
          return false;
        }
#else
        if (d->d_un.d_val != DT_REL) {
          DL_ERR("unsupported DT_PLTREL in \"%s\"; expected DT_REL", get_realpath());
          return false;
        }
#endif
        break;
 
      case DT_JMPREL:
........
link_image
//relocate() 函数进行重定位
     bool relocated = false;
      const uint8_t* packed_relocs = android_relocs_ + 4;
      const size_t packed_relocs_size = android_relocs_size_ - 4;
 
      relocated = relocate(
          version_tracker,
          packed_reloc_iterator<sleb128_decoder>(
            sleb128_decoder(packed_relocs, packed_relocs_size)),
          global_group, local_group);

//R_GENERIC_JUMP_SLOT:处理跳转槽重定位(PLT表相关)。在这种情况下,将符号地址(sym_addr)加上加数(addend),并将结果存储在重定位的位置(reloc)。跳转槽重定位通常与延迟绑定有关,用于查找未解析符号的正确地址。
//R_GENERIC_GLOB_DAT:处理全局数据重定位。这种类型的重定位类似于跳转槽重定位,将符号地址(sym_addr)加上加数(addend),然后将结果存储在重定位的位置(reloc)。全局数据重定位用于全局变量和数据符号。
//R_GENERIC_RELATIVE:处理相对重定位。在这种情况下,将加载偏移量(load_bias)加上加数(addend),然后将结果存储在重定位的位置(reloc)。相对重定位通常用于修复那些相对于共享库基地址的引用。
//R_GENERIC_IRELATIVE:处理间接相对重定位。这种类型的重定位与相对重定位类似,但需要间接引用一个函数来获取最终的重定位地址。在这种情况下,将加载偏移量(load_bias)加上加数(addend),然后在后面的代码中调用解析函数来获取重定位地址

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
case R_GENERIC_JUMP_SLOT:
        count_relocation(kRelocAbsolute);
        MARK(rel->r_offset);
        TRACE_TYPE(RELO, "RELO JMP_SLOT %16p <- %16p %s\n",
                   reinterpret_cast<void*>(reloc),
                   reinterpret_cast<void*>(sym_addr + addend), sym_name);
 
        *reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
        break;
      case R_GENERIC_GLOB_DAT:
        count_relocation(kRelocAbsolute);
        MARK(rel->r_offset);
        TRACE_TYPE(RELO, "RELO GLOB_DAT %16p <- %16p %s\n",
                   reinterpret_cast<void*>(reloc),
                   reinterpret_cast<void*>(sym_addr + addend), sym_name);
        *reinterpret_cast<ElfW(Addr)*>(reloc) = (sym_addr + addend);
        break;
      case R_GENERIC_RELATIVE:
        count_relocation(kRelocRelative);
        MARK(rel->r_offset);
        TRACE_TYPE(RELO, "RELO RELATIVE %16p <- %16p\n",
                   reinterpret_cast<void*>(reloc),
                   reinterpret_cast<void*>(load_bias + addend));
        *reinterpret_cast<ElfW(Addr)*>(reloc) = (load_bias + addend);
        break;
      case R_GENERIC_IRELATIVE:
        count_relocation(kRelocRelative);
        MARK(rel->r_offset);
        TRACE_TYPE(RELO, "RELO IRELATIVE %16p <- %16p\n",
                    reinterpret_cast<void*>(reloc),
                    reinterpret_cast<void*>(load_bias + addend));

• 装载和链接完毕这样我们的so就可以运行了。 更具体的可以看前辈的 Android Linker详解[1] 保姆级分析教程。
soinfo的替换
soinfo的替换其实是我们实现最基本的so加固的最后操作了,也是最麻烦的一步,各种表的地址修正和安卓版本兼容性的问题,真的是让人很头大,至于修复结构体重的字段在ida反汇编出来,可以和源代码进行对比参照来进行修复,修复过程可以借鉴安卓源码中重定位相关的代码,嗷嗷一顿借鉴。
图片描述
图片描述

 

详细的demo可以参考前辈的文章 基于linker实现so加壳技术基础[2]

 

参考文章
参考一[3]

 

参考二[4]

 

参考三[5]

 

参考四[6]

 

附录
ida中的代码解析

 

LOAD:000000000000BDD8 Elf64_Rela <0x31E68, 0xDF00000101, 0> ; R_AARCH64_ABS64 _ZNSt13runtime_errorD0Ev 重定位简析 0x31E68是需要重定位的内存偏移量, 0xDF0 表示对应的符号索引,101代表类型

 

对于 AARCH64,常见的重定位类型有:

 

R_AARCH64_JUMP_SLOT:这种重定位类型主要用于处理动态链接的库中的外部函数调用。在程序中调用一个外部共享库函数时,链接器需要知道函数的实际地址。为了解决这个问题,链接器会创建一个跳转表(Procedure Linkage Table,PLT),该表包含一个特殊的入口,用于在程序运行时解析函数的实际地址。

 

R_AARCH64_RELATIVE: 这种重定位类型主要用于处理共享库中的本地数据和函数引用。在共享库中,某些引用的地址需要根据库在内存中的实际加载地址进行调整。

 

R_AARCH64_GLOB_DAT: 这种重定位类型主要用于动态链接库中全局数据的引用。当一个应用程序需要访问一个外部共享库中的全局变量时,链接器会使用R_AARCH64_GLOB_DAT重定位类型来解析该变量的实际地址.

 

简化的relocate函数实现

 

#include <elf.h> // 包含ELF格式相关的定义

 

bool relocate(soinfo si) {
Elf64_Rela
rela_table = si->rela_table; // RELA表指针
int rela_count = si->rela_count; // RELA表中条目的数量
Elf64_Sym symtab = si->symtab; // 符号表指针
const char
strtab = si->strtab; // 字符串表指针

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
// 遍历RELA表
for (int i = 0; i < rela_count; ++i) {
    Elf64_Rela* rela = &rela_table[i]; // 获取当前RELA条目
    uint32_t type = ELF64_R_TYPE(rela->r_info); // 获取重定位类型
    uint32_t sym_index = ELF64_R_SYM(rela->r_info); // 获取符号表索引
    Elf64_Addr offset = rela->r_offset; // 获取重定位偏移
    Elf64_Sxword addend = rela->r_addend; // 获取重定位补偿值
 
    // 根据重定位类型执行操作
    switch (type) {
        case R_AARCH64_ABS64: { // 绝对地址64
            // 获取符号值
            Elf64_Addr sym_value = symtab[sym_index].st_value;
            // 计算重定位后的值
            Elf64_Addr target_value = sym_value + addend;
            // 将重定位后的值写入目标位置
            *((Elf64_Addr*)(si->base_addr + offset)) = target_value;
            break;
        }
        // 处理其他重定位类型...
        default:
            // 未知的重定位类型,返回错误
            return false;
    }
}
return true;

}

 

'''

 

简化的resolve_symbols函数实现

 

'''

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
#include <elf.h> // 包含ELF格式相关的定义
 
bool resolve_symbols(soinfo* si) {
    Elf64_Sym* symtab = si->symtab; // 符号表指针
    const char* strtab = si->strtab; // 字符串表指针
    int sym_count = si->sym_count; // 符号表中符号的数量
 
    // 遍历符号表
    for (int i = 0; i < sym_count; ++i) {
        Elf64_Sym* sym = &symtab[i]; // 获取当前符号
        const char* sym_name = strtab + sym->st_name; // 获取符号名
 
        if (sym->st_shndx == SHN_UNDEF) { // 如果符号未定义(需要从其他库中解析)
            // 从其他库中解析符号
            Elf64_Addr resolved_addr = find_symbol_in_linked_libraries(sym_name);
            if (resolved_addr == 0) {
                // 无法解析符号,返回错误
                return false;
            }
            // 更新符号值为解析后的地址
            sym->st_value = resolved_addr;
        } else {
            // 符号已定义,更新符号值为实际内存地址
            sym->st_value += si->base_addr;
        }
    }
    return true;
}
 
Elf64_Addr find_symbol_in_linked_libraries(const char* symbol_name) {
    // 在已链接的库中查找符号并返回其地址
    // 如果无法找到符号,返回0
}
 
'''
dump内存代码
function dump_so(so_name) {
    Java.perform(function () {
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
        var libso = Process.getModuleByName(so_name);
        var file_path = dir + "/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
        var file_handle = new File(file_path, "wb");
        if (file_handle && file_handle != null) {
            Memory.protect(ptr(libso.base), libso.size, 'rwx');
            var libso_buffer = ptr(libso.base).readByteArray(libso.size);
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();
            console.log("[dump]:", file_path);
        }
    });
}

so修复工具
https://github.com/F8LEFT/SoFixer
引用链接
[1] Android Linker详解: https://bbs.kanxue.com/thread-274573.htm
[2] 基于linker实现so加壳技术基础: https://bbs.kanxue.com/thread-269485.htm
[3] 参考一: https://android.googlesource.com/platform/bionic/
[4] 参考二: https://bbs.kanxue.com/thread-274573.htm
[5] 参考三: https://bbs.kanxue.com/thread-269891.htm
[6] 参考四: https://bbs.kanxue.com/thread-269485.htm

 

欢迎大家关注vx公众号,小瑶在工地,相互交流学习,共同进步。
图片描述


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-6-16 15:19 被涿州飞神编辑 ,原因: 排版问题
收藏
点赞9
打赏
分享
最新回复 (15)
雪    币: 229
活跃值: (213252)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
shinratensei 1 2023-4-23 16:38
2
0
tql
雪    币: 52
活跃值: (501)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-4-23 16:51
3
0
厉害了
雪    币: 414
活跃值: (934)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Andy_877594 2023-4-23 17:34
4
0
牛逼
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-23 17:40
5
0
666
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-23 17:40
6
0
Andy_877594 牛逼
666
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-25 14:40
7
0
万里星河 厉害了
666
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-26 07:12
8
0
shinratensei tql
感谢支持
雪    币: 19103
活跃值: (28707)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-4-26 09:23
9
1

tql

最后于 2023-4-27 09:49 被秋狝编辑 ,原因:
雪    币: 512
活跃值: (3719)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
龙飞雪 2023-4-26 13:51
10
0
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-27 06:57
11
0
龙飞雪
感谢支持
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-27 16:56
12
0
秋狝 tql
感谢支持
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-4-29 11:23
13
0
感谢大佬们的支持
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-5-5 08:09
14
0
雪    币: 123
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
涿州飞神 2023-5-10 09:00
15
0
大佬们 一起学习交流进步
雪    币: 0
活跃值: (1181)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Circ1e 2023-5-29 17:21
16
0
大佬,你这个装载上过后可以捕获异常不
游客
登录 | 注册 方可回帖
返回