敞开成长之旅!这是我参加「日新计划 12 月更文应战」的第3天,点击查看活动概况
作者简介
架构师李肯(全网同名)
一个专注于嵌入式IoT范畴的架构师。有着近10年的嵌入式一线开发经历,深耕IoT范畴多年,熟知IoT范畴的业务开展,深度把握IoT范畴的相关技能栈,包括但不限于干流RTOS内核的完结及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其完结、底层汇编及编译原理、编译优化及代码重构、干流IoT云渠道的对接、嵌入式IoT系统的架构规划等等。具有多项IoT范畴的发明专利,热衷于技能共享,有多年撰写技能博客的经历堆集,连续多月获得RT-Thread官方技能社区原创技能博文优秀奖,荣获CSDN博客专家、CSDN物联网范畴优质创作者、2021年度CSDN&RT-Thread技能社区之星、RT-Thread官方嵌入式开源社区认证专家、RT-Thread 2021年度论坛之星TOP4、华为云云享专家(嵌入式物联网架构规划师)等荣誉。深信【常识改动命运,技能改动世界】!
【汇编实战开发笔记】一段汇编代码怎么“反编译”成C代码?
假如给你一段汇编代码,要你反编译成C言语代码,你会怎么做?
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 sub sp, #72 ; 0x48
428636 10051ed0: 4605 mov r5, r0
428637 10051ed2: 460c mov r4, r1
428638 10051ed4: 4616 mov r6, r2
428639 10051ed6: 2100 movs r1, #0
428640 10051ed8: 2210 movs r2, #16
428641 10051eda: a801 add r0, sp, #4
428642 10051edc: f02d fda0 bl 1007fa20 <memset>
428643 10051ee0: 2221 movs r2, #33 ; 0x21
428644 10051ee2: 2100 movs r1, #0
428645 10051ee4: a809 add r0, sp, #36 ; 0x24
428646 10051ee6: f02d fd9b bl 1007fa20 <memset>
428647 10051eea: 2210 movs r2, #16
428648 10051eec: 2100 movs r1, #0
428649 10051eee: a805 add r0, sp, #20
428650 10051ef0: f02d fd96 bl 1007fa20 <memset>
428651 10051ef4: 782b ldrb r3, [r5, #0]
428652 10051ef6: b1f3 cbz r3, 10051f36 <decrypt_passwd+0x6a>
428653 10051ef8: 2310 movs r3, #16
428654 10051efa: aa01 add r2, sp, #4
428655 10051efc: 2120 movs r1, #32
428656 10051efe: 4628 mov r0, r5
428657 10051f00: f7c2 fa8a bl 10014418 <hexstr_convert>
428658 10051f04: 2310 movs r3, #16
428659 10051f06: 4620 mov r0, r4
428660 10051f08: aa05 add r2, sp, #20
428661 10051f0a: 2120 movs r1, #32
428662 10051f0c: f7c2 fa84 bl 10014418 <hexstr_convert>
428663 10051f10: 4620 mov r0, r4
428664 10051f12: aa09 add r2, sp, #36 ; 0x24
428665 10051f14: 2120 movs r1, #32
428666 10051f16: f7c4 ff96 bl 10016e46 <utils_sha256>
428667 10051f1a: 2201 movs r2, #1
428668 10051f1c: a905 add r1, sp, #20
428669 10051f1e: a809 add r0, sp, #36 ; 0x24
428670 10051f20: f7dd fb62 bl 1002f5e8 <aes128_init>
428671 10051f24: 4633 mov r3, r6
428672 10051f26: 4604 mov r4, r0
428673 10051f28: 2201 movs r2, #1
428674 10051f2a: a901 add r1, sp, #4
428675 10051f2c: f7dd fbb3 bl 1002f696 <aes128_cbc_decrypt>
428676 10051f30: 4620 mov r0, r4
428677 10051f32: f7dd fba3 bl 1002f67c <aes128_destroy>
428678 10051f36: 2000 movs r0, #0
428679 10051f38: b012 add sp, #72 ; 0x48
428680 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 sub sp, #72 ; 0x48
428636 10051ed0: 4605 mov r5, r0 #r5=r0 //encoded
428637 10051ed2: 460c mov r4, r1 #r4=r1 //p_ranodm_str
428638 10051ed4: 4616 mov r6, r2 #r6=r2 //passwd
428639 10051ed6: 2100 movs r1, #0
428640 10051ed8: 2210 movs r2, #16
428641 10051eda: a801 add r0, sp, #4
428642 10051edc: f02d fda0 bl 1007fa20 <memset> //memset(sp0, 0, 16)
428643 10051ee0: 2221 movs r2, #33 ; 0x21
428644 10051ee2: 2100 movs r1, #0
428645 10051ee4: a809 add r0, sp, #36 ; 0x24
428646 10051ee6: f02d fd9b bl 1007fa20 <memset> //memset(sp1, 0, 33)
428647 10051eea: 2210 movs r2, #16
428648 10051eec: 2100 movs r1, #0
428649 10051eee: a805 add r0, sp, #20
428650 10051ef0: f02d fd96 bl 1007fa20 <memset> //memset(sp2, 0, 16)
428651 10051ef4: 782b ldrb r3, [r5, #0]
428652 10051ef6: b1f3 cbz r3, 10051f36 <decrypt_passwd+0x6a>
428653 10051ef8: 2310 movs r3, #16
428654 10051efa: aa01 add r2, sp, #4
428655 10051efc: 2120 movs r1, #32
428656 10051efe: 4628 mov r0, r5
428657 10051f00: f7c2 fa8a bl 10014418 <hexstr_convert> //encoded->sp0(hex)
428658 10051f04: 2310 movs r3, #16
428659 10051f06: 4620 mov r0, r4
428660 10051f08: aa05 add r2, sp, #20
428661 10051f0a: 2120 movs r1, #32
void hexstr_convert(char *hexstr, uint8_t *out_buf, int len);
428662 10051f0c: f7c2 fa84 bl 10014418 <hexstr_convert> //p_ranodm_str->sp2(hex)
428663 10051f10: 4620 mov r0, r4
428664 10051f12: aa09 add r2, sp, #36 ; 0x24
428665 10051f14: 2120 movs r1, #32
void utils_sha256(const uint8_t *input, uint32_t ilen, uint8_t output[32])
428666 10051f16: f7c4 ff96 bl 10016e46 <utils_sha256> //sha256(p_ranodm_str,32,sp1)
428667 10051f1a: 2201 movs r2, #1
428668 10051f1c: a905 add r1, sp, #20
428669 10051f1e: a809 add r0, sp, #36 ; 0x24
p_HAL_Aes128_t aes128_init(_IN_ const uint8_t *key, _IN_ const uint8_t *iv,
_IN_ AES_DIR_t dir)
428670 10051f20: f7dd fb62 bl 1002f5e8 <aes128_init> //key=sp1,iv=sp2,dir=1(decrypt)
428671 10051f24: 4633 mov r3, r6
428672 10051f26: 4604 mov r4, r0
428673 10051f28: 2201 movs r2, #1
428674 10051f2a: a901 add r1, sp, #4
int aes128_cbc_decrypt(_IN_ p_HAL_Aes128_t aes, _IN_ const void *src,
_IN_ size_t blockNum, _OU_ void *dst)
428675 10051f2c: f7dd fbb3 bl 1002f696 <aes128_cbc_decrypt> //src=sp0,blockNum=1,dst=passwd
428676 10051f30: 4620 mov r0, r4
428677 10051f32: f7dd fba3 bl 1002f67c <aes128_destroy> // int aes128_destroy(_IN_ p_HAL_Aes128_t aes)
428678 10051f36: 2000 movs r0, #0
428679 10051f38: b012 add sp, #72 ; 0x48
428680 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 AES
const 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 decrypt
const void *src = sp0;
size_t blockNum = 1;
void *dst = passwd;
ret = aes128_cbc_decrypt(aes_eng, src, blockNum, dst);
//de-init AES
aes128_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
.align 1
.global decrypt_passwd
.code 16
.thumb_func
.type decrypt_passwd, %function
decrypt_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, -4
movs r4, r1
movs r5, r0
sub sp, sp, #108
.cfi_def_cfa_offset 128
.loc 1 88 0
movs r1, #0
.LVL1:
.loc 1 84 0
movs r6, r2
.loc 1 88 0
add r0, sp, #68
.LVL2:
movs r2, #33
.LVL3:
bl memset
.LVL4:
.loc 1 89 0
movs r1, r4
movs r2, #32
add r0, sp, #68
bl memcpy
.LVL5:
.loc 1 90 0
movs r0, r5
bl strlen
.LVL6:
movs r7, r0
.LVL7:
.loc 1 91 0
add r0, sp, #68
.LVL8:
bl strlen
.LVL9:
movs r4, r0
.LVL10:
.loc 1 95 0
movs r2, #16
movs r1, #0
mov r0, sp
.LVL11:
bl memset
.LVL12:
.loc 1 96 0
movs r2, #33
movs r1, #0
add r0, sp, #32
bl memset
.LVL13:
.loc 1 97 0
movs r2, #16
movs r1, #0
add r0, sp, #16
bl memset
.LVL14:
.loc 1 100 0
movs r0, r5
movs r3, #16
mov r2, sp
movs r1, r7
bl hexstr_convert
.LVL15:
.loc 1 103 0
movs r3, #16
add r2, sp, #16
movs r1, r4
add r0, sp, #68
bl hexstr_convert
.LVL16:
.loc 1 106 0
add r2, sp, #32
movs r1, r4
add r0, sp, #68
bl utils_sha256
.LVL17:
.loc 1 112 0
movs r2, #1
add r1, sp, #16
.LVL18:
add r0, sp, #32
.LVL19:
bl aes128_init
.LVL20:
subs r5, r0, #0
.LVL21:
.loc 1 113 0
beq .L3
.LVL22:
.loc 1 121 0
movs r3, r6
movs r2, #1
mov r1, sp
bl aes128_cbc_decrypt
.LVL23:
movs r4, r0
.LVL24:
.loc 1 124 0
movs r0, r5
.LVL25:
bl aes128_destroy
.LVL26:
.loc 1 126 0
movs r0, r4
b .L2
.LVL27:
.L3:
.loc 1 114 0
movs r0, #1
.LVL28:
rsbs r0, r0, #0
.LVL29:
.L2:
.loc 1 127 0
add sp, 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开发笔记】
有问题的话,能够跟我讨论,知无不答,谢谢咱们。