前段时间一直在学RealWorld相关的知识,对于近两个月做的比赛和题还没有进行整理。这里整理一下,主要包含红帽杯线上、津门杯线上和线下题解。
2021-红帽杯
manager
程序分析
程序总体实现了一个双子树链表结构,会将比当前结点size
大的结点插入右子树,比当前结点size
小的结点插入左子树:
1 | 00000000 node struc ; (sizeof=0x30, mappedto_8) |
删除的时候,需要遍历左右子树,先找到size
刚好必要删除node
的 size
大的结点。原本算法是需要将这个找到的结点用来替换要被删除的结点。但是这里存在漏洞,先将找到的node
的 data_chunk
进行了删除,然后又将 要删除的 node
用找到的Node
进行了更新,最后还会将这个node
删除一次,也就是存在一个 double free
漏洞:
1 | node *__fastcall sub_C00(void ***right_chunk1, int size) |
利用分析
2.27
下的 double free
利用很简单。首先填满 tcache
,将一个node
放到 unsortedbin
来泄露地址。然后tcache
构造一个 double free
,最后来劫持 free_hook
。
EXP
1 | from pwn import * |
parse
程序分析
1 | __int64 __fastcall parse(const char *buf, _BYTE *buf2, int a3) |
程序基本模拟了一个 http
包头的解析器,如果通过各种检测后,在最后会将报文通过Printf
输出,这里就存在一个 格式化字符串漏洞。
利用分析
利用格式化字符串来泄露地址,数据。然后修改紧邻main
函数返回地址的 libc_start_main
函数。
这里记录一下,格式化字符串实现任意地址写时: %numc%off$hn
,这里的 num
是输出的字符个数,如果在这个格式化字符前面还有字符,需要减去前面的字符数。
如果多次使用 任意地址写,第二次写时需要减去前面输出的总字符。
EXP
1 | from pwn import * |
SimpleVM
这道题是这次红帽杯让我感觉很有意思的一道题,以前对LLVM也一点都不了解。
基础知识
LLVM PASS
Pass
就是遍历一遍IR
,同时对它做一些操作。LLVM
的核心库中会给你一些pass
类去继承,我们需要实现它的一些方法,最后使用 LLVM
的编译器会把它翻译得到的 IR
传入 Pass
里,给你遍历和修改。
LLVM Pass
的作用:
- 进行插装,在
pass
遍历LLVM IR
的同时,自然就可以往里面插入新的代码 - 机器无关的代码优化:
IR
在被翻译成机器码前会做一些机器无关的优化,但是不同的优化方法之间需要解耦,所以自然要各自遍历一遍IR
,实现成了一个个LLVM Pass
,最终,基于LLVM
的编译器会在前端生成LLVM IR
后调用一些LLVM Pass
做机器无关优化,然后再调用LLVM
后端生成目标平台代码
LLVM IR
传给LLVM PASS
进行优化的数据是 LLVM IR
,即代码的中间表示,LLVM IR
有三种形式:
1 | 1、.ll 格式:人类可以阅读的文本。 |
从对应格式转化到另一格式命令如下:
1 | .c -> .ll:clang -emit-llvm -S a.c -o a.ll |
对于一个示例程序:
1 |
|
通过命令:
1 | clang -emit-llvm -S main.c -o main.ll |
可以生成IR
文本文件:
1 | ; ModuleID = 'main.c' |
从中可以看到 IR
中间代码表示的非常直观,而 LLVM PASS
就是用于处理 IR
,将一些能够优化掉的语句进行优化。
编写一个LLVM Pass
如果要编写一个 LLVM Pass
,示例如下:
1 |
|
该示例用于遍历 IR
中的函数,因此 结构体 hello
继承了 FunctionPass
,并重写了 runOnFunction
函数,那么每遍历到一个函数时,runOnFunction
都会被调用,因此该程序会输出函数名。为了测试,需要将其编译为模块:
1 | clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags` |
逆向分析
这里重点说一下如何对 LLVM Pass
进行逆向。在初始化函数中,调用 start
:
1 | int start() |
在 start
函数中,回去注册相应的处理函数:
1 | unsigned __int64 __fastcall sub_6510(llvm::PassRegistry *a1, int a2, int a3, int a4, int a5, char a6, char a7) |
这里再去看 funclist
:
1 | unsigned __int64 __fastcall sub_6720(llvm::FunctionPass *a1) |
1 | LOAD:000000000020DD20 funclist dq offset sub_6780 ; DATA XREF: sub_6720+30↑o |
然后重点就是分析 runOnFunction
里的函数处理流程。
程序分析
1 | __int64 __fastcall runOnFunction(__int64 a1, llvm::Value *a2) |
程序首先比较,函数名称是否叫 o0o0o0o0
,然后会获取程序的每一个基本块。然后会将每个基本块的指令进行处理:
1 | unsigned __int64 __fastcall sub_6AC0(__int64 a1, llvm::Function *a2) |
1 | else if ( !strcmp(chunk, "store") ) |
这里就实现了一个 vm
的操作,load
是读取一个地址的值到寄存器中,store
是将一个值写入一个地址。而这两个操作,都没有检查边界,所以存在越界读写。此外,add
函数可以帮助我们将一个值加到一个地址的值上。
利用分析
而这里由于 opt
没有开启 PIE
和 got
表保护。可以直接利用 越界读写来修改 free@got
为one_gadget
,实现getshell
EXP
1 | void store(int a); |
2021-津门杯线上
Hello
在 edit
,读入 phone number
和 name
时,存在一个溢出,所以可以直接向下越界,修改chunk_list
中 chunk
地址为 free_hook
,劫持 free_hook
来 getshell
。
1 | from pwn import * |
Pwn3
程序分析
Add
函数中使用 strcpy
向堆块中拷贝数据,会向末尾多添加一个 \x00
,所以存在一个 off-by-null
漏洞。
利用分析
注意:这里很坑的一个点是,这道题远程是 glibc-2.27 1.4
,也就是开启了 double free
检测,而题目没给libc
,一段时间比较自闭。
off-by-null
的常见用法,申请两虚堆块3个,将chunk0
放入 unsortedbin
,使其满足合并时的 unlink
检查。通过 chunk1
修改 相邻堆块chunk2
的 inuse
位和 伪造一个 fake_prevsize
。然后释放 chunk2
,chunk2
如果进入 unsortedbin
,此时会产生堆块合并。
就能够复用 chunk1
堆块。
EXP
1 | from pwn import * |
Pwnme
程序分析
1 | for ( num = 0; num < sec_num; ++num ) |
程序逻辑比较复杂,也导致了出现了几个bug,当然最后能利用的只有一个。这里最后是利用src_off
从 data_chunk
中取出数据复制到 lchunk
的 dst_off
偏移中。
虽然前面对 src_off
\ dst_off
和d_len
都有检查,但是这里的检查存在两个问题。
首先如果 d_len
前面为负数时,前面的检查是可以顺利通过的,而且由于 d_len
成为负数,后续执行 memcpy
时,将 d_len
转换为了无符号整数,也就是这里会变为一个巨大的正整数,拷贝时会存在越界拷贝。但是这个漏洞不能利用,因为 d_len
从负数转为正整数后,数值太大,将会导致拷贝错误,有可能会拷贝到一个不可读的内存。
后面,又找到一个漏洞。可以看到上面的检查,会对这三个变量进行一个循环检查。这里会检查当前的 dst_off
不能小于上一次的 dst_off
,并且检查 last_d_len+last_dst_off
不能大于当前的 dst_off
以及 last_dst_off
不能大于现在的 dst_off
。这里当我们第二次输入 dst_off
等于上一次的 dst_off
时,就可以完全满足前面的检查。但是其却没有检查d_len+dst_off
有没有超过 lchunk
的边界,如果超过边界,这里也是一个越界写的漏洞。
此外,在 edit
函数中,也存在一个 和第一个漏洞类似的 负数溢出漏洞,但是都无法直接利用。
利用分析
- 利用越界写,修改紧邻的下一个堆块的 size,构造堆重叠。这里只有输出
name
时才能泄漏地址,所以需要将 伪造size的这个大块释放到unsortedbin
。然后将unsortedbin libc
重叠到一个 以分配结构体的name
处,再通过输出name来获得libc
地址 - 然后,利用堆重叠,修改一个结构体中的
lchunk
指针,再通过edit
函数实现任意地址写。最终劫持free_hook
EXP
1 | from pwn import * |
Jerry
听搞浏览器的Ver
哥大概讲了一下JS
的原理和利用手法,利用很简单。但是找洞还是没搞懂是怎么找到的。
2021-津门杯线下
pad
tiktok
程序分析
程序挺复杂的,但是其实也能大概猜到,应该会留一个比较好利用的漏洞,只是比较难找到而已。
1 | _BYTE *__fastcall change_info(phone_c *a1) |
在 load_file
中的 change_info
函数中,可以看到对 phones
和 pwd
的写入长度都是由我们指定,而且并没有对这些长度进行检查。这就导致我们可以直接实现堆溢出。
而这里为了能够进入 load_file
功能,需要创建其他用户对要修改的用户进行点赞watch
。
利用分析
先利用 show_info
泄漏出 heap
地址,然后利用堆溢出修改后面一个结构体的管理指针,来劫持 free_hook
EXP
1 | from pwn import * |
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/05/30/2021-4月题解/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!