前言
今天对某外国的机器中的某个音频解码库进行反汇编,发现了一些有趣的知识,故而记录下来,以防以后重复遇到。
现笔者使用的反汇编工具为“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,r7 00016626 09 da bge LAB_0001663c 00016628 14 22 movs r2,#0x14 0001662a 62 43 muls r2,r4 0001662c 01 9b ldr r3,[sp,#local_2c ] 0001662e 32 20 movs r0,#0x32 00016630 9a 18 adds r2,r3,r2 00016632 11 1c adds r1,r2,#0x0 00016634 ff f7 38 ff bl Decryption 00016638 01 34 adds r4,#0x1 0001663a 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 ?? 00h
00019c0d 00 ?? 00h
00019c0e 00 ?? 00h
00019c0f 00 ?? 00h
00019c10 0c ?? 0Ch
00019c11 00 ?? 00h
00019c12 00 ?? 00h
00019c13 00 ?? 00h
00019c14 0d ?? 0Dh
# 为方便阅读,此处省略部分......
00019c4c 01 ?? 01h
00019c4d 00 ?? 00h
00019c4e 00 ?? 00h
00019c4f 00 ?? 00h
DAT_00019c50
00019c50 02 00 00 00 undefine 00000002h
DAT_00019c54
00019c54 07 00 00 00 undefine 00000007h
DAT_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,#0x4
000164de 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_000164d6
000164d6 1a 1c adds r2,r3,#0x0
000164d8 9d 00 lsls r5,r3,#0x2
000164da ee 59 ldr r6,[r5,r7]=>DAT_00019c58
000164dc 04 3d subs r5,#0x4
000164de 7d 59 ldr r5,[r7,r5]=>DAT_00019c54
000164e0 42 40 eors r2,r0
000164e2 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,#0x1
000164ea 94 46 mov r12 ,r2
000164ec 01 9a ldr r2,[sp,#local_3c ]
000164ee 65 44 add r5,r12
000164f0 55 40 eors r5,r2
000164f2 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,#0x3e
000164fe 50 40 eors r0,r2
00016500 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和逆向有更深层次的理解,最后祝你变得更强!