如何使用Binary ninja逆向
以前一直使用的是IDA,一时兴起尝试了下Binary ninja 发现使用起来还不错,和IDA结合起来使用或许会有不错的成效
这篇将以MoeCTF2023 中的一部分逆向题为例,来介绍一下Binary Ninja的使用方法
RRRRRc4
Binary Ninja 使用体感上来说感觉是要比IDA优化好一些,不是很卡

刚刚进来是这样的画面,左上角是符号表区,右边会呈现汇编与伪代码,默认是C格式的伪代码
主要用到的也是这两个部分
和IDA一样,可以在左侧使用Ctrl + r 来进行搜索,搜索main的时候,有时候是main,有时候会被解析成invoke_main

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

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

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

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


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

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

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

看一眼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 的反编译器会严格模拟这个过程:
- 把
S[var_174] + S[var_154_1]的和先提升为 64 位(int64_t),这是为了匹配cdq产生的 64 位结果。 HIGHD(...)就是这个 64 位值的高 32 位(即edx部分),也就是“进位”。LOWD(...)是这个 64 位值的低 32 位(即eax部分)。
之后反编译器看到的后续操作 (temp14_1 + (uint8_t)rdx_16) - rdx_16,就是汇编里 add eax, edx → and eax, 0xff → sub eax, edx 的精确翻译。这整套操作在数学上等价于 (S[var_174] + S[var_154_1]) % 256,但因为反编译器必须逐条指令保持语义,所以它不会自动简化成 % 256,而是原样输出这个带 HIGHD/LOWD 的表达式。
即,因为编译器用 32 位加法 + cdq 来模拟 8 位加法进位,反编译器看到 64 位结果后必须用 HIGHD 取高 32 位、LOWD 取低 32 位,再用加减掩码来完成“模 256”的效果。

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

选中之后要提取的数据后右键
点击Copy As -> C Array -> 8-bit Elements 一般都是使用这个格式来提取数据

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

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

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