首页
社区
课程
招聘
[原创] 自写简易Arm64模拟执行去除控制流平坦化
发表于: 2天前 2337

[原创] 自写简易Arm64模拟执行去除控制流平坦化

2天前
2337

为什么要自写模拟执行?

现在比较流行的反控制流平坦化 一般由Unidbg、 frida Stalker、这种runtime下线性的记录块的信息来完成patch, 此种方式有优势,也有缺陷。比如对else分支无法记录,如果手动干预else分支 那么就需要在上下文下补环境,简易的分支下还算较为简单,但是复杂的情况下,实现难度非常大(其实是地狱式的级别)。于是在猜想能不能实现一个基于特征的模拟执行,由于之前写过类似IDA的F5汇编解析,也手写过伪模拟执行的仿真器,所以写一个基于ollvm特征的模拟执行难度也不大,于是就开始了开发之路。。

先附上源码链接:AntiOllvm

功能

这是一个基于函数级别的模拟执行,并不分析整个So,也不需要补环境,基于IDA的CFG信息生成并且自动patch所有的真实块。但是需要对主分发块、次分发块进行标识(仅此而已,你并不需要对汇编、CFG构建有所了解 只需要标识这2个即可还原Fla)。Demo里包含比较通用的特征查找,具体可查看源码。如何食用请跳转github 。PS: 如果是魔改的Ollvm 请自行理解框架,并结合实际情况进行代码调整。目前仅兼容标准的ollvm特征。 更多兼容正在调整中.

原理介绍

现在先回顾一下Fla的特征。 先来一段伪代码

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
Label1:
      int a = 100;
      switch (a)
      {
          case 98:
          {
              //Logic
              a = 101;
              goto Label1;
          }
          case 99:
          {
              //Logic
              a = 98;
              goto Label1;
          }
          case 100:
          {
              //Logic
              a = 99;
              goto Label1;
          }
          default:
              break;
      }

以上其实就是比较简易的一个分发块伪代码; 一个简单的if逻辑编译成So 经过Fla会变成这样的CFG图

F5的结果是这样的

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
__int64 __fastcall sub_181E0C(__int64 a1)
{
  int v2; // w8
  __int64 v3; // x0
  __int64 v4; // x1
  __int64 v5; // x2
  __int128 v7; // [xsp+0h] [xbp-70h] BYREF
  char v8; // [xsp+10h] [xbp-60h]
  __int64 v9; // [xsp+28h] [xbp-48h]
 
  v2 = 1999739781;
  v9 = qword_7289F8;
  while ( 1 )
  {
    while ( v2 == 1087191716 )
    {
      v8 = -12;
      v7 = xmmword_587FDD;
      qword_7289F8 = (__int64)sub_1815C0(&v7, 17);
      v2 = -1880484146;
    }
    if ( v2 != 1999739781 )
      break;
    if ( v9 )
      v2 = -1880484146;
    else
      v2 = 1087191716;
  }
  sub_165910(*(_QWORD *)(a1 + 8), aVIsvSvS, *(_QWORD *)(a1 + 32));
  v3 = sub_15D4E4(*(_QWORD *)(a1 + 8), 3LL);
  return sub_181EE0(v3, v4, v5);

结合一下伪代码可以看出分支的控制由CSEL指令来决定的 即为

1
CSEL            W8, W24, W21, EQ

那么此项值在哪初始化呢? 在主分发器开始进入之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MOV             W21, #0x16CE
MOV             W23, #0x9B85
MOV             W24, #0x3AA4
ADRP            X26, #xmmword_587FDD@PAGE
MOV             W8, #0x9B85
MOV             X20, X1
MOV             X19, X0
MOVK            W21, #0x8FEA,LSL#16
MOVK            W23, #0x7731,LSL#16
MOVK            W24, #0x40CD,LSL#16
MOV             W25, #0xF4
ADD             X26, X26, #xmmword_587FDD@PAGEOFF
MOVK            W8, #0x7731,LSL#16
STR             X4, [SP,#0x70+var_48]

根据特征可以发现控制分发器的形式是一个确定的立即数写入寄存器来完成,那么我们的模拟执行也可以根据以上的特征来提取指令,并且可以伪造CSEL的执行来得到2个不同的分支。这样我们就完成了else分支的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var CSELAfterMove = IsCSELAfterMove(block, instruction);
                        if (CSELAfterMove != null)
                        {
                            SyncLogicInstruction(CSELAfterMove);
                        }
                        var needOperandRegister = instruction.Operands()[0].registerName;
                        var operandLeft = instruction.Operands()[1].registerName;
                        var left = _regContext.GetRegister(operandLeft).GetLongValue();
                        _regContext.SetRegister(needOperandRegister, left);
                        var nextBlock = block.GetLinkedBlocks(this)[0];
                        var leftBlock = FindRealBlock(nextBlock);
                        list.Add(leftBlock);
                        _regContext.RestoreRegisters(block.start_address);
                        var operandRight = instruction.Operands()[2].registerName;
                        var right = _regContext.GetRegister(operandRight).GetLongValue();
                        _regContext.SetRegister(needOperandRegister, right);
                        if (CSELAfterMove != null)
                        {
                            SyncLogicInstruction(CSELAfterMove);
                        }
                        var rightBlock = FindRealBlock(nextBlock);
                        list.Add(rightBlock);

源码中也是如此 在进入第一条满足条件之前,先对当前的寄存器快照的保存,读取到分支后还原快照,再写入第二个分支的立即数。这样即可得到if else 的所有分支。 如果单线的线性分支,那么直接根据B指令跳转的地址。判断该地址是什么块,以此为递归查找就可遍历出所有的分支。代码如下:

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
private Block FindRealBlock(Block block)
   {
       if (_analyzer.IsMainDispatcher(block, _regContext, _blocks))
       {
           var next = RunDispatchBlock(block);
           return FindRealBlock(next);
       }
 
       if (_analyzer.IsChildDispatcher(block, _mainDispatcher, _regContext))
       {
           Logger.InfoNewline(" is Child Dispatcher " + block.start_address);
           var next = RunDispatchBlock(block);
           if (!_childDispatcherBlocks.Contains(block))
           {
               _childDispatcherBlocks.Add(block);
           }
 
           return FindRealBlock(next);
       }
 
       if (_analyzer.IsRealBlock(block, _mainDispatcher, _regContext))
       {
           Logger.WarnNewline("Find Real Block \n" + block);
           block.RealChilds = GetAllChildBlockNew(block);
           if (!_realBlocks.Contains(block))
           {
               _realBlocks.Add(block);
           }
 
           return block;
       }

总体的代码量并不多,大部分的代码还是适配各种复杂情况下的汇编指令。核心的逻辑基本上就在上面。
接下来我们看看实际的效果:
原F5
还原后的F5

可能没有代表性。来个复杂一点点的
CFG图:
F5 图片描述
还原后:
图片描述

基本上是能完美的解出各种不同的条件分支,也不需要手动干预寄存器的状态。

如何进行Patch修复?

Patch的脚本描述起来较为复杂,主要还是尽量保持原Block的位置,根据条件进行跳转修复(如果指令的位置不够。尝试进行压缩指令实现)具体看查找源码

1
2
3
4
5
6
7
8
9
public static void FixMachineCodeNew(this Block block, Block main, Simulation simulation)
 {
     block.isFix = true;
        // if this not null  it's must be CFF_CSEL not other logic block
        if (block.HasCFF_CSEL())
        ...
        ...
        ...
}

关于分发器的查找在源码 CFFAnalyer 可自行实现或对块进行标识

1
2
3
4
5
6
7
8
public static void Init()
   {
       var readAllText = File.ReadAllText(@"E:\RiderDemo\AntiOllvm\AntiOllvm\cfg_output_0x181c6c.json");
       Simulation simulation = new(readAllText, @"E:\RiderDemo\AntiOllvm\AntiOllvm\fix.json");
       simulation.SetAnalyze(new CFFAnalyer()); //FLA 特征查找
       simulation.Run();
        
   }

关于反Ollvm 介绍在此结束。 希望对大家所有帮助。有兴趣维护项目也可进行Fork修改 @乐子 好兄弟 快来改代码


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

最后于 2天前 被IIImmmyyy编辑 ,原因:
收藏
免费 8
支持
分享
最新回复 (18)
雪    币: 2314
活跃值: (3067)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2天前
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
感谢分享,向大佬学习
2天前
0
雪    币: 2402
活跃值: (6808)
能力值: ( LV7,RANK:102 )
在线值:
发帖
回帖
粉丝
4
这种方式效果有限,遇到复杂的就处理不鸟了
2天前
0
雪    币: 213
活跃值: (615)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
5
fjqisba 这种方式效果有限,遇到复杂的就处理不鸟了
恰恰相反,我写这个的目的就是处理复杂情况,无论是何种情况下 块和块的链接只有2个结果 真实块或者分发块。满足条件即可遍历出结果
2天前
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
fjqisba 这种方式效果有限,遇到复杂的就处理不鸟了
大佬为啥arm的vmp没有ida插件呢,可否引用你的vm3.5插件呢
2天前
0
雪    币: 5160
活跃值: (4032)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
7
谢谢分享
2天前
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
IIImmmyyy 恰恰相反,我写这个的目的就是处理复杂情况,无论是何种情况下 块和块的链接只有2个结果 真实块或者分发块。满足条件即可遍历出结果
请问大佬,arm32 thumb适用吗
2天前
0
雪    币: 16533
活跃值: (6544)
能力值: ( LV13,RANK:923 )
在线值:
发帖
回帖
粉丝
9
IIImmmyyy 恰恰相反,我写这个的目的就是处理复杂情况,无论是何种情况下 块和块的链接只有2个结果 真实块或者分发块。满足条件即可遍历出结果
可曾遇到共用块
2天前
1
雪    币: 213
活跃值: (615)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
mb_ldbucrik 请问大佬,arm32 thumb适用吗
不适用 暂未有arm32 和x86的支持计划。 如果要做多架构的话 需要抽出一个中间语言作为桥接。 我闲麻烦没有做了
2天前
0
雪    币: 213
活跃值: (615)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
11
大帅锅 可曾遇到共用块

for循环就是标准的共用块情况。 查找过程中会对正在查找的block进行标记,如果递归发现是已经正在查找的则返回,同时链接已经存在的block。此处理就可完成for循环的处理

```

if (block.isFind)
{
   Logger.WarnNewline("block is Finding  " + block.start_address);
   return block.RealChilds;
}

```

最后于 2天前 被IIImmmyyy编辑 ,原因:
2天前
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
IIImmmyyy 不适用 暂未有arm32 和x86的支持计划。 如果要做多架构的话 需要抽出一个中间语言作为桥接。 我闲麻烦没有做了
好的,retdec优化貌似支持所有的架构
2天前
0
雪    币: 213
活跃值: (615)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
13
mb_ldbucrik 好的,retdec优化貌似支持所有的架构
其实这个也支持,也不难。 你可以仿造arm64的写法进行逻辑的剥离。 代码量很少 并不多的。不想支持的原因主要还是目前工作中需要单独去分析arm32 的情况实在是太少了。 arm64已经是主流了
2天前
0
雪    币: 862
活跃值: (4180)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
14
可以滴
1天前
0
雪    币: 1062
活跃值: (682)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
先拿 tersafe 练练手
1天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
读取 else 分支的时候,仅读取 else 分支下一个 block,还是会继续读取完整个 else 分支继续下去的情况呢?粗看你的实现是会继续读取的,能把所有可能的节点都走过去,和我之前的处理反混淆的时候差不多,我的之前也基本是可用状态,当时碰到过一些问题,目前也没想到好办法,不知道你有没有碰到

1. 因为会按照非常规进入的某段逻辑,所以有时可能会出现错误,导致未遍历完整
2. 要遍历很多,所以速度比较慢

另外一个想讨论的是公共块的问题(上面也有人提出来,不知道和我说的是不是一样),我碰到的公共块问题是,复杂情况下,真实区域中也是好几个块组成的,所以我叫真实区域,没说真实块,然后真实区域之间还会存在块是共用的,即公共块,部分公共块还是真实区域中的出口位置,可能这个块出去有四五个后继区域,这种情况下,我是将 patch 指令复制到了原本的分发器所在位置上,好像并没有看到你有这样分配的情况,不知道你是怎么处理的
1天前
0
雪    币: 213
活跃值: (615)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
17

首先关于

1. 因为会按照非常规进入的某段逻辑,所以有时可能会出现错误,导致未遍历完整
2. 要遍历很多,所以速度比较慢 

这个我并没有办法回答你,可能是因为样本的原因。我目前没有碰到。

我的设计理念是块只有2种情况,真实块或者分发块。 真实块无论是以什么样的形式存在 只要不是分发器它都是真实块。

当然这种就需要特殊处理真实块,比如情况 

```loc_17F750

STR             XZR, [SP,#0xF0+var_C8]

```

这个也是公共块 它的后继块 要么是分发器 要么是真实块。甚至他的后继块可能是它自己。 但是无论如何是什么块,都会进入循环进行标记。我的设计理念就是找到所有的分支。排除分发器。

另外一个问题关于patch指令的 不知道我理解的对不对 我附上一个情况

```

//loc_15E604
// LDR             X9, [SP,#0x2D0+var_2B0]
// ADRP            X8, #qword_7289B8@PAGE
// LDR             X8, [X8,#qword_7289B8@PAGEOFF]
// STR             X9, [SP,#0x2D0+var_260]
// LDR             X9, [SP,#0x2D0+var_2A8]
// STR             X8, [SP,#0x2D0+var_238]
// MOV             W8, #0x561D9EF8
// STP             X19, X9, [SP,#0x2D0+var_270]

// loc_15E628
// CMP             W8, W23
// B.GT            loc_15E6C0

```

loc_15e628 是在下一个连接块中。 此时对15e604进行patch修复的 我选择删除掉MOV W8  这个分发指令 然后调整STP X19 X9 的位置 让跳转指令下沉。这样分发块就可以正常的NOP掉。 如果没有MOV 指令, 其实这个块就不是分发块 也不需要进行NOP 修复 因为下个连接块大概率是一个中转指令 就一个 B locXXXX 这个可能连接到分发块 也有可能是真实块。 但是不重要 因为块与块之间是单独修复的 只要管好自己就行。

Patch 的原则就是不要动到真实块的位置。可以删减 但是不能对原块的指令进行扩充。 因为扩充在复杂的情况下我觉的会有分支错误的问题


最后于 1天前 被IIImmmyyy编辑 ,原因:
1天前
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
IIImmmyyy 其实这个也支持,也不难。 你可以仿造arm64的写法进行逻辑的剥离。 代码量很少 并不多的。不想支持的原因主要还是目前工作中需要单独去分析arm32 的情况实在是太少了。 arm64已经是主流了
好的,试着搞搞
1天前
0
雪    币: 1360
活跃值: (2816)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
打开安卓app一看,主流是arm64
2小时前
0
游客
登录 | 注册 方可回帖
返回
//