radare2逆向笔记

Posted 有价值炮灰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于radare2逆向笔记的主要内容,如果未能解决你的问题,请参考以下文章

用Radare2模拟shellcode运行

Android 逆向类加载器 ClassLoader ( 类加载器源码简介 | BaseDexClassLoader | DexClassLoader | PathClassLoader )(代码片段

Android 逆向Android 逆向通用工具开发 ( Android 平台运行的 cmd 程序类型 | Android 平台运行的 cmd 程序编译选项 | 编译 cmd 可执行程序 )(代码片段

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段

Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段