# pwn 学习笔记(11)–off_by_one
在处理 for 循环或者 while 循环的时候,有的可能会遇到如下情况:
1 2 3 4 5 6 7 8 9 10 11 12 #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 。
1 2 3 4 5 6 7 8 9 10 if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long ) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating" ); unlink_chunk (av, p); }
还有种情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #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 一下看看:
1 2 3 4 5 6 7 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 __int64 sub_F55 () { int v1; int v2; void *v3; void *ptr; void *v5; 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 (0x20 uLL); 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:
分配两次,代码类似这样:
1 2 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 作为名字之后,那段内存变成了这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 段内容有无变化发现了一些少量变化,由此定位结构体指针数组的地址):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 这个地址的内容变了,并且,还是某个书的结构体的数据域的地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 指令看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 的中间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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。