Radare 2 动态调试 - 数据修改简易入门示范
以下部分内容直接从 Telegram 频道 duangsuse::Echo 拷贝,许可 CC-BY
Radare 2 是著名的开源跨平台逆向分析框架,能够读取多种文件格式、支持很多动态分析调试器、有很多种前端(诸如基于 Qt 的 Cutter)可以使用,也可以远程调试,支持插件并且 CLI 非常方便,能够进行许多厉害的静态分析。
Radare 2 堪比商业软件 IDA,R2 工程组里是有前端的,只不过他们比较喜欢推广 CLI Shell 而已,有些人可能认为 R2 没有前端只能用 CLI,实际上 R2 的 HTML 前端是非常友好的,也非常适合进行远程调试 这里 R2 在开源系逆向工程工具里的地位就好像类似于 KODI 在 Home Theater 软件里的地位了,自由软件一家独大
个人认为,Radare 2 可能是世界上最好的『二进制编辑器、扇区编辑器、系统 I/O 工具、二进制可视化分析工具(like bindiff、base conversion)、反汇编器/JIT 替换汇编器、Shellcode 工具、逆向代码分析器、动态静态调试器、解析器』而且非常『可扩展、可移植、可嵌入』
一句话:Cross-platform UNIX-like multi-architecture reverse engineering framework and commandline toolchain for security/forensics. debug.exe(MSDOS) for the 21ₛₜ century
因为作者没有啥时间整理知识给你们学的缘故,所以就抄了一点代码下来你们自己理解吧很抱歉,因为我懒得讲一些基本的东西了...
r2 - # r2 malloc://512
# and write these rlang commands
w ascii # write 5 x ASCII Chars [unsigned byte, u8] "ascii"
V # open visual mode, or `x' to print head buffer bytes
pr 5 # print first 5 bytes directly into stdout
s/x 00 # seek to next NUL byte(after "ascii")
w z # write character 'z'
px # view the buffer now
? # Radare 2 is self-documented, show help!
s? # show help for 'S'eek family commands
=H # open a HTTP server for HTML-based front-end debugging
q # quit Radare 2, or use ^(Ctrl)D
man -s1 radare2 # read r2 command-line launcher manual
ls /usr/share/doc/radare2 # read bundled documents yourself!
r2 -v # show r2 version
r2 -V # show radare 2 lib versions
r2 -L # list supported IO kinds
r2 -c=H /bin/yes # debug/view a binary program using web UI
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char *msg = "duangsuse 菜鸡\n";
int main(int argc, const char **argv) {
char *str = malloc(sizeof(msg));
strcpy(str, msg);
if (argc > 1) printf(str);
return -1;
}
其中 strcpy(str, msg)
是把常量 msg
复制到 str
这个 malloc()
堆分配好的容器里,约等于 C \00
结尾字符串的 *dst = *src
然后这个程序就是 argc
数目大于 1 才会打印字符串,就是说 ./hck
(['./hck']
)不会打印 ./hck foo
(['./hck', 'foo']
)会
然后它要返回 -1,就是说执行完 $?
就是 255(因为一个 8 位的二进制下溢了,u8, unsigned char
的最小值是 0 最大值是 255)
- 修改这个字符串,把它改成
“duangsuse 笨蛋”
(吐槽:这么自轻自贱么 - 在不使用额外参数(输入
argc = 1
)的时候打印这个字符串 - 让
main()
返回 0 而不是 -1
gcc hack_me.c -o hck -g # 编译
./hck # 应该无输出退出
echo $?
#=> 255
./hck foo
#=> duangsuse 菜鸡
[DuangSUSE@duangsuse]~/Projects/reveng% r2 hck
-- I accidentally the kernel with radare2.
[0x004004a0]>
doo # 开始调试,使用 doo 而不是 do 指令可以添加参数
db main # 在 main 入口处处中断
dr # 找到 rsp
# 然后我们 seek 到 rsp, 也可以使用命令 "s rsp"
0x7ffc42efd520 # rsp 的地址
*rsp # 输出 0x1,就是 argc
*rsp = 0x2 # 随便弄个大于 1 的数,这里暂时不教大家怎么修改程序逻辑,暂时教怎么获取和修改数据
[0x7ffc42efd520]> dc
hit breakpoint at: 400586
[0x00400586]> dc
duangsuse 菜鸡
[0x7f2182d35f96]> dc
child exited with status 255
==> Process finished
我们在 main
上打上断点,这样,执行到 main()
调用后,程序执行中断,我们可以使用 radare 2 进行调试操作
程序中断后,我们预期的程序系统调用栈是这样的(可以使用 px
命令检查是否真的如此):
注意,在 x86_64 里,一个 int
类型数占用 8 个字节,或者 4 个半字,也叫两个字(一个字即为 4 字节)
int main(int argc, char **argv);
// sizeof(int) = 0x8
argc = *(rsp + 0x0) // offset of argc
argv = *(rsp + 0x8 * 1) // offset of argv(+1 * sizeof(int))
// e.g. 0x7fff55c3e3e0(rsp) [0200 0000 0000 0000], [fd06 c455 ff7f 0000]
// ',' 前面是 argc, 后面是 argv 的指针
// r2 调试一下!
// doo
// s rsp
// px
// *rsp+0x0
// *rsp+0x8*1
// s `*rsp+0x8*1`; px
那么,现在你能利用 rsp
寄存器访问到程序的参数了吗?试试找到 argv 的第一个字符串 offset (偏移量)吧!
我们会在 main()
的最后一条指令上打断点,然后使用 r2 修改 rax
寄存器指示下的函数返回值
首先,我们来看看在哪条指令上打断点。
db main
dc
#=> hit breakpoint at: 400586
aaaa # analyze here for better analyze performance
VVVV # View function branch graph
doo # reset debugging
pdf main # disassembly main()
我们在最后一条 leave
(即为 pop rbp
,重置基栈指针)指令上打下断点
db 0x004005d5
dc
#=> hit breakpoint at: 4005d5
dr rax
#=> 0xffffffff
dr rax=0x0
#=> 0xffffffff ->0x00000000
dc; dc # continue until program finishes
!echo $?
#=> 0
依据 x86_64 中 CDEF 调用约定,返回 int
函数的返回值被存储在 rax
寄存器中(因为函数正好只能有一个返回值)
我们在函数返回前修改 rax
寄存器的值,就达到了修改函数返回值的效果
我们先对 main
子程序的执行流程进行分析,试试上面的 VVVV
附近的指令片段
然后,我们将通过替换 printf()
调用的第一个参数的方法(复习刚才的技巧)完成此 hack
doo foo
db main
dc
db reloc.printf
dc
dbt
好吧,通过动态链接 relocation(重定位)插 stub 的 printf
方法调试有点累并且容易失败,我们直接在主逻辑上断点
db- reloc.printf
doo foo
# 然后在 call printf 前一条指令打断点 db
db 0x004005c6
dc
#=>hit breakpoint at: 4005c6
px @rsp
*`*rsp`
# 可以使用 ds、pd <insn count> 让程序执行到一个合适的时候
dbt
#=> 1 0x4005d0 sp: 0x7ffe8208b168 8 [??] main+74
# 找到数据区字符串的位置
px @0x7ffe8208b168
px @`*0x7ffe8208b168`
#=> - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
#=> 0x00400660 6475 616e 6773 7573 6520 e88f 9ce9 b8a1 duangsuse ......
# 好吧,那个是静态的(
px @ `*rsp+0x8*3` # rsp 为在 call printf 之前的状态
# 这个才是真正的参数,因为我们要去掉 ret_addr 和 rbp
# 好吧,不是这样的,这里利用了本地变量访问,具体看下面的 WHY
s `*rsp+0x8*3`
w duangsuse 笨蛋
dc
#=> duangsuse 笨蛋
dc
#=> child exited with status 255
调用 printf(str)
之前,程序应该使用 push
指令(或等价方法)将 str
的指针设置到了 rsp
栈顶,就像这样(汇编 HelloWorld 示例)
...好吧,我骗了你们,因为 x86_64 上的 CDEF 调用约定(Calling conventation)和 ix86 上的不是一样的,参数传递允许使用寄存器,自己试试用更好的方法拿到字符串指针吧?
section .rodata
fmt db `%s %i\n`, 0x0
section .data
msg db "Hello, world!", 0x0
section .text
extern printf
global _start
global main
global print
_start:
mov ebp, esp
; arguments to main:
; _start(int argc, char **argv)
call main
; exit(eax)
mov ebx, eax
mov eax, 1
int 0x80
print:
push ebp
mov ebp, esp
mov ecx, 100
add ecx, 1
push ecx
push msg
push fmt
call printf
; clean-up for `printf' call
; increase stack pointer by argument size
; to simply ignore them
; sizeof(int) + sizeof(char *) * 2
add esp, 12
leave
ret
main:
push ebp
mov ebp, esp
call print
xor eax, eax
leave
ret
nasm -felf 1p1helo.asm; ld -m elf_i386 1p1helo.o -lc -o helloworld -I /usr/lib/ld-2*;./helloworld
我们看看 print(str)
的编译结果是什么,顺便让你知道这个执行栈是如何维护的
; main() 里的其他代码
; variable qword(64) str @ rbp-0x8
; variable dword(32) argc @ rbp-0x14
; variable qword(64) argv @ rbp-0x14-0xc ; (8) ; (=-0x20)
push rbp
mov rbp, rsp
sub rsp, 0x20 ; 3 个 int 本地变量,0x8 * 3 + 1(偏移 offset) = 0x20
mov dword [rbp - 0x14], edi ; argc ; 0x14 = 20
mov qword [rbp - 0x20], rsi ; argv ; 0x20 = 32
mov edi, 8
call malloc ; rax = malloc(8)
mov qword [rbp - 0x8], rax ; ptr = malloc(8)
mov rdx, qword msg
mov rax, qword [rbp - 0x8]
mov rsi, rdx
mov rdi, rax
call strcpy ; rax = strcpy(rdi, rsi)
cmp [rbp - 0x14], 1
jle exit ; less, than if (obj <= 1) goto exit;
; http://unixwiz.net/techtips/x86-jumps.html
; HERE
mov rax, qword [rbp - 0x8]
mov rdi, rax
mov eax, 0
call printf ; printf(rdi)
; END
exit:
mov eax, 1
leave
ret
还是不懂?可以尝试自己调试、使用 *
和 px
什么的,现在貌似没有时间给你们画动图讲解汇编栈帧操作...
提示:尝试自己用更多的方法完成此 Hack 吧?
- 写入
malloc()
调用分配的堆内存以替换实际的字符串 - 利用本地变量偏移指针而不是
rsp
指针找到str
的值 - 仅在
write()
系统调用(syscall
)时打断点来完成修改str
值的任务 暴力扫描内存修改字符串
呃,其实因为我开始功课做得有点不足,然后就有点错漏,包括 x86 和 x86_64 的调用约定差别我没注意到,死认为参数必须靠栈传递了,导致教程写错
最后一段其实也有错误,不过不管哪里有错,希望大家能接受。
那么这教的大概就是一些基本的 native 动态分析技能,如此。在 x86 上有点区别,不过更简单一些了应该(x86 上参数基本都是栈上分配,不是按通用寄存器传递)
简化嘛... 比较难,因为我懒了而且都是干货,底层的事情不是几个接口定义就完成的,而且这写了两个小时了...