前言
今天对某外国的机器中的某个音频解码库进行反汇编,发现了一些有趣的知识,故而记录下来,以防以后重复遇到。
现笔者使用的反汇编工具为“Ghidra
”。可在GitHub上下载:NationalSecurityAgency/ghidra: Ghidra is a software reverse engineering (SRE) framework
Ghidra
是由美国国家安全局研究局创建和维护的软件逆向工程 (SRE) 框架 。该框架包括一套功能齐全的高端软件分析工具,使用户能够在包括 Windows
、macOS
和 Linux
在内的各种平台上分析编译代码。功能包括反汇编、汇编、反编译、绘图和脚本,以及数百个其他功能。
Ghidra
由java
编写,因此使用Ghidra
前先配置java
运行环境。
注:本文不介绍Ghidra的使用
adpcm_decoder1函数
C代码
笔者通过获取机器日志,发现了机器存在音频解码函数,通过grep
命令查询到了,存有相应解码函数的库文件,并将库文件pull
了出来,我们来看一下该库文件中相应的解码函数:
void adpcm_decoder1(char *param_1,undefined2 *param_2,int param_3,int *param_4){bool bVar1;uint uVar2;int iVar3;uint uVar4;int iVar5;int iVar6;int iVar7;uint uVar8;char *local_2c;int local_28;undefined2 *local_24;iVar3 = __divsi3(param_3,0x14);for (iVar6 = 0; iVar6 < iVar3; iVar6 = iVar6 + 1) {Decryption(0x32,param_1 + iVar6 * 0x14);}bVar1 = false;uVar8 = 0;iVar5 = param_4[1];iVar6 = *param_4;iVar3 = *(int *)(&DAT_00019c5c + iVar5 * 4);local_2c = param_1;local_24 = param_2;for (local_28 = param_3 << 1; 0 < local_28; local_28 = local_28 + -1) {uVar2 = uVar8;if (!bVar1) {uVar8 = (uint)*local_2c;local_2c = local_2c + 1;uVar2 = (int)uVar8 >> 4;}uVar4 = uVar2 & 0xf;bVar1 = (bool)(bVar1 ^ 1);iVar5 = iVar5 + (&DAT_00019dc0)[uVar4];if (iVar5 < 0) {iVar5 = 0;}else if (0x58 < iVar5) {iVar5 = 0x58;}iVar7 = iVar3 >> 3;if ((int)(uVar4 << 0x1d) < 0) {iVar7 = iVar7 + iVar3;}if ((int)(uVar4 << 0x1e) < 0) {iVar7 = iVar7 + (iVar3 >> 1);}if ((uVar2 & 1) != 0) {iVar7 = iVar7 + (iVar3 >> 2);}if ((uVar2 & 8) != 0) {iVar7 = -iVar7;}iVar6 = iVar6 + iVar7;if (iVar6 < -0x8000) {iVar6 = -0x8000;}if (0x7fff < iVar6) {iVar6 = 0x7fff;}iVar3 = *(int *)(&DAT_00019c5c + iVar5 * 4);*local_24 = (short)iVar6;local_24 = local_24 + 1;}*param_4 = iVar6;param_4[1] = iVar5;return;
}
以上是反汇编后的结果,Ghidra
将机器语言解析成汇编语言,再自动转换成相应的C语言。
Decryption函数并非ADPCM的一部分
可以看到,这是典型的ADPCM音频解码算法,笔者已经对比过经典的开源ADPCM音频解码算法的C语言版本,确实是相差无几,但在这段代码中,多出了以下内容:
iVar3 = __divsi3(param_3,0x14);for (iVar6 = 0; iVar6 < iVar3; iVar6 = iVar6 + 1) {Decryption(0x32,param_1 + iVar6 * 0x14);}
在解释这一段内容前,读者需要先了解:该机器通过蓝牙接收语音数据,每语音帧20字节。但机器处理不一定是20字节一处理,根据音频收发的速率,丢包率以及buffer的大小决定。
现在笔者来解释一下这一段的内容:
- param_3除以20,将结果赋值给iVar3变量,这里计算出一共进来多少语音帧需要处理,每帧20字节
- 一个for循环,将每帧数据丢入
Decryption
函数中进行处理 - Decryption函数接收两个参数,0x32,一个固定的常量值;param_1 + iVar6 * 0x14,param_1是语音数据的数组指针
Decryption函数的参数问题
这里解释一下Decryption函数接收的两个参数,实际上,Decryption函数接收三个参数,在这里,Ghidra在C中只给出了两个参数,笔者不清楚这是Ghidra的特性还是工具缺陷。对汇编不感兴趣的同学可以跳过这一部分,但推荐看看
下面会给出Decryption的函数体,在函数体中可以发现Decryption是接收三个参数的,这里我们讲解一下这一段for循环的汇编内容
C语言:
for (iVar6 = 0; iVar6 < iVar3; iVar6 = iVar6 + 1) {Decryption(0x32,param_1 + iVar6 * 0x14); }
汇编:
LAB_00016624 00016624 bc 42 cmp r4,r700016626 09 da bge LAB_0001663c00016628 14 22 movs r2,#0x140001662a 62 43 muls r2,r40001662c 01 9b ldr r3,[sp,#local_2c ]0001662e 32 20 movs r0,#0x3200016630 9a 18 adds r2,r3,r200016632 11 1c adds r1,r2,#0x000016634 ff f7 38 ff bl Decryption 00016638 01 34 adds r4,#0x10001663a f3 e7 b LAB_00016624
这是这个for循环中涉及到的汇编语言,我们尝试在这段汇编中找到Decryption的三个参数
cmp
指令表示比较,即比较r4
与r7
寄存器中的值,结合c代码,可以看出,r4
,r7
分别代表了iVar6
和iVar3
,但目前不清楚哪个对哪个。bge
指令是分支指令,表示条件跳转(branch if greater or equal),即如果大于等于,则跳转。这两个指令合起来为,如果r4≥r7
,则跳转到LAB_0001663c
代码段,可以看出,已经超出了这个for循环的地址,即退出了for循环。movs
指令表示移动,将立即数0x14
移动到r2
寄存器中,现在r2
寄存器中存储了0x14
这个值。muls
指令表示乘法,即将r2
与r4
寄存器中的值相乘,结合c代码可以看到,代码中仅有一个乘法,即iVar6 * 0x14
,可以得出,r2 = 0x14,r4 = iVar6
。ldr
指令表示加载寄存器(load register),语法为:ldr <寄存器>, [<基址寄存器>, <偏移量>]
,从基址寄存器中取出地址,加上偏移量,将结果加载到寄存器中,这里取出的地址加载到r3中,在c代码中,只有一个涉及到取址操作,即param_1
,这是一个adpcm_decoder1
函数接受的传参,传入的是一个指针,即param_1
其实是一个数组指针。这里是获取param_1
数组的地址,放到r3
中。movs
指令表示移动,将立即数0x32
移动到r0
寄存器中,现在r0
寄存器中存放了0x32
这个值。adds
指令表示加法,将r3
与r2
相加,并存放到r2
寄存器中,即,param_1
的地址加上一个计算的偏移量,本质上是从param_1
的语音数据中获取第n个语音帧的地址。adds
指令表示加法,将r2
寄存器的值加上立即数0x00
存放到r1
寄存器中,这操作让r2
寄存器存放的内容与r1
一致。bl
指令表示跳转,即跳转到Decryption
函数的位置去,执行Decryption
函数,并把下一条指令的地址存放到lr
寄存器中,以方便Decryption
函数执行完能回来,相当于压栈,将Decryption
函数压入函数栈。adds
指令表示加法,将r4
加上立即数0x01
,并存放到r4
中,即for循环的++操作。可以看到,在调用Decryption函数前,分别给三个寄存器存放了东西,分别为
r0
存放了0x32
,r1
存放了param_1的偏移地址,r2
存放与r1
相同的东西。这三个寄存器就是Decryption函数用的三个参数。如果还不确定,可以跳转到Decryption函数中查看相应的param_1
,param_2
,param_3
三个参数调用时所使用的寄存器,分别为r0
,r1
,r2
,本文给出结论,不做赘述。
小结
总而言之,这里的音频解码,虽然使用的是ADPCM算法,但是在raw data
与adpcm data
之间还加入了一个加密解密过程,即,实际上的过程为:
原始音频数据raw data → adpcm压缩→ 加密算法 encryption → 蓝牙传输 → 解密算法 decryption → adpcm解压 → 原始音频数据 raw data
因此,接下来看如何用解密算法还原加密算法
decryption函数
C代码
下面是decryption函数
void Decryption(byte param_1,int param_2,int param_3){int *piVar1;byte bVar2;int iVar3;int iVar4;byte local_30 [20];int local_1c;local_1c = __stack_chk_guard;iVar3 = 0;do {local_30[iVar3] = *(byte *)(param_2 + iVar3);iVar3 = iVar3 + 1;} while (iVar3 != 0x14);iVar3 = 0x13;do {iVar4 = iVar3 * 4;piVar1 = &DAT_00019c08 + iVar3;bVar2 = (byte)iVar3;iVar3 = iVar3 + -1;local_30[*(int *)(&DAT_00019c0c + iVar4)] =local_30[*(int *)(&DAT_00019c0c + iVar4)] + local_30[*piVar1] ^ bVar2 ^ param_1;} while (iVar3 != 0);local_30[0] = local_30[0] - 0x3e ^ param_1;iVar3 = 0;do {*(byte *)(param_3 + iVar3) = local_30[iVar3];iVar3 = iVar3 + 1;} while (iVar3 != 0x14);if (local_1c != __stack_chk_guard) {/* WARNING: Subroutine does not return */__stack_chk_fail();}return;
}
笔者依次解释上面的代码
数据本地化
iVar3 = 0;do {local_30[iVar3] = *(byte *)(param_2 + iVar3);iVar3 = iVar3 + 1;} while (iVar3 != 0x14);iVar3 = 0x13;
这一段代码其实很好看出是在做什么,就是将传入的数据存在函数内部的局部变量中,iVar3
变量是一个很重要的变量,在后续计算中会用到,记住它。在将所有数据搬完之后(一共20个字节,一语音帧20个字节),iVar3
变量来到了0x13
,十进制19
第19到1的数据解码
do {iVar4 = iVar3 * 4;piVar1 = &DAT_00019c08 + iVar3;bVar2 = (byte)iVar3;iVar3 = iVar3 + -1;local_30[*(int *)(&DAT_00019c0c + iVar4)] =local_30[*(int *)(&DAT_00019c0c + iVar4)] + local_30[*piVar1] ^ bVar2 ^ param_1;} while (iVar3 != 0);
这里有一个很容易混淆的地方,就是标题采用的“第19到第1”这个概念,我们只能说,从while循环,从iVar3
变量的角度,是“从19到1”,而从数据的角度说并不是这样。为了强化读者这个概念,我们先介绍local_30[*(int *)(&DAT_00019c0c + iVar4)]
与local_30[*piVar1]
这两个变量。
local_30[*(int *)(&DAT_00019c0c + iVar4)]
首先,从定义上看byte local_30 [20];
,local_30是一个数组的局部变量,且具有20个元素的大小,每个元素的大小为1byte,即,该数组是专门存放音频数据的。
其次,我们先看local_30[*(int *)(&DAT_00019c0c + iVar4)]
,数组的内部index
部分很复杂,我们来做介绍
&DAT_00019c0c
是内存中的一个地址&DAT_00019c0c + iVar4
表示这个地址 + 一个偏移量
,组成一个新的地址(int *)(&DAT_00019c0c + iVar4)
表示将这个地址被强制类型转换为int
类型的指针*(int *)(&DAT_00019c0c + iVar4)
表示取值这个int
指针,即从这个地址中取值,这个值是int
类型的
然后,我们来看这个iVar4
变量,iVar4 = iVar3 * 4
,在每一次循环中,iVar4
都会被更新一次,且更新为iVar3 * 4
,iVar3
在每次循环中会被做-1
操作。也就是说,第一次iVar4 = 19 * 4
,第二次为iVar4 = 18 * 4
,以此类推。
最后,我们查查这个DAT_00019c0c
,以及它对应的偏移量中究竟存了什么东西,通过Ghidra
的汇编地址中可以查到:
DAT_00019c0c 00019c0c 00 ?? 00h00019c0d 00 ?? 00h00019c0e 00 ?? 00h00019c0f 00 ?? 00h00019c10 0c ?? 0Ch00019c11 00 ?? 00h00019c12 00 ?? 00h00019c13 00 ?? 00h00019c14 0d ?? 0Dh# 为方便阅读,此处省略部分......00019c4c 01 ?? 01h00019c4d 00 ?? 00h00019c4e 00 ?? 00h00019c4f 00 ?? 00hDAT_00019c50 00019c50 02 00 00 00 undefine 00000002hDAT_00019c54 00019c54 07 00 00 00 undefine 00000007hDAT_00019c58 00019c58 08 00 00 00 undefine 00000008h
0x00019c0c地址存放的数据为0x00,隔4个字节后,0x00019c10地址存放的数据为0x0c…一直到19 * 4个字节,即0x00019c58存放的数据为0x08,通过查询并列出发现,其数据依次为:
0x0 0xc 0xd 0x3 0x4
0x9 0xa 0xb 0x10 0x11
0x12 0x13 0x5 0x6 0xe
0xf 0x1 0x2 0x7 0x8
刚好是0~19,即根据通过’iVar3’的依次从19到1的变化,local_30数组依次参与解码的数据为8,7,2,1,…,4,3,13,12,0,而非按顺序解码。
local_30[*piVar1]
在这里我们可以看到,该变量依然是local_30数组中的元素,只不过其index
表示为*piVar1
。
看piVar1 = &DAT_00019c08 + iVar3;
一些敏锐的同学可能注意到,在我们介绍的前一个变量中,地址都是四个字节一偏移,在内存地址中查询数据的分布,也是四个字节存一个数据,其它地方都是0,而按照我们的理解,piVar1 = &DAT_00019c08 + iVar3 * 4;
似乎才是正确的。我们查看一下相应的汇编代码:
000164da ee 59 ldr r6,[r5,r7]=>DAT_00019c58 000164dc 04 3d subs r5,#0x4000164de 7d 59 ldr r5,[r7,r5]=>DAT_00019c54
这一段代码,分别是做了*(int *)(&DAT_00019c0c + iVar4)
的计算以及piVar1 = &DAT_00019c08 + iVar3
的计算,我们来解释一下:
ldr
指令表示加载寄存器,先计算r5
与r7
寄存器相加的值,再传给r6
寄存器,ghidra
已经给出了第一次循环中指向的地址,即0x00019c58
subs
指令表示减法,将r5
寄存器中的值减去立即数0x04
并传给寄存器r5
- 与第一条指令相同,且
ghidra
给出了第一次循环中指向的地址,即0x00019c54
这样可以看出,实际上,寄存器r5
与r6
都是读地址值且相差仅有4个字节。也就是说,piVar1
比&DAT_00019c08 + iVar3 * 4
仅仅相差了4个字节,例如:在第一次循环中,&DAT_00019c08 + iVar3 * 4
中的值为0x8,piVar1
中的值为0x7,在最后一次循环中,&DAT_00019c08 + iVar3 * 4
的值为0xc,piVar1
的值为0x0。
为什么C代码会与汇编相差如此之大呢,第一,在GCC编译C语言的过程中,本身会对代码进行优化,即调整指令顺序,甚至优化掉部分操作,以提升效率;第二,从已经编译过的二进制文件反推出C语言代码本身就是非常难的一件事情,正如数学中,求导的难度往往比求原函数的难度要低。所以有些细微的差池是可能的,因此在逆向工程中,除了要看C代码,还要检查汇编代码以及内存的走向。
解码过程
现在我们了解了那两个复杂的变量,现在我们来讲一讲解码的算法
local_30[*(int *)(&DAT_00019c0c + iVar4)] =local_30[*(int *)(&DAT_00019c0c + iVar4)] + local_30[*piVar1] ^ bVar2 ^ param_1;
在这条运算中,我们可以了解到local_30[*(int *)(&DAT_00019c0c + iVar4)]
表示当前解码运算的数据,local_30[*piVar1]
表示下一个解码运算的数据,例如:当*(int *)(&DAT_00019c0c + iVar4)=8
时,*piVar1 = 7
,这样上式就变成了:
local_30[8] = local_30[8] + local_30[7] ^ bVar2 ^ param_1;
依次类推,到最后一次循环时,上式变成:
local_30[12] = local_30[12] + local_30[0] ^ bVar2 ^ param_1;
我们假设,存在一个table[20]
table[20] = {0x0,0xc,0xd,0x3,0x4,\0x9,0xa,0xb,0x10,0x11,\0x12,0x13,0x5,0x6,0xe\0xf,0x1,0x2,0x7,0x8
}
那么整个while循环可以改写成:
do {local_30[table[iVar3]] =local_30[table[iVar3]] + local_30[table[iVar3-1]] ^ bVar2 ^ param_1;iVar3 = iVar3 + -1;} while (iVar3 != 0);
这样,读者应该可以理解,我们从iVar3
的角度上来讲,是“从第19到1的解码”,但从数据实际的解码顺序来讲,其实是“8,7,2,1,15,14,6,5,19,18,17,16,11,10,9,4,3,13,12”的顺序解码,0在后面单独解码。
这19个数据,每一个解码,都依赖于前一个还未解码的数据,即当解码第8个数据的时候,需要依赖第7个数据,当解码第12个数据时,需要依赖第0个数据。
现在我们仔细讲一下式子,在该式子中, bVar2 = (byte)iVar3;
即,bVar2
与iVar3
相同,从19到1根据循环次数递减,param_1
是一个常量,为0x32
,是从adpcm_decoder1中传进来的第一个参数,如果不记得可往上翻阅。
在这条式子中,既有加法,又有异或运算,那么问题来了,该怎样计算呢?或者说,先算哪个呢?
这时熟悉C语言的同学们会说,在C语言中,加减法的优先级高于异或运算的优先级,因此先计算local_30[table[iVar3]] + local_30[table[iVar3-1]]
,再计算 ^ bVar2 ^ param_1
,答案是没错的,在不同的语言中,异或运算与加减法的优先级可能会有所不同,因此这里的运算优先级要额外注意,但同理,在逆向工程中,不要过分相信逆向工具给你的C代码,它不一定是你想象的那样,因此我们还是来看一下它的汇编代码
LAB_000164d6000164d6 1a 1c adds r2,r3,#0x0000164d8 9d 00 lsls r5,r3,#0x2000164da ee 59 ldr r6,[r5,r7]=>DAT_00019c58000164dc 04 3d subs r5,#0x4000164de 7d 59 ldr r5,[r7,r5]=>DAT_00019c54000164e0 42 40 eors r2,r0000164e2 01 92 str r2,[sp,#local_3c ]000164e4 4a 5d ldrb r2,[r1,r5]000164e6 8d 5d ldrb r5,[r1,r6]000164e8 01 3b subs r3,#0x1000164ea 94 46 mov r12 ,r2000164ec 01 9a ldr r2,[sp,#local_3c ]000164ee 65 44 add r5,r12000164f0 55 40 eors r5,r2000164f2 8d 55 strb r5,[r1,r6]000164f4 00 2b cmp r3,#0x0
因为式子中有一个“+”和两个“^”,因此我们只要着重看“add”指令与“eor”指令即可。
adds r2,r3,#0x0
:这条指令并不是加法指令,而是将r3
寄存器中的值赋值给r2
寄存器,这个寄存器中实际存了什么值呢,我们看后续有一个subs r3,#0x1
,因此可知,r3
寄存器是在循环中递减的,这符合bVar2
的特征,因此可以断定,r2
寄存器存储的实际上就是bVar2
。eors r2,r0
:r2
寄存器与r0
寄存器的值做异或运算,将结果存放到r2
中,在我们之前提到,r0
寄存器存储的是param_1
传参,即0x32
固定参数,因此这条指令实际上就是bVar2 ^ param_1
。ldrb r2,[r1,r5]
:指令表示加载寄存器,r5
与r6
两个寄存器的内容我们上面分析过,[r1,r5]
表示目标内存地址,这个地址是通过将寄存器r1
与r5
的值相加得来的,r1
为param_2
,即数据数组指针,r5
为上面分析时讲到的乱序查表得到的偏移量。两者相加则索引到对应的数据。将其存放在r2
中,下面ldrb r5,[r1,r6]
一样。mov r12 ,r2
:该指令将r2
寄存器中的值转移到r12
寄存器,因此,现在r12
与r2
的内容一样。add r5,r12
:该指令将r12
与r5
相加,即式子中的两个local_30
相加,并将结果存放到r5
中。eors r5,r2
:将r5
寄存器与r2
寄存器中的值做异或运算。
通过汇编指令,我们可以推算出运算的优先级为:
r 1 = b V a r 2 ⊕ p a r a m 1 r 2 = l o c a l _ 30 [ ∗ ( i n t ∗ ) ( & D A T _ 00019 c 0 c + i V a r 4 ) ] + l o c a l _ 30 [ ∗ p i V a r 1 ] r e s = r 1 ⊕ r 2 r1 = bVar2 \oplus param_1\\ r2 = local\_30[*(int *)(\&DAT\_00019c0c + iVar4)] + local\_30[*piVar1]\\ res = r1 \oplus r2 r1=bVar2⊕param1r2=local_30[∗(int∗)(&DAT_00019c0c+iVar4)]+local_30[∗piVar1]res=r1⊕r2
第0个数据的解码
local_30[0] = local_30[0] - 0x3e ^ param_1;iVar3 = 0;
第0个数据不依赖于任何其它的数据,仅靠自身便可完成解码
其对应的汇编代码也很简单,如果读者理解了上面的过程,那么理解这一段汇编代码也是轻而易举,在此笔者不做过多赘述
000164fc 3e 38 subs r0,#0x3e000164fe 50 40 eors r0,r200016500 08 70 strb r0,[r1,#0x0 ]=>local_30
返回数据
do {*(byte *)(param_3 + iVar3) = local_30[iVar3];iVar3 = iVar3 + 1;} while (iVar3 != 0x14);
在此过程中,通过while循环将local_30
中已经完成解码的数据全部放到param_3
参数中,完成了数据的返回(因为param_3
是一个指针,因此对指针数据做修改在函数退出时修改依然成立,而不会随着函数出栈而消失)。
总结
在这个反汇编的过程中,我们通过C代码与汇编代码的结合,还原出了解码过程的真实情况,因此,在反汇编中除了看反汇编工具提供的c语言代码,也需要看原来的汇编代码。所以,在逆向工程中,学会阅读汇编代码对你的帮助时巨大的,会避免因编译优化等问题带来的困扰,同时,笔者推荐读一读《深入理解计算机系统》,会让你对C和逆向有更深层次的理解,最后祝你变得更强!