Ptrace的简单实现(出题小计)

参考文章:

威力巨大的系统调用——ptrace

系统学习vm虚拟机逆向

偶然间看到一个有关ptrace的题目,对ptrace有点兴趣,于是研究了一下Ptrace的简单实现,下面是我写的一个简单的逆向Ptrace题目(Ptrace+VM)

一.Ptrace的父子进程的实现

1.父进程(father)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h> 
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h> //提供ptrace系统调用的接口
#include <sys/types.h> //定义系统相关的数据类型
#include <sys/wait.h> //提供进程控制函数
#include <unistd.h> //提供POSIX操作系统API
#include <errno.h> //提供错误报告机制

int main()
{
printf("Input your key:");
char key[64];
scanf("%32s", key);
pid_t child;
child = fork();

if(child)
{
if (child < 0)
{
perror("fork");
return -1;
}

printf("Waiting for child...\n");
fflush(stdout);
wait(NULL);
printf("Wait done.\n");
fflush(stdout);

long patch_value = 3;
if (ptrace(PTRACE_POKEDATA, child, (void*)0x404060, (void*)patch_value) == -1) {
perror("ptrace POKEDATA");
} else {
printf("Patched success\n");
}
fflush(stdout);

if (ptrace(PTRACE_CONT, child, NULL, NULL) == -1) {
perror("ptrace CONT");
}
printf("Continued child.\n");
fflush(stdout);
wait(NULL);
printf("Child exited.\n");
fflush(stdout);
}
else
{
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
printf("NONONO!!!\n");
exit(1);
}
execl("./son", "son", key, 0);
perror("execl");
exit(1);
}

return 0;
}

pid_t 是一个系统定义的数据类型,通常在 或 头文件中声明。

  • 有符号整数类型:通常是 int 或 long,具体取决于系统架构。
  • 足够大:能表示系统中可能存在的最大进程 ID。

fork() 是 Unix/Linux 系统中创建新进程的核心函数,它允许一个进程(父进程)复制自身,创建一个几乎完全相同的子进程。

  • 返回值

    • -1:创建子进程失败(如内存不足或进程数达到系统限制)。
    • 0:在子进程中返回。
    • 正整数在父进程中返回,该值是子进程的进程 ID(PID)一个大于0的整数

perror 的主要作用是:

  • 读取全局变量 errno:errno 存储了最近一次系统调用的错误码(整数)。
  • 转换错误码为文本信息:将 errno 映射为对应的错误描述字符串(如 “No such file or directory”)。
  • 输出错误信息:格式为 [自定义字符串]: [系统错误描述]。

perror(“fork”) 是 C 语言中用于输出系统错误信息的函数调用,通常在系统调用失败后使用。

fflush(stdout)强制将标准输出缓冲区中的所有内容立即输出到终端或文件,无论缓冲区是否已满或是否遇到换行符。

wait() 是 C 语言中用于进程同步的系统调用,主要用于父进程等待子进程结束并获取其退出状态。下面从多个角度详细解析其功能、应用场景和注意事项:

函数原型与头文件

1
2
#include <sys/wait.h>
pid_t wait(int *status);
  • 参数:status 是一个整型指针,用于存储子进程的退出状态(若不关心状态,可传 NULL)。

  • 返回值

    • 成功:返回结束的子进程的 PID(正整数)。
    • 失败:返回 -1,并设置 errno(如无子进程可等待)。

wait系统调用:

wait系统调用是一个用来进行进程控制的系统调用,它可以用来阻塞父进程,当父进程接收到子进程传来信号或者子进程退出时,父进程才会继续运行。所以这里的wait系统调用很显然用来接收子进程调用execl时产生的SIGTRAP信号。

execl 系统调用语句:

execl语句可以将当前进程替换成一个新进程。在本例中,execl(“./son”, “son”, key, 0);语句将原本的子进程替换成了son文件里面的指令。值得注意的是,如果execl系统调用的进程处于PTRACE_TRACEME状态的话,就会发送一个SIGTRAP信号给父进程,并让自身处于Traced状态。

PTRACE_TRACEME 的作用

PTRACE_TRACEME 是 Linux 系统中 ptrace() 函数的一个重要请求类型,用于将当前进程设置为被调试状态,允许父进程对其进行跟踪。这是实现调试器(如 GDB)和反调试技术的基础。

ptrace() 函数原型:

1
2
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
  • 参数

    • request:指定 ptrace 的操作类型(如 PTRACE_TRACEME、PTRACE_POKEDATA 等)。
    • pid:目标进程的 PID(对 PTRACE_TRACEME 无效,填 0 或任意值)。
    • addr:内存地址(用于读写内存等操作,对 PTRACE_TRACEME 无效,填 0 或任意值)。
    • data:数据值(对 PTRACE_TRACEME 无效,填 0 或任意值)。

当一个进程调用 ptrace(PTRACE_TRACEME, 0, 0, 0) 时:

  1. 标记自身为被跟踪进程:该进程会成为被调试者(tracee),其父进程自动成为调试者(tracer)
  2. 触发 SIGTRAP 信号:调用后,进程会立即给自己发送一个 SIGTRAP 信号,导致自身暂停执行。
  3. 父进程控制:父进程可通过 wait() 捕获该信号,并使用 ptrace 的其他请求(如 PTRACE_CONT、PTRACE_PEEKTEXT)控制子进程的执行。
可以用于反调试操作(最简单的一类)

由于 ptrace 一个进程同一时间只能被一个调试器跟踪ptrace 是独占的),所以如果在程序启动时调用 PTRACE_TRACEME

当子进程调用:

1
ptrace(PTRACE_TRACEME, 0, NULL, NULL);

它会告诉内核:

“允许我的父进程跟踪我(调试我)”

  • 如果成功,子进程会被暂停,并发送 SIGTRAP 信号给父进程,父进程可以通过 wait() 等待并控制子进程。
  • 如果失败(例如已经有一个调试器在跟踪该进程),ptrace 会返回 -1,并设置 errno = EPERM(权限错误)。
当然,绕过这个反调试的方法也很简单

(1)可以在在 ptrace 调用前断点,修改返回值,这样程序会认为没有被调试。

(2) 使用 LD_PRELOAD 劫持 ptrace

编写一个假的 ptrace 函数,返回 0

1
2
3
4
// fake_ptrace.c
long ptrace(int request, pid_t pid, void *addr, void *data) {
return 0; // 总是返回成功
}

编译并注入:

1
2
gcc -shared -fPIC fake_ptrace.c -o fake_ptrace.so
LD_PRELOAD=./fake_ptrace.so ./anti_debug

这样 ptrace 调用会被劫持,返回 0,绕过检测。

PTRACE_POKEDATA

用于向被跟踪进程(tracee)的内存写入数据。这是实现调试器、内存补丁和代码注入等功能的基础。返回值:成功返回 0,失败返回 -1 并设置 errno。

  • 内存写入:将 data 的值(long 类型,通常为 4 字节或 8 字节,取决于系统架构)写入目标进程 pid 的内存地址 addr 处。
  • 原子操作:写入操作是原子的,即一次写入一个完整的 long 类型值。

ptrace() 函数原型同上

ptrace(PTRACE_POKEDATA, child, (void*)0x404060, (void*)patch_value)

这里的作用是将son文件0x404060地址处的数据跟改为3

什么是 EIP/RIP?
  • x86 架构下: EIP = 指令指针寄存器 (Instruction Pointer),保存下一条指令的地址。
  • x86-64 架构下: RIP = 64位指令指针

任何CPU执行都依赖它:

CPU每执行一条指令,都用EIP/RIP去内存取指令。

注意:怎么找子进程实际地址?

可以直接

1
nm -n son

来进行查找整个子进程的的偏移(如果你没开PIE程序的话这直接就是这个地址,开了的话需要加载基址 + 偏移)

子进程运行时,查看 /proc//maps 中 .data 或 .bss 区域加载地址。

查看:

1
readelf -h son

如果Type: DYN (共享目标文件),就表示是PIE

如果是Type:EXEC (可执行文件)就表示非PIE

  • PIE程序会随机加载基址
  • 非PIE程序,地址是固定的。

你可以用指令来生成非PIE程序的ELF文件

1
gcc -no-pie -g -o son son.c

这里用-no-pie确保是非PIE程序,地址固定。

PTRACE_CONT

用于 让被调试的进程(tracee)继续执行。**Ptrace函数原型同上,**这里讲解一下参数

参数 类型 说明
request enum __ptrace_request 必须是PTRACE_CONT
pid pid_t 目标进程的 PID(子进程)
addr void* 通常传 NULL(历史遗留参数,一般不用)
data void* 可选信号(如SIGTRAP0表示无信号)
关键注意事项

(1) 子进程必须处于暂停状态

  • PTRACE_CONT 只能用于已经被 ptrace 暂停的进程(比如调用了 PTRACE_TRACEMEPTRACE_ATTACH)。
  • 常见错误:如果子进程没有暂停,PTRACE_CONT 会返回 -1errno = ESRCH

(2) addr 参数通常忽略

  • 由于历史原因,PTRACE_CONTaddr 参数没有实际用途,一般传 NULL

(3) data 参数可以传递信号

  • data 可以是一个信号编号(如 SIGTRAP),子进程恢复时会收到该信号。

  • 典型用途

    • data = 0:普通继续执行。
    • data = SIGTRAP:用于单步调试(类似 PTRACE_SINGLESTEP)。

(4) 子进程恢复后的行为

  • 子进程会从上次停止的地方继续执行。
  • 如果传递了信号(如 SIGTRAP),子进程会先处理该信号。

父进程总结

实现对子进程固定地址数值的修改,加入一些输出来检测各个部分的情况,这里父进程有两个wait我觉得需要单独讲解一下,第一个wait是停止父进程等待子进程调用execl时产生的SIGTRAP信号并暂停(执行exec时,内核会自动给子进程发送SIGTRAP,这时子进程就暂停了。)。第二个wait是等待子进程运行完退出,获取其退出码,避免出现僵尸进程。

2.子进程(son)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 全局变量模拟伪代码中的byte数组
unsigned char aaaaa[32] = { 0xCC, 0x8D, 0x2C, 0xEC, 0x6F, 0x88, 0xED, 0xEB, 0x2F, 0xED,
0xAE, 0xEB, 0x4E, 0xAC, 0x2C, 0x8D, 0x8D, 0x2F, 0xEB, 0x6D,
0xCD, 0xED, 0xEE, 0xEB, 0x0E, 0x8E, 0x4E, 0x2C, 0x6C, 0xAC,
0xE7, 0xAF}; // 正确的flag转换结果
unsigned char byte_60004080[32] = {0};
int dword_60004040 = 4; // 偏移位数

int main(int argc, char **argv) {

char *s = argv[1];
int v6 = strlen(s);

// 实际验证逻辑
for (int i = 0; i < v6; ++i) {
byte_60004080[i] = (s[i] >> dword_60004040) | (s[i] << (8 - dword_60004040));
}

int correct = 1;
for (int i = 0; i < v6; ++i) {
if (byte_60004080[i] != aaaaa[i]) {
puts("this is Wrong~");
correct = 0;
break;
}
}

if (correct) {
puts("this is right~");
}
return 0;
}

这串代码关键要说明的就是int argc, char **argv这两个参数

1
2
3
int main(int argc, char *argv[]) {
return 0;
}
  • argc:argument count,命令行参数的数量。

  • argv:argument vector,命令行参数的数组。

argc:参数个数

当你在命令行运行程序,比如:

1
./son 123 abc def

系统就会把你输入的参数传给main。

在这个例子中:

  • argc = 4

    • 参数0: ./son(程序名)
    • 参数1: 123
    • 参数2: abc
    • 参数3: def

所以,argc永远 >=1,因为第0个参数总是程序本身的路径(或者至少是它的名字)。

argv:参数数组

argv是一个数组指针,每个元素是一个字符串指针(char*)。

把刚才例子展开:

1
2
3
4
5
argv[0] = "./son"
argv[1] = "123"
argv[2] = "abc"
argv[3] = "def"
argv[4] = NULL

注意:

argv的最后一个元素必定是NULL

这是C语言对字符串数组的一种约定:数组结束用NULL标记。

和execl的关系

当你调用:

c复制编辑execl(“./son”, “son”, key, NULL);

它就会把这些参数传递到被加载的新程序的main()里:

  • “son” -> argv[0]
  • key -> argv[1]
  • NULL -> 标记结束

所以,在你的son程序里:

1
2
3
4
5
6
int main(int argc, char **argv)
{
// argc = 2
// argv[0] = "son"
// argv[1] = key (你输入的字符串)
}

上面讲的两个参数是main函数的标准形式,接下来简单过一下扩展形式:带三个参数(POSIX 标准)

1
2
3
int main(int argc, char *argv[], char *envp[]) {
return 0;
}

第三个参数 envpEnvironment Variables):

  • 功能:指向环境变量的指针数组,用于访问系统环境变量(如 PATH、HOME)。
  • 格式:每个元素是形如 NAME=VALUE 的字符串,最后一个元素为 NULL。

子进程总结

实现了一个简单的字节内部左右移加密,子进程的偏移量是假的

这里子进程的key1:flag{Do_you_really_know_ptrace?}

父子进程初步实现了Ptrace的修改被调试进程(tracee)的内存数据,下面是运行截图

二.VM保护实现

下面是我依葫芦画瓢编写的简单小型虚拟机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdint.h>

#define OPCODE_N 6

unsigned char vm_code[] =
{ 0xf1, 0xe0, 0x66, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00,0x00, 0x00, 0x00,
0xf1, 0xe0, 0x6c, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x61, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x67, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x7b, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x5d, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x6d, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x75, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7e, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x5d, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x5f, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x75, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x73, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x65, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7e, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x63, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x6c, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7f, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x5f, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x7a, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7f, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x62, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x65, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x63, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x77, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x7f, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x69, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x6e, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x67, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x68, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x69, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x7d, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf7
};

typedef struct
{
unsigned char opcode;
void (*handle)(void*);
}vm_opcode;


typedef struct
{
unsigned long r0;
unsigned long r1;
unsigned long r2;
unsigned long r3;
unsigned char *eip;
vm_opcode op_list[OPCODE_N];
}vm_cpu;



void mov(vm_cpu* cpu);
void add(vm_cpu* cpu);
void sub(vm_cpu* cpu);
void mul(vm_cpu* cpu);
void _div(vm_cpu* cpu);
void _xor (vm_cpu* cpu);
void vm_dispatcher(vm_cpu* cpu);
void vm_start(vm_cpu* cpu);

unsigned char* vm_stack = NULL;

char key2[32]{};

vm_cpu* vm_init()
{
vm_cpu* cpu = (vm_cpu*)malloc(sizeof(vm_cpu));
memset(cpu, 0, sizeof(vm_cpu));
cpu->r0 = 0;
cpu->r1 = 0;
cpu->r2 = 0;
cpu->r3 = 0;
cpu->eip = vm_code;
cpu->op_list[0].opcode = 0xf1;
cpu->op_list[0].handle = (void (*)(void*))mov;
cpu->op_list[1].opcode = 0xf2;
cpu->op_list[1].handle = (void (*)(void*))add;
cpu->op_list[2].opcode = 0xf3;
cpu->op_list[2].handle = (void (*)(void*))sub;
cpu->op_list[3].opcode = 0xf4;
cpu->op_list[3].handle = (void (*)(void*))mul;
cpu->op_list[4].opcode = 0xf5;
cpu->op_list[4].handle = (void (*)(void*))_div;
cpu->op_list[5].opcode = 0xf6;
cpu->op_list[5].handle = (void (*)(void*))_xor;

vm_stack = (unsigned char*)malloc(0x512);
memset(vm_stack, 0, 0x512);
return cpu;
}


//虚拟机的入口函数,对虚拟机环境进行初始化
void vm_start(vm_cpu* cpu)
{
if (!cpu) return;
cpu->eip = (unsigned char*)vm_code;
while (*(cpu->eip) != 0xf7)//如果opcode不为RET,就调用vm_dispatcher来解释执行
{
vm_dispatcher(cpu);
}
}


//分发器,解释opcode,并选择对应的handle函数执行,当handle执行完后会跳回这里,形成一个循环。
void vm_dispatcher(vm_cpu* cpu)
{
if (!cpu) return;

for (int i = 0; i < OPCODE_N; i++)
{
if (*cpu->eip == cpu->op_list[i].opcode)
{
cpu->op_list[i].handle(cpu);
break;
}
}
}


// add指令实现
void add(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r1 + 2;
cpu->eip += 1;
printf("R0 = R1 + 2\n");
}


// sub指令实现
void sub(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r2 - 6;
cpu->eip += 1;
printf("R0 = R2 - 6\n");
}


// mul指令实现
void mul(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r3 * 3;
cpu->eip += 1;
printf("R0 = R3 * 3\n");
}


// _div指令实现
void _div(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r3 / 3;
cpu->eip += 1;
printf("R0 = R3 / 3\n");
}


// _XOR指令实现
void _xor (vm_cpu* cpu) {
if (!cpu) return;

cpu->r0 = cpu->r0 ^ 26;
cpu->eip += 1; // 指令长度1字节
printf("R0 = R0 ^ 26\n");
}


// MOV指令实现
void mov(vm_cpu* cpu) {
static int i = 0;
unsigned char reg = *(cpu->eip + 1); // 寄存器标识
int offset = *(int*)(cpu->eip + 2); // 栈偏移量

switch (reg) {
case 0xe0: // 从栈加载到R0
cpu->r0 = *(int*)(cpu->eip + 2);
printf("mov R0 %x\n", *(int*)(cpu->eip + 2));
break;
case 0xe1: // 从栈加载到R1
cpu->r1 = *(int*)(cpu->eip + 2);
printf("mov R1 %x\n", *(int*)(cpu->eip + 2));
break;
case 0xe2: // 从栈加载到R2
cpu->r2 = *(int*)(cpu->eip + 2);
printf("mov R2 %x\n", *(int*)(cpu->eip + 2));
break;
case 0xe3: // 从栈加载到R3
cpu->r3 = *(int*)(cpu->eip + 2);
printf("mov R3 %x\n", *(int*)(cpu->eip + 2));
break;
case 0xe4: // 从R1存储到栈
key2[i] = cpu->r0;
printf("mov key2[%d] R0\n", i);
i++;
break;
}

cpu->eip += 6; // MOV指令占6字节
}


int main()
{
vm_cpu* cpu = vm_init();
if (cpu) {
vm_start(cpu);
for(int i = 0;i<32;i++)
printf("%c", key2[i]);
free(vm_stack);
free(cpu);
}
return 0;
}

这里代码部分不多赘述,不懂直接问AI,我这里直接讲解一下改代码的功能

这里实现的是一个从第6个字符开始,按照_xor→add→sub的顺序轮流操作

每个操作的结果存储在key2数组中前五个字节和最后一个字节是flag{}头

这里直接会输出第二部分密钥 flag{Good_You_defeated_yyyspark}

调试截图

0

三.最终题目代码

(1)father

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<stdint.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>

#define OPCODE_N 6

unsigned char vm_code[] =
{
0xf1, 0xe0, 0x66, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00,0x00, 0x00, 0x00,
0xf1, 0xe0, 0x6c, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x61, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x67, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x7b, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x5d, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x6d, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x75, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7e, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x5d, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x5f, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x75, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x73, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x65, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7e, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x63, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x6c, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7f, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x5f, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x7a, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x7f, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x62, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x65, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x63, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x77, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x7f, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x69, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x6e, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe2, 0x67, 0x00, 0x00, 0x00,
0xf3,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe0, 0x68, 0x00, 0x00, 0x00,
0xf6,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf1, 0xe1, 0x69, 0x00, 0x00, 0x00,
0xf2,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,
0xf1, 0xe0, 0x7d, 0x00, 0x00, 0x00,
0xf1, 0xe4, 0x00, 0x00, 0x00, 0x00,

0xf7
};

typedef struct
{
unsigned char opcode;
void (*handle)(void*);
}vm_opcode;


typedef struct
{
unsigned long r0;
unsigned long r1;
unsigned long r2;
unsigned long r3;
unsigned char *eip;
vm_opcode op_list[OPCODE_N];
}vm_cpu;



void mov(vm_cpu* cpu);
void add(vm_cpu* cpu);
void sub(vm_cpu* cpu);
void mul(vm_cpu* cpu);
void _div(vm_cpu* cpu);
void _xor (vm_cpu* cpu);
void vm_dispatcher(vm_cpu* cpu);
void vm_start(vm_cpu* cpu);

unsigned char* vm_stack = NULL;

char key2[32];

vm_cpu* vm_init()
{
vm_cpu* cpu = (vm_cpu*)malloc(sizeof(vm_cpu));
memset(cpu, 0, sizeof(vm_cpu));
cpu->r0 = 0;
cpu->r1 = 0;
cpu->r2 = 0;
cpu->r3 = 0;
cpu->eip = vm_code;
cpu->op_list[0].opcode = 0xf1;
cpu->op_list[0].handle = (void (*)(void*))mov;
cpu->op_list[1].opcode = 0xf2;
cpu->op_list[1].handle = (void (*)(void*))add;
cpu->op_list[2].opcode = 0xf3;
cpu->op_list[2].handle = (void (*)(void*))sub;
cpu->op_list[3].opcode = 0xf4;
cpu->op_list[3].handle = (void (*)(void*))mul;
cpu->op_list[4].opcode = 0xf5;
cpu->op_list[4].handle = (void (*)(void*))_div;
cpu->op_list[5].opcode = 0xf6;
cpu->op_list[5].handle = (void (*)(void*))_xor;

vm_stack = (unsigned char*)malloc(0x512);
memset(vm_stack, 0, 0x512);
return cpu;
}


//虚拟机的入口函数,对虚拟机环境进行初始化
void vm_start(vm_cpu* cpu)
{
if (!cpu) return;
cpu->eip = (unsigned char*)vm_code;
while (*(cpu->eip) != 0xf7)//如果opcode不为RET,就调用vm_dispatcher来解释执行
{
vm_dispatcher(cpu);
}
}


//分发器,解释opcode,并选择对应的handle函数执行,当handle执行完后会跳回这里,形成一个循环。
void vm_dispatcher(vm_cpu* cpu)
{
if (!cpu) return;

for (int i = 0; i < OPCODE_N; i++)
{
if (*cpu->eip == cpu->op_list[i].opcode)
{
cpu->op_list[i].handle(cpu);
break;
}
}
}


/// add指令实现
void add(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r1 + 2;
cpu->eip += 1;
}


// sub指令实现
void sub(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r2 - 6;
cpu->eip += 1;
}


// mul指令实现
void mul(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r3 * 3;
cpu->eip += 1;
}


// _div指令实现
void _div(vm_cpu* cpu)
{
if (!cpu) return;
cpu->r0 = cpu->r3 / 3;
cpu->eip += 1;
}


// _XOR指令实现
void _xor (vm_cpu* cpu) {
if (!cpu) return;
cpu->r0 = cpu->r0 ^ 26;
cpu->eip += 1; // 指令长度1字节
}


// MOV指令实现
void mov(vm_cpu* cpu) {
static int i = 0;
unsigned char reg = *(cpu->eip + 1); // 寄存器标识
int offset = *(int*)(cpu->eip + 2); // 栈偏移量

switch (reg) {
case 0xe0: // 从栈加载到R0
cpu->r0 = *(int*)(cpu->eip + 2);
break;
case 0xe1: // 从栈加载到R1
cpu->r1 = *(int*)(cpu->eip + 2);
break;
case 0xe2: // 从栈加载到R2
cpu->r2 = *(int*)(cpu->eip + 2);
break;
case 0xe3: // 从栈加载到R3
cpu->r3 = *(int*)(cpu->eip + 2);
break;
case 0xe4: // 从R1存储到栈
key2[i] = cpu->r0;
i++;
break;
}

cpu->eip += 6; // MOV指令占6字节
}

int main()
{
vm_cpu* cpu = vm_init();
if (cpu) {
vm_start(cpu);
free(vm_stack);
free(cpu);
}

printf("Input your key:");
fflush(stdout);
char key1[64];
scanf("%32s", key1);

pid_t child;
child = fork();

if(child)
{
if (child < 0)
{
perror("fork");
return -1;
}

printf("Waiting for child...\n");
fflush(stdout);
wait(NULL);
printf("Wait done.\n");
fflush(stdout);

long patch_value = 3;
if (ptrace(PTRACE_POKEDATA, child, (void*)0x404060, (void*)patch_value) == -1) {
perror("ptrace POKEDATA");
} else {
printf("Patched success\n");
}
fflush(stdout);

if (ptrace(PTRACE_CONT, child, NULL, NULL) == -1) {
perror("ptrace CONT");
}
printf("Continued child.\n");
fflush(stdout);

wait(NULL);
printf("Child exited.\n");
fflush(stdout);
}
else
{
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
printf("NONONO!!!\n");
exit(1);
}
execl("./son1", "son1", key1, 0);
perror("execl");
exit(1);
}

return 0;
}

(2)son

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<stdint.h>

// 全局变量模拟伪代码中的byte数组
unsigned char aaaaa[32] = { 0xCC, 0x8D, 0x2C, 0xEC, 0x6F, 0x88, 0xED, 0xEB, 0x2F, 0xED,
0xAE, 0xEB, 0x4E, 0xAC, 0x2C, 0x8D, 0x8D, 0x2F, 0xEB, 0x6D,
0xCD, 0xED, 0xEE, 0xEB, 0x0E, 0x8E, 0x4E, 0x2C, 0x6C, 0xAC,
0xE7, 0xAF}; // 正确的flag转换结果
unsigned char byte_60004080[32] = {0};
int dword_60004040 = 4; // 偏移位数

int main(int argc, char **argv) {

char *s = argv[1];
int v6 = strlen(s);

// 实际验证逻辑
for (int i = 0; i < v6; ++i) {
byte_60004080[i] = (s[i] >> dword_60004040) | (s[i] << (8 - dword_60004040));
}

int correct = 1;
for (int i = 0; i < v6; ++i) {
if (byte_60004080[i] != aaaaa[i]) {
puts("key1 is Wrong~");
correct = 0;
break;
}
}

if (correct) {
puts("key1 is right~\n\n");
puts("flag is flag{The MD5 value of (key1 XOR key2)}");
}
return 0;
}

运行截图

0

最终flag:

flag{95f88fa864969b354911b3793c7e49d3}