记一次so文件动态解密
2020-07-08 17:15:44 Author: bbs.pediy.com(查看原文) 阅读量:678 收藏

写在前面

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

[看雪官方培训]《安卓高级研修班(网课)》9月班开始招生!顶尖技术、挑战极限、工资翻倍!

最后于 4小时前 被小白abc编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-260547.htm
如有侵权请联系:admin#unsafe.sh