pwn学习笔记(11)–off_by_one
在处理for循环或者while循环的时候,有的可能会遇到如下情况:
#include<stdio.h>
int main(){
char buf[0x10];
for (int i = 0 ; i <= 0x10 ; i ++){
buf[i] = getchar();
}
puts(buf);
}
多次输入几个a之后,发现了最后输出的时候输出了17个a,我的目的仅仅只是需要16个a,结果输出了17个a,像这种,在写入字符串的时候多写入了一个字节的情况,就是off by one。
在堆中,这种问题尤为严重,可能会导致输入的字符覆盖了heap info的prev_in_use或者其他的数据:
- 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
- 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得
prev_in_use
位被清,这样前块会被认为是 free 块。(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。(2) 另外,这时prev_size
域就会启用,就可以伪造prev_size
,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照prev_size
找到的块的大小与prev_size
是否一致。
最新版本代码中,已加入针对 2 中后一种方法的 check ,但是在 2.28 及之前版本并没有该 check 。
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
/* 后两行代码在最新版本中加入,则 2 的第二种方法无法使用,但是 2.28 及之前都没有问题 */
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
还有种情况:
#include<stdio.h>
#include<string.h>
char bss[0x20] = "aaaaaaaaaaaaaaaa";
int main(){
char buf[0x10];
if (strlen(bss) == 0x10){
strcpy(buf,bss);
}
puts(buf);
}
这种情况,乍看上去没啥问题,但是,strlen不会计算结尾的\x00,而strcpy在拷贝的时候又会多拷贝一个\x00进去,造成多写入了一个字节。
上一个题:
Asis CTF 2016 b00ks(只看前面off by one的部分)
checksec 一下看看:
g01den@MSI:/mnt/c/Users/20820/Downloads$ checksec pwn
[*] '/mnt/c/Users/20820/Downloads/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
激活了PIE,以及题目附件被strip过,抱歉,我一个菜鸡误入了大佬的世界,啥都看不懂,反编译之后看到那么抽象突然想放弃了,不过还是得做。
题目是一个寻常的图书管理,有创建书,删除书,编辑描述内容,输出书籍信息,修改最近访问的作者名,退出。
先不看别的,main没啥用,先看add:
__int64 sub_F55()
{
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)readName(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)readName(v5, v1 - 1) )
{
printf("Unable to read description");
}
else
{
v2 = sub_B24();
if ( v2 == -1 )
{
printf("Library is full");
}
else
{
v3 = malloc(0x20uLL);
if ( v3 )
{
*((_DWORD *)v3 + 6) = v1;
*((_QWORD *)off_202010 + v2) = v3;
*((_QWORD *)v3 + 2) = v5;
*((_QWORD *)v3 + 1) = ptr;
*(_DWORD *)v3 = ++unk_202024;
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;
}
分析一波,有一些需要记住作用的阿变量名,比如v1:
v1可以很明显看出来,v1是存放的是book的name的大小,ptr就是name存放的地址:
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);
之后重新用了v1来存放book的description的大小v5就是description存放的地址:
printf("\nEnter book description size: "); __isoc99_scanf("%d", &v1); if ( v1 < 0 ) { LABEL_2: printf("Malformed size"); } else { v5 = malloc(v1);
之后开了个v3,存放了book的所有信息:
v3 = malloc(0x20uLL); if ( v3 ) { *((_DWORD *)v3 + 6) = v1; *((_QWORD *)off_202010 + v2) = v3; *((_QWORD *)v3 + 2) = v5; *((_QWORD *)v3 + 1) = ptr; *(_DWORD *)v3 = ++unk_202024; return 0LL; }
这里似乎,v3+6的偏移存放的是书的描述的长度,v2是与book的结构体数组头的偏移,
*((_QWORD *)off_202010 + v2) = v3;
这里是将新分配的结构体地址放入结构体数组指针里,之后将v5,也就是book的description的地址放给v3偏移为2的地方,ptr,也就是name的地址放入v3偏移为2的地方。
这里面还存在一个函数,就是readName:
__int64 __fastcall sub_9F5(_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;
return 0LL;
}
分配两次,代码类似这样:
add(0x20,"book1_name",200,"book1_destruct")
add(0x21000,"book1_name",0x21000,"book1_destruct")
这个函数存在一些问题,a1 是我们想要写入的字符串的起始地址,a2是判定边缘,但是,从0开始,一直到a2为止,很显然多进行了一次读入,因为这里的逻辑是先读入,再判断i与a2是否相等,所以这里就多循环了一次,造成了offbyone,结束循环之后,又将后一位的内存修改成了\x00,因此发生了溢出,例如一个数组是32字节,这个程序调用这个函数的时候,一直都是用的size-1,所以传入的是31,这个程序就刚好做到了让整个数组刚好可以写满,也就是写道buf[31],这里刚好写满,但是,有个关键的问题,最后一个还操作了一下,让*a1=0,这也就导致了buf[32]=0的发生,溢出了一个字节,也就造成了offbyone,或者说off by null。
上gdb看看,先输入32个a作为名字之后,那段内存变成了这样:
pwndbg> search aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Searching for value: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
pwn 0x555555602040 0x6161616161616161 ('aaaaaaaa')
pwndbg> x/30gx 0x555555602040
0x555555602040: 0x6161616161616161 0x6161616161616161
0x555555602050: 0x6161616161616161 0x6161616161616161
0x555555602060: 0x0000000000000000 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000
0x555555602080: 0x0000000000000000 0x0000000000000000
0x555555602090: 0x0000000000000000 0x0000000000000000
0x5555556020a0: 0x0000000000000000 0x0000000000000000
0x5555556020b0: 0x0000000000000000 0x0000000000000000
0x5555556020c0: 0x0000000000000000 0x0000000000000000
0x5555556020d0: 0x0000000000000000 0x0000000000000000
0x5555556020e0: 0x0000000000000000 0x0000000000000000
0x5555556020f0: 0x0000000000000000 0x0000000000000000
0x555555602100: 0x0000000000000000 0x0000000000000000
0x555555602110: 0x0000000000000000 0x0000000000000000
0x555555602120: 0x0000000000000000 0x0000000000000000
这里我想着直接通过IDA反编译的来确定这俩BSS段数据的地址的,结果IDA里莫名其妙的,有点怪怪的,这里就直接GDB调算偏移然后算真实地址之类的吧。首先,刚刚那里确定了作者name的那个地址为:0x555555602040,gdb里调的时候查到elf的基地址为:0x555555400000(手动计算出来的),然后算出bss里作者name的偏移为:0x202040,加起来之后和0x555555602040这个地址一样,所以,可以断定,这个地址就是存放作者名字的地方,之后,经过两次申请内存之后,再看看0x555555602040地址的内存(根据结构体指针数组在bss段上,然后暴力经过两次malloc之后查询bss段内容有无变化发现了一些少量变化,由此定位结构体指针数组的地址):
pwndbg> x/30gx 0x555555602040
0x555555602040: 0x6161616161616161 0x6161616161616161
0x555555602050: 0x6161616161616161 0x6161616161616161
0x555555602060: 0x00005555556037a0 0x00005555556037d0
0x555555602070: 0x0000000000000000 0x0000000000000000
0x555555602080: 0x0000000000000000 0x0000000000000000
0x555555602090: 0x0000000000000000 0x0000000000000000
0x5555556020a0: 0x0000000000000000 0x0000000000000000
0x5555556020b0: 0x0000000000000000 0x0000000000000000
0x5555556020c0: 0x0000000000000000 0x0000000000000000
0x5555556020d0: 0x0000000000000000 0x0000000000000000
0x5555556020e0: 0x0000000000000000 0x0000000000000000
0x5555556020f0: 0x0000000000000000 0x0000000000000000
0x555555602100: 0x0000000000000000 0x0000000000000000
0x555555602110: 0x0000000000000000 0x0000000000000000
0x555555602120: 0x0000000000000000 0x0000000000000000
发现0x555555602060这个地址的内容变了,并且,还是某个书的结构体的数据域的地址:
pwndbg> x/30gx 0x00005555556037a0
0x5555556037a0: 0x0000000000000001 0x00005555556036b0
0x5555556037b0: 0x00005555556036d0 0x00000000000000c8
0x5555556037c0: 0x0000000000000000 0x0000000000000031
0x5555556037d0: 0x0000000000000002 0x00007ffff7d66010
0x5555556037e0: 0x00007ffff7d44010 0x0000000000021000
0x5555556037f0: 0x0000000000000000 0x0000000000020811
0x555555603800: 0x0000000000000000 0x0000000000000000
0x555555603810: 0x0000000000000000 0x0000000000000000
0x555555603820: 0x0000000000000000 0x0000000000000000
0x555555603830: 0x0000000000000000 0x0000000000000000
0x555555603840: 0x0000000000000000 0x0000000000000000
0x555555603850: 0x0000000000000000 0x0000000000000000
0x555555603860: 0x0000000000000000 0x0000000000000000
0x555555603870: 0x0000000000000000 0x0000000000000000
0x555555603880: 0x0000000000000000 0x0000000000000000
刚好整个程序存在一个修改作者名字的功能,可以修改作者名字,进行第二次off by null,修改0x00005555556036f0为0x0000555555603600:
pwndbg> x/30gx 0x555555602040
0x555555602040: 0x6262626262626262 0x6262626262626262
0x555555602050: 0x6262626262626262 0x6262626262626262
0x555555602060: 0x0000555555603700 0x00005555556037d0
0x555555602070: 0x0000000000000000 0x0000000000000000
0x555555602080: 0x0000000000000000 0x0000000000000000
0x555555602090: 0x0000000000000000 0x0000000000000000
0x5555556020a0: 0x0000000000000000 0x0000000000000000
0x5555556020b0: 0x0000000000000000 0x0000000000000000
0x5555556020c0: 0x0000000000000000 0x0000000000000000
0x5555556020d0: 0x0000000000000000 0x0000000000000000
0x5555556020e0: 0x0000000000000000 0x0000000000000000
0x5555556020f0: 0x0000000000000000 0x0000000000000000
0x555555602100: 0x0000000000000000 0x0000000000000000
0x555555602110: 0x0000000000000000 0x0000000000000000
0x555555602120: 0x0000000000000000 0x0000000000000000
那么,0x0000555555603600这个地址指向的地方是哪里呢?用heap指令看看:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555603000
Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE
Addr: 0x555555603290
Size: 0x410 (with flag bits: 0x411)
Allocated chunk | PREV_INUSE
Addr: 0x5555556036a0
Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSE
Addr: 0x5555556036c0
Size: 0xd0 (with flag bits: 0xd1)
Allocated chunk | PREV_INUSE
Addr: 0x555555603790
Size: 0x30 (with flag bits: 0x31)
Allocated chunk | PREV_INUSE
Addr: 0x5555556037c0
Size: 0x30 (with flag bits: 0x31)
Top chunk | PREV_INUSE
Addr: 0x5555556037f0
Size: 0x20810 (with flag bits: 0x20811)
发现这个地址是在book1_desc的中间:
pwndbg> x/50gx 0x5555556036c0
0x5555556036c0: 0x0000000000000000 0x00000000000000d1
0x5555556036d0: 0x65645f316b6f6f62 0x0000000000006373
0x5555556036e0: 0x0000000000000000 0x0000000000000000
0x5555556036f0: 0x0000000000000000 0x0000000000000000
0x555555603700: 0x0000000000000000 0x0000000000000000 <-------------------
0x555555603710: 0x0000000000000000 0x0000000000000000
0x555555603720: 0x0000000000000000 0x0000000000000000
0x555555603730: 0x0000000000000000 0x0000000000000000
0x555555603740: 0x0000000000000000 0x0000000000000000
0x555555603750: 0x0000000000000000 0x0000000000000000
0x555555603760: 0x0000000000000000 0x0000000000000000
0x555555603770: 0x0000000000000000 0x0000000000000000
0x555555603780: 0x0000000000000000 0x0000000000000000
0x555555603790: 0x0000000000000000 0x0000000000000031
0x5555556037a0: 0x0000000000000001 0x00005555556036b0
0x5555556037b0: 0x00005555556036d0 0x00000000000000c8
0x5555556037c0: 0x0000000000000000 0x0000000000000031
0x5555556037d0: 0x0000000000000002 0x00007ffff7d66010
0x5555556037e0: 0x00007ffff7d44010 0x0000000000021000
0x5555556037f0: 0x0000000000000000 0x0000000000020811
0x555555603800: 0x0000000000000000 0x0000000000000000
0x555555603810: 0x0000000000000000 0x0000000000000000
0x555555603820: 0x0000000000000000 0x0000000000000000
0x555555603830: 0x0000000000000000 0x0000000000000000
0x555555603840: 0x0000000000000000 0x0000000000000000
内存布局大概有了,这里借用某位大佬的图(hollk):
修改了book1的结构体指针地址之后,因为book1_name这里是可控的,所以可以在指向的那个地址伪造一个fake_chunk,
因为后面确实对我而言有点逆天,所以之后就简述了吧,之后就是伪造chunk泄露libc地址,然后继续伪造fakechunk修改__free_hook
为one_gadget,即可拿到shell。
0x0000000000000000
0x555555603830: 0x0000000000000000 0x0000000000000000
0x555555603840: 0x0000000000000000 0x0000000000000000
内存布局大概有了,这里借用某位大佬的图(hollk):
[外链图片转存中...(img-sbaqzceF-1731158930159)]
修改了book1的结构体指针地址之后,因为book1_name这里是可控的,所以可以在指向的那个地址伪造一个fake_chunk,
因为后面确实对我而言有点逆天,所以之后就简述了吧,之后就是伪造chunk泄露libc地址,然后继续伪造fakechunk修改`__free_hook`为one_gadget,即可拿到shell。