De1CTF 2019 (二)
Last updated on January 27, 2024 am
signal-vm signal-vm-delta
虽然同属于2019年的De1CTF,但是这两道题却能学到很多东西并且都是vm类型,值得再开一篇细细的记录一下。
signal-vm
整体分析
题目的整体逻辑比较简单,输入,然后fork一个子进程,子进程执行loc_40a4ca
函数,父进程执行sub_400b60
函数。
先跟去子进程函数看看
发现一大堆非法指令,在一大堆非法指令上面调用了一个ptrace
函数,并且参数是0,0,0,0
即相当于ptrace(PTRACE_TRACEME,0,0,0)
,也就是子进程希望有一个父进程来调试自己。
那么跳去父进程的函数sub_400B6D
看看,果然也调用了ptrace
函数
PTRACE函数
再继续往下之前,需要先了解PTRACE函数,ptrace()
系统调用能提供追踪进程执行状态的功能。ptrace()
系统调用为一个进程提供了观察和控制另一个进程的执行过程的能力,同时也提供检查和改变另一个进程的内存值以及相关注册信息。其中,被控制的进程被称为tracee,控制进程被称为tracer
该函数签名如下
1 |
|
request: 要执行的操作类型;
pid: 被追踪的目标进程ID;
addr: 被监控的目标内存地址;
data: 保存读取出或者要写入的数据。
通过ptrace
系统调用可以让一个进程进入Traced
状态,让一个进程进入Traced
状态可以通过两种方式:
tracee
进程调用ptrace
系统调用,并在request
参数处传递PTRACE_TRACEME
这个值,表示想要被tracer
进程追踪。通过这种方式的进程想要进入Traced
状态有两种方式:- 主动调用
exec
系列的系统调用; tracer
发送进入Traced
状态的相关信号。
- 主动调用
tracer
进程调用ptrace
系统调用,并在request
参数处传递PTRACE_ATTACH
这个值,并给出tracee
进程的pid
,从而让tracee
进程进入Traced
状态。
在这道题目中,父进程采取tracer发送进入Traced
状态的相关信号的方式来调试子进程。
ptrace
函数中第一个枚举类型的参数也值得说道,一般有如下取值
enum | func | example |
---|---|---|
PTRACE_TRACEME | 子进程唯一使用,表示希望得到父进程的调试,可用于反调试 | ptrace(PTRACE_TRACEME, i_pid, NULL, NULL)| |
PTRACE_PEEKTEXT | 父进程读出子进程内存中的一个字节数据,子进程由pid给出,地址由addr给出 | data = ptrace(PTRACE_PEEKTEXT, i_pid, addr,NULL) |
PTRACE_PEEKDATA | 同上 | 同上 |
PTRACE_POKETEXT | 父进程写入子进程内存中的一个字节数据,子进程由pid给出,地址由addr给出,数据由data给出 | ptrace(PTRACE_POKETEXT, i_pid, addr, data) |
PTRACE_POKEDATA | 同上 | 同上 |
PTRACE_PEEKUSR | 父进程从子进程的user字段读出一个字节,偏移量为addr,user字段是一个user_regs_struct结构体 | orig_rax = ptrace(PTRACE_PEEKUSER, pid, 8 * ORIG_RAX, NULL) |
PTRACE_POKEUSR | 父进程向子进程的user字段写入一个字节,偏移量为addr,数据为data,user字段是一个user_regs_struct结构体 | ptrace(PTRACE_PEEKUSER, pid, 8 * ORIG_RAX, data) |
PTRACE_CONT | 父进程向子进程发送继续运行信号,当data 参数不为零时,data 将会被解释为一个signal (信号),传递给被跟踪进程 |
ptrace(PTRACE_CONT, pid, 0, 0) |
PTRACE_SYSCALL | 同上 | 同上 |
PTRACE_KILL | 父进程杀死子进程 | ptrace(PTRACE_KILL, pid, 0, 0) |
PTRACE_ATTACH | 尝试附加到pid 指定的进程上。附加成功后,目标进程将处于挂起状态。子进程进程会发送SIGSTOP 信号,父进程需要调用waitpid(2)以接收此信号 |
ptrace(PTRACE_ATTACH, pid, NULL, NULL) |
PTRACE_GETREGS | 父进程读出子进程寄存器数据,存放至data处,是一个struct user_regs_struct结构体 | ptrace(PTRACE_GETREGS, pid, 0, regs) |
PTRACE_SETREGS | 父进程写入子进程寄存器数据,数据由data提供,是一个struct user_regs_struct结构体 | ptrace(PTRACE_GETREGS, pid, 0, regs) |
1 |
|
而本题就基于ptrace
函数实现了一个vm,具体流程如下:
子进程调用
ptrace(PTRACE_TRACEME,0,0,0)
函数表示希望被调试子进程执行非法指令,发出信号
父进程调用
wait
函数截获子进程的异常信号,wait()函数的参数是子进程的返回状态,第一个字节如果是0x7f则表示子进程异常返回,第二个字节是返回值,eg: exit(2)则第二个字节为02,子进程出现异常的时候第二个字节是linux的异常信号码。同时调用ptrace
函数得到子进程的寄存器中rip
的值(user_regs_struct结构体的第16位就是rip)如下则是所有的异常码
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
35SIGHUP 1 /* Hangup (POSIX). */ 终止进程 终端线路挂断
SIGINT 2 /* Interrupt (ANSI). */ 终止进程 中断进程 Ctrl+C
SIGQUIT 3 /* Quit (POSIX). */ 建立CORE文件终止进程,并且生成core文件 Ctrl+\
SIGILL 4 /* Illegal instruction (ANSI). */ 建立CORE文件,非法指令
SIGTRAP 5 /* Trace trap (POSIX). */ 建立CORE文件,跟踪自陷
SIGABRT 6 /* Abort (ANSI). */
SIGIOT 6 /* IOT trap (4.2 BSD). */ 建立CORE文件,执行I/O自陷
SIGBUS 7 /* BUS error (4.2 BSD). */ 建立CORE文件,总线错误
SIGFPE 8 /* Floating-point exception (ANSI). */ 建立CORE文件,浮点异常
SIGKILL 9 /* Kill, unblockable (POSIX). */ 终止进程 杀死进程
SIGUSR1 10 /* User-defined signal 1 (POSIX). */ 终止进程 用户定义信号1
SIGSEGV 11 /* Segmentation violation (ANSI). */ 建立CORE文件,段非法错误
SIGUSR2 12 /* User-defined signal 2 (POSIX). */ 终止进程 用户定义信号2
SIGPIPE 13 /* Broken pipe (POSIX). */ 终止进程 向一个没有读进程的管道写数据
SIGALARM 14 /* Alarm clock (POSIX). */ 终止进程 计时器到时
SIGTERM 15 /* Termination (ANSI). */ 终止进程 软件终止信号
SIGSTKFLT 16 /* Stack fault. */
SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
SIGCHLD 17 /* Child status has changed (POSIX). */ 忽略信号 当子进程停止或退出时通知父进程
SIGCONT 18 /* Continue (POSIX). */ 忽略信号 继续执行一个停止的进程
SIGSTOP 19 /* Stop, unblockable (POSIX). */ 停止进程 非终端来的停止信号
SIGTSTP 20 /* Keyboard stop (POSIX). */ 停止进程 终端来的停止信号 Ctrl+Z
SIGTTIN 21 /* Background read from tty (POSIX). */ 停止进程 后台进程读终端
SIGTTOU 22 /* Background write to tty (POSIX). */ 停止进程 后台进程写终端
SIGURG 23 /* Urgent condition on socket (4.2 BSD). */ 忽略信号 I/O紧急信号
SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */ 终止进程 CPU时限超时
SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */ 终止进程 文件长度过长
SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */ 终止进程 虚拟计时器到时
SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */ 终止进程 统计分布图用计时器到时
SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */ 忽略信号 窗口大小发生变化
SIGPOLL SIGIO /* Pollable event occurred (System V). */
SIGIO 29 /* I/O now possible (4.2 BSD). */ 忽略信号 描述符上可以进行I/O
SIGPWR 30 /* Power failure restart (System V). */
SIGSYS 31 /* Bad system call. */
SIGUNUSED 31本题中只会出现四种信号分别是
SIGILL
,SIGTRAP
,SIGSEGV
,SIGFPE
,父进程就会根据他们这几个信号来执行对应的操作根据截获的信号和从寄存器读出的
rip
值决定操作,同时设置rip
- 将设置好的
rip
写回子进程,同时发送继续运行的信号,然后继续等待子进程的异常
系统调用
根据以上分析可以发现,此程序父子进程之间主要是是通过ptrace
以及异常信号进行通信,那么可以使用strace
系统命令来进行跟踪
图中红色框圈出来的就是子进程发出的信号,可以进一步提取出来
只需要再重定向一下然后写个脚本把信号提取出来即可。
Opcode分析
有了信号,就有了对应的操作流程,那么可以尝试分析一下程序操作码是如设计的,这里选择直接将源程序打印出来,并且模拟一遍执行流程。
不过还需要注意几点
- 输入存放的位置,经过调试发现输入存放在
Almost
字符串后50个地方,那么在模拟的时候也要相应的存入。 - 子进程也会执行指令,比如
cc
,这时候rip
会自动加1,在模拟的时候也要这样做(因为rip指向的就是指令执行的地址)
1 |
|
如下就是实际执行的代码以及对应的汇编操作
1 |
|
最后几条指令可能会有问题,不过并不重要。
有了机器码以及对应的汇编语句,现在翻译原来那一大堆指令也就比较简单了。而且通过对原伪代码分析,可以将信号分为四个类,
case 4:mov 类
case 5:逻辑运算
case 8:比较
case 11: 跳转
反汇编
熟悉了机器码以及对应的汇编后,就可以开始进行反汇编它的opcode了
1 |
|
1 |
|
阅读汇编,发现就是简单做了一个矩阵相乘的运算,我们输入70个字符相当于10*7
的矩阵,然后和原始49个字符的7*7
矩阵相乘,再作比较
解题
最后就是简单用z3来解方程了,不能直接算逆矩阵是因为原来运算都是字节型的,会有数据损失
1 |
|
signal-vm-delta
待做>_<