使用Binary Ninja去除ollvm流程平坦混淆
2019-12-09 16:35:46 Author: bbs.pediy.com(查看原文) 阅读量:506 收藏

流程平坦化混淆是ollvm里面最难还原,最影响分析效率的一个混淆pass,最近花了点时间,终于把它搞掉了.
刚开始是打算使用IDA的微码进行反混淆,搜到了Rolf Rolles大佬的HexRaysDeob,
还有它的Python版本PyHexRaysDeob.
研究了下发现微码API确实是太难用了,而且很多分析好像都得自己做,所以我暂时放弃该方案.
随后转到了binja, 发现这篇文章Dissecting LLVM Obfuscator Part 1,貌似简单可行,修改了下,用无名侠大佬基于Unicorn 的ARM64 OLLVM反混淆帖子的样本跑,失败了.
看来还是得自己干.

下文中反混淆用的样本是无名侠的libvdog.so(vdog)和自己编译的另外一个小程序(cff).

反混淆主要分两步:分析和修复.

分析

这里思路源于llvm-defobfuscator,熟悉它的话可以直接跳到后面修复内容.

流程平坦混淆代码结构:

...
state_var = n;
while(1) {
    switch (state_var) {
        case x:
            code_region_x
        case y:
            code_region_y
        case z:
            code_region_z
        case ...
    }
}

反混淆需要的信息主要有:

  • switch语句各case的入口, 出口基本块.即上面各个case对应的代码区域(code_region_xxx).
  • case编号和case入口对应关系
  • 各case出口的跳转目标case.对于条件跳转,我们还得知道真假分支和目标case的对应关系.

这些信息通过binja还是很容易分析出来的.
vdog中的代码:

  43 @ 0007067c  bool cond:4_1 = x9.w9 != 0x76579ace
  44 @ 00070680  uint64_t x8_1 = zx.q(x9.w9)
  45 @ 00070684  if (cond:4_1) then 10 @ 0x704f0 else 47 @ 0x70688

我们可以看到,最后一条指令是状态变量与0x76579ace进行比较,相等的情况下走0x70688(假分支),这个地址就是case 0x76579aced对应代码区域的起始地址.

 423 @ 000710c0  if (x8_26.w8 == 0xdcf244e1) then 439 @ 0x71080 else 444 @ 0x710c4

对于这个测试, 0xdcf244e1对应的case 入口是0x71080(真分支)

用这个方式来获取case代码区域的起始地址是比较可靠的.
因为llvm在给switch生成代码时,据我观察,在无法使用跳转表的情况下,会转到使用二分查找实现,查找到最后一般都会有这么一条测试case编号的指令.

找到case出口也比较简单:包含对状态变量进行更新基本块的出口就是case出口.

 104 @ 000707bc  aop_init()
 105 @ 000707c4  uint64_t x8_1 = 0xc88d21cd
 106 @ 000707c8  goto 10 @ 0x704f0

实际上,现实代码中会存在有多个出口的情况,这个问题在修复的时候再讨论.

如果是条件跳转,我们还需要知道跳转目标与测试条件的对应关系.

 134 @ 00070854  bool cond:12_1 = x0_4.w0 != 0
 135 @ 00070868  if (cond:12_1) then 144 else 146
 ---
 144 @ 00070868  uint64_t x8_1 = 0x91c5439 ; 真分支
 145 @ 00070868  goto 155 @ 0x7086c ; 跳转到分发器
 ---
 146 @ 00070868  uint64_t x8_1 = 0xf291f0e9 ; 假分支
 147 @ 00070868  goto 155 @ 0x7086c ; 跳转到分发器

case 0x91c5439 对应的条件可以通过查看入口边的类型得到

def resolve_branch_condition(state):
    il_bb = state.il_basic_block
    assert len(il_bb.incoming_edges) <= 1
    if len(il_bb.incoming_edges) == 1:
        return il_bb.incoming_edges[0].type == BranchType.TrueBranch
    else:
        return True

到这反混淆所需信息就完全获取到了.

以上我主要说明了思路, 省略了很多细节.

例如cff中下面这种case

  62 @ 00000814  bool cond:5_1 = x19_1.w19 == 0xcb0c4ceb
  63 @ 0000081c  uint64_t x7_1 = 0xd40c7864
  64 @ 00000820  uint64_t x20 = zx.q(x1.w1)
  65 @ 00000824  if (cond:5_1) then 16 @ 0x768 else 76 @ 0x828

这个基本块既是case入口,又是出口,还是分发器代码的一部分.

vdog JNI_OnLoad里面的一个case:

 313 @ 00070e54  bool cond:60_1 = x9.w9 != x8_22.w8
 314 @ 00070e58  uint64_t x8_1 = zx.q(x9.w9)
 315 @ 00070e5c  if (cond:60_1) then 10 @ 0x704f0 else 320 @ 0x70e64

313处, 在分发器里面,状态变量(x9.w9)跟另外一个变量比较(x8_22.w8), 这时候x8_22.w8的值是多少?
上面两个问题我在代码里面都有处理,有兴趣可以参考代码.
即使没有处理也没有关系,我反混淆的修复是保守的,对于无法处理的情况,仍然会走原来逻辑,只是最终反混淆效果会差一点.

下面开始修复部分内容,修复难度在我看来要比分析难. 修复cff这个看起来简单的样本要比vdog难.

代码修复主要有两个难点:

  • patch二进制
  • 处理编译器优化移动的代码

patch二进制

修复时,在代码中寻找用来patch代码空洞还是比较困难的.
之前考虑使用IDA微码进行反混淆,这是最主要的原因.
llvm-deobfuscator是把混淆框架代码作为空洞使用.
而我反混淆目标是为了在IDA中, 使用他的反编译器进行静态分析, 不需要创建一个新的可执行的二进制, 所有我是用IDA创建一个新的代码段来存放patch代码.
这种处理方法能让我保留原来的分发器,有意的保留分发器, 可以在未识别到真实后继基本块或者case出入口的情况下,走原来分发器流程.
如果所有状态的后继都被我处理了, 分发器就变成不可达代码, IDA的反编译器会自动把它优化掉.

处理编译器优化移动的代码

目前看到的反混淆文章好像都没有提到如何处理这个问题的.
如下函数:

void foo1()
{
    int state_var = 0;
    while(1) {
        switch (state_var) {
            case 0: {
                        puts("enter case 0");
                        state_var = 1;
                        break;
                    }
            case 1: {
                        puts("enter case 1 or 2");
                        puts("enter case 1");
                        state_var = 2;
                        puts("exit case 1 or 2");
                        break;
                    }
            case 2: {
                        puts("enter case 1 or 2");
                        puts("enter case 2");
                        state_var = 3;
                        puts("exit case 1 or 2");
                        break;
                    }
            case 3: {
                        puts("enter case 3");
                        state_var = 4;
                        break;
                    }
        }
        if (state_var == 4) {
            break;
        }
    }
}

有可能会被编译器优化成:

void foo2()
{
    int state_var = 0;
    while(1) {
        switch (state_var) { // 分发器入口
            case 0: {
                        puts("enter case 0");
                        state_var = 1;
                        break;
                    }
            case 1: case 2: {
                           puts("enter case 1 or 2"); // 1, 如何处理这部分代码?
                           if (state_var == 1) {
                               puts("enter case 1");  // case 1 入口
                               state_var = 2;
                           } else {
                               puts("enter case 2");  // 2, 用上文提到的方法只能识别到case 1的入口, 怎么确定这是case 2的入口?
                               state_var = 3;
                           }
                           puts("exit case 1 or 2");  // 3, 如何处理这部分代码?
                           break;
                       }
            case 3: {
                        puts("enter case 3");
                        state_var = 4;
                        break;
                    }
        }
        if (state_var == 4) {
            break;
        }
    }
}

我解决的办法是通过代码拷贝,把foo2转换成foo1.具体做法是

  • 拷贝从分发器入口,到case入口的所有代码到patch代码区域,并在执行case代码前,执行这些复制过来的指令.
  • 同样的,在离开case时, 拷贝case出口到分发器入口所有代码到patch代码区域,并在执行后继case代码前,执行这些复制过来的指令.

问题2, 属于分析阶段的问题, 我现在还没有处理.
当前反混淆方案会在进入case 1之后, 由于state 2对应case 2的入口没有识别到, 控制流会重新进入分发器, 从分发器进入case 2.

现实代码中,还会出现下面的情况

CMP             W9, #0
CSET            W9, GT
EOR             W8, W9, W8
TBNZ            W8, #0, to_dispatcher ; 出口1
CBZ             W0, to_dispatcher     ; 出口2
...
CMP             W8, #0x30 ; '0'
MOV             W8, #0xdeadbeef     ; next case
CSEL            W24, W24, W8, EQ
B               to_dispatcher       ; 出口3

case有多个出口情况,伪码:

...
while(1) {
    switch (state_var) {
    ....
        case x: {
                    state_var = next_case; // 设置跳转目标
                    // 真实后继case只有一个, 但这个case有多个出口
                    if (cond) {
                        puts("exit 1");
                        break;
                    }
                    puts("exit 2");
                    break;
                }
    ....
}

为了处理这种代码, 我们在修复的时候, 需要找到case的所有出口,在每个出口处都进行patch.

综上,我们需要进行如下修复操作:

  • 找到case的所有出口
  • patch case 的出口, 使其跳转到我们的path代码
  • 拷贝patch case 出口时覆盖掉的指令到patch代码区域
  • 拷贝case出口到分发器入口所有代码到patch代码区域
  • 拷贝从分发器入口,到case入口的所有代码到patch代码区域
  • 使用汇编器,生成从patch代码到真实目标跳转指令

修复后的代码有如下两种布局.

  • 只有一个跳转目标的代码
;原始代码
tbnz w8,  #0, to_dispatcher ; 出口1
cont:
cbz  w0, to_dispatcher     ; 出口2
;修复出口1后的代码
b   patch_code ;  跳转到我们的patch代码
cont:
cbz w0, to_dispatcher     ; 出口2

; patch 代码区域
patch_code:
tbnz w8, #0, to_case_entry ; 准备进入真实目标
b cont ; 返回
to_case_entry:
patch时覆盖掉的指令
离开当前基本块到进入分发器的所有指令
离开分发器到目标case的所有指令
跳转到目标case
  • 有两个跳转目标的代码
这个基本块位于cff 0x00000798
csel    w7, w15, w14, eq
mov     w20, w0
b       to_dispatcher
cset    w7, eq ; !!!不能在此直接跳转到patch代码, 下面的mov指令是活跃的!!!
mov     w20, w0
b       patch_code

; patch代码区域
patch_code:
cbnz    w7, true_branch (0x8)
cbz     w7, false_branch

true_branch:
patch时覆盖掉的指令
离开当前基本块到进入分发器的所有指令
离开分发器到真分支入口的所有指令
跳转到真分支


false_branch:
patch时覆盖掉的指令
离开当前基本块到进入分发器的所有指令
离开分发器到假分支的所有指令
跳转到假分支

下面是两个具体的修复实例.

  • 修复cff函数入口跳转到真实目标

cff样本的分发器位于0x00000768
分发器入口代码

MOV             W0, W20 ; W0活跃
MOV             W19, W7
CMP             W19, W10

这个块内的W0是函数返回值,不可以删除这条指令.
从函数入口到真实后继的修复代码:

loc_4000000
MOV             W0, W20 ; 复制的dispatcher代码
MOV             W19, W7
CMP             W19, W10

CMP             W19, W11 
CMP             W19, W8
MOV             W7, W19
MOV             W20, W0
B               loc_7D0 ; 0x2495548b 的后继

可以看到,修复代码复制了分发器到case入口路径上的所有指令.

  • 修复vdog

再来看下vdog中起始地址为0x000704dc的基本块

STR             W9, [X19,#0x1C]
B               loc_704F0 ; 跳转到分发器

从函数入口到真实后继的修复代码:

loc_4000000

STR             W9, [X19,#0x1C] ; 0x000704dc基本块指令

MOV             W9, W8 ; 分发器指令
CMP             W9, W27
CMP             W9, W23
CMP             W9, W26
CMP             W9, W20
MOV             W8, #0x76579ACD
CMP             W9, W8
...
MOV             W8, #0x667472C5
CMP             W9, W8
MOV             W8, W9
B               loc_707E0 ; 0x667472c5对应的case入口

0x000704dc基本块另外一个前驱

loc_704CC
LDR             X8, [X19,#0x80]
MOV             W8, #0x6FA40FC9
LDR             W9, [X19,#0x18] ; 该指令将被覆盖
; 下落到0x000704dc
STR             W9, [X19,#0x1C]
B               loc_704F0 ; 跳转到分发器

修复后:

loc_704CC
LDR             X8, [X19,#0x80]
MOV             W8, #0x6FA40FC9
B               loc_4006800 ; 跳转到修复代码, 这里覆盖掉了原始指令 LDR W9, [X19,#0x18]
loc_4006800
LDR             W9, [X19,#0x18] ; loc_704CC被覆盖的指令

STR             W9, [X19,#0x1C] ; 0x000704dc基本块指令

MOV             W9, W8          ; 复制分发器代码
CMP             W9, W27
CMP             W9, W23
CMP             W9, W26
CMP             W9, W20
MOV             W8, #0x76579ACD
CMP             W9, W8
...
MOV             W8, #0x6FA40FC9
CMP             W9, W8
MOV             W8, W9
B               loc_70778 ; 0x6fa40fc9的后继

改进

  • 拷贝指令时,过滤死代码

当前实现拷贝了大量的垃圾指令,有时候函数过大会导致IDA无法反编译;同时也会增加IDA分析用时.修复vdog JNI_OnLoad大概用了21K字节代码,还原用时15分.

  • 拷贝时修复pc指令

拷贝pc相关指令,如函数调用,全局变量引用,需要进行重定位修复.

  • 自动查找状态变量和分发器

vdog的JNI_OnLoad里面有6个分发器,不知道是改了ollvm还是inline进来的.

参考资源

附件包含所有的代码和样本.
所有代码和样本也可从github下载: ollvm-breaker
另外, 反混淆脚本是在binja GUI-less模式下运行, 需要Binary Ninja商业版.

最后,放效果.

在IDA中打开cff或vdog后,运行对应的fix-xxx.py脚本反混淆.
我修复了vdog五个函数,JNI_OnLoad,crazy::GetPackageName,prevent_attach_one,attach_thread_scn,crazy::CheckDex.在IDA中运行修复脚本,重新分析分析程序后,可以查看这些函数的反混淆效果.

先看cff的原始代码

unsigned int target_function(unsigned int n)
__attribute((__annotate__(("fla"))))
__attribute((noinline))
{
  unsigned int mod = n % 4;
  unsigned int result = 0;

  if (mod == 0) result = (n | 0xBAAAD0BF) * (2 ^ n);

  else if (mod == 1) result = (n & 0xBAAAD0BF) * (3 + n);

  else if (mod == 2) result = (n ^ 0xBAAAD0BF) * (4 | n);

  else result = (n + 0xBAAAD0BF) * (5 & n);

  return result;
}

还原后

unsigned int __fastcall target_function(unsigned int n)
{
  unsigned int v2; // [xsp+Ch] [xbp-14h]

  v2 = n & 3;
  if ( (n & 3) == 0 )
    return (n | 0xBAAAD0BF) * (n ^ 2);
  if ( v2 == 1 )
    return (n & 0xBAAAD0BF) * (n + 3);
  if ( v2 == 2 )
    return (n ^ 0xBAAAD0BF) * (n | 4);
  return (n - 1163210561) * (n & 5);
}

vdog的JNI_OnLoad还原后

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  int v2; // w9
  _JNIEnv *v3; // x1
  _BOOL4 v5; // w8
  crazy *v6; // x0
  int v7; // w8
  crazy *v8; // x0
  _JNIEnv *v9; // x1
  const char *v10; // x0
  _JNIEnv *v12; // x1
  crazy *v13; // x0
  char *v14; // x0
  const char *v15; // x0
  crazy *v16; // x0
  _JNIEnv *v17; // x1
  int v18; // w0
  crazy *v19; // x8
  unsigned int v20; // w0
  crazy *v21; // x0
  __int64 v23; // [xsp-FD0h] [xbp-1240h]
  __int64 v24; // [xsp-FC0h] [xbp-1230h]
  __int64 v25; // [xsp-FA0h] [xbp-1210h]
  __int64 v26; // [xsp-7D0h] [xbp-A40h]
  char *v27; // [xsp+8h] [xbp-268h]
  void *v28; // [xsp+10h] [xbp-260h]
  int v29; // [xsp+18h] [xbp-258h]
  int v30; // [xsp+1Ch] [xbp-254h]
  JavaVM *v31; // [xsp+20h] [xbp-250h]
  __int64 *v32; // [xsp+28h] [xbp-248h]
  __int64 *v33; // [xsp+30h] [xbp-240h]
  crazy::String *v34; // [xsp+38h] [xbp-238h]
  void (__fastcall **v35)(JavaVM *, void *); // [xsp+40h] [xbp-230h]
  const char *v36; // [xsp+48h] [xbp-228h]
  int v37; // [xsp+54h] [xbp-21Ch]
  int v38; // [xsp+58h] [xbp-218h]
  int v39; // [xsp+5Ch] [xbp-214h]
  char *v40; // [xsp+60h] [xbp-210h]
  __int64 *v41; // [xsp+68h] [xbp-208h]
  const char *v42; // [xsp+70h] [xbp-200h]
  int v43; // [xsp+7Ch] [xbp-1F4h]
  __int64 *v44; // [xsp+80h] [xbp-1F0h]
  FILE *v45; // [xsp+88h] [xbp-1E8h]
  int v46; // [xsp+94h] [xbp-1DCh]
  crazy *v47; // [xsp+98h] [xbp-1D8h]
  __int64 v48; // [xsp+A0h] [xbp-1D0h]
  int v49; // [xsp+A8h] [xbp-1C8h]
  int v50; // [xsp+ACh] [xbp-1C4h]
  crazy *v51; // [xsp+B0h] [xbp-1C0h]
  char *v52; // [xsp+B8h] [xbp-1B8h]
  crazy *v53; // [xsp+C0h] [xbp-1B0h]
  crazy *v54; // [xsp+C8h] [xbp-1A8h]
  _QWORD v55[3]; // [xsp+D0h] [xbp-1A0h]
  _QWORD *v56; // [xsp+E8h] [xbp-188h]
  const char *v57; // [xsp+F0h] [xbp-180h]
  char *v58; // [xsp+F8h] [xbp-178h]
  int v59; // [xsp+104h] [xbp-16Ch]
  const char *v60; // [xsp+108h] [xbp-168h]
  JavaVM *v61; // [xsp+110h] [xbp-160h]
  _QWORD v62[32]; // [xsp+118h] [xbp-158h]
  __int64 v63; // [xsp+218h] [xbp-58h]

  v28 = reserved;
  v31 = vm;
  v63 = *(_QWORD *)off_DFF90;
  v29 = v2;
  v30 = v2;
  v32 = &v26;
  v33 = &v25;
  v34 = (crazy::String *)&v24;
  v35 = (void (__fastcall **)(JavaVM *, void *))&v23;
  v54 = 0LL;
  v61 = vm;
  v55[0] = (char *)*vm + 48;
  v62[0] = *(_QWORD *)v55[0];
  if ( ((unsigned int (__fastcall *)(JavaVM *, crazy **, __int64))v62[0])(vm, &v54, 65540LL) != 0 )
    return -1;
  v60 = (const char *)v54;
  v61 = *(JavaVM **)v54;
  v55[0] = v61 + 219;
  v62[0] = v61[219];
  ((void (*)(void))v62[0])();
  v16 = v54;
  *off_DFFF8 = (__int64)v54;
  v18 = crazy::GetApiLevel(v16, v17);
  v19 = v54;
  *off_DFF18 = v18;
  v47 = v19;
  v10 = (const char *)crazy::GetPlatformVersion(v19, v9);
  if ( strchr(v10, 77) != 0LL )
    *off_DFF18 = 23;
  v38 = *off_DFF18;
  if ( v38 > 23 )
    *off_DFEE8 = 1;
  v49 = sub_2E738();
  if ( v49 == 2 )
  {
    v53 = v54;
    v7 = sub_76F60(v53) & 1 ? -1618151004 : -990688990;
    if ( v7 > -990688991 )
      return -1;
  }
  v44 = v32;
  memset(v32, 0, 0x7D0u);
  v27 = (char *)v32;
  v20 = getpid();
  sprintf(v27, "/proc/%d/cmdline", v20);
  v45 = fopen(v27, "r");
  if ( v45 != 0LL )
  {
    v41 = v33;
    memset(v33, 0, 0x7D0u);
    v42 = (const char *)v33;
    fscanf(v45, "%s", v33);
    fclose(v45);
    v5 = strchr(v42, 58) == 0LL;
    if ( v5 || (v52 = strstr(v42, "sg.bigo.enterprise.live:service"), v52 != 0LL) )
      anti_debug_start();
  }
  v43 = *off_DFF18;
  if ( v43 == 15 )
    j_aop_init();
  anti_section_hook();
  v13 = (crazy *)crazy::checkSignature_1(v54, v12);
  if ( ((unsigned __int8)v13 & 1) == 0 )
    crazy::AbortProcess(v13);
  v14 = (char *)sub_2E998();
  v40 = v14;
  if ( *v14 )
  {
    crazy::GetPackageName((crazy *)v14);
    v62[0] = v34;
    v36 = *(const char **)v34;
    v6 = (crazy *)strcmp(v36, v40);
    v37 = (int)v6;
    if ( v37 != 0 )
      crazy::AbortProcess(v6);
    crazy::String::~String(v34);
  }
  v46 = sub_2E738();
  if ( v46 == 1 )
  {
    v50 = sub_F688();
    if ( v50 == 0 )
      return -1;
  }
  v51 = v54;
  v8 = (crazy *)crazy::checkdex_1(v54, v3);
  if ( ((unsigned __int8)v8 & 1) == 0 )
    crazy::AbortProcess(v8);
  v39 = sub_E7EC(*off_DFE98[0], "JNI_OnLoad", v35);
  if ( v39 != 0 )
    (*v35)(v31, v28);
  v15 = (const char *)sub_2E728();
  v48 = strlen(v15);
  if ( v48 != 0 )
  {
    v56 = v62;
    v21 = (crazy *)memset(v62, 0, sizeof(v62));
    crazy::GetPackageName(v21);
    v61 = (JavaVM *)v55;
    v57 = (const char *)v55[0];
    crazy::String::~String((crazy::String *)v55);
    v58 = (char *)v62;
    v60 = (const char *)sub_2E728();
    sprintf(v58, "/data/data/%s/.hide/%s", v57, v60);
    v59 = remove(v58);
  }
  return 65540;
}

[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com


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