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

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

2020-7-8 11:32
5753

写在前面

整个程序基本上就是一个 动态注册 + 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层的"函数抽取壳"的实现。。神奇的题目,最后,附上附件


[培训]12月3日2020京麒网络安全大会《物联网安全攻防实战》训练营,正在火热报名中!地点:北京 · 新云南皇冠假日酒店

最后于 2020-7-8 11:32 被小白abc编辑 ,原因:
上传的附件:
收藏
点赞3
打赏
分享
最新回复 (13)
雪    币: 122
活跃值: 活跃值 (7934)
能力值: ( LV9,RANK:166 )
在线值:
发帖
回帖
粉丝
0x指纹 活跃值 3 2020-7-8 14:18
2
0
感谢分享~
雪    币: 5832
活跃值: 活跃值 (3271)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
浅笑不语 活跃值 2020-7-9 18:37
3
0
感谢楼主分享
雪    币: 15
活跃值: 活跃值 (67)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Necopy 活跃值 2020-7-10 10:42
4
0
请问这个是2w班的练习题吗?
雪    币: 241
活跃值: 活跃值 (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
THT-EX 活跃值 2020-7-13 07:33
5
0
更新搂主分享
雪    币: 6833
活跃值: 活跃值 (2567)
能力值: ( LV9,RANK:148 )
在线值:
发帖
回帖
粉丝
neilwu 活跃值 1 2020-7-14 13:23
6
0
请问脱壳那里 native onCreate是怎么还原的?
雪    币: 2035
活跃值: 活跃值 (1072)
能力值: ( LV6,RANK:84 )
在线值:
发帖
回帖
粉丝
小白abc 活跃值 2020-7-14 13:47
7
0
Necopy 请问这个是2w班的练习题吗?
雪    币: 2035
活跃值: 活跃值 (1072)
能力值: ( LV6,RANK:84 )
在线值:
发帖
回帖
粉丝
小白abc 活跃值 2020-7-14 13:47
8
0
neilwu 请问脱壳那里 native onCreate是怎么还原的?
不需要还原
雪    币: 6833
活跃值: 活跃值 (2567)
能力值: ( LV9,RANK:148 )
在线值:
发帖
回帖
粉丝
neilwu 活跃值 1 2020-7-14 14:20
9
0

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

雪    币: 2035
活跃值: 活跃值 (1072)
能力值: ( LV6,RANK:84 )
在线值:
发帖
回帖
粉丝
小白abc 活跃值 2020-7-14 14:43
10
0
neilwu 这里是脱壳之后就有的嘛? 请问你用的什么脱壳的?
对的,FART脱的壳
雪    币: 845
活跃值: 活跃值 (533)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
看雪高研 活跃值 2020-7-14 17:07
11
0
不论是dex保护中的函数抽取,还是so中函数的保护,时机都是非常关键的。时机不对,即使是正确的内存地址偏移,dump得到的smali指令流以及汇编代码都可能是不对的,依然是未解密的字节流。这道题是对so中函数的一个保护,在被保护的函数的关键逻辑执行前进行解密,执行结束后会再次对关键逻辑进行加密。因此,正确的时机便是关键逻辑被解密后,抓住这一点便能成功拿到flag
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
本菜很懒 活跃值 2020-7-29 11:04
12
0
大佬 牛i逼
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_丘献平 活跃值 2020-11-21 22:10
13
0
牛i逼
雪    币: 0
活跃值: 活跃值 (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lujunhuaqq 活跃值 3天前
14
0
牛大佬佩服
游客
登录 | 注册 方可回帖
返回