逆向(二): 常用的逆向方法

常用的逆向方法

正如前文所述,逆向的核心在于理解程序的逻辑和数据流。以下是一些常用的逆向方法:

  1. 静态分析:通过查看程序的二进制文件或反编译后的代码,理解其结构和逻辑。常用工具包括IDA Pro、Hopper等。
  2. 动态分析:运行程序并监控其行为,使用调试器(如GDB、OllyDbg)和动态分析工具(如Frida、Xposed)来观察程序的运行时状态。

在不使用额外的工具的情况下,下面主要介绍一下比较原始的逆向方法。

静态分析

一般情况下,无论是C还是Objective-C,编译后产生的二进制文件中都会包含符号表(如果没有被剥离的话)。符号表中包含了函数名、变量名等信息,这些信息可以帮助我们理解程序的结构。

查看符号表

比如下面的c代码:

1
2
3
4
5
6
7
8
#include <stdio.h>
void hello() {
printf("Hello, World!\n");
}
int main() {
hello();
return 0;
}

使用clang编译:

1
clang -o hello hello.c

编译后生成的二进制文件中会包含hellomain函数的符号信息。可以使用nm命令来查看二进制文件的符号表:

1
nm -gU hello

如果这里使用static关键字修饰hello函数:

1
2
3
4
5
6
7
8
#include <stdio.h>
static void hello() {
printf("Hello, World!\n");
}
int main() {
hello();
return 0;
}

重新编译后,使用nm命令查看符号表:

1
nm -gU hello

可以看到,hello函数的符号信息已经不在符号表中。关于强符号和弱符号的区别,可以参阅CSAPP相关章节。

这个特点很重要,大部份关键的验证函数可能在动态链接库中,那么这些函数的符号信息一般不会被剥离掉,因为程序需要通过这些符号信息来链接动态库中的函数。

因为这个特点,我们可有针对性的对这部分内容进行分析。如果是C程序,关键的验证函数在二进制文件里面,但符号信息没有被剥离掉,那么我们可以使用系统提供的调试能力在运行时对这些函数的行为做修改。如果是Objective-C程序,那么我们可以使用runtime的能力对这些函数进行hook。或者直接修改二进制汇编代码,然后重新编译生成新的二进制文件。如果关键的验证函数在动态库中,那么可以在加载动态库前,在动态链接库中查找函数的地址,插入特殊的指令,跳转到我们自己实现的函数中。

修改的方式大致有以下几类:

  1. 修改汇编代码:直接修改二进制文件中的汇编代码,插入跳转指令,跳转到我们自己实现的函数中。
  2. 使用系统提供的调试能力:使用系统的陷入指令,允许父进程查看和修改子进程的资源,在运行时修改函数的行为。
  3. 注入动态库:通过注入动态库,使用动态库中的函数在运行时修改函数的行为。

修改汇编代码

修改汇编代码是最原始的逆向方法之一。通过修改二进制文件中的汇编代码,可以改变程序的行为。但每次应用更新后,都需要重新修改汇编代码,比较繁琐,需要有一定的汇编基础。应用更新时,绝大部分情况下,关键的验证函数不会有太大的变化,所以这种方式用的比较少,优点是简单直接,不要用依赖别的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int is_vip(int user_id) {
if (user_id == 12345) {
return 1; // VIP
} else {
return 0; // Not VIP
}
}

int main() {
int user_id;
printf("Enter your user ID: ");
scanf("%d", &user_id);

if (is_vip(user_id)) {
printf("Welcome, VIP user!\n");
} else {
printf("Access denied. You are not a VIP user.\n");
}

return 0;
}

使用clang编译:

1
clang -o vip_check vip_check.c

编译后生成的二进制文件中会包含is_vipmain函数的符号信息。把编译后的二进制文件拖进Hopper中,找到is_vip函数的汇编代码:

反汇编

鼠标点击右边流程图的_is_vip函数,按下键盘上的X键,可以查看引用符号的地址:

引用符号

可以看到,is_vip函数在main函数中被调用,并且是无条件跳转。

调用关系

查看is_vip函数的汇编代码:

is_vip函数

点击右上角的if(b) f(x);按钮,可以切换到伪代码视图:

接下来,我们修改is_vip函数的汇编代码。视图切到汇编代码视图,选中is_vip函数的第一条指令,我们的目标是让is_vip函数无论输入什么值都返回1。我们可以把函数的第一条指令修改为mov w0, #0x1,然后在函数的最后一条指令前插入ret指令。

修改汇编代码

修改入口

修改完成后是这样的

修改完成

查看一下反汇编代码:

查看汇编代码

至于为什么使用w0寄存器,是因为在ARM64架构中,函数的返回值通常存储在x0寄存器中,而w0x0的低32位部分。由于is_vip函数返回的是一个整数值(1或0),我们只需要修改w0寄存器即可。

当然我们也可以修改源代码,然后重新编译生成新的二进制文件,查看具体会生成什么样的汇编代码,直接返回1的汇编代码如下:

1
2
3
4
5
6
                     _is_vip:
0000000100000460 sub sp, sp, #0x10 ; CODE XREF=_main+56
0000000100000464 str w0, [sp, #0x10 + var_4]
0000000100000468 mov w0, #0x1
000000010000046c add sp, sp, #0x10
0000000100000470 ret

这里面除了过程调用导致的栈空间调整和保存参数外,关键的就是mov w0, #0x1ret两条指令。

保存修改,重新编译生成新的二进制文件:
重新编译

保存并重新签名运行:

1
2
codesign -fs - --deep ./vip_check_modified
./vip_check_modified

Hopper是付费应用,保存修改需要付费。如果不想付费,可以查看之前的文章修改二进制可执行文件

Xcode调试

打开Xcode,选择菜单栏的Debug -> Debug Executable..,选择二进制文件,点击Debug按钮。

Xcode调试

或者选择Attach to Process,选择正在运行的进程。

Xcode调试

使用Debug Executable..时,弹出的窗口里面修改一下终端类型:

修改终端类型

这样才可以在Xcode的控制台中输入数据。

点击左上角的运行按钮,输入用户ID,点击回车:

Xcode调试

接下来,我们在左边的导航栏中选择Debug,然后添加一个符号断点

添加符号断点

选择符号断点,点击右下角的+按钮,选择Symbolic Breakpoint
添加符号断点
,在弹出的窗口中输入函数名is_vip,然后点击Done按钮。
添加符号断点

然后重新运行程序,输入用户ID,点击回车:

Xcode调试

在ret这行语句添加一个断点:

添加断点

我们在lldb控制台中输入以下命令:

1
2
3
reg write w0 1
reg r w0
c

修改寄存器

继续运行程序:
继续运行

除了使用Xcode自带的调试功能外,还可以使用lldb命令行调试工具。lldb是LLVM项目的一部分,是一个强大的调试器,支持多种编程语言和平台。通过操作系统提供的ptrace接口,允许父进程查看和修改子进程的资源。这类常用的逆向方法,一般使用frida、xposed等工具来实现。使用Xcode或者lldb命令行调试工具,可以更直观地理解程序的逻辑和数据流,但不够自动化,比较适合简单的逆向任务。

未完待续…