hans7 发表于 2022-9-25 20:59

【reverse】虚假控制流入门:Ubuntu20.04安装ollvm4.0踩坑记+用IDApython去除BCF

本帖最后由 hans7 于 2022-9-25 21:10 编辑

### 引言

> **虚假控制流(Bogus Control Flow,BCF)**,通过加入包含**不透明谓词**的条件跳转(也就是跳转与否在运行之前就已经确定的跳转,但IDA无法分析)和**不可达的基本块**,来干扰IDA的控制流分析和F5反汇编。

### 依赖

- IDA7.7
- 虚拟机Ubuntu20.04

### Ubuntu20.04安装ollvm+各种踩坑记录

根据参考链接1,主要的命令就这些:

```bash
# 截至2022.09.25,这玩意已经5年没更新了……
git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git
# 这里build文件夹和obfuscator-llvm-4.0文件夹同级
mkdir build-llvm-4.0 && sudo chmod 777 -R build-llvm-4.0 && cd build-llvm-4.0
cmake -DCMAKE_BUILD_TYPE=Release ../obfuscator-llvm-4.0/
# 防止出现Permission Denied
sudo make -j5
```

但你先别急,这里水很深,不看完**参考链接1**以及我总结的踩坑记录的话,泥巴握不住!

**作者:(https://blog.csdn.net/hans774882968)以及(https://juejin.cn/user/1464964842528888)以及(https://www.52pojie.cn/home.php?mod=space&uid=1906177)**

本文52pojie:https://www.52pojie.cn/thread-1692596-1-1.html

本文juejin:https://juejin.cn/post/7147302252846252046/

本文csdn:https://blog.csdn.net/hans774882968/article/details/127043163

#### 1、gcc和g++需要降级

如果用的是9及以后的版本,`make`时会没有任何提示,忽然报错`make: *** 错误 2`。直接执行下面这些命令进行降级就行:

```bash
sudo apt install gcc-8 g++-8 -y

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 8
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-8 8
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 9
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 9

sudo update-alternatives --config gcc
sudo update-alternatives --config g++

# 最后可以看看版本是否修改成功
gcc -v
g++ -v
```

效果



#### 2、编译前要先修改源码

根据参考链接1,不修改源码会踩坑。找到`<你的ollvm目录>/include/llvm/ExecutionEngine/Orc/OrcRemoteTargetClient.h`,按照下图把690行的`readMem`的返回类型从`Expected<vector<char>>`改为`Expected<vector<uint8_t>>`(这里参考链接1错误地说成了`uint_8`了)。



#### 3、注意权限问题

如果你在编译时,看到`make`失败前有一大堆`Permission Denied`,说明你权限没给够。

1. 建议编译前给到`build-llvm-4.0`和`obfuscator-llvm-4.0`的父文件夹777权限:`sudo chmod 777 -R <父文件夹名>`,防止新生成的文件`Permission Denied`。
2. 不要在挂载点的文件夹进行编译,否则会有权限错误。
3. 建议编译期间每次看到`build-llvm-4.0/bin`新生成一个文件,都给它777权限,防止`Permission Denied`造成失败(其实编译失败也没事,编译好的文件不会重新编译,不是很耽误时间。每次看到新生成的文件出现`Permission Denied`,先给它权限再重新编译即可)。

#### 4、给足虚拟机内存

不给足内存的话虚拟机会死机。也可以选择降一降作业数,比如`sudo make -j7`降到`sudo make -j5`。

#### 编译成功

大概等了一小时,终于成功了!纪念一下!



### 先看一个demo

写个`bcf_demo.cpp`:

```cpp
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

int main (int argc, char const *argv[]) {
    char name;
    scanf ("%s", name);
    if (strcmp (name, "Alice") == 0) {
      printf ("hello, %s.\n", name) ;
    } else if (strcmp (name, "Bob") == 0) {
      printf ("hello, %s\n", name);
    } else {
      printf ("no permission.\n");
    }
    return 0;
}
```

用clang正常编译

```bash
'build-llvm-4.0/bin/clang++ 的绝对路径' 'bcf_demo.cpp 的绝对路径' -o bcf_demo_normal
```

IDA反汇编效果:

```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1; // BYREF
const char **v5; //
int v6; //
int v7; //

v7 = 0;
v6 = argc;
v5 = argv;
scanf("%s", s1);
if ( !strcmp(s1, (const char *)(unsigned int)"Alice") )
{
    printf("hello, %s.\n", s1);
}
else if ( !strcmp(s1, (const char *)(unsigned int)"Bob") )
{
    printf("hello, %s\n", s1);
}
else
{
    printf("no permission.\n");
}
return 0;
}
```

流程图:



加上bcf,编译:

```bash
'build-llvm-4.0/bin/clang++ 的绝对路径' -mllvm -bcf 'bcf_demo.cpp 的绝对路径' -o bcf_demo
```

IDA反汇编效果:

```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1; // BYREF
const char **v5; //
int v6; //
int v7; //

v7 = 0;
v6 = argc;
v5 = argv;
scanf("%s", s1);
if ( !strcmp(s1, (const char *)(unsigned int)"Alice") )
{
    if ( y_12 >= 10 && ((((_BYTE)x_11 - 1) * (_BYTE)x_11) & 1) != 0 )
      goto LABEL_9;
    while ( 1 )
    {
      printf("hello, %s.\n", s1);
      if ( y_12 < 10 || ((((_BYTE)x_11 - 1) * (_BYTE)x_11) & 1) == 0 )
      break;
LABEL_9:
      printf("hello, %s.\n", s1);
    }
}
else if ( !strcmp(s1, (const char *)(unsigned int)"Bob") )
{
    printf("hello, %s\n", s1);
}
else
{
    printf("no permission.\n");
}
return 0;
}
```

流程图:



对比两图,bcf确实让程序更复杂了。

> 这些跳转中的**x_11**和**y_12**位于**.bss段**,并且通过交叉引用发现没有被修改过,也就是说**x_11**和**y_12**在运行过程中一直为**0**。这里的**x_11**和**y_12**被称为**不透明谓词**,所谓不透明,就是IDA难以推断其在运行时的值,但我们都知道它就是0。

简单分析一下bcf加入的干扰语句。`y_12 >= 10 && ((((_BYTE)x_11 - 1) * (_BYTE)x_11) & 1) != 0`:因为相邻两个数的乘积必为偶数,故此式总是false。根据德摩根定律,`y_12 < 10 || ((((_BYTE)x_11 - 1) * (_BYTE)x_11) & 1) == 0`就总是true。因此`printf("hello, %s.\n", s1);`恰好只执行一次。那些永远不会执行到的代码块,就叫做**不可达的基本块**。这些跳转和不可达基本块并不会影响程序原有的逻辑,但会干扰我们的分析,这就是虚假控制流混淆达到的效果。

### 尝试用IDApython去除bcf

我们把上面的demo写得更复杂一点:

```cpp
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

int main (int argc, char const *argv[]) {
    char name;
    scanf ("%s", name);
    if (strcmp (name, "Alice") == 0) {
      printf ("hello, %s.\n", name) ;
    } else if (strcmp (name, "Bob") == 0) {
      printf ("hello, %s\n", name);
    } else {
      printf ("no permission.\n") ;
      return 0;
    }
    re_ (i, 0, 10) {
      if (i & 1) dbg (i << 1);
      else dbg (i << 1 | 1);
    }
    return 0;
}
```

加上bcf编译,用IDA看看patch前的效果:

```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // BYREF
int v5; // BYREF
int i; //
char s1; // BYREF
const char **v8; //
int v9; //
int v10; //

v10 = 0;
v9 = argc;
v8 = argv;
scanf("%s", s1);
if ( !strcmp(s1, (const char *)(unsigned int)"Alice") )
{
    printf("hello, %s.\n", s1);
}
else
{
    if ( strcmp(s1, (const char *)(unsigned int)"Bob") )
    {
      if ( y_13 >= 10 && ((((_BYTE)x_12 - 1) * (_BYTE)x_12) & 1) != 0 )
      goto LABEL_18;
      while ( 1 )
      {
      printf("no permission.\n");
      v10 = 0;
      if ( y_13 < 10 || ((((_BYTE)x_12 - 1) * (_BYTE)x_12) & 1) == 0 )
          return v10;
LABEL_18:
      printf("no permission.\n");
      v10 = 0;
      }
    }
    printf("hello, %s\n", s1);
}
for ( i = 0; ; ++i )
{
    while ( y_13 >= 10 && ((((_BYTE)x_12 - 1) * (_BYTE)x_12) & 1) != 0 )
      ;
    if ( i >= 10 )
      break;
    if ( (i & 1) != 0 )
    {
      v5 = 2 * i;
      dbg<int>(&v5);
      continue;
    }
    if ( y_13 >= 10 && ((((_BYTE)x_12 - 1) * (_BYTE)x_12) & 1) != 0 )
    {
LABEL_20:
      v4 = (2 * i) | 1;
      dbg<int>(&v4);
    }
    v4 = (2 * i) | 1;
    dbg<int>(&v4);
    if ( y_13 >= 10 && ((((_BYTE)x_12 - 1) * (_BYTE)x_12) & 1) != 0 )
      goto LABEL_20;
}
return 0;
}
```

我们随便找个例子,看看干扰代码的汇编长什么样:

```assembly
.text:000000000040151C 8B 04 25 B4 41 40 00          mov   eax, ds:x_12
.text:0000000000401523 8B 0C 25 9C 41 40 00          mov   ecx, ds:y_13
.text:000000000040152A 89 C2                         mov   edx, eax
.text:000000000040152C 83 EA 01                      sub   edx, 1
.text:000000000040152F 0F AF C2                      imul    eax, edx
.text:0000000000401532 83 E0 01                      and   eax, 1
.text:0000000000401535 83 F8 00                      cmp   eax, 0
.text:0000000000401538 40 0F 94 C6                   setz    sil
.text:000000000040153C 83 F9 0A                      cmp   ecx, 0Ah
.text:000000000040153F 40 0F 9C C7                   setl    dil
.text:0000000000401543 40 08 FE                      or      sil, dil
.text:0000000000401546 40 F6 C6 01                   test    sil, 1
.text:000000000040154A 0F 85 05 00 00 00             jnz   loc_401555
.text:000000000040154A
.text:0000000000401550 E9 F3 01 00 00                jmp   loc_401748
```

我们不需要管这些干扰指令具体是true还是false,只需要知道:它们不影响原有代码。我们简单分析可知,这里的`jnz loc_401555`一定会执行,因此我们只需要把`jnz loc_401555`改成`jmp loc_401555`,即可去除所有的干扰效果。

IDApython脚本:

```python
import idc


def next_instr(addr):
    # item_size返回addr处指令长度
    return addr + idc.get_item_size(addr)


def main():
    print('-' * 40)
    st_addr = 0x401470
    ed_addr = 0x401793
    addr = st_addr
    while addr < ed_addr:
      next = next_instr(addr)
      if 'x_12' in idc.GetDisasm(addr):
            # 向下找到jnz
            while addr < ed_addr and 'jnz' not in idc.GetDisasm(addr):
                addr = next
                next = next_instr(addr)
            if addr >= ed_addr:
                break
            print(idc.GetDisasm(addr))# dbg
            # 获取jnz跳转的目的地址
            dest = idc.get_operand_value(addr, 0)
            print('dest', hex(dest))# dbg
            # 将jnz patch成jmp
            idc.patch_byte(addr, 0xE9)
            # 计算目的地址相对addr的偏移offset
            offset = dest - (addr + 5)
            # 将jmp操作数patch为offset
            idc.patch_dword(addr + 1, offset)
            # patch jnz指令最后一个字节为nop
            idc.patch_byte(addr + 5, 0x90)
      addr = next
    print('-' * 40)


main()
```

输出:

```
jnz   loc_401555
dest 0x401555
jnz   loc_4015AC
dest 0x4015ac
jnz   loc_4015F9
dest 0x4015f9
jnz   loc_401642
dest 0x401642
jnz   loc_4016C0
dest 0x4016c0
jnz   loc_401717
dest 0x401717
```

在上述例子中,patch后,只有一条指令被修改了:

```assembly
.text:000000000040151C 8B 04 25 B4 41 40 00          mov   eax, ds:x_12
.text:0000000000401523 8B 0C 25 9C 41 40 00          mov   ecx, ds:y_13
.text:000000000040152A 89 C2                         mov   edx, eax
.text:000000000040152C 83 EA 01                      sub   edx, 1
.text:000000000040152F 0F AF C2                      imul    eax, edx
.text:0000000000401532 83 E0 01                      and   eax, 1
.text:0000000000401535 83 F8 00                      cmp   eax, 0
.text:0000000000401538 40 0F 94 C6                   setz    sil
.text:000000000040153C 83 F9 0A                      cmp   ecx, 0Ah
.text:000000000040153F 40 0F 9C C7                   setl    dil
.text:0000000000401543 40 08 FE                      or      sil, dil
.text:0000000000401546 40 F6 C6 01                   test    sil, 1
.text:000000000040154A E9 06 00 00 00                jmp   loc_401555
.text:000000000040154A
.text:000000000040154A                               ; ---------------------------------------------------------------------------
.text:000000000040154F 90                            db90h
.text:0000000000401550 E9 F3 01 00 00                jmp   loc_401748
```

patch后反汇编效果:

```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // BYREF
int v5; // BYREF
int i; //
char s1; // BYREF
const char **v8; //
int v9; //
int v10; //

v10 = 0;
v9 = argc;
v8 = argv;
scanf("%s", s1);
if ( !strcmp(s1, (const char *)(unsigned int)"Alice") )
{
    printf("hello, %s.\n", s1);
}
else
{
    if ( strcmp(s1, (const char *)(unsigned int)"Bob") )
    {
      printf("no permission.\n");
      return 0;
    }
    printf("hello, %s\n", s1);
}
for ( i = 0; i < 10; ++i )
{
    if ( (i & 1) != 0 )
    {
      v5 = 2 * i;
      dbg<int>(&v5);
    }
    else
    {
      v4 = (2 * i) | 1;
      dbg<int>(&v4);
    }
}
return 0;
}
```

效果不错,去得很干净。

地球人用angr去除bcf的做法以后(~~下辈子~~)再学。

### 参考资料

1. Ubuntu20.04安装ollvm各种踩坑记录:https://www.bilibili.com/read/cv13148974/
2. 地球人yyds%%%:https://bbs.pediy.com/thread-266005.htm

hans7 发表于 2022-9-26 23:21

lht64877586 发表于 2022-9-26 22:53
软件包降级可以用aptitude

我不懂apt这些,不过搜到apt和aptitude最好别混用 https://www.cnblogs.com/alanjiang/p/16322566.html

hans7 发表于 2022-9-26 21:40

dilankaiwei 发表于 2022-9-26 10:05
谢谢分享,实话没看懂

我看其他blog的时候也没看懂,后来跟着做一遍就感觉能弄懂一点点了{:1_936:}

tukuai88ya 发表于 2022-9-25 21:11

感谢分享

z28761789 发表于 2022-9-25 21:11

火钳刘明

zhphs88 发表于 2022-9-25 21:26

感谢分享

jim19 发表于 2022-9-25 21:34

感谢分享

chenhaooooo 发表于 2022-9-26 00:04

感谢楼主分享

歪喇叭 发表于 2022-9-26 00:24

感谢分享

fengrui99 发表于 2022-9-26 08:30

火钳刘明{:301_999:}

jjsyx123 发表于 2022-9-26 09:03

感谢分享

lws0318 发表于 2022-9-26 09:49

不明觉厉,谢分享
页: [1] 2 3 4 5 6
查看完整版本: 【reverse】虚假控制流入门:Ubuntu20.04安装ollvm4.0踩坑记+用IDApython去除BCF