彙編調試技巧及簡單破解
為研究競品,學習了下如何破解,同時也可以更好地防禦自己的產品。
本文適合對彙編調試感興趣或想入門破解的同學。
以下以arm linux(android)為例。
elf簡介
linux系統的可執行文件(也包括obj,.a,.so)採用的格式是elf。
在網上找到的一張圖,很好地詮釋了elf的格式大致內容、布局,以及如何被載入和運行。
對於我們接下來要作的破解而言,最關鍵的信息是知道elf主要由頭、表、段組成。
一些常用的工具是objdump
和readelf
。當然gdb更是必不可少的。
要查看各個段的分布:
readelf -S a.out
要(16進位)輸出一個段(這裡輸出.rodata
):
readelf -x .rodata a.out
要反彙編代碼段(.text
),可以:
objdump -d a.out > a.out.dump
彙編指令
由於要破解的庫一般都是strip的,看不到源碼信息,所以,要很經常和彙編指令打交道。
不同的架構有不同的指令,如x86、arm。但基本不變的是彙編原理,也就是寄存器、PC(程序指針)、SP(棧指針)、常量/棧/內存讀寫。只有了解了這些基本原理,再看指令和了解架構差異,才能遊刃有餘。
這裡我要破解一個arm的庫,所以提前了解了一些arm指令,可以參考arm infocenter.
gdb
直接從objdump
解析的彙編代碼去推斷源碼是一件很費腦的事情,要強迫自己像機器一樣工作,在大腦中想像各個寄存器、指針的狀態。(而且往往彙編代碼是開了-O
優化過的)
所以藉助gdb
在運行態對目標文件進行「調試」,可以實時列印寄存器的值,跟蹤流程調用順序,快速理清代碼原理。
由於要調試的是彙編指令,所以和常規的基於源碼的調試略有不同。
這裡介紹幾個常用的gdb命令。
顯示反彙編代碼:
layout asm
彙編代碼單步進入:
si
彙編代碼單步跳過:
ni
顯示寄存器信息:
info registers
列印寄存器值:
p /x $r0
列印內存值(假設r0放著內存地址):
x $r0
我喜歡這樣調試:
左邊是objdump
的代碼,右邊是gdb環境,gdb執行layout asm
後也可以看到代碼,然後左右對照,邊調試,邊在左邊作筆記。
實戰HelloWorld
HelloWorld的好處是簡單,干擾元素少。可以基於HelloWorld驗證各種猜想,或理解編譯器行為。
比如,這裡我們可以用HelloWorld看看如何找到輸出"hello world"的代碼片段。
源碼:
#include <stdio.h>int main(int argc, char* argv[]) { printf("hello world"); return 0;}
反彙編:
arm-linux-androideabi-objdump -d a.out > a.out.dump56 00000428 <main>: 57 428: e59f0010 ldr r0, [pc, #16] ; 440 <main+0x18> 58 42c: e92d4008 push {r3, lr} 59 430: e08f0000 add r0, pc, r0 60 434: ebffffe9 bl 3e0 <printf@plt> 61 438: e3a00000 mov r0, #0 62 43c: e8bd8008 pop {r3, pc} 63 440: 00001730 .word 0x00001730
從彙編代碼中看不到"hello world"字元串,因為,字元串常量被放在了.rodata
段:
> arm-linux-androideabi-readelf -x .rodata a.outHex dump of section .rodata: 0x00001b68 68656c6c 6f20776f 726c6400 hello world.
那0x00001b68
地址是如何在代碼段中引用呢?偏移!
不妨調試下,跟到printf
那一行,我們知道函數調用的第一個參數,放在寄存器r0
,也就是printf的format
參數,即要輸出的hello world
,驗證下:
(gdb) p /x $r0$2 = 0xb6f1fb68(gdb) x /s $r00xb6f1fb68: "hello world"
再看r0
的計算過程,分兩步,取pc
+16給r0
,以及對r0
作加pc
操作,我們從main
開始再調試一次,這次觀察整個計算過程:
B+ │0xb6f90428 <main> ldr r0, [pc, #16] ; 0xb6f90440 <│ │0xb6f9042c <main+4> push {r3, lr} │ │0xb6f90430 <main+8> add r0, pc, r0 │ >│0xb6f90434 <main+12> bl 0xb6f903e0 <printf@plt> (gdb) ni0xb6f90430 in main ()(gdb) p /x $r0$2 = 0x1730(gdb) p $pc$3 = (void (*)()) 0xb6f90430 <main+8>(gdb) ni0xb6f90434 in main ()(gdb) p $pc$4 = (void (*)()) 0xb6f90434 <main+12>(gdb) p /x $r0$5 = 0xb6f91b68
可以看到ldr r0, [pc, #16]
之後,r0
的值是$2 = 0x1730
,也就是這行63 440: 00001730 .word 0x00001730
中的0x00001730
。
注意gdb的時候是裝載後的地址,比如
0xb6f90428
對應的是objdump中的428
(偏移了0xb6f90000
,這個值不是固定值)
接著在0xb6f90434
對r0
加pc
,r0 = 0x1730+0xb6f90430=0x0xb6f91b60
。明明gdb列印的是0xb6f91b68
,怎麼差了8?不對是嗎?
需要注意的是arm的pc超前2條指令:https://blog.csdn.net/lee244868149/article/details/49488575
所以應該是r0 = 0x1730+0xb6f90438=0x0xb6f91b68
,而0xb6f91b68
就是hello world
.
以上為讀者演示了如何用彙編調試的技巧去探索一個字元串如何在代碼中被引用。當目標程序在被盜版情況下有輸出調試信息時,這個調試信息就會成為一個突破口。
類似地,讀者可以用"Hello World"去嘗試其他想法。
實戰簡易破解
上面學習了如何定位字元串在代碼中如何被使用,下面給一個簡單的程序破解。
為模擬破解,這裡不給源碼。先觀察結果:
授權模式下運行:
# ./a.out hello world
盜版模式下運行:
# ./a.out thief!!!
字元串thief
是一個切入點,列印這個字元串的附近很可能有進行盜版檢查的代碼。
反彙編:
61 00000454 <main>: 62 454: e59f0044 ldr r0, [pc, #68] ; 4a0 <main+0x4c> 63 458: e92d4010 push {r4, lr} 64 45c: e24dd068 sub sp, sp, #104 ; 0x68 65 460: e08f0000 add r0, pc, r0 66 464: e1a0100d mov r1, sp 67 468: ebffffe4 bl 400 <stat@plt> 68 46c: e2504000 subs r4, r0, #0 69 470: 1a000005 bne 48c <main+0x38> 70 474: e59f0028 ldr r0, [pc, #40] ; 4a4 <main+0x50> 71 478: e08f0000 add r0, pc, r0 72 47c: ebffffe2 bl 40c <puts@plt> 73 480: e1a00004 mov r0, r4 74 484: e28dd068 add sp, sp, #104 ; 0x68 75 488: e8bd8010 pop {r4, pc} 76 48c: e59f0014 ldr r0, [pc, #20] ; 4a8 <main+0x54> 77 490: e08f0000 add r0, pc, r0 78 494: ebffffdc bl 40c <puts@plt> 79 498: e3e00000 mvn r0, #0 80 49c: eafffff8 b 484 <main+0x30> 81 4a0: 0000175c .word 0x0000175c 82 4a4: 0000175c .word 0x0000175c
這個程序比較簡單,這裡有2處puts
,可以從.rodata
結合代碼很容易推斷出代碼意圖,找到防盜版原理。
結合gdb,也容易發現,代碼運行路徑: 454一直運行到470,分支跳轉到48c,載入thief!!!
到r0
並列印。
那麼要破解,可以簡單地反轉470的判斷條件(可以用ghex
修改),即使盜版,也跳到正常代碼(當然還有更好的破解方案,留給讀者發揮了):
470: 1a000005 bne 48c <main+0x38>改為:470: 0a000005 bne 48c <main+0x38>
到此為止,基本的彙編調試技巧就介紹完了,對於簡(hen)易(shui)的防盜版,也可以破解了。
最後一節並沒有詳細展開,留給讀者思考。讀者可以參者上一節的方法自行調試,如果有疑問,或需要源碼,請留言告訴我~
推薦閱讀:
※ARM NEON 優化
※一文看懂ARM公司
※指令cache 為什麼比數據cache失效率低?
※ARM Neon 指令 解釋