这道题目的malloc和free是自己实现的,并且给了main.c和malloc.c的源码,通过源码我们可以看到存在UAF漏洞同时我们看malloc函数在自己实现的堆管理器中简化掉了tcache和fastbin,仅留下了unsorted bin,因此后续的攻击要关注unsorted bin很明显看到这里给了个hook,只要控制hook的内容就能挟持执行流,同时unlink的check被注释掉了,因此最原始的unlink也能执行到这里其实攻击思路已经很明确了:IDA打开发现是rust写的,反编译比较复杂因此AI辅助逆向了一下,主要逻辑是:如果没有./flag_hash这个文件,就读取/flag并生成其哈希,将其哈希写入./flag_hash中如果已经有./flag_hash这个文件的话,就将文件中的内容直接读取进缓冲区中接下来是主循环,获取了用户的一行输入,将这个输入进行处理之后与刚刚的哈希值传入guess::verify进行字符对比,如果对比结果相同则直接拿到shell将用户的输入进行处理的过程我们可以通过测试得到,断点下在guess::verify(src, &v42)这一行,运行之后在命令行中输入ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB双击src就可以看到处理之后的数据所以数据处理就是将两个字节合并成一个字节而已,将其合并后的结果与flag的哈希值对比,如果flag的哈希值是0xA2 0xBC那我们就要输入A2BC哈希值在本地的python中可以通过如下程序进行计算可以看到上述python程序生成的哈希和rust程序生成的哈希是一样的(其实这个结论在这道题没有用)我们整理一下思路:要获取flag就要拿到shell,要拿到shell就要获取flag的哈希,要获取flag的哈希就要知道flag……没招,闭环了( •́ .̫ •̀ )毕竟爆破哈希在有限的时间内是不可能的,因此这个思路是行不通的,肯定哪里有疏漏在哈希对比失败后,程序会输出一段调试信息我们进行环境变量的配置之后,可以输出完整的调试信息我们回头看看verify函数的流程图,发现这个函数及其抽象其实就是每一行异常的处理程序地址是不同的比如第一个字符错误由0x18491-0x184B7代码处理,第二个字符错误由0x184B7-0x184DD代码处理这样配合调试信息中的这一行,就可以通过侧信道的方式爆破哈希的每一位,从而得到完整的哈希所以正确的思路是通过侧信道爆破哈希,随后获取shell栈上有可执行权限,因此可以考虑在栈上注入shellcodeIDA打开分析一下程序的功能可以看到这道题目的canary是个全局变量,其中的内容是个栈地址退出main函数的时候会对canary进行校验,如果canary被修改则执行exit(0)当输入0时为add功能,此时会让我们输入way和distance,输入way的时候很明显存在一个栈溢出,但是由于canary的限制无法直接修改,要先想办法泄露一个栈地址,qmemcpy这一串就是将我输入的way复制到可写地址中,可以忽略;完成qmemcpy后会将buf内容置零随后让我们输入distance存入dis[i]中delete功能就是将指定位置i的dis[i % 201]清空show功能就是将v2取绝对值,随后输出dis[v2]edit功能是对v6取绝对值,随后修改dis[v6]这道题的漏洞点在于,show和edit中我们的输入都是short 整型,其最小值为-32768因为-32768的二进制表示是 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0,对于一个负数取反就要按位取反,再加1按位取反之后为 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1,将结果加一为 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0,可以看到与原先的-32768表示一样因此当我们在show功能输入-32768时,取反后仍为-32768,其小于199,因此可以成功泄露dis[-32768]的数据,我们从gdb的角度看一下这个地址存储着什么可以看到这里存储的就是canary的值——栈地址,通过这种方式我们就可以泄露出canary这样我们就可以通过栈溢出挟持返回地址了,由于存在memset因此我们要将shellcode写在没有被初始化的空间中这样构造payload我们一方面测试返回地址有无被覆盖,一方面看那一段payload没有被清空因此要在字符A的地方写入shellcode,将字符B的地方写上字符A的地址,就可以成功执行到shellcode,但是由于shellcode只有0x10字节,因此可以先构造一个read再输入一段shellcode,在第二段shellcode中执行execve("/bin/sh", 0, 0)很明显是个多线程题,将sem初始化为8,随后每调用一次sem_wait(&sem)都会将其值减一,每调用一次sem_post(&sem)都会将其值加一,退出work函数之后将其结果复制到dest中,复制字节数是8 * num这是work函数的内容,可以看到它通过我们的输入启动produce线程和consume线程这是consume的主要功能,其实有用的就只有sem_post(&sem)一行这是produce函数的内容,可以看到程序先抢锁,抢到锁之后如果num小于等于7则直接释放锁。在锁mutex释放时线程会sleep一段时间,在这里就存在线程竞争的问题了。比如现在num是7,有两个线程来抢锁,一个线程是A一个线程是B,A比B快0.1s,这样A先抢到锁,随后释放后执行sleep(1),在sleep过程中B也抢到锁了,此时由于A线程还未执行到num = (num + 1) % 11,因此线程B在判断num小于等于7时,num还是7通过了判断,这样的结果就是num最终会被加两次,num的最终值是9,进而引发main函数中的memcpy(dest, src, 8 * num)造成栈溢出在这道题目中,根据线程当前的num不同会像堆上的相邻地址写入不同的值,最后通过memcpy函数复制到栈上。为了造成栈溢出,同时覆盖返回地址,要求num最终的结果是10,同时需要严格控制好每个线程,使其写入的数据不冲突,不混乱经过多轮测试,按照上述脚本的流程执行时,各个线程写入数据不会发生冲突,先不冲突地创建6个线程分别写入数据并将num加到6,随后开始竞争,快速创建4个进程写入数据,最终的效果就是num加到10,同时堆上与栈上数据有序需要注意的是,要提前执行三次consume功能以提高sem变量的值,要不然后面创建用来竞争的几个线程会陷入阻塞状态可以看到,程序提供了个堆地址,通过这个堆地址我们可以找到线程原始写入的数据由于只能够覆盖到返回地址,因此只能进行栈迁移我们可以将返回地址覆盖为leave ; ret,将rbp覆盖为堆地址,然后通过栈迁移迁移到上述堆区域,这样就可以在这里布置ROP链在造链
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290116.htm
[原创]2026 HGAME PWN writeup
495 浏览
0 回复
暂无回复,快来抢沙发吧!