首页
社区
课程
招聘
[原创] 记一次so文件动态解密
2020-7-8 11:32 20927

[原创] 记一次so文件动态解密

2020-7-8 11:32
20927

写在前面

整个程序基本上就是一个 动态注册 + so函数加密 的逻辑,中间加了一些parser的东西

 

主要考察了elf文件结构的一些知识以及在攻防对抗中防止IDA静态分析的姿势

题目描述

找到flag

WriteUp

360加固,先脱壳,看入口函数MainActivity

 

 

具体的逻辑写到so里了,使用IDA打开so文件,先看有没有.init.init_array,发现只有.init_array节,

 

 

跟进去一看又是字符串解密函数,解密之后,代码如下(这里我根据解密后的数据进行了重命名)

unsigned int datadiv_decode4192348989750430380()
{

  v29 = 0;
  do
  {
    v0 = v29;
    Find_ooxx_failed[v29++] ^= 0x14u;
  }
  while ( v0 < 0x10 );
  v28 = 0;
  do
  {
    v1 = v28;
    mem_privilege_change_failed[v28++] ^= 0xD3u;
  }
  while ( v1 < 0x1B );
  v27 = 0;
  do
  {
    v2 = v27;
    kanxuetest[v27++] ^= 0x63u;
  }
  while ( v2 < 0xA );
  v26 = 0;
  do
  {
    v3 = v26;
    Hello_from_Cjiajia[v26++] ^= 0x3Fu;
  }
  while ( v3 < 0xE );
  v25 = 0;
  do
  {
    v4 = v25;
    test[v25++] ^= 0xF3u;
  }
  while ( v4 < 4 );
  v24 = 0;
  do
  {
    v5 = v24;
    sig_Ljava_lang_Object_Z[v24++] ^= 0xFAu;
  }
  while ( v5 < 0x15 );
  v23 = 0;
  do
  {
    v6 = v23;
    com_kanxue_test_MainActivity[v23++] ^= 0x2Du;
  }
  while ( v6 < 0x1C );
  v22 = 0;
  do
  {
    v7 = v22;
    maps[v22++] ^= 0xF5u;
  }
  while ( v7 < 0xD );
  v21 = 0;
  do
  {
    v8 = v21;
    r[v21++] ^= 0xF8u;
  }
  while ( !v8 );
  v20 = 0;
  do
  {
    v9 = v20;
    open_failed[v20++] ^= 0xE6u;
  }
  while ( v9 < 0xB );
  v19 = 0;
  do
  {
    v10 = v19;
    heng[v19++] ^= 0x66u;
  }
  while ( !v10 );
  v18 = 0;
  do
  {
    v11 = v18;
    Find__dynamic_segment[v18++] ^= 0x2Du;
  }
  while ( v11 < 0x15 );
  v17 = 0;
  do
  {
    v12 = v17;
    Find_needed__section_failed[v17++] ^= 9u;
  }
  while ( v12 < 0x1C );
  v16 = 0;
  do
  {
    v13 = v16;
    basic_string[v16++] ^= 0x9Eu;
  }
  while ( v13 < 0xC );
  v15 = 0;
  do
  {
    result = v15;
    allocate_exceeds_maximum_supported_size[v15++] ^= 0xDBu;
  }
  while ( result < 0x43 );
  return result;
}

回过来看JNI_Onload函数,

 

 

其实就是将native函数test函数动态注册到ooxx函数,直接看ooxx函数

 

 

可以发现除了调用了sub_8930之外,就是一堆垃圾代码,先跟进sub_8930函数

 

 

这里我把函数分为三块,先看第一块

 

经过分析,实际上就是读/proc/self/maps的标准输出,从而获取到对应于libnaitve-lib.so的那一行,然后以-分割字符串,并将分割后的第一段解析为16进制的数,实际上就是获取libnaitve-lib.so的加载基地址。

 

 

再看第二块,也就是sub_8B90函数的实现

int __fastcall find_symbol_value_and_size(int base_addr, char *a2, _DWORD *a3)
{
  int v3; // ST38_4
  _DWORD *ELF_Hash_Table; // ST28_4
  unsigned int v5; // ST20_4
  int elf_hash_chain; // [sp+14h] [bp-5Ch]
  int ELF_Symbol_Table; // [sp+24h] [bp-4Ch]
  int elf_hash_table; // [sp+28h] [bp-48h]
  int string_table; // [sp+2Ch] [bp-44h]
  int elf_symbol_table; // [sp+30h] [bp-40h]
  _DWORD *v12; // [sp+34h] [bp-3Ch]
  int dynamic_segment_base_addr; // [sp+40h] [bp-30h]
  _DWORD *header_table; // [sp+44h] [bp-2Ch]
  signed int i; // [sp+4Ch] [bp-24h]
  unsigned int j; // [sp+4Ch] [bp-24h]
  int elf_hash_bucket; // [sp+4Ch] [bp-24h]
  char v18; // [sp+57h] [bp-19h]
  char v19; // [sp+57h] [bp-19h]
  char v20; // [sp+57h] [bp-19h]
  _DWORD *value; // [sp+58h] [bp-18h]
  char *s2; // [sp+5Ch] [bp-14h]
  int so_base_addr; // [sp+60h] [bp-10h]

  so_base_addr = base_addr;
  s2 = a2;
  value = a3;
  v18 = -1;
  header_table = (base_addr + *(base_addr + 0x1C));// header_table_offset
  for ( i = 0; i < *(base_addr + 0x2C); ++i )   // *(base_addr + 0x2C) = 8
  {
    if ( *header_table == 2 )
    {
      v18 = 0;
      puts_0();                                 // find_dynamic_segment
      break;
    }
    header_table += 8;
  }
  if ( v18 )
    goto LABEL_27;
  dynamic_segment_base_addr = header_table[2] + so_base_addr;// 找到dynamic_segment的虚拟地址
  v19 = 0;
  for ( j = 0; j < header_table[4] >> 3; ++j )
  {
    v12 = (dynamic_segment_base_addr + 8 * j);
    if ( *(dynamic_segment_base_addr + 8 * j) == 6 )
    {
      elf_symbol_table = v12[1];                // 0x1f0
      ++v19;
    }
    if ( *v12 == 4 )
    {
      elf_hash_table = v12[1];                  // 0x46e0
      v19 += 2;
    }
    if ( *v12 == 5 )
    {
      string_table = v12[1];                    // 0x1d00
      v19 += 4;
    }
    if ( *v12 == 10 )
    {
      v3 = v12[1];                              // 0x1eb6
      v19 += 8;
    }
  }
  if ( (v19 & 0xF) != 0xF )
  {
    puts_0();
LABEL_27:
    return -1;
  }
  ELF_Hash_Table = (so_base_addr + elf_hash_table);// v4 =elf_hash_table
  v5 = turn_ooxx(s2);                            // v5 = 0x766f8
  ELF_Symbol_Table = so_base_addr + elf_symbol_table;// ELF Symbol Table
  elf_hash_chain = &ELF_Hash_Table[*ELF_Hash_Table + 2];
  v20 = -1;
  for ( elf_hash_bucket = ELF_Hash_Table[v5 % *ELF_Hash_Table + 2];// ELF_Hash_Table[v5 % *ELF_Hash_Table + 2] = 0x4918
        elf_hash_bucket;
        elf_hash_bucket = *(elf_hash_chain + 4 * elf_hash_bucket) )
  {
    if ( !strcmp((so_base_addr + string_table + *(ELF_Symbol_Table + 16 * elf_hash_bucket)), s2) )// string_table[] = "ooxx"
    {
      v20 = 0;
      break;
    }
  }
  if ( v20 )
    goto LABEL_27;
  *value = *(ELF_Symbol_Table + 16 * elf_hash_bucket + 4);
  value[1] = *(ELF_Symbol_Table + 16 * elf_hash_bucket + 8);
  return 0;
}

这个地方你仔细地去分析对比,会发现其实就是一个读so文件的对应于symbol nameooxxsymbol table表项中的valuesize,其实就是读ooxx的函数起始地址以及函数大小。其实也就是一个parser的过程之一

 

对了,这个函数中的一行,也就是v5 = turn_ooxx(s2);这里调用的turn_ooxx函数中的伪代码直接copy出来跑一跑,就可以得到v5的值。我也没有分析这个过程,直接跑的。。

 

接着看sub_8930函数的第三块。

 

 

经过分析会发现,围绕mprotect函数将这个部分再次分成三块,分别实现功能为

  1. 第一块,设置ooxx函数所在内存页为rwx
  2. 第二块,还原ooxx函数中code
  3. 第三块,恢复内存页为r-x

这里第二块中的*i ^= byte_1C180[&i[-v5]];这个部分,再加上byte_1C180实际上在bss段,不想再去分析了,直接动态吧。
这里使用objection在动态运行时dump出对应内存中的数据,

 

 

使用010 editor查看对应文件

 

 

很明显那就是0-255的字节咯,继续看伪码,会发现实际上这里的&i[-v5]实际上就相当于i-v5,而v5i的初值,那么patch脚本就有了

def patchBytes(addr,length):
    for i in range(0,length):
        byte=get_bytes(addr + i,1)
        byte = ord(byte) ^ (i%0xff)
        patch_byte(addr+i,byte)
patchBytes(0x8e00,0x8fd0-0x8e00)

执行这个脚本之后,查看ooxx函数内容

int __fastcall ooxx(JNIEnv *a1, int a2, int a3)
{
  JNIEnv *v3; // ST20_4
  int input; // r0
  int v5; // r0
  unsigned __int8 v7; // [sp+17h] [bp-19h]

  v3 = a1;
  sub_8930(); // 
  v7 = 0;
  input = getStringUtf(v3);
  if ( input )
  {
    input = strcmp(aKanxuetest, input);
    if ( !input )
    {
      input = 1;
      v7 = 1;
    }
  }
  v5 = *(input + 8);
  sub_8930();
  return v7;
}

最终会发现,实际上ooxx就是拿我的输入和kanxuetest进行对比。。验证下

 

 

拿到flag

后记

整个程序实际上真正难的地方在于看出parser的过程,不过我猜如果写过parser相信会很容易的看出来,还有
另外,这个程序有点类似于之前寒冰师傅说的在函数执行开始之前对函数内容进行恢复,函数执行结束时再还原回加密状态,再加上插入了一堆MOV R0, R0这种无效代码,让我感觉真像so层的"函数抽取壳"的实现。。神奇的题目,最后,附上附件


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2020-7-8 11:32 被Simp1er编辑 ,原因:
上传的附件:
收藏
点赞11
打赏
分享
最新回复 (23)
雪    币: 3509
活跃值: (17979)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2020-7-8 14:18
2
0
感谢分享~
雪    币: 19786
活跃值: (4862)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
浅笑不语 2020-7-9 18:37
3
0
感谢楼主分享
雪    币: 1
活跃值: (425)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Necopy 2020-7-10 10:42
4
0
请问这个是2w班的练习题吗?
雪    币: 241
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
THT-EX 2020-7-13 07:33
5
0
更新搂主分享
雪    币: 10531
活跃值: (6644)
能力值: ( LV12,RANK:214 )
在线值:
发帖
回帖
粉丝
neilwu 1 2020-7-14 13:23
6
0
请问脱壳那里 native onCreate是怎么还原的?
雪    币: 1911
活跃值: (4925)
能力值: ( LV8,RANK:146 )
在线值:
发帖
回帖
粉丝
Simp1er 2020-7-14 13:47
7
0
Necopy 请问这个是2w班的练习题吗?
雪    币: 1911
活跃值: (4925)
能力值: ( LV8,RANK:146 )
在线值:
发帖
回帖
粉丝
Simp1er 2020-7-14 13:47
8
0
neilwu 请问脱壳那里 native onCreate是怎么还原的?
不需要还原
雪    币: 10531
活跃值: (6644)
能力值: ( LV12,RANK:214 )
在线值:
发帖
回帖
粉丝
neilwu 1 2020-7-14 14:20
9
1

这里是脱壳之后就有的嘛? 请问你用的什么脱壳的?

雪    币: 1911
活跃值: (4925)
能力值: ( LV8,RANK:146 )
在线值:
发帖
回帖
粉丝
Simp1er 2020-7-14 14:43
10
0
neilwu 这里是脱壳之后就有的嘛? 请问你用的什么脱壳的?
对的,FART脱的壳
雪    币: 177
活跃值: (5366)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
看雪高研 2020-7-14 17:07
11
1
不论是dex保护中的函数抽取,还是so中函数的保护,时机都是非常关键的。时机不对,即使是正确的内存地址偏移,dump得到的smali指令流以及汇编代码都可能是不对的,依然是未解密的字节流。这道题是对so中函数的一个保护,在被保护的函数的关键逻辑执行前进行解密,执行结束后会再次对关键逻辑进行加密。因此,正确的时机便是关键逻辑被解密后,抓住这一点便能成功拿到flag
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
本菜很懒 2020-7-29 11:04
12
0
大佬 牛i逼
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
qxpy_ 2020-11-21 22:10
13
0
牛i逼
雪    币: 160
活跃值: (86)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lujunhuaqq 2020-11-29 00:00
14
0
牛大佬佩服
雪    币: 6
活跃值: (1010)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
菜鸟也想飞 2021-7-23 09:24
15
0
感谢分享
雪    币: 62
活跃值: (566)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2021-7-27 02:04
16
0
大佬牛逼
雪    币: 158
活跃值: (796)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
教教我吧~ 2023-4-1 10:41
17
0
请问2w班有对该练习题进行讲解吗?
雪    币: 158
活跃值: (796)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
教教我吧~ 2023-4-1 11:34
18
0

您好,执行您的patch脚本之后,0x8e00——0x8fd0的汇编全变成MOV             R0, R0,然后反编译后,出现下面的问题,请问您知道原因吗?

雪    币: 320
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Losir 2023-5-11 16:10
19
0
我也是和楼上一样
雪    币: 1911
活跃值: (4925)
能力值: ( LV8,RANK:146 )
在线值:
发帖
回帖
粉丝
Simp1er 2023-5-15 09:28
20
0
Losir 我也是和楼上一样
时间太过久远,我已经忘了 实在抱歉,麻烦自己再研究研究吧,可以先把原来的byte show出来,然后看看执行完函数的byte是什么样,确定下是否函数执行成功了,关注下output窗口是否有错误日志出来吧
雪    币: 320
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Losir 2023-5-23 11:57
21
0
Simp1er 时间太过久远,我已经忘了[em_5] 实在抱歉,麻烦自己再研究研究吧,可以先把原来的byte show出来,然后看看执行完函数的byte是什么样,确定下是否函数执行成功了,关注下output窗口是否有 ...
大佬,感谢回复,你的patch代码有问题,当i=255,取余为0,但实际上应该和0xff亦或,当i=256,取余为1,但实际上应该和0亦或,也就是说,从i=255开始,后面的patch全错了。
雪    币: 320
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Losir 2023-5-23 12:15
22
0
Simp1er 时间太过久远,我已经忘了[em_5] 实在抱歉,麻烦自己再研究研究吧,可以先把原来的byte show出来,然后看看执行完函数的byte是什么样,确定下是否函数执行成功了,关注下output窗口是否有 ...
def patchBytes(addr, length):
    for i in range(length):
        if i < 255:
            byte = get_bytes(addr + i,1)
            byte = ord(byte) ^ (i % 0xff)
            patch_byte(addr + i, byte)
        elif i == 255:
            byte = get_bytes(addr + i,1)
            byte = ord(byte) ^ 0xff
            patch_byte(addr + i, byte)
        else:
            byte = get_bytes(addr + i,1)
            byte = ord(byte) ^ ((i - 1) % 0xff)
            patch_byte(addr + i, byte)

patchBytes(0x8e00, 0x8fd0 - 0x8e00)   修改后的脚本
雪    币: 320
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Losir 2023-5-23 12:17
23
1
教教我吧~ 您好,执行您的patch脚本之后,0x8e00——0x8fd0的汇编全变成MOV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; & ...
可以看我的回复,patch之后记得保存,然后用ida 重新打开就ok啦。
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
ivy1 2024-4-19 09:45
24
0

APK样本方便再上传下 不? 可以了 我网络问题一开始显示过期了

最后于 2024-4-19 09:46 被ivy1编辑 ,原因:
游客
登录 | 注册 方可回帖
返回