查看原文
其他

记一次so文件动态解密

小白abc 看雪学院 2021-03-07

本文为看雪论坛优秀文章

看雪论坛作者ID:小白abc



本文为看雪安卓高研2w班(3月班)优秀学员作品。


下面先让我们来看看讲师对学员学习成果的点评,以及学员的学习心得吧!



讲师点评 


不论是dex保护中的函数抽取,还是so中函数的保护,时机都是非常关键的。时机不对,即使是正确的内存地址偏移,dump得到的smali指令流以及汇编代码都可能是不对的,依然是未解密的字节流。
这道题是对so中函数的一个保护,在被保护的函数的关键逻辑执行前进行解密,执行结束后会再次对关键逻辑进行加密。
因此,正确的时机便是关键逻辑被解密后,抓住这一点便能成功拿到flag。



学员感想 

整个程序基本上就是一个 动态注册 + so函数加密 的逻辑,中间加了一些parser的东西。 主要考察了elf文件结构的一些知识以及在攻防对抗中防止IDA静态分析的姿势。

ps. 题目附件请点击“阅读原文”下载。


现在,看雪《安卓高级研修班(网课)》9月班开始招生啦!点击查看详情报名吧~





题目描述


找到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 name为ooxx的symbol table表项中的value和size,其实就是读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,而v5为i的初值,那么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层的"函数抽取壳"的实现。神奇的题目。




- End -


看雪ID:小白abc

https://bbs.pediy.com/user-715334.htm

  *本文由看雪论坛 小白abc 原创,转载请注明来自看雪社区。



推荐文章++++

* VSCode搭建轻量驱动开发环境

* 恶意代码分析之反射型DLL注入

* 使用Frida简单实现函数粒度脱壳

* 从三道题目入门frida

* 物联网的基石-mqtt 协议初识


好书推荐













公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



ps. 觉得对你有帮助的话,别忘点分享,点赞和在看,支持看雪哦~


“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存