radare2逆向筆記

radare2
最近剛開始學習逆向(Reverse Engineering), 發現其學習曲線也是挺陡峭的, 而網上的
許多writeup文章主旨總結就六個字:"你們看我屌嗎?" ...幾近炫技而對初學者不太友好.
所以我打算以初學者的身份來寫寫自己從入門到深入的經歷.

準備

當前許多逆向的writeup傾向於使用IDA-Pro, 而且似乎都依賴於F5(反編譯的快捷鍵), 直接
從二進制文件轉成了可讀的C代碼. 這對於實際工作來說也許是個捷徑, 但對於學習來說卻
沒什麼好處. 所以本文逆向採用了另一個開源的(但也同樣強大的)二進制分析工具--Radare2.
如果你是個資深的逆向人員, 那麼從本文也可以瞭解下radare2的一些功能.

知識準備

逆向軟件的時候往往面對的是彙編代碼, 所以對於指令集要有個大致的認識, 另外對於一些
模式(pattern), 比如函數入口(prologue), 出口(epilogue)和函數調用約定(calling convention)
等也要有所瞭解. 關於這類知識可以將RE4B(Reverse Engineering for Beginners)
這本書作爲參考. 書雖然比較厚, 但比較全面, 包括了X86/ARM/MIPS的內容和許多有趣的歷史典故,
一開始可以粗略掃一遍, 遇到問題再回頭仔細閱讀相關部分即可.

工具準備

當了解了基本彙編知識後(目前x86足矣), 就可以開始準備工具了. 說起工具我想起了一個寓言:

年輕人學有所成, 出山前問師父: "我準備練習武器, 請問哪種武器能讓我戰無不勝呢?"
師父說: "武器? 如果你的武器比你的頭腦更加鋒利, 那你將一無是處".

無論何時, 個人的頭腦和思維永遠是最重要的, 而武器只是工具, 永遠不要讓工具取代了你的思考.
當然也不是要你肉眼反彙編, 總之...你懂就行了! 我在Linux環境工作, 用到的幾個工具如下:

目標準備

初步打算是這一系列逆向文章使用IOLI的crackme文件來作爲目標, 總共3個平臺(Linux/Win32和Arm),
每個平臺有10個二進制文件, 都是從同樣的源碼編譯而來的. 可以從radare的git上下載.

crackme0x00

好的, 這是第一個目標, 首先了解你的敵人:

rabin2 -I crackme0x00

rabin2radare2套件中的一個工具, 主要用來提取二進制文件中的信息, 輸出如下:

arch     x86
binsz    7537
bintype  elf
bits     32
canary   false
class    ELF32
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
static   false
stripped false
subsys   linux
va       true

然後查看.rodata字段裏的字符串:

rabin2 -z crackme0x00

輸出如下:

000 0x00000568 0x08048568  24  25 (.rodata) ascii IOLI Crackme Level 0x00\n
001 0x00000581 0x08048581  10  11 (.rodata) ascii Password: 
002 0x0000058f 0x0804858f   6   7 (.rodata) ascii 250382
003 0x00000596 0x08048596  18  19 (.rodata) ascii Invalid Password!\n
004 0x000005a9 0x080485a9  15  16 (.rodata) ascii Password OK :)\n

運行一下該程序:

$ ./crackme0x00 
IOLI Crackme Level 0x00
Password: 123456
Invalid Password!

看樣子是要輸入密碼, 從rabin2的字符串輸出裏看到250382也許就是密碼, 我們可以輸入試試:

$ ./crackme0x00 
IOLI Crackme Level 0x00
Password: 250382
Password OK :)

好吧! 看樣子確實是. 畢竟是第一關, 有點簡單了. 但我還是先假裝不知道吧:)
如果目的是進入到Password OK分支, 那麼我們可以有多種解法, 如分析密碼的算法,
修改原文件(打patch), 利用漏洞, fuzzy等, 下面挑幾個說說.

解法1: 逆向分析

話不多說, 打開radare2:

r2 -A crackme0x00

進入後自動跳轉到了函數入口0x08048360, 然後用pdf命令來查看彙編代碼:

[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
 -- You can 'copy/paste' bytes using the cursor in visual mode 'c' and using the 'y' and 'Y' keys
[0x08048360]> pdf @ sym.main

pdf表示p(打印)d(反彙編)f(函數), @表示取地址, sym.main爲函數符號, 也可以用十六進制整數地址表示.
在r2中查看這些指令的幫助只要在後面輸入?即可, 比如p?查看打印類的命令, pd?查看打印反彙編類的命令.

打印反彙編的輸出如下:

            ;-- main:
/ (fcn) main 127
|   main ();
|           ; var int local_18h @ ebp-0x18
|           ; var int local_4h @ esp+0x4
|              ; DATA XREF from 0x08048377 (entry0)
|           0x08048414      55             push ebp
|           0x08048415      89e5           mov ebp, esp
|           0x08048417      83ec28         sub esp, 0x28               ; '('
|           0x0804841a      83e4f0         and esp, 0xfffffff0
|           0x0804841d      b800000000     mov eax, 0
|           0x08048422      83c00f         add eax, 0xf
|           0x08048425      83c00f         add eax, 0xf
|           0x08048428      c1e804         shr eax, 4
|           0x0804842b      c1e004         shl eax, 4
|           0x0804842e      29c4           sub esp, eax
|           0x08048430      c70424688504.  mov dword [esp], str.IOLI_Crackme_Level_0x00 ; [0x8048568:4]=0x494c4f49 ; "IOLI Crackme Level 0x00\n"
|           0x08048437      e804ffffff     call sym.imp.printf         ; int printf(const char *format)
|           0x0804843c      c70424818504.  mov dword [esp], str.Password: ; [0x8048581:4]=0x73736150 ; "Password: "
|           0x08048443      e8f8feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x08048448      8d45e8         lea eax, [local_18h]
|           0x0804844b      89442404       mov dword [local_4h], eax
|           0x0804844f      c704248c8504.  mov dword [esp], 0x804858c  ; [0x804858c:4]=0x32007325
|           0x08048456      e8d5feffff     call sym.imp.scanf          ; int scanf(const char *format)
|           0x0804845b      8d45e8         lea eax, [local_18h]
|           0x0804845e      c74424048f85.  mov dword [local_4h], str.250382 ; [0x804858f:4]=0x33303532 ; "250382"
|           0x08048466      890424         mov dword [esp], eax
|           0x08048469      e8e2feffff     call sym.imp.strcmp         ; int strcmp(const char *s1, const char *s2)
|           0x0804846e      85c0           test eax, eax
|       ,=< 0x08048470      740e           je 0x8048480
|       |   0x08048472      c70424968504.  mov dword [esp], str.Invalid_Password ; [0x8048596:4]=0x61766e49 ; "Invalid Password!\n"
|       |   0x08048479      e8c2feffff     call sym.imp.printf         ; int printf(const char *format)
|      ,==< 0x0804847e      eb0c           jmp 0x804848c
|      ||      ; JMP XREF from 0x08048470 (main)
|      |`-> 0x08048480      c70424a98504.  mov dword [esp], str.Password_OK_: ; [0x80485a9:4]=0x73736150 ; "Password OK :)\n"
|      |    0x08048487      e8b4feffff     call sym.imp.printf         ; int printf(const char *format)
|      |       ; JMP XREF from 0x0804847e (main)
|      `--> 0x0804848c      b800000000     mov eax, 0
|           0x08048491      c9             leave
\           0x08048492      c3             ret

一個典型的32位Linux程序, 這時候要想起函數的調用約定是通過棧來傳參, 如果忘了可以再看看RE4B哦.
看到main函數的彙編代碼了, 就開始分析了, 我不想像一些文章那樣貼個圖就說"顯而易見, 這裏的作用是XXX",
畢竟我也只是個新手, 雖然這只是一個超簡單的crackme, 但因爲是第一次, 我還是把流程完整地走一遍.

有了彙編接下來就開始分析了, r2和IDA一樣可以自己寫註釋和修改變量名稱, 在此之前我們先創建一個工程,
以保存這些修改:

[0x08048360]> Ps ioli0x00

這條指令的意思是保存一個名爲ioli0x00的(新)項目, 通常默認保存在~/.config/radare2/projects裏.

以P開頭的命令是項目工程管理相關(Project managment), 還記得之前說的嗎,
如果不記得命令, 可以通過P?來查看幫助.

然後跳轉到main並打印本地局部變量:

[0x08048360]> s main
[0x08048414]> afv
var int local_4h @ esp+0x4
var int local_18h @ ebp-0x18

s表示seek, 跳轉後發現我們已經到了0x08048414, afv表示a(分析)f(函數)v(變量), 可以看到有兩個局部變量.
(其實只有一個, 因爲esp+0x4是傳給子函數的參數).
再回到前面的彙編看看, 從0x08048448到0x08048456這幾條彙編可以發現local_4hlocal_18h的地址(指針),
而且local_4h是scanf的第二個參數, scanf的第一個參數爲0x804858c, 這個地址應該是個字符串, 我們打印下看看:

[0x08048414]> ps @ 0x804858c
%s

那麼local_18h應該就是用戶輸入的字符串了, 我們先給他們改個好聽的名字:

afvn local_18h input
afv-local_4h

afv-表示刪除某個名字, 這裏刪除了local_4h因爲它其實不是本地變量, 這時再次打印彙編就能看到改好的名字了:

0x08048448      8d45e8         lea eax, [input]
0x0804844b      89442404       mov dword [esp+4], eax
0x0804844f      c704248c8504.  mov dword [esp], 0x804858c  ; [0x804858c:4]=0x32007325
0x08048456      e8d5feffff     call sym.imp.scanf          ; int scanf(const char *format)
0x0804845b      8d45e8         lea eax, [input]
0x0804845e      c74424048f85.  mov dword [esp+4], str.250382 ; [0x804858f:4]=0x33303532 ; "250382" 
0x08048466      890424         mov dword [esp], eax        ; 這句和上一句相當於push str.250382; push eax
0x08048469      e8e2feffff     call sym.imp.strcmp         ; int strcmp(const char *s1, const char *s2)
0x0804846e      85c0           test eax, eax

這下就能比較清楚的看出上述代碼的核心目的了, 大約是:

char input[N];
scanf("%s", input)
strcmp(input, "250382")

由前面的sub esp, 0x28可知, 這裏的N應該小於40(沒錯! 這裏有一個棧溢出漏洞!). 不過對於函數
prologue之後的幾條彙編我還不是很明白作用是是啥, 希望有大神能告知一下~

至此, crackme0x00的分析基本完成. 雖然有些步驟看起來很繁瑣, 但對於分析大型項目還是很有用的.
尤其是給變量/參數命名, 給函數/代碼塊命名, 這樣會使得分析過程步步爲營, 柳暗花明.

解法2: 修改程序

當我們能直接接觸程序並且有修改權限時, 那麼修改該二進制文件也是個快速通關的好辦法!
回到剛剛的彙編輸出, 我們看到0x08048470這行有一個跳轉分支:

     0x0804846e      85c0           test eax, eax
 ,=< 0x08048470      740e           je 0x8048480
 |   0x08048472      c70424968504.  mov dword [esp], str.Invalid_Password ; [0x8048596:4]=0x61766e49 ; "Invalid Password!\n"
 |   0x08048479      e8c2feffff     call sym.imp.printf         ; int printf(const char *format)
,==< 0x0804847e      eb0c           jmp 0x804848c
||      ; JMP XREF from 0x08048470 (main)
|`-> 0x08048480      c70424a98504.  mov dword [esp], str.Password_OK_: ; [0x80485a9:4]=0x73736150 ; "Password OK :)\n"
|    0x08048487      e8b4feffff     call sym.imp.printf         ; int printf(const char *format)
|       ; JMP XREF from 0x0804847e (main)
`--> 0x0804848c      b800000000     mov eax, 0

test a, b的意思是若a AND b爲0, 則設置ZF位(以及SF/PF), je表示若ZF位被設置則跳轉,
說人話就是判斷前一個函數的返回值是否爲0(eax保存strcmp的返回值), 若爲0則跳轉到0x8048480(打印"Password OK :)\n"),
所以, 我們只需要把je改成無條件跳轉jmp就可以啦!
不過這時候要重新打開r2:

r2 -w crackme0x00

-w參數表示以可寫的方式打開程序, 而之前我們的打開方式是隻讀滴! 或者在之前的會話中輸入:

[0x08048414]> eval cfg.write=true

也同樣能獲得修改文件的權限.

改好之後首先我們先跳轉到想要修改指令的地方:

[0x08048414]> s 0x08048470
[0x08048470]>

這裏的機器碼是0x740e, 反彙編爲je 0x10, 不懂怎麼反? 這時候就要介紹radare2中的另一個工具rasm2了:

$ rasm2 -a x86 -b 32 -d "0x740e"
je 0x10
$ rasm2 -a x86 -b 32 "je 0x10"
740e

-a爲CPU架構, -b爲CPU寄存器位數, -d表示反彙編, 是不是很直觀? 接下來我們要把jz改爲jmp:

$ rasm2 -a x86 -b 32 "jmp 0x10"
eb0e

也就是說把74改爲eb就行了! 只改一個字節, 怎麼操作? 如下:

[0x08048470]> px 20
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x08048470  740e c704 2496 8504 08e8 c2fe ffff eb0c  t...$...........
0x08048480  c704 24a9                                ..$.
[0x08048470]> wx eb
[0x08048470]> px 20
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x08048470  eb0e c704 2496 8504 08e8 c2fe ffff eb0c  ....$...........
0x08048480  c704 24a9                                ..$.

px表示以hexdump形式打印當前位置的N個字節, wx表示在當前位置寫入
如果有疑問, 別忘了你的好幫手?哦, px?wx?都能查看對應的幫助.

OK現在改好了, 退出r2, 再運行下試試:

./crackme0x00 
IOLI Crackme Level 0x00
Password: fuckme
Password OK :)

現在輸入任何密碼都能通關啦!

解法3: 利用漏洞

剛剛在說解法1時, 最後看到了在scanf函數的執行過程中, 存在一個棧溢出漏洞, 先寫個小bash腳本驗證下:

#!/bin/bash
for i in {1..50};do
    input=$(cat /dev/urandom | tr -dc 'a-z0-9' | head -c $i)
    echo $input | ./crackme0x00
    if [ $? -ne 0 ];then
        echo "OOOOOps!!! input($input) of length $i crush this program!"
        break
    fi
done

輸出爲:

...
...
550 Segmentation fault      | ./crackme0x00
OOOOOps!!! input(rvibn45fg3eugqg6257rtyazsqclz) of length 29 crush this program!

實錘溢出跑不掉了. 那麼利用這個漏洞, 我們同樣可以實現通關, 甚至可以獲得任意系統命令執行!
首先查看二進制安全選項:

$ gdb crackme0x00 
Reading symbols from crackme0x00...(no debugging symbols found)...done.
(gdb) source ~/tools/peda/peda.py 
gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
gdb-peda$ aslr
ASLR is OFF

~/tools/peda就是下載的peda路徑, 可以直接從github上拉最新的.

可以看到NX是開着的, 但是很幸運, ASLR沒有開, 不過就算開了並不影響這節的示例, 結尾會說原因.
剛剛bash腳本的輸出告訴我們輸入長爲29字節的時候程序就崩潰了, 那麼猜測29字節開始就覆蓋了EIP,
但畢竟是猜測, 二進制的世界不接受模棱兩可的答案!

首先生成一組De Bruijn模式的序列, 目的只是爲了在溢出時候確認位置, 你用自己的方法也可以,
這裏用ragg2生成(ragg2也是radare2套件中的一個小工具):

$ ragg2 -P 40 -r > patt.txt
$ cat patt.txt
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAAN

然後啓動radare2並且將patt.txt文件的內容作爲調試程序的標準輸入:

$ cat > crackme0x00.rr2 <<EOF
#!/usr/bin/env rabin2
stdin=./patt.txt
EOF
$ r2 -e dbg.profile=crackme0x00.rr2 -d crackme0x00

進入r2交互界面之後, 依次輸入d(調試)c(繼續)wopO:

Process with PID 7943 started...
= attach 7943 7943
bin.baddr 0x08048000
Using 0x8048000
asm.bits 32
 -- See you at the defcon CTF
[0xf76e6a20]> dc
IOLI Crackme Level 0x00
Password: Invalid Password!
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x414b4141 code=1 ret=0
[0x414b4141]> wopO eip
28

wopO value表示在De Bruijn序列中找到value所代表的偏移量, 當然你也可以在patt.txt
中數數, eip當前的位置爲0x414b4141(即ascii AKAA), 也就是說輸入28字節以後就開始覆蓋eip
地址了, 確定具體的溢出位置之後, 接下來要做的就是控制PC指針, 劫持運行流程.

還記得解法2時要跳轉到正確分支的地址嗎? 不錯就是0x08048480, 來試試唄, 注意字節序哦:

$ python2 -c "print 'A'*28+'\x80\x84\x04\x08'" | ./crackme0x00
IOLI Crackme Level 0x00
Password: Invalid Password!
Password OK :)
Segmentation fault

成功執行了打印"Password OK"的分支, 但還是有點不完美, 因爲最後沒有清理好現場, 不過這足以說明我們
能夠掌控執行流程了!
一個優雅的exploit在劫持控制流程後還應該優雅退出, 而爲了執行更多函數, 比如system,exit等, 就需要知道
libc的(基)地址, 這裏ASLR沒開所以很容易獲得所有libc函數的地址, 利用ROP鏈就能做任何你想做的事,
這裏就不深入了!

解法4: 利用Fuzzy

這個方法綜合了上述的一些方式, 我們可以用暴力破解的方式來獲取密碼, 也可以利用afl或者libFuzzer
來自動化找出該程序潛在的bug(配合QEMU). 這種方式的壞處是太暴力了, 讓妹子不敢靠近(逃); 好處
則是在一定程度上解放了大腦, 用計算機來幫我們計算, 算力越強就越有可能找到突破點!

總結

本來是想多寫幾個crackme的, 但是由於這是第一篇, 就講詳細一點, 以後會深入一些更復雜和的程序,
寫幾篇真正意義上的writeup. 總結一下求解crackme類問題的方法, 1)逆向分析, 2)修改程序, 3)利用漏洞,
4)利用Fuzzy. 通常我們在遇到實際問題時是會將這些方式結合起來用的, 比如雖然逆向分析了一部分代碼,
但某部分算法特別複雜, 那麼就會借用Fuzzy的思想, 對這部分邏輯進行Symbolic Execution, 以最快的方式解決戰鬥!

  • 能力有限, 文中錯誤在所難免, 歡迎交流!
  • 文章轉載請註明來源: https://www.pppan.net/ 謝謝!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章