RCTF2020 一部分pwn(bf,mginx,vm,no_write,note)和re-cipher题解
bf
c++写的一个brain fuck的解释器,程序用到的结构体:
1 | struct brain_fck{ |
程序的漏洞在'>'
操作的一个off_by_one
:
判断条件是v21 > &v25
而不是 >=
,正好可以读取或者修改brain_fck.code
的低字节
而brain_fck.code
是个string
类,string类的大致结构(当然我省略了大部分内容)为:
1 | class string{ |
当string
的长度小于16的时候是把字符串放在buf
里的,即ptr
指向的是自己的buf
,超出16个字节的时候就会进行malloc了。
再回头看漏洞,我们可以修改brain_fck.code
的低字节,也就是意味着我们能修改code
的ptr
,在修改了ptr
之后,我们就可以向ptr
所指的内存写入数据或者读取数据,但是由于我们只能写一个字节,所以范围被限定在了[ptr&0,(ptr&0)+0xff]
的范围内,不过这已经足够了。
因为一开始code是在栈上的,所以我们只要保证输入的brain fuck代码长度不超过16就能让ptr指向栈内存而不会被分配到堆上,在利用每次执行完代码的输出来泄露libc和栈地址,最后修改返回地址进行rop
为了确保exp的成功率,我们可以先泄露出brain_fck.code
的低字节,在进行后续的攻击,最后返回的时候要修补一下brain_fck.code
,不然析构函数可能会报错
exp:
1 | from pwn import * |
vm
这题没啥新的东西,程序唯一的输出点就是父进程会输出子进程的退出码,所以我们可以先读取flag,在用exit来一位一位的泄露出flag。
vm的结构体:
1 | typedef struct{ |
vm支持的操作
1 | enum{ |
程序先读取0x1000长度的指令,接着check指令是否合法,并把指令的数量一起传入到run函数,run函数在开始执行
程序的check函数中,并没有对jmp类指令进行check,这就是漏洞所在,在看程序一开始对vm的初始化:
stack
在pc
之后分配,意味着stack
在高地址,所以我们可以先把指令压到栈中,在利用jmp指令,跳到栈中执行我们的指令,这里的指令就没有进行check了,我们可以越界读写
利用步骤大致为:
- 先把我们要执行的指令都压入vm的栈中,然后跳到栈中来执行
- 把vm的栈的所在堆的大小改小,使得free之后不会和top_chunk进行合并,这样我们就可以利用堆上残留的libc地址
- 利用堆上残留的libc地址,在配合add,sub,mov指令来进行任意地址写,修改
__free_hook
为setcontext+53
- 最后触发free来劫持程序,利用ROP进行orw读取flag,并把flag每一位当成状态码调用exit函数
exp:
1 | from pwn import * |
no_write
程序只允许我们使用open,read,exit和exit_group系统调用,没有write,所以我们得找到一种方法来泄露flag
首先是没有了write系统调用,我们不能进行泄露,但是我们可以用这个gadget来获得我们想要的libc地址:
1 | .text:00000000004005E8 add [rbp-3Dh], ebx |
rbp和ebx都是我们可控的
我们可以用栈迁移的方法把栈迁移到bss段上,接着调用__libc_start_main
使得bss段上残留下libc的地址,再利用上述的gadget,我们就可以达到任意call的目的
现在的问题就是怎么泄露flag了,这里我给出的一种方法是利用strncmp函数来一个个的比较flag
具体思路如下:
- 先open和read,把flag读取到bss段上
- 再把我们要爆破的一个字符输入到bss段的末尾(0x601FFFF)
- 接着调用strncmp(flag,0x601FFFF,2),如果我们上一步输入的字符和flag开头一样的话,程序就会
segment fault
,打远程的时候就表现为收到了EOF,因为strncmp正尝试读取0x602000处的内容 - 如果比较不正确,程序可以继续运行下去,我们可以在使程序多调用几次read来读取我们的输入,以此来和比较正确的情况进行区分
exp:
1 | from pwn import * |
mginx
这题得给各位师傅道个歉,赛后我发现是可以getshell的,设置下execve的argv参数即可,比赛的时候说不能getshell导致做这题的师傅体验极其不好,实在是对不起各位师傅
mips64 大端序,没有去除符号表,打开Ghidra进行反编译,Ghidra反编译的结果还是相当可以的
程序逻辑也不是很复杂,就是一个超级简陋的http server,多调试调试就会发现下面代码里有一处栈溢出:
这个sStack4280就是我们的Content-Length
在加上body
的长度,如果我们设置Content-Length
为4095,body里在填入多个字节,那sStack4280就会超过0x1000,直接导致了栈溢出,以下是一个poc:
1 | req = """GET /index \r |
有了这个栈溢出,我们就可以劫持main函数的返回地址,程序没有开启PIE,而且数据段是可以执行的,但是远程的环境是开启了ASLR,所以我们得先绕过下ASLR
既然栈溢出了,首先想到的是ROP,但是这个程序没有多少可以用的gadget,所以ROP几乎不太行,可以考虑下栈迁移,我们先看下main函数返回时的几条指令:
我们可以控制ra,s8和gp,但是要跳到哪里呢,这时候没啥好办法了,只能都试试,我选择跳到了:
即开头处的那个read,我们可以把s8覆盖为数据段的地址,这样我们就可以把shellcode读入到data段,在利用main函数中的栈溢出把返回地址覆盖为数据段上shellcode的地址,这样我们不需要泄露任何地址就可以执行shellcode了
orw的exp:
1 | #python exp.py REMOTE=124.156.129.96 DEBUG |
PS: 由于远程环境问题,得多跑几次脚本,不过几十次差不多了
结果:
getshell的exp:
1 | from pwn import * |
note
Step 1:get enough money
money初始值为0x996,size * 857 > money则⽆法申请,delete函数中有money += size的操作,所以
⾸先需要通过乘法溢出修改money的值为后续操作准备
Step 2:leak libc and heap
calloc函数申请 的是⼀个MMAP的chunk是不会执⾏memset清空操作的,⽽super_edit(choice 7)中是
存在堆溢出的,通过堆溢出修改mmap标志位为1 ,从⽽泄漏libc 和 heap
Step 3: tcache smashing unlink
通过edit函数中存在的off by null 实现unlink,进⽽tcache smashing unlink到malloc_hook上⽅,
super_buy(choice 6)会调⽤malloc,从⽽覆盖malloc_hook为onegadget
exp:
1 | #coding:utf-8 |
cipher
mips64 大端序,理清程序逻辑,写出对应的解密函数即可,但是由于key的初始值是由rand()函数来的,所以需要爆破下key
exp:
1 | from struct import pack, unpack |