论坛首页 CTF竞赛交流区 阅读主题

[原创]NepCTF 2025 Pwn赛题ASTRAY解析:从结构体逆向到逻辑漏洞,用任意地址读写攻破glibc2.35

289 浏览 1 回复
#1 楼主 2026-06-01 21:09:20
题目名:ASTRAY解题数:26题目描述:There are a lot of forks in the road here, kid, are you lost?知识点:代码审计、逆向结构体还原、逻辑漏洞、高版本glibc(>=2.35)任意地址读写本题的难点在于逆向分析与结构体还原,熟悉程序逻辑后漏洞利用步骤比较常规。拖入IDA分析,找到main函数:发现它会执行init函数并返回a1,然后根据输入的perm权限选择执行user_operation()或manager_operation(a1)。跟进init函数分析:申请0x2000堆空间,并将地址存储到v2指针。然后对全局变量进行一系列的初始化操作,我们可以结合后续使用到这些变量的地方理解它们的含义。先来分析user_operation()函数:让我们输入idx和op,然后调用check()函数检查权限。我们跟进check()函数分析:它会先判断idx是否<=20,也就是说我们有20个可以操作的元素。接着判断op是否合法,只允许5种操作。然后,从manage_physic[]中根据idx取出元素地址存储到v5指针。对于perm == 1000的情况:·这个函数可能是user和manager共用的检查函数,并且如果user使用manager的op也可以检查通过(可能存在逻辑漏洞)。通过check()函数的检查之后,程序会根据idx从manage_physic[]和dword_4068[]中取出元素存储到qword_41A8结构体中。然后调用permission_confirm()函数,我们跟进分析:这里会根据我们之前输入的op,为qword_41A8+16设置一个值。这个函数也是user和manager共用的函数,如果user输入manager的op也可以成功设置值。最后根据qword_41A8+16中的值决定执行USER_read和USER_write:根据上面的分析,我们发现,user_operation()函数允许用户输入idx和op,然后根据op指令去读或写数组中下标为idx的指针指向的区域。但是,可能存在安全风险,因为check()和permission_confirm()函数的共用,程序并没有严格校验,使得user也可以输入manager的op。这样会导致qword_41A8被错误的赋值manage_physic[]和dword_4068[]中的值,但不进行任何操作。我们继续分析另一个分支,manager_operation()函数:这个函数同样要求我们输入idx和op,然后调用check()函数进行权限检查。对于check()函数,前面的操作和之前一样,区别在于:执行完毕check()后,程序会根据输入的idx为全局变量a1赋值(暂不具体分析)。随后,程序调用permission_confirm()函数根据op为*(*(a1 + 8) + 16)赋值,并根据赋值选择后续执行的分支。同理,check()和permission_confirm()被共用,且检查不严谨,manager也可以输入user的指令。最后,程序根据*(*(a1 + 8) + 16)处的值执行不同分支:分析完这两个函数后,我们可以对结构体尝试还原。首先,我们来到init函数,分析全局变量数组的初始化:双击manager_physic[]数组查看内存空间布局:发现manager_physic[]只占8个字节,所以这个循环初始化会进行越界操作写入到紧邻的dword_4068[]数组中。结合前面的逆向分析,可以推断出manager_physic[]数组的大小被错误的识别,我们将其修改为20个元素:然后根据初始化的赋推断Physic结构体:修复后的结构体如下图所示,代码可读性大幅度提高:继续分析init()函数,全局变量qword_41A8存储0x18大小空间的地址(后续用到再分析这个结构体):接着,将manage_physic[0].content_ptr赋值给局部变量 content_ptr,然后进行一系列的赋值操作:根据这些赋值,我们可以推测出Content结构体:其中,unknown_ptr的大小与初始化与qword_41A8相似,可以推断出它们类型应该一致。然后将Physic结构体的content_ptr类型修改为Content *。还原后的结构体如下所示:最终,程序返回manage_physic[0]的地址给全局变量a1指针。此时,再次进入user_operation()函数:可以发现,代码可读性有所提高。结合代码,我们可以推断出qword_41A8类型的的结构体:它在Physic结构体的基础上,多了一个permission_confirm字段用于根据op选择程序分支。此时,user_operation()函数非常清晰:我们之前提到过,Content结构体中的unknown_ptr与该变量类型一致,将其修改为PhysicWithPermissionConfirm *类型并改名。此时,我们再次进入manager_operation()函数,发现代码可读性提高:main函数:init函数:user_operation函数:manager_operation函数:至此,我们已经完成逆向分析的所有工作,后续只需要理清程序逻辑即可进行进一步的漏洞利用。逆向分析完成后,我们发现manager_operation()函数中有一段代码非常奇怪:它没有写入到某个变量中,而是写入到计算后的地址指向的内存空间中。而a1->only_user_ptr在init()函数中被初始化为onlyuser变量的地址:我们可以在内存空间中查看onlyuser变量:而(a1->only_user_ptr + 8)刚好是physicEntry变量的地址(在user_operation()函数中被赋值的全局变量):内存结构如图所示:如果我们可以控制a1->only_user_ptr就可以实现任意地址读写。我们在init()函数中可以发现一个很有意思的事情,manage_physic[]数组的有效存储空间实际为下标1-19。而全局变量a1始终指向manage_physic[0].content_ptr,它在初始化时被赋值:如果我们能通过程序修改manage_physic[0]->content_ptr的内容,就能进一步控制a1->only_user_ptr。当physicEntry = manage_physic[0]时,内存布局如下图所示:此时,我们修改**(a1->only_user_ptr + 8)就是在修改最右侧的结构体,进而实现篡改only_user_ptr指针。但我们还没有考虑check()函数的权限校验,它可能不允许我们操作下标为0的元素,我们再次进入check()函数:如果我们输入USER_write,程序要求该元素perm的最低为1。如果我们输入MANAGER_write,程序要求该元素的次低位为1。而在init()函数中,manage_physic[0]->perm被赋值为16,无法满足这两个write操作的条件。这时候,我们只能寄希望于MANAGER_visit进行读写

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-287856.htm
#2 2026-06-01 21:09:20
可以私聊吗

请登录后参与讨论

立即登录 注册账号