如何使用Binary ninja逆向

如何使用Binary ninja逆向

以前一直使用的是IDA,一时兴起尝试了下Binary ninja 发现使用起来还不错,和IDA结合起来使用或许会有不错的成效

这篇将以MoeCTF2023 中的一部分逆向题为例,来介绍一下Binary Ninja的使用方法

RRRRRc4

Binary Ninja 使用体感上来说感觉是要比IDA优化好一些,不是很卡

1

刚刚进来是这样的画面,左上角是符号表区,右边会呈现汇编与伪代码,默认是C格式的伪代码

主要用到的也是这两个部分

和IDA一样,可以在左侧使用Ctrl + r 来进行搜索,搜索main的时候,有时候是main,有时候会被解析成invoke_main

2

双击左边invoke_main会跳到invoke_main函数,如果是main的话就会直接跳到main

3

这里需要再点击一下invoke_main中的

4

然后这里引用了一个jump函数,再点一下就可以jump到main中了

可以看到这里就是主逻辑了

5

可以看到显然j_sub_1400795e0函数就是Rc4算法,为了让程序更好看我们有时候会给函数改个名字,这时候会用到右键菜单

6

7

点击这个选项进行重命名

需要注意的是,我们命名的时候建议进函数里面来重命名,不然可能会不成功

8

可以看到外面变成了j_Rc4里面是Rc4

9

同样的,也可以用Rename Variable选项来重命名函数参数

10

看一眼Rc4函数

1400795e0    uint32_t Rc4(char* arg1, char* arg2, char* arg3, uint32_t arg4, int64_t arg5, int32_t arg6)

1400795e0    {
1400795e0        uint32_t i_2 = arg4;
140079609        uint32_t result = j___CheckForDebuggerJustMyCode(&data_1401a7007);
14007960e        uint32_t var_154 = 0;
140079615        int32_t var_f4 = 0;
140079615        
140079637        for (uint32_t i = 0; i < 0x100; i = result)
140079637        {
140079637            arg1[(int64_t)i] = (uint8_t)i;
140079671            arg2[(int64_t)i] = *(uint8_t*)(arg5 + (uint64_t)(COMBINE(0, i) % arg6));
14007962b            result = i + 1;
140079637        }
140079637        
14007968e        for (uint32_t i_1 = 0; i_1 < 0x100; i_1 = result)
14007968e        {
14007968e            char temp7_1;
1400796bb            char temp8_1;
1400796bb            temp7_1 = HIGHD((int64_t)(var_154 + (uint32_t)arg1[(int64_t)i_1]
1400796bb                + (uint32_t)arg2[(int64_t)i_1]));
1400796bb            temp8_1 = LOWD((int64_t)(var_154 + (uint32_t)arg1[(int64_t)i_1]
1400796bb                + (uint32_t)arg2[(int64_t)i_1]));
1400796bc            uint32_t rdx_5 = (uint32_t)temp7_1;
1400796cb            var_154 = (uint32_t)(temp8_1 + (uint8_t)rdx_5) - rdx_5;
1400796d9            char rax_16 = arg1[(int64_t)var_154];
1400796fb            arg1[(int64_t)var_154] = arg1[(int64_t)i_1];
14007970d            arg1[(int64_t)i_1] = rax_16;
140079682            result = i_1 + 1;
14007968e        }
14007968e        
140079715        uint32_t var_174 = 0;
14007971c        uint32_t var_154_1 = 0;
14007971c        
14007972a        for (; i_2 > 0; i_2 = result)
14007972a        {
14007972a            char temp9_1;
140079735            char temp10_1;
140079735            temp9_1 = HIGHD((int64_t)(var_174 + 1));
140079735            temp10_1 = LOWD((int64_t)(var_174 + 1));
140079736            uint32_t rdx_9 = (uint32_t)temp9_1;
140079745            var_174 = (uint32_t)(temp10_1 + (uint8_t)rdx_9) - rdx_9;
14007975e            char temp11_1;
14007975e            char temp12_1;
14007975e            temp11_1 = HIGHD((int64_t)(var_154_1 + (uint32_t)arg1[(int64_t)var_174]));
14007975e            temp12_1 = LOWD((int64_t)(var_154_1 + (uint32_t)arg1[(int64_t)var_174]));
14007975f            uint32_t rdx_11 = (uint32_t)temp11_1;
14007976e            var_154_1 = (uint32_t)(temp12_1 + (uint8_t)rdx_11) - rdx_11;
14007977c            char rax_34 = arg1[(int64_t)var_154_1];
14007979e            arg1[(int64_t)var_154_1] = arg1[(int64_t)var_174];
1400797b0            arg1[(int64_t)var_174] = rax_34;
1400797d3            char temp13_1;
1400797d3            char temp14_1;
1400797d3            temp13_1 = HIGHD((int64_t)((uint32_t)arg1[(int64_t)var_174]
1400797d3                + (uint32_t)arg1[(int64_t)var_154_1]));
1400797d3            temp14_1 = LOWD((int64_t)((uint32_t)arg1[(int64_t)var_174]
1400797d3                + (uint32_t)arg1[(int64_t)var_154_1]));
1400797d4            uint32_t rdx_16 = (uint32_t)temp13_1;
140079817            arg3[(int64_t)var_f4] ^=
140079817                arg1[(int64_t)((uint32_t)(temp14_1 + (uint8_t)rdx_16) - rdx_16)];
140079822            var_f4 += 1;
14007982e            result = i_2 - 1;
14007972a        }
14007972a        
140079844        return result;
1400795e0    }

我们来对比一下正常的Rc4是怎样的:

//程序开始
#include<stdio.h>
#include<string.h>
typedef unsigned longULONG;

/*初始化函数*/
void rc4_init(unsigned char*s, unsigned char*key, unsigned long Len)
{
    int i = 0, j = 0;
    char k[256] = { 0 };
    unsigned char tmp = 0;
    for (i = 0; i<256; i++)
    {
        s[i] = i;
        k[i] = key[i%Len];
    }
    for (i = 0; i<256; i++)
    {
        j = (j + s[i] + k[i]) % 256;
        tmp = s[i];
        s[i] = s[j];//交换s[i]和s[j]
        s[j] = tmp;
    }
}

/*加解密*/
void rc4_crypt(unsigned char*s, unsigned char*Data, unsigned long Len)
{
    int i = 0, j = 0, t = 0;
    unsigned long k = 0;
    unsigned char tmp;
    for (k = 0; k<Len; k++)
    {
        i = (i + 1) % 256;
        j = (j + s[i]) % 256;
        tmp = s[i];
        s[i] = s[j];//交换s[x]和s[y]
        s[j] = tmp;
        t = (s[i] + s[j]) % 256;
        Data[k] ^= s[t];
    }
}

int main()
{
    unsigned char s[256] = { 0 }, s2[256] = { 0 };//S-box
    char key[256] = { "justfortest" };
    char pData[512] = "这是一个用来加密的数据Data";
    unsigned long len = strlen(pData);
    int i;

    printf("pData=%s\n", pData);
    printf("key=%s,length=%d\n\n", key, strlen(key));
    rc4_init(s, (unsigned char*)key, strlen(key));//已经完成了初始化
    printf("完成对S[i]的初始化,如下:\n\n");
    for (i = 0; i<256; i++)
    {
        printf("%02X", s[i]);
        if (i && (i + 1) % 16 == 0)putchar('\n');
    }
    printf("\n\n");
    for (i = 0; i<256; i++)//用s2[i]暂时保留经过初始化的s[i],很重要的!!!
    {
        s2[i] = s[i];
    }
    printf("已经初始化,现在加密:\n\n");
    rc4_crypt(s, (unsigned char*)pData, len);//加密
    printf("pData=%s\n\n", pData);
    printf("已经加密,现在解密:\n\n");
    //rc4_init(s,(unsignedchar*)key,strlen(key));//初始化密钥
    rc4_crypt(s2, (unsigned char*)pData, len);//解密
    printf("pData=%s\n\n", pData);
    return 0;
}

//程序完

可以看到反编译的时候出现了大量冗余代码,其原因是MSVC 调试版本在编译时会加入大量校验和未优化掉的多余操作,反编译器只能忠实地还原这些运算。

所以你看到的arg2[(int64_t)i] = *(uint8_t*)(arg5 + (uint64_t)(COMBINE(0, i) % arg6));对应的其实是k[i] = key[i%Len];

140079637        for (uint32_t i = 0; i < 0x100; i = result)
140079637        {
140079637            arg1[(int64_t)i] = (uint8_t)i;
140079671            arg2[(int64_t)i] = *(uint8_t*)(arg5 + (uint64_t)(COMBINE(0, i) % arg6));
14007962b            result = i + 1;
140079637        }

这里多余的这个result本质上是Binary Ninja 反编译器生成的“伪变量”,它本身没有实际意义。

同时你会发现Rc4中的


for(i=0;i<256;i++) {
        j=(j+s[i]+k[i])%256;
        tmp=s[i];
        s[i]=s[j];//交换s[i]和s[j]
        s[j]=tmp;
    }

变成了

1400797d3            temp13_1 = HIGHD((int64_t)((uint32_t)arg1[(int64_t)var_174]
1400797d3                + (uint32_t)arg1[(int64_t)var_154_1]));
1400797d3            temp14_1 = LOWD((int64_t)((uint32_t)arg1[(int64_t)var_174]
1400797d3                + (uint32_t)arg1[(int64_t)var_154_1]));
1400797d4            uint32_t rdx_16 = (uint32_t)temp13_1;
140079817            arg3[(int64_t)var_f4] ^=
140079817                arg1[(int64_t)((uint32_t)(temp14_1 + (uint8_t)rdx_16) - rdx_16)];
140079822            var_f4 += 1;
14007982e            result = i_2 - 1;

所有变量都是 8 位无符号整数(uint8_t),加法后会自动对 256 取模,溢出直接丢掉,但是在 x86-64 汇编中,这种操作可能会被编译成:

先用 32 位寄存器加法(避免反复截断)

然后把结果的高位(进位)提取出来,再通过巧妙的减法实现“模 256”

看一眼对应的汇编

140079d1  add     eax, ecx          ; eax = S[var_174] + S[var_154_1]  (两个 8 位零扩展到 32 位相加)
140079d3  cdq                       ;  eax 符号扩展到 edx:eax  64 
140079d4  and     edx, 0xff         ; edx = (32) & 0xff   取进位
140079da  add     eax, edx          ; eax = 32 + 进位
140079dc  and     eax, 0xff         ; eax = (32 + 进位) & 0xff
140079e1  sub     eax, edx          ; eax = (32 + 进位) - 进位   最终得到 (原和) mod 256

cdq 指令把 eax 的值按符号位扩展成 edx:eax 组成的64 位值。 Binary Ninja 的反编译器会严格模拟这个过程:

  1. S[var_174] + S[var_154_1] 的和先提升为 64 位(int64_t),这是为了匹配 cdq 产生的 64 位结果。
  2. HIGHD(...) 就是这个 64 位值的高 32 位(即 edx 部分),也就是“进位”。
  3. LOWD(...) 是这个 64 位值的低 32 位(即 eax 部分)。

之后反编译器看到的后续操作 (temp14_1 + (uint8_t)rdx_16) - rdx_16,就是汇编里 add eax, edxand eax, 0xffsub eax, edx 的精确翻译。这整套操作在数学上等价于 (S[var_174] + S[var_154_1]) % 256,但因为反编译器必须逐条指令保持语义,所以它不会自动简化成 % 256,而是原样输出这个带 HIGHD/LOWD 的表达式。

即,因为编译器用 32 位加法 + cdq 来模拟 8 位加法进位,反编译器看到 64 位结果后必须用 HIGHD 取高 32 位、LOWD 取低 32 位,再用加减掩码来完成“模 256”的效果。

11

回到main中,可以看到这一部分是一个比较函数,data_140197260是输入,将输入经过Rc4处理之后与data_140196000中的数据进行对比,看是否是正确的flag,也就是说逆过来把data_140196000中的数据经过Rc4处理一下就能得到flag

双击,就可以进data段看数据

12

选中之后要提取的数据后右键

点击Copy As -> C Array -> 8-bit Elements 一般都是使用这个格式来提取数据

13

byte_6000 = [
    0x1b, 0x9b, 0xfb, 0x19, 0x06, 0x6a, 0xb5, 0x3b, 0x7c, 0xba, 0x03, 0xf3, 0x91, 0xb8, 0xb6, 0x3d,
	0x8a, 0xc1, 0x48, 0x2e, 0x50, 0x11, 0xe7, 0xc7, 0x4f, 0xb1, 0x27, 0xcf, 0xf3, 0xae, 0x03, 0x09,
	0xb2, 0x08, 0xfb, 0xdc, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
a5 = "moectf2023"
a6 = 10
a1 = []
a2 = []
for i in range(256):
    a1.append(i)
    a2.append(
ord(a5[i%a6]))
v10 = 0
v14 = 0
for j in range(256):
    v10 = (a2[j]+a1[j]+v10)%256
    v12 = a1[v10]
    a1[v10] = a1[j]
    a1[j] = v12
v9 = 0
v11 = 0
a4 = 38
flag = ""
while(a4>0):
    v9 = (v9+1)%256
    v11 = (a1[v9]+v11)%256
    v13 = a1[v11]
    a1[v11] = a1[v9]
    a1[v9] = v13
    n = byte_6000[v14]^(a1[(a1[v11]+a1[v9])%256])
    flag += chr(n)
    v14 += 1
    a4 -= 1
print(flag)

就可以拿到flag了

相信眼尖的同学已经能注意到右键菜单经常出现的patch

14

在Binary Ninja中自带了patch功能,可以直接在右键菜单进行patch

15

可以直接在这里输入汇编指令,然后点击Tab,就能发现伪代码会自动重新反编译(和IDA一样,可以用Tab来切换汇编界面和伪代码界面)

Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计