失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 【汇编实战开发笔记】一段汇编代码如何“反编译”成C代码?

【汇编实战开发笔记】一段汇编代码如何“反编译”成C代码?

时间:2022-04-30 00:37:01

相关推荐

【汇编实战开发笔记】一段汇编代码如何“反编译”成C代码?

文章目录

1 前言2 问题描述2.1 大家可能遇到的问题2.2 我们遇到的问题 3 解决思路4 实战推演4.1 汇编基础知识学习4.2 得到SoCx平台的汇编代码4.3 将汇编代码还原成C代码4.3.1 汇编转C伪代码4.3.2 伪代码转C代码 4.4 将C代码嵌入编译得到新的固件 5 成果验证5.1 正向验证5.2 反向验证 6 扩展延伸7 经验总结8 更多分享

1 前言

作为一位从事嵌入式底层开发的软件攻城狮,少不了要经常要跟汇编代码打交道:比如你要移植一款操作系统到某颗芯片上,比如你想直接操作底层寄存器来实现某个外设功能,比如你想使用汇编代码编程实现一段高效的算法逻辑,等等。

在某种程度上说,熟练掌握汇编语言编程既是一种必备工作技能的硬件要求,同时也可能是你解决某些工作难题的能力延伸。

2 问题描述

2.1 大家可能遇到的问题

笔者在过往的工作经历中,有遇到这么一个场景,我相信不少的底层开发攻城狮也可能会遇到类似的困惑。事情是这样的:

我们的项目开展是基于一个开源项目在做的,这个开源项目的绝大部分代码都是开源的,但是唯独某些“核心”的数据加解密流程控制算法是闭源的,这部分功能是通过静态库(.a)的形式开发出来,这样我们作为开发者,在不需要了解其内容实现的情况下,仅需要调用其提供的API就可以跑通整个流程。

我相信大部分的厂商,凡是认为自己的代码稍微有些价值但又不愿意开放源代码的都可能会采用这种方式来操作。

这样做的好处是,厂商无需开放其苦心专研的源代码,外部调用者也无需关注具体的实现,只管接口调用即可。

但是,坏处就是,如果你发现它的实现是有问题的,或者你想从中添加一些功能的时候,就不得不经过厂商来完成,谁让你没有源码呢?

2.2 我们遇到的问题

这不,我们就遇到这样一个很棘手的问题,可能单靠文字描述起来比较费劲,我索性画了一个图来表达我们的问题,如下图所示:

我们的 问题具体是这样的:

我们使用一个官方的SDK,并做了功能扩展,且同事跨平台支持多款SoC的移植运行;进行到某一个阶段的时候,官方SDK释放了一个新的版本,解决了空闲状态下内存占用更多的问题;经我方内部评估,觉得这个功能对我们益处很大,觉得同步这个功能;因我们不是原样搬运官方的SDK,所以只能部分拉取它的功能代码;同步完其功能代码之后,发现我们编译处理的固件在ISO的APP上某个功能始终跑不过,但在Android的APP是没有问题的;(我们固件需要手机APP做某些功能的交互)当即我们判断可能是ISO版本的APP走了不同的功能分支,而我们的固件里面不支持该功能分支,所以流程跑不通;与此同时,我们了解到官方的SDK是会同步到一个第三方SDK中,这个第三方SDK使用的SoCx与我们使用的SoC是竞品关系,于是我们决定分析第三方的SDK;经过多方分析,包括基础代码对比分析,流程分析;得出的结论是:的确第三方的代码比我们的代码多了一个功能版本的判断分支,当识别到新版本(2.0版本)就启用新的方法去解密数据,但是非常遗憾的是,关键的解密方法,以是静态库(.a)的形式导出的,也就是说其源码是不公开的;虽然对于SoCx而言,我们可以采用其现成的静态库,但是由于我们得支持多SoC跨平台,这样采用静态库显然不是一个好方法;我们需要更优雅的方式去解决这个问题。

3 解决思路

从上面的问题描述,我们已经知道了目前遇到的问题瓶颈,它就是:某个核心的数据解密函数,我们只有静态库及其调用方法,但没有其源码实现,无法进行跨平台的移植

要想优雅地解决此问题,必须绕不开反汇编技术,也就是说,我们能不能通过静态库完成固件的编译,然后再对固件程序进行反编译,得到这段数据解密函数的汇编实现,再结合对汇编代码的逻辑分析,尽可能地还原其C语言版本的源代码?

大致的流程如下图所示:

对流程图的各个节点进行困难度分析,我们可以知道最核心要解决的就是从汇编代码如何得到C代码

4 实战推演

4.1 汇编基础知识学习

从上面的各个分析,我们可以知道关键得从第三方的SDK中寻找突破口,我们得知第三方SDK中使用的SoCx是ARMv5架构的,所以我们得提前预热学习下ARM架构下的汇编基础知识。

在我看来,对汇编基础有个大致的掌握,主要先从三个方面入口:寻址方式、寄存器用途、以及汇编指令的基本用法。

基本上,有了这三大块的知识,看懂基础的汇编代码,甚至说写一写基础的汇编程序,也应该问题不大。

关于ARM的这三大块知识,可以参考下我的另一篇博文:【汇编实战开发笔记】ARM汇编基础的三大块知识。

如果没有ARM汇编的相关基础知识,强烈建议提前了解下,否则可能对下面的实战会有所疑问。

4.2 得到SoCx平台的汇编代码

首先得大家第三方SDK的编译环境,索性的是,并不是很负责,整一个构建是使用bash shell + Makefile来完成的,需要安装的交叉编译工具链是arm-none-eabi-xxx;这个工具链之前在别的项目中有用过,所以在环境搭建方面,很顺利就完成了,参考第三方SDK的构建指南,很顺利就编译取得了其输出的固件。

输出的固件中,包含有:xxx.elf、xxx.map、xxx.bin等核心文件。

我们需要使用的正是这个xxx.elf文件,关于ELF文件的内容,感兴趣的可以参考下这篇文章的第五章节。

我们知道,ELF文件包含了丰富的控制信息,我们的bin文件正是输入这个ELF文件,使用objcopy命令得到的。

那么我们要想从ELF文件中得到汇编代码,我们需要使用的命令是objdump,对应交叉编译工具链就是arm-none-eabi-objdump

它的具体使用方法如下:

arm-none-eabi-objdump -l -d -x -s -S xxx.elf > xxx.dmp具体的参数含义,可参考 objdump --help.

其中xxx.elf即为SoCx编译出来的ELF文件,而xxx.dmp就是反编译得到的汇编代码文件。

注意:执行这行指令,可能会耗时有些久,取决于你的编译环境,处理能力如何。

没有提示失败就代表成功,我们可以使用vi/vim打开这个xxx.dmp文件,简单浏览下。

如果使用windows的文本编辑器可能会加载非常慢,因为这个汇编文件,可能会多达几十MB。

从汇编文件中,搜索我们要找的那个decrypt函数,大致它的内容如下所示:

428632 10051ecc <decrypt_passwd>:428633 decrypt_passwd():428634 10051ecc: b570 push {r4, r5, r6, lr}428635 10051ece: b092 subsp, #72 ; 0x48428636 10051ed0: 4605 movr5, r0 428637 10051ed2: 460c movr4, r1 428638 10051ed4: 4616 movr6, r2 428639 10051ed6: 2100 movs r1, #0428640 10051ed8: 2210 movs r2, #16428641 10051eda: a801 addr0, sp, #4428642 10051edc: f02d fda0 bl1007fa20 <memset> 428643 10051ee0: 2221 movs r2, #33 ; 0x21428644 10051ee2: 2100 movs r1, #0428645 10051ee4: a809 addr0, sp, #36; 0x24428646 10051ee6: f02d fd9b bl1007fa20 <memset> 428647 10051eea: 2210 movs r2, #16428648 10051eec: 2100 movs r1, #0428649 10051eee: a805 addr0, sp, #20428650 10051ef0: f02d fd96 bl1007fa20 <memset> 428651 10051ef4: 782b ldrb r3, [r5, #0]428652 10051ef6: b1f3 cbzr3, 10051f36 <decrypt_passwd+0x6a>428653 10051ef8: 2310 movs r3, #16428654 10051efa: aa01 addr2, sp, #4428655 10051efc: 2120 movs r1, #32428656 10051efe: 4628 movr0, r5428657 10051f00: f7c2 fa8a bl10014418 <hexstr_convert> 428658 10051f04: 2310 movs r3, #16428659 10051f06: 4620 movr0, r4428660 10051f08: aa05 addr2, sp, #20428661 10051f0a: 2120 movs r1, #32428662 10051f0c: f7c2 fa84 bl10014418 <hexstr_convert> 428663 10051f10: 4620 movr0, r4428664 10051f12: aa09 addr2, sp, #36; 0x24428665 10051f14: 2120 movs r1, #32428666 10051f16: f7c4 ff96 bl10016e46 <utils_sha256> 428667 10051f1a: 2201 movs r2, #1428668 10051f1c: a905 addr1, sp, #20428669 10051f1e: a809 addr0, sp, #36; 0x24428670 10051f20: f7dd fb62 bl1002f5e8 <aes128_init> 428671 10051f24: 4633 movr3, r6428672 10051f26: 4604 movr4, r0428673 10051f28: 2201 movs r2, #1428674 10051f2a: a901 addr1, sp, #4428675 10051f2c: f7dd fbb3 bl1002f696 <aes128_cbc_decrypt> 428676 10051f30: 4620 movr0, r4428677 10051f32: f7dd fba3 bl1002f67c <aes128_destroy>428678 10051f36: 2000 movs r0, #0428679 10051f38: b012 addsp, #72 ; 0x48428680 10051f3a: bd70 pop{r4, r5, r6, pc}

4.3 将汇编代码还原成C代码

下面我们分两步走:先翻译汇编成C伪代码,再把伪代码还原成C代码。

4.3.1 汇编转C伪代码

到了这一步,就必须要用ARM汇编的基础知识了,其中最关键的就是:函数的参数传递,以及函数调用。

在ARM汇编中,一般使用R0-R3传递参数,分别对应第1-4个形参。

在ARM汇编中,带返回的函数调用使用的bl指令,这个指令后面接的是调用的地址,也就是需要调用的函数。

为了好对比,我采取的做法是直接在关键的汇编代码后面,转换成伪代码,得到的内容如下所示:

428632 10051ecc <decrypt_passwd>: //int decrypt_password(const char *cipher, const uint8_t *random, char *passwd)428633 decrypt_passwd():428634 10051ecc: b570 push {r4, r5, r6, lr}428635 10051ece: b092 subsp, #72 ; 0x48428636 10051ed0: 4605 movr5, r0 #r5=r0 //encoded428637 10051ed2: 460c movr4, r1 #r4=r1 //p_ranodm_str428638 10051ed4: 4616 movr6, r2 #r6=r2 //passwd428639 10051ed6: 2100 movs r1, #0428640 10051ed8: 2210 movs r2, #16428641 10051eda: a801 addr0, sp, #4428642 10051edc: f02d fda0 bl1007fa20 <memset> //memset(sp0, 0, 16)428643 10051ee0: 2221 movs r2, #33 ; 0x21428644 10051ee2: 2100 movs r1, #0428645 10051ee4: a809 addr0, sp, #36; 0x24428646 10051ee6: f02d fd9b bl1007fa20 <memset> //memset(sp1, 0, 33)428647 10051eea: 2210 movs r2, #16428648 10051eec: 2100 movs r1, #0428649 10051eee: a805 addr0, sp, #20428650 10051ef0: f02d fd96 bl1007fa20 <memset> //memset(sp2, 0, 16)428651 10051ef4: 782b ldrb r3, [r5, #0]428652 10051ef6: b1f3 cbzr3, 10051f36 <decrypt_passwd+0x6a>428653 10051ef8: 2310 movs r3, #16428654 10051efa: aa01 addr2, sp, #4428655 10051efc: 2120 movs r1, #32428656 10051efe: 4628 movr0, r5428657 10051f00: f7c2 fa8a bl10014418 <hexstr_convert> //encoded->sp0(hex)428658 10051f04: 2310 movs r3, #16428659 10051f06: 4620 movr0, r4428660 10051f08: aa05 addr2, sp, #20428661 10051f0a: 2120 movs r1, #32void hexstr_convert(char *hexstr, uint8_t *out_buf, int len);428662 10051f0c: f7c2 fa84 bl10014418 <hexstr_convert> //p_ranodm_str->sp2(hex)428663 10051f10: 4620 movr0, r4428664 10051f12: aa09 addr2, sp, #36; 0x24428665 10051f14: 2120 movs r1, #32void utils_sha256(const uint8_t *input, uint32_t ilen, uint8_t output[32])428666 10051f16: f7c4 ff96 bl10016e46 <utils_sha256> //sha256(p_ranodm_str,32,sp1)428667 10051f1a: 2201 movs r2, #1428668 10051f1c: a905 addr1, sp, #20428669 10051f1e: a809 addr0, sp, #36; 0x24p_HAL_Aes128_t aes128_init(_IN_ const uint8_t *key, _IN_ const uint8_t *iv,_IN_ AES_DIR_t dir)428670 10051f20: f7dd fb62 bl1002f5e8 <aes128_init> //key=sp1,iv=sp2,dir=1(decrypt)428671 10051f24: 4633 movr3, r6428672 10051f26: 4604 movr4, r0428673 10051f28: 2201 movs r2, #1428674 10051f2a: a901 addr1, sp, #4int aes128_cbc_decrypt(_IN_ p_HAL_Aes128_t aes, _IN_ const void *src,_IN_ size_t blockNum, _OU_ void *dst)428675 10051f2c: f7dd fbb3 bl1002f696 <aes128_cbc_decrypt> //src=sp0,blockNum=1,dst=passwd428676 10051f30: 4620 movr0, r4428677 10051f32: f7dd fba3 bl1002f67c <aes128_destroy> // int aes128_destroy(_IN_ p_HAL_Aes128_t aes)428678 10051f36: 2000 movs r0, #0428679 10051f38: b012 addsp, #72 ; 0x48428680 10051f3a: bd70 pop{r4, r5, r6, pc}

得到上面的伪代码,其实还有一点也比较重要,我们必须得知道这个decrypt函数的原型以及它调用的几个函数的原型,索性的是,这些个函数原因都可以在第三方的SDK中找到,毕竟头文件是开源的。

int decrypt_passwd(const char *cipher, const uint8_t *random, char *passwd);void *memset(void *s, int c, size_t n);void hexstr_convert(char *hexstr, uint8_t *out_buf, int len);void utils_sha256(const uint8_t *input, uint32_t ilen, uint8_t output[32]);p_HAL_Aes128_t aes128_init(_IN_ const uint8_t *key, _IN_ const uint8_t *iv,_IN_ AES_DIR_t dir);int aes128_cbc_decrypt(_IN_ p_HAL_Aes128_t aes, _IN_ const void *src,_IN_ size_t blockNum, _OU_ void *dst);int aes128_destroy(_IN_ p_HAL_Aes128_t aes);

只有知道了原型,我们才能更好地分析其入参的顺序,这样才能知道R0-R4寄存器放的是什么值。

不过,我们还有一点可以想想的是,有时候其函数名也是一个重要的信息,比如其中的hexstr_convert是hex2string的转换,比如其中的utils_sha256是SHA256摘要的基础算法。这些在没有更多信息的时候,就只能靠猜和尝试,再验证了。

4.3.2 伪代码转C代码

有了上面的伪代码,基本上捋一捋上下文就可以得到比较像样的C代码了,就像这样的:

/* get this function logic from ARM ASM section. */int decrypt_passwd(const char *cipher, const uint8_t *_random, char *passwd){uint8_t sp0[16];uint8_t sp1[33];uint8_t sp2[16];char random[33] = {0};memcpy(random, _random, 32);int cipher_len = strlen(cipher);int random_len = strlen(random);p_HAL_Aes128_t aes_eng;int ret = -1;memset(sp0, 0, 16);memset(sp1, 0, 33);memset(sp2, 0, 16);//cipher(32 bytes) -> sp0(16 bytes)utils_str_to_hex(cipher, cipher_len, sp0, sizeof(sp0));//random(32 bytes) -> sp2(33 bytes)utils_str_to_hex(random, random_len, sp2, sizeof(sp2));//random(32 bytes) -> sp1(16 bytes)utils_sha256(random, random_len, sp1);//init AESconst uint8_t *key = sp1;const uint8_t *iv = sp2;AES_DIR_t dir = AES_DECRYPTION;aes_eng = aes128_init(key, iv, dir);if (!hal_aes_eng) {return ret;}//AES128-CBC decryptconst void *src = sp0;size_t blockNum = 1;void *dst = passwd;ret = aes128_cbc_decrypt(aes_eng, src, blockNum, dst);//de-init AESaes128_destroy(aes_eng);return ret;}

基本上,得到的C代码,可读性还是比较强的,前后的逻辑关联性也保留地比较完整,具备移植使用的条件。

4.4 将C代码嵌入编译得到新的固件

这一步就比较简单了,仅仅是将上面得到的C代码,填入一个新建的C文件,把代码中依赖的相关函数的头文件找出来,使用include包含,重新编译新的工程即可。

确保工程能够编译通过,正确输出固件包,即可进行下面的功能验证了。

5 成果验证

5.1 正向验证

所谓的正向验证,就是按照上面给出的操作流程,一步步将停留在静态库里面的汇编逻辑,转换成C代码,再把C代码编译得到新的固件包,烧录验证,从而确保之前那个未跑通的功能能够顺利跑通。

既然,我能把这篇总结发出来,自然这个功能层面的验证肯定是通过了的。

5.2 反向验证

所谓的反向验证,就是在上面的步骤基础之上,我们再讲得到的C代码源码编译成汇编代码,再对比下我们编译出来的汇编代码,与最初从SoCx中取得的汇编代码,差异究竟大不大,有没有与之想冲突的地方存在。

如果使用gcc编译器的话,添加-save-temps=obj编译选项,即可得到C代码对应的汇编代码。

下面我罗列下,两者的汇编的代码:

/* SoCx取得的汇编代码:如4.2章节所示 */

/* 得到的C代码经新的编译所得的汇编代码 */.section.text.decrypt_passwd,"ax",%progbits.align1.globaldecrypt_passwd.code16.thumb_func.typedecrypt_passwd, %functiondecrypt_passwd:.LFB1:.file 1 .loc 1 84 0.cfi_startproc@ args = 0, pretend = 0, frame = 104@ frame_needed = 0, uses_anonymous_args = 0.LVL0:push{r4, r5, r6, r7, lr}.cfi_def_cfa_offset 20.cfi_offset 4, -20.cfi_offset 5, -16.cfi_offset 6, -12.cfi_offset 7, -8.cfi_offset 14, -4movsr4, r1movsr5, r0subsp, sp, #108.cfi_def_cfa_offset 128.loc 1 88 0movsr1, #0.LVL1:.loc 1 84 0movsr6, r2.loc 1 88 0addr0, sp, #68.LVL2:movsr2, #33.LVL3:blmemset.LVL4:.loc 1 89 0movsr1, r4movsr2, #32addr0, sp, #68blmemcpy.LVL5:.loc 1 90 0movsr0, r5blstrlen.LVL6:movsr7, r0.LVL7:.loc 1 91 0addr0, sp, #68.LVL8:blstrlen.LVL9:movsr4, r0.LVL10:.loc 1 95 0movsr2, #16movsr1, #0movr0, sp.LVL11:blmemset.LVL12:.loc 1 96 0movsr2, #33movsr1, #0addr0, sp, #32blmemset.LVL13:.loc 1 97 0movsr2, #16movsr1, #0addr0, sp, #16blmemset.LVL14:.loc 1 100 0movsr0, r5movsr3, #16movr2, spmovsr1, r7blhexstr_convert.LVL15:.loc 1 103 0movsr3, #16addr2, sp, #16movsr1, r4addr0, sp, #68blhexstr_convert.LVL16:.loc 1 106 0addr2, sp, #32movsr1, r4addr0, sp, #68blutils_sha256.LVL17:.loc 1 112 0movsr2, #1addr1, sp, #16.LVL18:addr0, sp, #32.LVL19:blaes128_init.LVL20:subsr5, r0, #0.LVL21:.loc 1 113 0beq.L3.LVL22:.loc 1 121 0movsr3, r6movsr2, #1movr1, spblaes128_cbc_decrypt.LVL23:movsr4, r0.LVL24:.loc 1 124 0movsr0, r5.LVL25:blaes128_destroy.LVL26:.loc 1 126 0movsr0, r4b.L2.LVL27:.L3:.loc 1 114 0movsr0, #1.LVL28:rsbsr0, r0, #0.LVL29:.L2:.loc 1 127 0addsp, sp, #108@ sp needed.LVL30:.LVL31:.LVL32:pop{r4, r5, r6, r7, pc}

通过对比以上汇编代码,我们可以知道大体上两者是相同的,这也就证明了我们反编译的操作是成功的。

6 扩展延伸

前文,我也提到了我们的SDK是支持跨平台的,目前支持的SoC的CPU架构有ARMv5、ARMv7、RISC-V、X86等等。

由于这次遇到的第三方的SoCx恰好是ARMv5架构的,所以我们很熟练地使用ARM汇编的基础知识,就基本把汇编代码还原成C语言的伪代码了。

那么,如果SoCx是RISC-V架构的呢?如果SoCx是X86架构的呢?

万变不离其中,我们关键还是需要掌握SoC对应的汇编基础三大块内容:寻址方式、寄存器的用途、以及汇编指令的说明。

这里将RISC-V架构和X86架构,对应的这三大块知识放在这里,感兴趣的可以一看。

RISC-V架构的汇编基础三大块:详情请戳我。

X86架构的汇编基础三大块:详情请戳我。

7 经验总结

经过这次项目问题的历练,我得出了几个比较重要的经验,对我今后研发工作的开展也带来了一些新的思路,也希望对大家有所帮助。

几点总结如下:

从官方渠道解决不了的情况下,能否从其他第三方渠道挖掘一些重要信息,往往可能会带来一些不一样的惊喜;反编译技术是一门偏方,关键时刻可能救你一命;静态库只不过是个“纸老虎”,本质上说它并不是“安全的”;汇编代码并不可怕,掌握了汇编代码的核心分析方法,阅读它跟遇到C代码也没多大区别;反编译仅供技术研究,切勿违反商业约定;读过一些其他架构的汇编代码,你可能会发现ARM汇编还是比较简单的;开源不是万能的,且用且珍惜!

8 更多分享

欢迎关注我的github仓库01workstation,日常分享一些开发笔记和项目实战,欢迎指正问题。

同时也非常欢迎关注我的CSDN主页和专栏:

【CSDN主页:架构师李肯】

【RT-Thread主页:架构师李肯】

【C/C++语言编程专栏】

【GCC专栏】

【信息安全专栏】

【RT-Thread开发笔记】

【freeRTOS开发笔记】

【BLE蓝牙开发笔记】

【ARM开发笔记】

【RISC-V开发笔记】

有问题的话,可以跟我讨论,知无不答,谢谢大家。

如果觉得《【汇编实战开发笔记】一段汇编代码如何“反编译”成C代码?》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。