首页
社区
课程
招聘
ollvm混淆的基本处理方法:IDA如何不去混淆分析so文件?
发表于: 2024-3-27 07:33 5160

ollvm混淆的基本处理方法:IDA如何不去混淆分析so文件?

2024-3-27 07:33
5160

夏天来了,许久听不到蝉鸣,生活只剩下工作了,再这么下去感觉就要完蛋了。

Android Java层的混淆在一些风控安全类SDK比较常见,严格来说,一般对Java层逆向得到的代码都失去了原先的语义了,如命名为abc,这也属于混淆了。这个就不展开讨论了,但是这涉及一个重要的方法,静态分析非常依赖该方法,不少人都已经有用到了。IDA针对汇编的静态分析同样如此,常见的例如定义结构体。

ollvm混淆和Java层有一些不同,如F5之后默认都是混淆了符号的,ollvm打乱了原先的代码编写顺序,或者增加了一些虚假的代码块。下面我们来探讨一些IDA分析的问题,以及如何在ollvm混淆的情况下分析代码,这不是处理ollvm混淆高效的方法,这是为了保证至少能够手动处理ollvm混淆。就如处理花指令一样,很多手动处理一下就行了。简单来说,既然它都混淆了,就按它现在的样子来处理吧。

IDA静态分析的重心

不少人谈到F5之后的伪代码都不怎么重视,认为简单参考就行。我以前试过不使用F5(因为确实不怎么准确)根据汇编指令手动还原一个简单的HEX算法,首先就是符号标记的问题,对于某个变量很容易就跟丢了,如果有多条件嵌套的会更痛苦;对比下来,F5就是最优的选择,但是也要清楚它是不正确的,有些时候还得必须去对着汇编进行还原才行。现在我会以F5后的代码为核心,它不需要完全准确,只需要不脱离对应函数的意思就足够了,而且我认为整个静态分析都应该以F5为重心。
第二就是汇编,如果F5不正确,就要回去分析汇编了。汇编指令比较原子化,这就为什么用F5,但终究还是要会分析汇编才行。我想这得会一目十行,也不是真的十行,只是汇编上往往需要好几个指令来对应一般编程语言的最简单的操作。
之所以说把重心放到F5上,那是为了标准化IDA静态分析,IDA的静态分析方法和技巧很多,如果我们把所有这些处理的目的就是为了得到更加准确的F5,那么我们就知道自己的静态分析的方向是什么了。例如花指令,如call analysis failed等,不过即使没有这些问题,按下F5也是不能解决问题的,还得继续进行处理,不得不说,这个工作比一般java的繁杂很多。

搭建IDA动态调试

具体步骤就不说了,可以参考这篇文章:https://bbs.kanxue.com/thread-259633.htm。
其中我想说的是IDA动态调试so的一些问题,首先是版本的问题,IDA版本<=7.0我试过无法在Android 10上调试,例如会出现jdb无法附加到目标VM。针对这个问题,建议使用高版本的IDA或者低版本的Android(后面这种没试过)。
我目前用的IDA 8.3会出现F5卡死,但是用低版本IDA F5又没问题,完蛋,目前不知道怎么解决,有遇到的朋友可以说下。

IDA动态调试如何下断点?

对于这个,我之前还花了几天研究,在我遇到的情况中,在原so位置下断点不一定有效。另外怎么更轻便处理断点也是个问题,有的时候既要patch、又要更改寄存器的值,加的断点多了——因为需要不断重复启动调试,F9按多了也是个痛苦的体力活,当然也需要配合python脚本进行处理,不过这个先不多说了。
在目前的样本中,需要给.init下断点,需要分析的函数就是init_proc。

分析so文件的ollvm混淆

首先启动IDA调试,一直按F9进入init函数中,该函数看起来是这样的:

这种嵌套的while循环就是ollvm混淆后的效果,但是在这之前,我先明确一个关键点。

对ollvm混淆的整体阅读

假设有一个C函数func,ollvm混淆是将func按顺序切分成n个块,然后打乱n个块的编写顺序,使用一个动态的整数标记(如上截图的v0)来调节乱序的块的执行顺序,也就是说这个整数标记使得func的乱序版本的执行顺序和原func的执行顺序是一样的。
例如上面的v0初始值是0xA0BA64CE,当进入while循环后,它一般就会到达v0==0xA0BA64CE的地方进行处理(一个块),处理完成后再更改v0的值(下一个块的标记),直至函数执行完毕,当然也会有一些变化,但是大体就是这样。
如果我们手动把这些块连起来,那就还原了该函数了。对于这个整数标记,简单的情形也可以静态分析的,但是如果while循环比较多,那就比较麻烦了,所以这里为什么使用IDA动态调试,其中一个作用就是真实的执行能直接知道具体的代码块的顺序。当然,这其实还是因为IDA F5预览不够完善,如果它能够近似一个编辑器就好了,例如VSCode,如能查找折叠等。
如上面的v0到v0 > (int)0xB945DFF9这个判断就头痛了,后面的else-if和else在哪里?如果有代码折叠,那就清晰方便很多。

手动还原每个代码块

当v0==0xA0BA64CE,执行的第一个代码块是:

StatusReg一般不需要分析,这不会影响当前函数的的主要逻辑。但是按X发现,v5在后面会使用到——可以判断它是一个函数栈内的变量。在IDA F5中,一个栈内的变量可能会直接被声明,甚至不赋值,如果是结构体可能会分散声明或赋值,这时就需要看汇编手动处理了。
v5后面是这样使用的:

那么我们可以判断v5是一个数组,char*类型的字节数组,用于保存一个字符串。
现在我们可以先命名一下v0为tick,然后将第一个代码块的代码复制到一个文本或编辑器中,最后再该代码旁边添加一个注释:tick 1
v5 = &StatusReg + -0xFA;
接下来按F8跟踪每一个代码就行,那么下一个是:

其中一个块出现了反调试,那就得先nop掉这行再进行调试。简单处理后的伪代码如下,其中右侧注解标记了代码块的执行顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
void init_proc()
{
  int tick; // w8
  const char *v1; // x23
  __int64 v2; // x0
  unsigned int v3; // w0
  unsigned __int64 StatusReg; // [xsp+0h] [xbp-870h] BYREF
  void *cmdline_content; // [xsp+8h] [xbp-868h]
  bool v6; // [xsp+17h] [xbp-859h]
  int v7; // [xsp+18h] [xbp-858h]
  int v8; // [xsp+1Ch] [xbp-854h]
  void *cmdline_content2; // [xsp+20h] [xbp-850h]
  char *v10; // [xsp+28h] [xbp-848h]
  char *v11; // [xsp+30h] [xbp-840h]
  FILE *v12; // [xsp+38h] [xbp-838h]
  char v13[2000]; // [xsp+40h] [xbp-830h] BYREF
  __int64 v14; // [xsp+810h] [xbp-60h]
 
  StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 0xD, 0, 2));
  v14 = *(StatusReg + 40);
  tick = 0xA0BA64CE;
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          while ( tick <= 0xE3BA312 )
          {
            if ( tick > 0xB945DFF9 )
            {
              if ( tick <= 0xDE366C1C )
              {
                if ( tick == 0xB945DFFA )
                {
                  tick = 0x19437FAB;
                }
                else if ( (sub_760A0EFA48() & 1) != 0 )
                {
                  tick = 0x6B8494DB;
                }
                else
                {
                  tick = 0x5B4C847C;
                }
              }
              else if ( tick == -566858723 )
              {
                if ( v8 )
                  tick = 0x4F03BFB3;
                else
                  tick = -1706423140;
              }
              else if ( tick == -195526736 )
              {
                v7 = sub_760A0D6830();          // tick 13
                tick = 33264006;
              }
              else if ( v7 == 1 )
              {
                tick = -1960142196;
              }
              else
              {
                tick = 0x4F03BFB3;
              }
            }
            else if ( tick <= 0x9C5EB96E )
            {
              if ( tick == -1960142196 )
              {
                v8 = sub_760A0D35C8();          // tick 14
                tick = -566858723;
              }
              else
              {
                tick = 1803850971;
              }
            }
            else if ( tick == 0x9C5EB96F )
            {
              if ( v6 )                         // tick 5
                tick = 0x76541A4F;
              else
                tick = 0xBB1E2113;
            }                                   // tick 4
            else if ( tick == 0x9F46953A )
            {
              v6 = *off_760A111FB8 > 23;
              tick = -1671513745;
            }
            else
            {
              cmdline_content = &StatusReg + -0xFA;// tick 1
              tick = 0xE3BA313;
            }
          }
          if ( tick > 1325645746 )
            break;
          if ( tick <= 423853994 )
          {
            if ( tick == 238789395 )
            {
              *off_760A111FB8 = sub_760A0DC3F0();// tick 2
              sub_760A0DC550();
              tick = 371708096;
            }
            else
            {
              sub_760A0DC440();                 // tick 3
              tick = -1622764230;
            }
          }
          else if ( tick == 0x19437FAB )
          {
            v2 = sub_760A0DD728();              // tick 12
            sub_760A0EDAD4(v2);
            tick = -195526736;
          }
          else if ( tick == 0x23D89166 )
          {
            memset(v11, 0, 0x7D0uLL);           // tick 8
            v3 = getpid();
            _sprintf_chk(v13, 0LL, 2000LL, "/proc/%d/cmdline", v3);
            v12 = fopen(v13, "r");
            if ( v12 )
              tick = 0x65C6D61D;                // tick 9
            else
              tick = 0x19437FAB;
          }
          else
          {
            sub_760A0E5EC4();                   // tick 11
            tick = -1186603014;
          }
        }
        if ( tick > 1803850970 )
          break;
        if ( tick == 0x4F03BFB3 )
        {
          sub_760A0D3150();
          tick = -1706423140;
        }
        else if ( tick == 1531741308 )
        {
          v11 = v13;                            // tick 7
          tick = 601395558;
        }
        else
        {
          cmdline_content2 = cmdline_content;   // tick 10
          memset(cmdline_content, 0, 0x7D0uLL);
          v1 = cmdline_content;
          fscanf(v12, "%s", cmdline_content);
          fclose(v12);
          v10 = strchr(v1, 58);
          tick = 2139705484;
        }
      }
      if ( tick != 1985223247 )
        break;
      *off_760A111ED8 = 1;                      // tick 6
      tick = -1155653357;
    }
    if ( tick != 2139705484 )
      break;
    if ( v10 )
      tick = -1186603014;
    else
      tick = 965695107;
  }
}

这里就不分析函数的具体作用了,就是跟踪标记一下代码块的序号,不算难。其实这里面有一些反调试的,目前忙,时间原因也不处理了。按照标记好的序号,将对应的代码块拼接起来就约等于原来的代码了。
不过,和阅读分析汇编代码一样,由于ollvm混淆比较常见,个人的目的主要是理清楚ollvm混淆后的代码和原代码的区别,这样我们看这种混淆代码的时候也能够一目十行,大概知道它的执行逻辑。
本来想把这部分写得更详细的,暂时没时间了,就先这样先。

总结

这严格来说都不算一个方法,甚至比较耗时,如果可以建议还是直接反混淆处理。但是熟悉它的结构可以让我们马上可以找到真实代码块,掌握整体的函数的逻辑,甚至不用反混淆就可以分析算法了,如果是还原算法,也不影响。至于F5恒常会出错的那一部分——即使完美反混淆也没用,那这部分我们还得自己阅读分析汇编进行处理。毕竟,要时刻记住,即使没有混淆,F5出错的概率也是恒常存在的。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (7)
雪    币: 3004
活跃值: (30866)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-3-27 09:29
1
雪    币: 1229
活跃值: (1760)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
ollvm 块不多的话 你这个标记 方法 原来我也用过,挺好用的感谢分享,而且手修起来挺虚浮的,就怕遇到偏移跳转表的手修起来费死劲了
2024-3-27 09:59
0
雪    币: 251
活跃值: (503)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2024-3-27 10:57
0
雪    币: 435
活跃值: (2641)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
5
补充一下,如果追踪路径有分支未覆盖,可能会导致一些分析误差,当然这是另一个范围的问题了。
2024-3-28 10:55
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
有没有demo啊,跟着做个试试
2024-4-15 15:36
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
支持一下
2024-4-15 16:24
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
你这是放弃反混淆了啊。。
2024-5-27 21:05
0
游客
登录 | 注册 方可回帖
返回
//