反汇编容易反编译难

您所在的位置:网站首页 c程序反编译 反汇编容易反编译难

反汇编容易反编译难

2024-05-24 00:36| 来源: 网络整理| 查看: 265

最近受同学所托,将5个内嵌了MASM语法格式的汇编代码之C函数翻译成纯C函数,以支持多种CPU指令集(比如x86/x64, arm, sparc, ...)。整个过程充满了艰辛,但也充满了乐趣。作为一个既喜欢C又喜欢汇编的程序员,在废寝忘食之余深深地体会到,“反汇编(disassemble)容易,反编译(decompile)难”。逆向工程实在是太不容易啦!下面给出一个简单点儿的例子予以说明。[P.S. 一肚子的Asm, C和gdb,终于派上了用场:-)]

o void add16(word *, word *, word n)

1 typedef unsigned char byte; /* 1 byte */ 2 typedef unsigned short word; /* 2 bytes */ 3 typedef unsigned int dword; /* 4 bytes */ 4 typedef unsigned long long qword; /* 8 bytes */ 5 6 void add16(word *a, word *b, word n) 7 { 8 word *dstp = a; 9 word *srcp = b; 10 word count = n; 11 12 __asm { 13 xor ecx, ecx 14 mov cx, count 15 mov esi, srcp 16 mov edi, dstp 17 xor ebx, ebx 18 19 LOOP01: 20 xor eax, eax 21 mov ax, [esi] 22 add eax, ebx 23 xor edx, edx 24 mov dx, [edi] 25 add eax, edx 26 27 mov [edi], ax 28 shr eax, 16 29 mov ebx, eax 30 31 add esi, 2 32 add edi, 2 33 loop LOOP01 34 35 add [edi], bx 36 } 37 }

作为一个对x86汇编熟悉N(>=12) 年的程序员,翻译上面的内嵌代码,花了足足5个小时。 5小时的艰苦历程如下:

第1步: 将汇编代码摘取出来,用NASM实现 (本人不喜欢Windows编程,所以在Linux上用NASM)

第2步: 给每一行汇编代码加注释,加完注释后的汇编源文件foo.asm如下:

1 BITS 32 2 3 SECTION .data 4 5 count: equ 0x5 6 dstp: dw 0x1234, 0x3456, 0xffff, 0x789a, 0x9abc, 0x0000 7 srcp: dw 0xabcd, 0xffff, 0xffff, 0x0123, 0x2345, 0x0000 8 9 SECTION .text 10 11 global _start 12 13 _start: 14 xor ecx, ecx ; ecx = 0 15 mov cx, count ; ecx = count 16 mov esi, srcp ; esi = srcp 17 mov edi, dstp ; edi = dstp 18 xor ebx, ebx ; ebx = 0 /* carry */ 19 20 LOOP01: 21 xor eax, eax ; eax = 0 22 mov ax, [esi] ; ax = *esi = *srcp 23 add eax, ebx ; eax += ebx /* eax += carry */ 24 xor edx, edx ; edx = 0 25 mov dx, [edi] ; dx = *edi = *dstp 26 add eax, edx ; eax += edx; /* eax += *dstp */ 27 28 mov [edi], ax ; *edi = ax, i.e. *dstp = ax 29 shr eax, 16 ; eax >>= 16 30 mov ebx, eax ; ebx = eax /* next carry */ 31 32 add esi, 2 ; esi += 2 /* srcp++ */ 33 add edi, 2 ; edi += 2 /* dstp++ */ 34 loop LOOP01 ; jmp back to LOOP01 until ecx == 0 35 36 add [edi], bx ; *edi += bx 37 38 _exit: 39 mov eax, 1 ; syscall num of exit 40 mov ebx, 0 ; error code 41 int 0x80

第3步: 编译foo.asm,用gdb调试

$ nasm -f elf32 -g -F stabs foo.asm $ ld -o foo foo.o

用gdb单步调试的过程比较冗长,这里贴出最简版调试过程,

$ gdb foo GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 ....................................... (gdb) set disassembly-flavor intel (gdb) # (gdb) (gdb) disas _start Dump of assembler code for function _start: 0x08048080 : xor ecx,ecx 0x08048082 : mov cx,0x5 0x08048086 : mov esi,0x80490cc 0x0804808b : mov edi,0x80490c0 0x08048090 : xor ebx,ebx End of assembler dump. (gdb) # (gdb) (gdb) disas LOOP01 Dump of assembler code for function LOOP01: 0x08048092 : xor eax,eax 0x08048094 : mov ax,WORD PTR [esi] 0x08048097 : add eax,ebx 0x08048099 : xor edx,edx 0x0804809b : mov dx,WORD PTR [edi] 0x0804809e : add eax,edx 0x080480a0 : mov WORD PTR [edi],ax 0x080480a3 : shr eax,0x10 0x080480a6 : mov ebx,eax 0x080480a8 : add esi,0x2 0x080480ab : add edi,0x2 0x080480ae : loop 0x8048092 0x080480b0 : add WORD PTR [edi],bx End of assembler dump. (gdb) # (gdb) (gdb) disas _exit Dump of assembler code for function _exit: 0x080480b3 : mov eax,0x1 0x080480b8 : mov ebx,0x0 0x080480bd : int 0x80 End of assembler dump. (gdb) # (gdb) (gdb) (gdb) (gdb) (gdb) (gdb) set disassembly-flavor intel (gdb) display /i $eip (gdb) # (gdb) (gdb) b _start Breakpoint 1 at 0x8048080 (gdb) b _exit Breakpoint 2 at 0x80480b3 (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x08048080 2 breakpoint keep y 0x080480b3 (gdb) # (gdb) (gdb) r Starting program: /var/tmp/sandbox/fanli/raw/05/cnblog/foo Breakpoint 1, 0x08048080 in _start () 1: x/i $eip => 0x8048080 : xor ecx,ecx (gdb) # (gdb) (gdb) ni 0x08048082 in _start () 1: x/i $eip => 0x8048082 : mov cx,0x5 (gdb) 0x08048086 in _start () 1: x/i $eip => 0x8048086 : mov esi,0x80490cc (gdb) 0x0804808b in _start () 1: x/i $eip => 0x804808b : mov edi,0x80490c0 (gdb) 0x08048090 in _start () 1: x/i $eip => 0x8048090 : xor ebx,ebx (gdb) info r ecx edi esi ecx 0x5 5 edi 0x80490c0 134516928 esi 0x80490cc 134516940 (gdb) x /5hx 0x80490c0 0x80490c0 : 0x1234 0x3456 0xffff 0x789a 0x9abc (gdb) x /5hx 0x80490cc 0x80490cc : 0xabcd 0xffff 0xffff 0x0123 0x2345 (gdb) # (gdb) (gdb) c Continuing. Breakpoint 2, 0x080480b3 in _exit () 1: x/i $eip => 0x80480b3 : mov eax,0x1 (gdb) # (gdb) (gdb) x /5hx 0x80490c0 0x80490c0 : 0xbe01 0x3455 0xffff 0x79be 0xbe01 (gdb) x /5hx 0x80490cc 0x80490cc : 0xabcd 0xffff 0xffff 0x0123 0x2345 (gdb) # (gdb) (gdb) ni 0x080480b8 in _exit () 1: x/i $eip => 0x80480b8 : mov ebx,0x0 (gdb) 0x080480bd in _exit () 1: x/i $eip => 0x80480bd : int 0x80 (gdb) [Inferior 1 (process 21920) exited normally] (gdb)

从上面的调试过程可以看出,

a: 0x1234 0x3456 0xffff 0x789a 0x9abc b: 0xabcd 0xffff 0xffff 0x0123 0x2345 After a = a + b is done, a: 0xbe01 0x3455 0xffff 0x79be 0xbe01 That is, (1) a = 0x9abc789affff34561234 (2) b = 0x23450123ffffffffabcd (3) a += b (4) a = 0xbe0179beffff3455be01

第4步:用Python验证一下上面的计算结果,

$ python Python 2.7.6 (default, Oct 26 2016, 20:32:47) ....................................... >>> a = 0x9abc789affff34561234 >>> b = 0x23450123ffffffffabcd >>> a += b >>> print " a = 0x%x" % a a = 0xbe0179beffff3455be01

第5步: 将foo.asm翻译成foo2.c

1 #include 2 3 typedef unsigned char byte; /* 1 byte */ 4 typedef unsigned short word; /* 2 bytes */ 5 typedef unsigned int dword; /* 4 bytes */ 6 typedef unsigned long long qword; /* 8 bytes */ 7 8 void add16(word *a, word *b, word n) 9 { 10 word *dstp = a; 11 word *srcp = b; 12 word count = n; 13 14 word carry = 0; 15 for (word i = 0; i < count; i++) { 16 word p = *dstp; 17 word q = *srcp; 18 19 dword n = (dword)p + (dword)carry + (dword)q; 20 21 *dstp = n & 0xFFFF; // *dstp : low 16 bits of n 22 carry = (word)(n >> 16); // carry : high 16 bits of n 23 24 srcp++; 25 dstp++; 26 } 27 28 *dstp += carry; 29 } 30 31 static void dump(word a[], word n) 32 { 33 printf("\t%p: ", a); 34 for (word i = 0; i < n; i++) 35 printf("0x%04x ", a[i]); 36 printf("\n"); 37 } 38 39 int main(int argc, char *argv[]) 40 { 41 word src[] = {0xabcd, 0xffff, 0xffff, 0x0123, 0x2345, 0x0000}; 42 word dst[] = {0x1234, 0x3456, 0xffff, 0x789a, 0x9abc, 0x0000}; 43 word n = 0x5; 44 45 dump(dst, sizeof(dst)/sizeof(word)); 46 printf("+\n"); 47 dump(src, sizeof(src)/sizeof(word)); 48 49 add16(dst, src, n); 50 51 printf("=\n"); 52 dump(dst, sizeof(dst)/sizeof(word)); 53 54 return 0; 55 }

o add16()截图

第6步: 编译foo2.c并运行

$ gcc -g -Wall -m32 -std=c99 -o foo2 foo2.c $ ./foo2 0xbf9c1bc4: 0x1234 0x3456 0xffff 0x789a 0x9abc 0x0000 + 0xbf9c1bb8: 0xabcd 0xffff 0xffff 0x0123 0x2345 0x0000 = 0xbf9c1bc4: 0xbe01 0x3455 0xffff 0x79be 0xbe01 0x0000

与foo.asm对应的运算结果做比较,

0xbf9c1bc4: 0xbe01 0x3455 0xffff 0x79be 0xbe01 0x0000

二者运算的结果完全一致,由此可见,add16()其实就是做大数加法。

a = {a[0], a[1], ..., a[N]}, a[i] 为一个word, 占两个字节 b = {b[0], b[1], ..., b[N]},  b[i]为一个word, 占两个字节 a + b = {a[0]+b[0], a[1]+b[1], ..., a[N]+b[N]} a[i] + b[i] 可能发生进位(carry), 将carry加到a[i+1]的位置上即可

P.S. 在翻译过程中,我参考了下面两个与转移指令有关的文档。 (如果有兴趣,请阅读)

汇编语言转移指令规则汇总 常用汇编指令与标志位关系

结束语: 上面给出的例子foo.asm只有一重循环,所以翻译成C代码相对简单。但是,如果有多重循环和多次跳转, 那么翻译起来就困难许多。例如:

1 BITS 32 2 3 SECTION .data 4 5 adp: dd 0x12345678, 0x9abcdef0, 0xffffffff, 0x9abcdefa, \ 6 0x00000000 7 adp_size: equ $ - adp 8 bdp: dd 0xbfffc061, 0xfedcba99, 0x76543211, 0xfedcba99, \ 9 0x00000000 10 bdp_size: equ $ - bdp 11 cdp: dd 0x00000000, 0x00000000, 0x00000000, 0x00000000, \ 12 0x00000000, 0x00000000, 0x00000000, 0x00000000, \ 13 0x00000000, 0x00000000 14 cdp_size: equ $ - cdp 15 16 SECTION .text 17 18 global _start 19 20 _start: 21 mov ecx, cdp ; 001 ecx = cdp, cdp[] = adp[] * bdp[] 22 add ecx, cdp_size ; 002 ecx += cdp_size 23 24 xor edi, edi ; 003 edi = 0 25 26 LOOP1: ; 004 27 mov ebx, adp ; 005 ebx = adp 28 add ebx, edi ; 006 ebx += edi 29 mov eax, [ebx] ; 007 eax = *ebx 30 cmp eax, 0 ; 008 if (eax == 0) 31 je NEXT3 ; 009 goto NEXT3 32 xor esi, esi ; 010 esi = 0 33 push eax ; 011 save eax to stack (eax was *ebx = [adp+edi]) 34 35 LOOP2: ; 012 36 mov ebx, bdp ; 013 ebx = bdp 37 add ebx, esi ; 014 ebx += esi 38 mov eax, [ebx] ; 015 eax = *ebx 39 cmp eax, 0 ; 016 if (eax == 0) 40 je NEXT2 ; 017 goto NEXT2 41 pop edx ; 018 get edx from stack, which was pushed @ 011 42 push edx ; 019 save edx back to stack 43 mul edx ; 020 edxeax = eax * edx 44 mov ebx, cdp ; 021 ebx = cdp 45 add ebx, esi ; 022 ebx += esi; 46 add ebx, edi ; 023 ebx += edi; 47 push ebx ; 024 push ebx to stack 48 add [ebx], eax ; 025 *ebx += eax; 49 50 LOOP3: ; 026 51 jnc NEXT1 ; 027 == jae (>=) 52 add ebx, 4 ; 028 ebx += 4 53 cmp ebx, ecx ; 029 if (ebx >= ecx) ; ecx = cdp + cdp_size 54 jae NEXT1 ; 030 goto NEXT1 55 add dword [ebx], 1 ; 031 *ebx += 1 56 jmp LOOP3 ; 032 go back to LOOP3 57 58 NEXT1: ; 033 only used by LOOP3 59 pop ebx ; 034 get ebx from stack 60 add ebx, 4 ; 035 ebx += 4 61 add [ebx], edx ; 036 *ebx += edx 62 63 LOOP4: ; 037 64 jnc NEXT2 ; 038 jnc (=jae) 65 add ebx, 4 ; 039 ebx += 4 66 cmp ebx, ecx ; 040 if (ebx >= ecx) 67 jae NEXT2 ; 041 goto NEXT2 68 add dword [ebx], 1 ; 042 *ebx += 1 69 jmp LOOP4 ; 043 go back to LOOP4 70 71 NEXT2: ; 044 72 add esi, 4 ; 045 esi += 4 73 cmp esi, bdp_size ; 046 if (esi < bdp_size) 74 jb LOOP2 ; 047 go back to LOOP2 75 pop eax ; 048 get eax from stack 76 77 NEXT3: ; 049 78 add edi, 4 ; 050 edi += 4 79 cmp edi, adp_size ; 051 if (edi < adp_size) 80 jb LOOP1 ; 052 go back to LOOP1 81 82 _exit: 83 mov eax, 1 ; syscall num of exit 84 mov ebx, 0 ; error code 85 int 0x80

上面的代码一共包括4个LOOP,3个NEXT和10个跳转指令(e.g. jmp, jb, jae, jnc, je),翻译成C代码难度非常大。究其本质,实为做大数乘法。有关其翻译实现后的C代码,请参考前文。 反汇编(将C代码翻译成汇编代码)有现成的工具可用(e.g. gdb, objdump), 所以很容易。反编译(将汇编代码翻译成C代码),国外有收费的软件可以用(e.g. Hex-Rays Decompiler),但是也不能保证100%的正确性。所以,反汇编容易,反编译难,逆向工程很不容易。 (p.s. 有一个学信息工程专业的(但从来没做过程序员的)同学在微信群里用略带不屑的语气说“汇编是一种很古老的语言”,我真的很无语Orz...再高级的语言写的代码,也要变成机器码才能运行不是,似乎汇编是无法绕过的...)



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3