Skip to content

Instantly share code, notes, and snippets.

@duangsuse
Created December 23, 2018 09:03
Show Gist options
  • Save duangsuse/5ca62db55f06d4d97d72729d11a3e6d3 to your computer and use it in GitHub Desktop.
Save duangsuse/5ca62db55f06d4d97d72729d11a3e6d3 to your computer and use it in GitHub Desktop.
Radare 2 动态调试基本使用和基本 x86_64 汇编教程

Radare 2 动态调试 - 数据修改简易入门示范

以下部分内容直接从 Telegram 频道 duangsuse::Echo 拷贝,许可 CC-BY

PART 0x00 认识 Radare 2 逆向工程/取证框架

Radare 2 是著名的开源跨平台逆向分析框架,能够读取多种文件格式、支持很多动态分析调试器、有很多种前端(诸如基于 QtCutter)可以使用,也可以远程调试,支持插件并且 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

PART 0x01 代码示例

因为作者没有啥时间整理知识给你们学的缘故,所以就抄了一点代码下来你们自己理解吧 很抱歉,因为我懒得讲一些基本的东西了...

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

PART 0x02 开始,Hacking!

Naive Sample Program in C99/Glibc

#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)

Three Targets

  • 修改这个字符串,把它改成 “duangsuse 笨蛋”(吐槽:这么自轻自贱么
  • 在不使用额外参数(输入 argc = 1)的时候打印这个字符串
  • main() 返回 0 而不是 -1

Let's Test it!

gcc hack_me.c -o hck -g # 编译
./hck # 应该无输出退出
echo $?
#=> 255
./hck foo
#=> duangsuse 菜鸡

Hack #1,修改 argc 参数

[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

WHY?

我们在 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 (偏移量)吧!

Hack #2,让 main() 返回 0

我们会在 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

WHY??

依据 x86_64 中 CDEF 调用约定,返回 int 函数的返回值被存储在 rax 寄存器中(因为函数正好只能有一个返回值)

我们在函数返回前修改 rax 寄存器的值,就达到了修改函数返回值的效果

Hack #3,修改 main() 中的 str 指针

我们先对 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

WHY???

调用 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

word assembly

还是不懂?可以尝试自己调试、使用 *px 什么的,现在貌似没有时间给你们画动图讲解汇编栈帧操作...

提示:尝试自己用更多的方法完成此 Hack 吧?

  • 写入 malloc() 调用分配的堆内存以替换实际的字符串
  • 利用本地变量偏移指针而不是 rsp 指针找到 str 的值
  • 仅在 write() 系统调用(syscall)时打断点来完成修改 str 值的任务
  • 暴力 扫描内存修改字符串
@duangsuse
Copy link
Author

呃,其实因为我开始功课做得有点不足,然后就有点错漏,包括 x86 和 x86_64 的调用约定差别我没注意到,死认为参数必须靠栈传递了,导致教程写错
最后一段其实也有错误,不过不管哪里有错,希望大家能接受。

那么这教的大概就是一些基本的 native 动态分析技能,如此。在 x86 上有点区别,不过更简单一些了应该(x86 上参数基本都是栈上分配,不是按通用寄存器传递)

简化嘛... 比较难,因为我懒了而且都是干货,底层的事情不是几个接口定义就完成的,而且这写了两个小时了...

@duangsuse
Copy link
Author

https://godbolt.org/ 推荐一个在线编译结果对比检查工具

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment