Featured image of post 逆向工程实战,在反汇编中发生的事

逆向工程实战,在反汇编中发生的事

在反汇编的过程中,发现了一个有趣的decryption函数,从这个函数可以看到,反汇编工具整理的C与汇编代码的不一致

前言

今天对某外国的机器中的某个音频解码库进行反汇编,发现了一些有趣的知识,故而记录下来,以防以后重复遇到。

现笔者使用的反汇编工具为“Ghidra”。可在GitHub上下载:NationalSecurityAgency/ghidra: Ghidra is a software reverse engineering (SRE) framework

Ghidra是由美国国家安全局研究局创建和维护的软件逆向工程 (SRE) 框架 。该框架包括一套功能齐全的高端软件分析工具,使用户能够在包括 WindowsmacOSLinux 在内的各种平台上分析编译代码。功能包括反汇编、汇编、反编译、绘图和脚本,以及数百个其他功能。

Ghidrajava编写,因此使用Ghidra前先配置java运行环境。

注:本文不介绍Ghidra的使用

adpcm_decoder1函数

C代码

笔者通过获取机器日志,发现了机器存在音频解码函数,通过grep命令查询到了,存有相应解码函数的库文件,并将库文件pull了出来,我们来看一下该库文件中相应的解码函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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语言版本,确实是相差无几,但在这段代码中,多出了以下内容:

1
2
3
4
  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语言:

1
2
3
  for (iVar6 = 0; iVar6 < iVar3; iVar6 = iVar6 + 1) {
    Decryption(0x32,param_1 + iVar6 * 0x14);
  }

汇编:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
                             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 指令表示比较,即比较r4r7寄存器中的值,结合c代码,可以看出,r4r7分别代表了iVar6iVar3,但目前不清楚哪个对哪个。
  • bge 指令是分支指令,表示条件跳转(branch if greater or equal),即如果大于等于,则跳转。这两个指令合起来为,如果r4≥r7,则跳转到LAB_0001663c代码段,可以看出,已经超出了这个for循环的地址,即退出了for循环。
  • movs 指令表示移动,将立即数0x14移动到r2寄存器中,现在r2寄存器中存储了0x14这个值。
  • muls 指令表示乘法,即将r2r4寄存器中的值相乘,结合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 指令表示加法,将r3r2相加,并存放到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存放了0x32r1存放了param_1的偏移地址,r2存放与r1相同的东西。这三个寄存器就是Decryption函数用的三个参数。如果还不确定,可以跳转到Decryption函数中查看相应的param_1,param_2,param_3三个参数调用时所使用的寄存器,分别为r0r1r2,本文给出结论,不做赘述。

小结

总而言之,这里的音频解码,虽然使用的是ADPCM算法,但是在raw dataadpcm data之间还加入了一个加密解密过程,即,实际上的过程为:

原始音频数据raw data → adpcm压缩→ 加密算法 encryption → 蓝牙传输 → 解密算法 decryption → adpcm解压 → 原始音频数据 raw data

因此,接下来看如何用解密算法还原加密算法

decryption函数

C代码

下面是decryption函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}

笔者依次解释上面的代码

数据本地化

1
2
3
4
5
6
  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的数据解码

1
2
3
4
5
6
7
8
  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 * 4iVar3在每次循环中会被做-1操作。也就是说,第一次iVar4 = 19 * 4,第二次为iVar4 = 18 * 4,以此类推。

最后,我们查查这个DAT_00019c0c,以及它对应的偏移量中究竟存了什么东西,通过Ghidra的汇编地址中可以查到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
                             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;似乎才是正确的。我们查看一下相应的汇编代码:

1
2
3
        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指令表示加载寄存器,先计算r5r7寄存器相加的值,再传给r6寄存器,ghidra已经给出了第一次循环中指向的地址,即0x00019c58
  • subs指令表示减法,将r5寄存器中的值减去立即数0x04并传给寄存器r5
  • 与第一条指令相同,且ghidra给出了第一次循环中指向的地址,即0x00019c54

这样可以看出,实际上,寄存器r5r6都是读地址值且相差仅有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代码,还要检查汇编代码以及内存的走向。

解码过程

现在我们了解了那两个复杂的变量,现在我们来讲一讲解码的算法

1
2
    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,这样上式就变成了:

1
    local_30[8] = local_30[8] + local_30[7] ^ bVar2 ^ param_1;

依次类推,到最后一次循环时,上式变成:

1
    local_30[12] = local_30[12] + local_30[0] ^ bVar2 ^ param_1;

我们假设,存在一个table[20]

1
2
3
4
5
6
table[20] = {
    0x0,0xc,0xd,0x3,0x4,\
    0x9,0xa,0xb,0x10,0x11,\
	0x12,0x13,0x5,0x6,0xe\
	0xf,0x1,0x2,0x7,0x8
}

那么整个while循环可以改写成:

1
2
3
4
5
  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;即,bVar2iVar3相同,从19到1根据循环次数递减,param_1是一个常量,为0x32,是从adpcm_decoder1中传进来的第一个参数,如果不记得可往上翻阅。

在这条式子中,既有加法,又有异或运算,那么问题来了,该怎样计算呢?或者说,先算哪个呢?

这时熟悉C语言的同学们会说,在C语言中,加减法的优先级高于异或运算的优先级,因此先计算local_30[table[iVar3]] + local_30[table[iVar3-1]],再计算 ^ bVar2 ^ param_1,答案是没错的,在不同的语言中,异或运算与加减法的优先级可能会有所不同,因此这里的运算优先级要额外注意,但同理,在逆向工程中,不要过分相信逆向工具给你的C代码,它不一定是你想象的那样,因此我们还是来看一下它的汇编代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
                             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,r0r2寄存器与r0寄存器的值做异或运算,将结果存放到r2中,在我们之前提到,r0寄存器存储的是param_1传参,即0x32固定参数,因此这条指令实际上就是bVar2 ^ param_1
  • ldrb r2,[r1,r5]:指令表示加载寄存器,r5r6两个寄存器的内容我们上面分析过,[r1,r5]表示目标内存地址,这个地址是通过将寄存器r1r5的值相加得来的,r1param_2,即数据数组指针,r5为上面分析时讲到的乱序查表得到的偏移量。两者相加则索引到对应的数据。将其存放在r2中,下面ldrb r5,[r1,r6]一样。
  • mov r12 ,r2:该指令将r2寄存器中的值转移到r12寄存器,因此,现在r12r2的内容一样。
  • add r5,r12:该指令将r12r5相加,即式子中的两个local_30相加,并将结果存放到r5中。
  • eors r5,r2:将r5寄存器与r2寄存器中的值做异或运算。

通过汇编指令,我们可以推算出运算的优先级为:

$$ r1 = bVar2 \oplus param_1\\ r2 = local\_30[*(int *)(\&DAT\_00019c0c + iVar4)] + local\_30[*piVar1]\\ res = r1 \oplus r2 $$

第0个数据的解码

1
2
  local_30[0] = local_30[0] - 0x3e ^ param_1;
  iVar3 = 0;

第0个数据不依赖于任何其它的数据,仅靠自身便可完成解码

其对应的汇编代码也很简单,如果读者理解了上面的过程,那么理解这一段汇编代码也是轻而易举,在此笔者不做过多赘述

1
2
3
        000164fc 3e  38           subs       r0,#0x3e
        000164fe 50  40           eors       r0,r2
        00016500 08  70           strb       r0,[r1,#0x0 ]=>local_30

返回数据

1
2
3
4
  do {
    *(byte *)(param_3 + iVar3) = local_30[iVar3];
    iVar3 = iVar3 + 1;
  } while (iVar3 != 0x14);

在此过程中,通过while循环将local_30中已经完成解码的数据全部放到param_3参数中,完成了数据的返回(因为param_3是一个指针,因此对指针数据做修改在函数退出时修改依然成立,而不会随着函数出栈而消失)。

总结

在这个反汇编的过程中,我们通过C代码与汇编代码的结合,还原出了解码过程的真实情况,因此,在反汇编中除了看反汇编工具提供的c语言代码,也需要看原来的汇编代码。所以,在逆向工程中,学会阅读汇编代码对你的帮助时巨大的,会避免因编译优化等问题带来的困扰,同时,笔者推荐读一读《深入理解计算机系统》,会让你对C和逆向有更深层次的理解,最后祝你变得更强!

written by LyricalWander
使用 Hugo 构建
主题 StackJimmy 设计