マルウェアのバイナリコードの静的解析には,ツールで完全に自動で行う方法もありますが,コードを人間が読んで理解する方法もあります.
私もたくさん経験していますが,逆アセンブルしたアセンブリコードの処理や意図を理解するのはかなり骨が折れる作業です.アセンブリコードがちょっと複雑になったりちょっと長くなったりすると,とたんにわからなくなります.時間もかかります.
逆アセンブルして得られたアセンブリコードを読んで理解するよりも,デコンパイラ(逆コンパイラ)でデコンパイル(逆コンパイル)して得られたCプログラムのコードを読んで理解するほうが,はるかに楽だし短時間で終わります.
現在入手可能なデコンパイラですぐ思いつくものを挙げておきます.
- Hex-Rays Decompiler IDAと組み合わせて使う.商用.個人での購入を躊躇したくなる価格.
- Ghidra オープンソース.
- RetDec (Repository in GitHub) オープンソース.かつてはAvastの製品.
- Boomerang Decompiler (Repository in GitHub) オープンソース.
C言語ではこんなに単純なコードも
#include <stdio.h>
#include <stdlib.h>
int fact(int n)
{
if (n <= 1) {
return 1;
} else {
return n * fact(n - 1);
}
}
int main(void)
{
int i, ans;
for (i = 1; i <= 10; i++) {
ans = fact(i);
printf("fact(%d) = %d\n", i, ans);
}
return 0;
}
アセンブリ言語レベルでは処理の流れや意図をつかむのすら大変で時間がかかる.以下はobjdump -dでの逆アセンブル結果.バイナリは32ビットでビルドした.
0000051d <fact>:
51d: 55 push %ebp
51e: 89 e5 mov %esp,%ebp
520: 83 ec 08 sub $0x8,%esp
523: e8 92 00 00 00 call 5ba <__x86.get_pc_thunk.ax>
528: 05 b0 1a 00 00 add $0x1ab0,%eax
52d: 83 7d 08 01 cmpl $0x1,0x8(%ebp)
531: 7f 07 jg 53a <fact+0x1d>
533: b8 01 00 00 00 mov $0x1,%eax
538: eb 16 jmp 550 <fact+0x33>
53a: 8b 45 08 mov 0x8(%ebp),%eax
53d: 83 e8 01 sub $0x1,%eax
540: 83 ec 0c sub $0xc,%esp
543: 50 push %eax
544: e8 d4 ff ff ff call 51d <fact>
549: 83 c4 10 add $0x10,%esp
54c: 0f af 45 08 imul 0x8(%ebp),%eax
550: c9 leave
551: c3 ret
00000552 <main>:
552: 8d 4c 24 04 lea 0x4(%esp),%ecx
556: 83 e4 f0 and $0xfffffff0,%esp
559: ff 71 fc pushl -0x4(%ecx)
55c: 55 push %ebp
55d: 89 e5 mov %esp,%ebp
55f: 53 push %ebx
560: 51 push %ecx
561: 83 ec 10 sub $0x10,%esp
564: e8 b7 fe ff ff call 420 <__x86.get_pc_thunk.bx>
569: 81 c3 6f 1a 00 00 add $0x1a6f,%ebx
56f: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)
576: eb 2d jmp 5a5 <main+0x53>
578: 83 ec 0c sub $0xc,%esp
57b: ff 75 f0 pushl -0x10(%ebp)
57e: e8 9a ff ff ff call 51d <fact>
583: 83 c4 10 add $0x10,%esp
586: 89 45 f4 mov %eax,-0xc(%ebp)
589: 83 ec 04 sub $0x4,%esp
58c: ff 75 f4 pushl -0xc(%ebp)
58f: ff 75 f0 pushl -0x10(%ebp)
592: 8d 83 68 e6 ff ff lea -0x1998(%ebx),%eax
598: 50 push %eax
599: e8 12 fe ff ff call 3b0 <printf@plt>
59e: 83 c4 10 add $0x10,%esp
5a1: 83 45 f0 01 addl $0x1,-0x10(%ebp)
5a5: 83 7d f0 0a cmpl $0xa,-0x10(%ebp)
5a9: 7e cd jle 578 <main+0x26>
5ab: b8 00 00 00 00 mov $0x0,%eax
5b0: 8d 65 f8 lea -0x8(%ebp),%esp
5b3: 59 pop %ecx
5b4: 5b pop %ebx
5b5: 5d pop %ebp
5b6: 8d 61 fc lea -0x4(%ecx),%esp
5b9: c3 ret
以下はGhidraでのデコンパイル結果.はるかにわかりやすくなった.元のCプログラムを理解する作業とほとんど変わらない.forループはwhileループに変わった.
int fact(int param_1)
{
int iVar1;
__x86.get_pc_thunk.ax();
if (param_1 < 2) {
iVar1 = 1;
}
else {
iVar1 = fact(param_1 + -1);
iVar1 = iVar1 * param_1;
}
return iVar1;
}
undefined4 main(void)
{
undefined4 uVar1;
int local_18;
local_18 = 1;
while (local_18 < 0xb) {
uVar1 = fact(local_18);
printf("fact(%d) = %d\n",local_18,uVar1);
local_18 = local_18 + 1;
}
return 0;
}
以下はHex-Rays Decompilerでのデコンパイル結果.こちらもわかりやすい.変数名がiだし.if文のthen節とelse節は逆転.
int __cdecl fact(int a1)
{
int result; // eax
if ( a1 > 1 )
result = a1 * fact(a1 - 1);
else
result = 1;
return result;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+0h] [ebp-10h]
int v5; // [esp+4h] [ebp-Ch]
for ( i = 1; i <= 10; ++i )
{
v5 = fact(i);
printf("fact(%d) = %d\n", i, v5);
}
return 0;
}
デコンパイラによって,10分かかっていた静的解析の作業が1分で終わるようになったと感じます.