[原创] 分析一个有趣的so双重壳
背景
去年四五月份的时候客户反馈过一个so加壳的问题,分析后发现是和某Android应用市场SDK的libYSDK.so冲突。当时该so解壳过程中会操作linker的soinfo结构的部分数据,壳作者把dlopen返回的handle强制转成soinfo结构体指针,这操作个必然有兼容性问题。后来该壳也做了升级,其最新版本有一些挺有趣的技术点,跟大家分享下。
两层壳:
Ida直接打开:

函数列表只有31个sub_**(__mprotect是笔者改名来的), 再看下Section表 :

很多内容被擦掉了,dynsym 和 dynstr在7.0+上linker需要,必须保留,其他保留项对分析没什么用,壳的作者也保留了,这里直接抹掉elf-header中的section表偏移和大小,让ida使用动态链接表解析:

再用IDA打开后函数列表变了:

但是往下看代码区(JNI_OnLoad),代码加密了:

继续往后边拖拽,符号表竟然都在第二个loadsegment:

init解密壳
先考虑第一个loadsegment代码加密的问题,一般在linker的init中执行解密逻辑,init对应的那部分code一定是明文状态,greadelf –d libYSDK.so:

该so编译时的名字是“legu”,后来重命名为“YSDK”的,没有找到init段,只有init_array,跳过去看下:

看来sub_838就是解密函数了,贴上它的伪代码:
int sub_838()
{
unsigned int code_segment_end_segment; // ST28_4
unsigned int data_segment_begin_page; // ST24_4
unsigned int data_segment_end_page; // ST20_4
unsigned __int64 v3; // kr00_8
char v4; // ST13_1
int result; // r0
int v6; // [sp+4h] [bp-40h]
int v7; // [sp+8h] [bp-3Ch]
int j; // [sp+2Ch] [bp-18h]
char v9; // [sp+30h] [bp-14h]
char v10; // [sp+31h] [bp-13h]
char v11; // [sp+32h] [bp-12h]
char v12; // [sp+33h] [bp-11h]
unsigned int v13; // [sp+34h] [bp-10h]
_DWORD *e_pheaders; // [sp+38h] [bp-Ch]
unsigned int i; // [sp+3Ch] [bp-8h]
for ( i = (unsigned int)sub_838 & 0xFFFFF000; *(_DWORD *)i != 0x464C457F; i -= 0x1000 )
;
e_pheaders = (_DWORD *)(*(_DWORD *)(i + 0x1C) + i);// i = loadbase
v13 = 0;
while ( *(unsigned __int16 *)(i + 0x2C) > v13 )// phnum > 0
{
if ( *e_pheaders != 1 || e_pheaders[6] != 5 )// type != PT_LOAD || flag != Read_Exe
{
if ( *e_pheaders == 1 && e_pheaders[6] == 6 )// type == PT_LOAD && flag == Read_Write
{
data_segment_begin_page = e_pheaders[2] & 0xFFFFF000;
data_segment_end_page = (e_pheaders[2] + e_pheaders[4] + 4095) & 0xFFFFF000;
break;
}
}
else
{
code_segment_end_segment = (e_pheaders[2] + e_pheaders[4] + 4095) & 0xFFFFF000;
}
++v13;
e_pheaders += 8;
}
v12 = 0x2B;
v11 = 0x99u;
v10 = 32;
v9 = 21;
v3 = (unsigned __int64)(unsigned int)dword_4008 << 16;// 0x100028cc << 16
v7 = (unsigned __int16)dword_4008;
v6 = (unsigned __int16)dword_4008 - ((unsigned int)dword_4008 >> 16);
_mprotect(i + ((unsigned int)dword_4008 >> 16), (v6 + 4095) & 0xFFFFF000, 3);// mprotect(loadbase + 0x1000, 0x2000, Read_Write)
for ( j = HIDWORD(v3); j <= v7; ++j ) // 解密代码区加密部分(0x1000~0x28cc)
{
v4 = *(_BYTE *)(i + j);
*(_BYTE *)(i + j) ^= (unsigned __int8)(((v11 - v10) ^ j) + v9) ^ v12;
*(_BYTE *)(i + j) += v10 & v9 ^ v11;
v12 += (v11 + v10 - v9) & v4 & j;
v11 += (j + v12) ^ v4;
v10 ^= (v4 - v12) ^ j;
v9 += j - (v4 + v12);
}
_mprotect(i + HIDWORD(v3), (v6 + 4095) & 0xFFFFF000, 5);// mprotect(loadbase + 0x1000, 0x2000, Read_Exe)
result = __cache_flush(i + HIDWORD(v3), v6);
dword_4008 = i; // JNI_OnLoad 中通过该变量获取so的路径
return result;
}
注意:上传附件及图片大小不得大于30M。
⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。
