首页
社区
课程
招聘
[原创]DEFCON-2024-Quals nloads ida批处理反编译+正则 处理so混淆和提取表达式常量
2024-5-22 22:50 5054

[原创]DEFCON-2024-Quals nloads ida批处理反编译+正则 处理so混淆和提取表达式常量

2024-5-22 22:50
5054

DEFCON-2024-Quals nloads ida批处理反编译+正则 处理so混淆和提取表达式常量

刚刚改完毕业论文提交了,byd本科论文格式真操蛋;赛后把这题复现了一下,终于有空就整理了一下这题的方案


题目分析

题目基本情况

题目有数字命名从0开始到13612共13613个文件夹

每个文件夹里有一个名为beatme的64位elf可执行文件和若干个so

beatme中会加载so文件,并调用so的导出函数

so只有一个导出函数,并且有可能加载别的so并调用别的so的导出函数

每个beatme的代码都非常相似,都是encrypt(input, key)==secret?的问题;解题过程就是提取key,secret,实现encrypt的逆,然后反求input;最后各个beatme解出来的input按顺序组成一个jpeg文件

解题方向应该是自动化分析这些题目;或者因为encrypt都一样,只需实现decrypt,然后自动化提取key和secret(本文的解决方案,参考了大神Q7的解决方案),其中需要去除 表达式分散在so里 的混淆

so文件分析

如上所述so只有一个导出函数

so有两种类型,第一种是"直接型",比如output\0\AmiOWZLBXmVOGVXC.so的导出函数,直接是一条表达式:

__int64 __fastcall ZREWkEkEJCljQjvN(int a1, int a2)
{
 return (unsigned int)(a1 + a2);
}

第二种是"间接型",比如output\0\AOinIPkXvMtrtbha.so的导出函数,调用了别的so的导出函数:

__int64 __fastcall pnstlKQzXnehUbWP(unsigned int a1, unsigned int a2, unsigned int a3)
{
 void *v4; // rax
 void *v5; // rax

 v4 = dlopen("./WDhDHuuesoPtRCMX.so", 1);
 if ( v4 && (v5 = dlsym(v4, "XdpJmBLmALOHLqWC"), (wrapped_func = v5) != 0LL) )
   return ((__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD))v5)(a1, a2, a3);
 else
   return 0xFFFFFFFFLL;
}

从而呈现出一条:间接型so 调用 间接型so 调用 间接型so...最后调用直接型so的调用关系单向链

一个小细节是,不是所有so都会被使用到(包括直接型,有的也根本就没被执行过)

beatme的main函数分析

beatme的main函数首先读取8字节的输入

然后加载so并获取导出函数的地址,反编译出来的代码非常有规律,有些被调用过,ida会识别出类型为函数,有些没被调用ida会当成64位整型:

// output\0\beatme:main
// 被调用过的导出函数,ida识别出其为函数
 v3 = dlopen("./hESMAGmJLcobKdDM.so", 1);
 if ( !v3 )
   goto LABEL_41;
 qword_40A0 = (__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD))dlsym(v3, "vpUjPultKryajeRF");
 if ( !qword_40A0 )
   goto LABEL_41;
// 没被调用过的导出函数,ida识别其为64位整型
 v8 = dlopen("./oSeJlOQqzYFRkBXO.so", 1);
 if ( !v8 )
   goto LABEL_41;
 qword_4078 = (__int64)dlsym(v8, "tYUJnAiKvXEypybB");
 if ( !qword_4078 )
   goto LABEL_41;

最后多次调用一个加密函数(调用次数不是固定的),然后和目标进行对比

这是加密函数调用次数大于3次的ida反汇编:

// output\0\beatme:main
 v21 = 4; // 加密函数调用次数
 v24[0] = 0x11BC44D6247B0D72LL; // 原始key
 v24[1] = 0xA9B5A0DEDE1121C5LL; // 原始key
 do
 {
   sub_1670((unsigned int *)&buf, (unsigned int *)v24); // 调用加密函数
   --v21;
 }
 while ( v21 );
 if ( buf == 0x6DC5E11CB9C04362LL ) // 比对加密后的结果
 {
   puts(":)");
   return 0LL;
 }
 else
 {
LABEL_41:
   puts(":(");
   return 0xFFFFFFFFLL;
 }

但加密函数调用次数小于等于3次时,反编译结果有所不一样(猜测是编译优化把循环展开了,也可能是出题人故意的)

如果是使用ida的反编译,次数小于等于3次时,最后几个加载so获取导出函数的规律也有所变化

这是加密函数调用次数为1次的ida反汇编(前40个中,3、5、28都是这种情况):

// output\5\beatme:main
 v19 = dlopen("./jRetpolsrphrQPtF.so", 1);
 if ( v19
   && (qword_4020 = (__int64)dlsym(v19, "fLlwjsNFHveEXhbW")) != 0
   && (v20 = dlopen("./GHITvdoIgYolDeOt.so", 1)) != 0LL
   && (qword_4018 = (__int64)dlsym(v20, "hYiKvwpyPcByLGBL")) != 0
   && (v23[0] = 0x29D8E96D6458B149LL, v23[1] = 0xE5B375A8205A7EA0LL, sub_1660(&buf, v23), buf == 0xF1151196B7E3EEC1LL) )
 {
   puts(":)");
   return 0LL;
 }
 else
 {
LABEL_37:
   puts(":(");
   return 0xFFFFFFFFLL;
 }

这是加密函数调用次数为2次的ida反汇编(前40个中,8、12、36都是这种情况):

// output\8\beatme:main
 v19 = dlopen("./dXLsupdunzHSBdjV.so", 1);
 if ( !v19 )
   goto LABEL_39;
 qword_4020 = (__int64)dlsym(v19, "KlIIGXPuIRUJEvgE");
 if ( qword_4020
   && (v20 = dlopen("./gxDqevBaXMJekHMu.so", 1)) != 0LL
   && (qword_4018 = (__int64)dlsym(v20, "bkBLrwUXLBTMrIVr")) != 0
   && (v23[0] = 0x67C77F122DE22A1BLL,
       v23[1] = 0x86399F4E521A3DAELL,
       sub_1670(&buf, v23),
       sub_1670(&buf, v23),
       buf == 0xA5BEF5332059C1BELL) )
 {
   puts(":)");
   return 0LL;
 }
 else
 {
LABEL_39:
   puts(":(");
   return 0xFFFFFFFFLL;
 }

这是加密函数调用次数为3次的ida反汇编(前60个中,只有51是这种情况):

// output\51\beatme:main
 v20 = dlopen("./joVUJgIesOtuXwqZ.so", 1);
 if ( v20
   && (qword_4018 = (__int64)dlsym(v20, "XzocYrjvPODlZHPH")) != 0
   && (v23[0] = 0xEA455F54018BB47DLL,
       v23[1] = 0xCB8F0ED6858DB4F8LL,
       sub_1680(&buf, v23),
       sub_1680(&buf, v23),
       sub_1680(&buf, v23),
       buf == 0x81A1A5064491BBD4LL) )
 {
   puts(":)");
   return 0LL;
 }
 else
 {
LABEL_39:
   puts(":(");
   return 0xFFFFFFFFLL;
 }

beatme的加密函数

原本长这个样子:

// output\0\beatme:sub_1670
// a1:buf,用户输入的8字节内容
// a2:key
 v3 = *a1;
 v4 = a1[1];
 v5 = qword_4030(*a2, a2[1], 346850382LL);
 v6 = qword_4028(a2[1], a2[3], 9128242LL);
 v7 = qword_4020(a2[2], a2[4], 1916420973LL);
 v8 = 0;
 v44 = 16;
 v27 = qword_4018(a2[3], v5, 2064015388LL);
 do
 {
   v9 = qword_4088(v8, 2654435769LL, 353937606LL);
   v36 = (__int64 (__fastcall *)(_QWORD, _QWORD, __int64))qword_4040;
   // ...
   v12 = v36(v11, v40, 1456051295LL);
   // ...
   v25 = v44-- == 1;
   v4 = v24;
 }
 while ( !v25 );
 result = a1;
 *a1 = v3;
 a1[1] = v4;
 return result;

将qword_xxxx都替换成前面so文件分析中所说的直接型so里面的代码,去掉混淆

分析去掉混淆后的代码发现,key会被修改,然后是16*2=32轮tea,对比不同beatme的加密函数

只有修改key那四句代码里的8个常数会变:

// output\0\beatme:sub_1670 fix
// a1:buf,用户输入的8字节内容
// a2:key
 v3 = *a1;
 v4 = a1[1];
 v5 =  346850382LL ^ *a2 ^ 0x11824E8Au; // 修改key第0个DWORD
 v6 =  a2[1] ^ (unsigned int)(9128242LL + 1447939560); // 修改key第1个DWORD
 v7 =  (a2[2] + 1916420973LL) ^ 0x17FF8240u; // 修改key第2个DWORD
 v8 = 0;
 v44 = 16;
 v27 =  (a2[3] - 2064015388LL) ^ 0x3CC50BEBu; // 修改key第3个DWORD
 do
 {
   v9 =  (unsigned int)(v8 + 2654435769LL);
   v40 =  (unsigned int)((v4 >> 5) + v6);
   v28 =  (unsigned int)(v4 + v9 + 1);
   v10 =  (unsigned int)((16 * v4) + v5);
   v11 =  v28 ^ v10;
   v12 =  v40 ^ v11;
   v13 =  (unsigned int)(v3 + v12);
   v41 =  (unsigned int)((v13 >> 5) + v27);
   v29 =  (unsigned int)(v13 + v9);
   v14 =  (unsigned int)((16 * v13) + v7 + 1);
   v15 =  v29 ^ v14;
   v16 =  v41 ^ v15;
   v17 =  (unsigned int)(v4 + v16);
   v8 =  (unsigned int)(v9 + 2654435769LL);
   v42 =  (unsigned int)((v17 >> 5) + v6);
   v30 =  (unsigned int)(v17 + v8);
   v18 =  (unsigned int)((16 * v17) + v5);
   v19 =  v30 ^ v18;
   v20 =  v42 ^ v19;
   v3 =  (unsigned int)(v13 + v20);
   v43 =  (unsigned int)((v3 >> 5) + v27);
   v31 =  (unsigned int)(v3 + v8);
   v21 =  (unsigned int)((16 * v3) + v7);
   v22 =  v31 ^ v21;
   v23 =  v43 ^ v22;
   v24 =  (unsigned int)(v17 + v23);
   v25 = v44-- == 1;
   v4 = v24;
 }
 while ( !v25 );
 result = a1;
 *a1 = v3;
 a1[1] = v4;
 return result;

解题方案:ida批处理模式反编译+正则匹配

可行性

单线程情况下完整分析一个beatme大概19~20s;如果已经生成好i64并输出了反编译C代码,分析一个beatme只需要0.5~0.8s

多进程(python multiprocessing,processes=8;笔记本R7 4800H 8核16线程)分析一个beatme大概24~34s

考虑最坏情况34s一个,开32核,4小时解完全部:13613*34/32/3600=4h

主要时间花在ida批处理反编译上了

ida批处理模式

Igor’s tip of the week #08: Batch mode under the hood – Hex Rays (hex-rays.com)

用ida批处理模式可以输出指定函数的反编译到文件:

os.system('idat64.exe -Ohexrays:{filename}.{func}.c:{func} -A {pathname}')

但是有个很奇怪的问题,如果没有i64文件的情况下,第一次指定反编译main函数反编译结果会为空,似乎是反编译没有被执行,需要再重新指定反编译main一次

解决思路

  1. 获取so文件的调用单向链,遍历所有so文件:

    1. 根据是否导入dlopen和dlsym判断是否为"间接型"的so;

    2. 记录"间接型"so导入的so名和调用的导出函数名:

      1. 导入的so的名称和调用的导出函数的名称是相邻的,可通过正则匹配二进制内容找出。

  2. 反编译beatme的main函数(ida批处理反编译)得到main.c;

  3. 获取main函数中变量对应的加载的so和导出函数(正则匹配main.c内容)

  4. 获取main函数中调用加密函数相关信息(正则匹配main.c内容):

    1. 加密函数的名称(反编译加密函数时用于通过名称指定函数);

    2. 加密函数调用轮次;

    3. 原始key;

    4. secret;

    5. 注意:前面分析调用轮次有4种情况,每种情况都做一个正则的规则。

  5. 处理beatme的加密函数,反编译与去掉混淆得到sub_xxxx.c:

    1. 反编译beatme的加密函数(ida批处理反编译,通过函数名指定)得到sub_xxxx.c;

    2. 匹配sub_xxxx.c中所有v?? = qword_????,将v??替换成qword_????

    3. 根据 变量对应的加载的so和导出函数,找到sub_xxxx.c中每个qword_xxxx(p0, p1, p2)对应的(so名,函数名)和该函数调用的实参;

      接着去到qword_xxxx对应的"直接型"so的代码:

      1. 根据 调用单向链 找到最终的"直接型"(so名,函数名);

      2. 反编译"直接型"so的导出函数(ida批处理反编译)得到xxxx.so.xxxx.c;

      3. 获取xxxx.so.xxxx.c中的表达式并将形参替换成实参(正则)。

      将sub_xxxx.c中qword_xxxx(p0, p1, p2)替换成前面处理完的表达式。

  6. 正则匹配sub_xxxx.c中修改key的四条语句的常数,然后修改key;

  7. decrypt(加密函数调用轮次, secret, 修改后的key)得到输入。



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2024-5-22 22:57 被wx_御史神风编辑 ,原因:
收藏
免费 1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回