CTF PWN题总结

pwn题总结

拟态ctf

这周打拟态的比赛,做PWN题的时候发现一件事,题目感觉都不太难,但是我却做的很慢,一题差不多都需要好几个小时才能做出来。

分析了一下原因,首先是我本人性格有点慢悠悠的,做题速度本来就慢。其次,就是工作了以后,打CTF的次数大大减少了,一个月都不一定会做一道PWN题,以前学的一些知识点自然而然就忘了,生疏了。然后又要重新开始学习,这就大大增加了我做题(对我来说简单的题,以前会的)的时间。

所以我现在试着想一些办法,来减少一下重新学习这部分的时间。所以准备对这次的拟态ctf的几道pwn题进行一次总结。

本次比赛,一共有7道题,其中一道题无解,剩下的6道题都很简单,两道栈溢出的题,4道堆溢出的题。

做题的第一部都是先找洞,然后才是根据存在的漏洞研究利用方法。找洞的过程就没办法总结了,全看个人灵性。

主要记录下每道题的利用思路。

easy-stack

最简单的栈溢出,还提供了格式化字符串。这种题太简单了,没啥好说的,感觉我几年不做pwn,再看到这种题,也还是能很快做出来。

格式化字符串泄漏地址,栈溢出ROP执行one_gadget/system(/bin/sh)

goodnote

本题情况简述:堆题,64位,libc 2.27,可控malloc长度,可以free,可以edit堆,可以show堆信息

限制:保护全开,只能同时分配16个堆

该题存在两个漏洞:

  1. malloc(input * 4),可以让input * 4 < input
  2. 字符串输出堆的内容,而不是通过长度字段输出

第一个漏洞可以导致在编辑堆的时候,导致堆溢出,第二个洞可以泄漏地址。

在保护全开的堆题中,大部分的解题思路都需要先泄漏地址(house of romen可以不泄漏地址)。

泄漏libc地址

分配一个unsortedbin,然后释放,这样该堆的fd/bk位置就是libc地址。

通过第一漏洞,在unsortedbin上面一个堆进行堆溢出,把unsortedbin的头部都填充非0字符。这样show上面这个堆的时候,就可以泄漏libc地址。

利用

堆的题目一般的利用思路都是写malloc_hook或者free_hook

本题的libc为2.27,所以可以利用tcache进行任意写。

  1. 连续申请3个长度为0x20的堆A, B, C,首先释放A,然后释放C。两个堆都会被放入tcache中,然后C指向A。
  2. B堆利用第一个漏洞,可以编辑C堆的fd,让其指向free_hook,这样再申请的第二个0x20的堆,就是free_hook地址,可以任意编辑free_hook的值。
  3. free_hook可以改成system,也可以改成one_gadget,利用到这里就很简单了。

rbsystem

本题情况简述:堆题,64位,libc 2.27,可控malloc长度,可以edit堆,可以show堆信息

限制:保护全开,需要fopen(/dev/urandom)才能edit堆,而且edit堆使用的是fread,所以edit堆的内容不可控,

该题存在两个漏洞:

  1. edit堆有一个offset参数,可以是负数,也就是说可以edit该堆之前的所有堆
  2. 字符串输出堆的内容,而不是通过长度字段输出

让edit可控

edit如果不可控的话,很难进行后续的利用了,所以之前,首先需要找一个方法,让edit可控。
因为edit的数据都是通过fread写入堆的,所以我们可以想办法把urandom的文件描述符改成0,这样就可以变成从标准输入读数据了。

再通过fopen打开一个文件后,FILE_IO结构体会被储存在堆上,所以我们可以首先fopen文件,生成FILE_IO的堆,然后我们在自己add一个堆,利用第一个漏洞,算好到FILE_IO -> fileno的偏移。fread一个字节到该地址,因为fread的数据是随机的,所以有1/256的概率成功写入0。直接爆破就完事了,不过fread默认会直接读0x1000字节到缓冲区,我们还需要把缓冲区给情况,要不然无法check是否成功把fileno改成了0。

泄漏libc地址

泄漏libc地址我使用的方法是,修改FILE_IO的read_ptr和read_end指针到FILE_IO->chain的地址,然后通过fread写入数据到堆,这时候写入的数据就是chain的值,就是libc的地址,然后通过show功能,就能泄漏出libc地址了。

利用

因为libc的版本是2.27,所以这里又利用到了tcache的特性,因为tcache的结构体也储存在堆上,我们可以通过第一个漏洞来修改tcache的链表,达到任意写的目的。

pwn_emmm

本题情况简述:堆题,64位,libc 2.23,可控malloc长度,可以free,可以edit堆,可以show堆信息

限制:保护全开,每次add,都会自动分配一个长度为0x20的堆,只能输出可以edit堆的信息,该堆内容不可控,只有该堆可以被free,只能同时分配21个堆

该题存在两个漏洞:

  1. free无限制,可以导致2free
  2. 字符串输出堆的内容,而不是通过长度字段输出

泄漏libc地址

本题的libc泄漏比较复杂,堆构造也比较麻烦,感觉一段时间后再做这种题,我还是需要花大量时间。

总结下大致思路吧。

通过2free,只修改fd的最后一字节,比如有一个能被释放的堆A,长度为0x20字节。那么通过2free,修改fd的最后一字节,使fd的地址指向A-0x10,然后通过对fastbin的控制,让这个地址的堆分配给可以edit内容的堆,并且把A也分配给可以edit内容的堆。这样我们就可以修改堆A的size字段,修改到unsortedbin的长度,再次释放A,这样fd和bk都是libc地址。

然后再输出A,这样就能得到libc地址。

这里需要注意,size不是随意修改的,2.23的libc会通过该size查找下一个堆块,确定下一个堆块size的有效性。

利用

泄漏libc虽然复杂,但是利用的难度会更低,2.23版本的libc中,fastbin的分配会checkfastbin的size字段的,所以没法想2.27的tcache做到任意地址写。

一般在free_hook之前的内存中是找不到有效size的。但是在malloc_hook中却找得到有效size:0x7f。所以这题的利用思路就是通过2free,写malloc_hook。

但是在最后利用的时候,却发现one_gadget没一个能用的。随后通过google搜到一个姿势,大致逻辑如下:

malloc_hook -> libc_relloc + 0x14 -> relloc_hook -> one_gadget

最后使用这个思路成功执行shell了。

不过还有一种思路,可以通过unsorted bin在free_hook上方踩出size,但是因为没有具体的例题,还不清楚该利用的具体逻辑,下次一定研究🐦。

newpad

本题情况简述:堆题,64位,libc 2.23,可控malloc长度,可以free,可以edit堆,可以show堆信息

限制:保护全开,malloc的长度限制在0x100以内,edit肯定会附加’\0’结尾,只能同时分配4个堆

该题存在两个漏洞:

  1. off-by-one
  2. 就算堆被释放了,也能输出

泄漏libc地址

这个就简单了,申请一个unsorted bin,然后再释放,然后再输出,就能泄漏出libc地址了

利用

off-by-one的利用思路好像是挺多的,我使用的思路如下:

  1. 申请堆A,B,C,通过off-by-one漏洞,修改B的size,大小为B+C的大小。
  2. 然后释放B,再申请一个B+C大小的堆块,这个堆块的地址就是B了,这个时候通过edit B就能修改C。
  3. A和C大小一样,先释放A,再释放C,这样C的fd指向了A,然后我们再修改fd为malloc_hook,这样我们再次申请的第二个堆就为malloc_hook。

之后的利用思路上面有类似的,就不多说了。

easy-stack2

这次比赛除了常规题还新增了提高题,其实就是一道正常题,挂上了他们的拟态系统。我个人感觉他们就是来宣传他们产品的,就是不让选手做的。但是不知道他们处于什么原因,这道easy-stack改简单了,变得能做了。

先来说说这种题的大致逻辑,简单的来说,就是有多台挂了该题的机器,我们就假设3台吧。我们访问的ip地址,可以算总控机器,把我们发送的请求同时发给这三台设备,然后得到返回,如果返回不一样(一开始我们还猜测是判断相似度,但是问了主办方,确认了是需要完全一样),则表示收到攻击,退出。

所以在开了aslr的机器上,不能进行leak地址,因为三台机器的libc地址只有极端情况下才会遇到一样的情况。所以只要leak libc地址,那么三台机器的返回肯定不一样,程序就退出了。

所以我们只能在不leak的情况下进行攻击,或者能让三台设备leak返回的结果一样。

最开始easy-stack2题目是开了canary的,所以根本没法泄漏出canary,也导致这题长时间没人做出来(估计是无解题)。

随后该题改简单了,就一个read的栈溢出,也没有canary,不需要leak的利用方法可以使用dlresolve。

虽然前段时间学过,但是现在我已经玩了dlresolve的各个结构体了,太难了真的。不过后来发现我学弟veritas[1]写了一个快速利用dlresovle的函数:

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
def ret2dl_resolve_linkmap_x64(ELF_obj,known_offset_addr,two_offset,linkmap_addr):
'''
WARNING: assert *(known_offset_addr-8) & 0x0000ff0000000000 != 0
WARNING: fake_linkmap is 0x100 bytes length,be careful
WARNING: two_offset = target - *(known_offset_addr)

_dl_runtime_resolve(linkmap,reloc_arg)
reloc_arg=0

linkmap:
0x00: START
0x00: l_addr = two_offset
0x08: fake_DT_JMPREL : 0
0x10: fake_DT_JMPREL : p_fake_JMPREL
0x18: fake_JMPREL = [p_r_offset,r_info,append],p_r_offset
0x20: r_info
0x28: append
0x30: r_offset
0x38: fake_DT_SYMTAB: 0
0x40: fake_DT_SYMTAB: known_offset_addr-8
0x48: /bin/sh(for system)
0x68: P_DT_STRTAB = linkmap_addr(just a pointer)
0x70: p_DT_SYMTAB = fake_DT_SYMTAB
0xf8: p_DT_JMPREL = fake_DT_JMPREL
0x100: END
'''
plt0 = ELF_obj.get_section_by_name('.plt').header.sh_addr

linkmap=""
linkmap+=p64(two_offset&(2**64-1))
linkmap+=p64(0)+p64(linkmap_addr+0x18)
linkmap+=p64((linkmap_addr+0x30-two_offset)&(2**64-1))+p64(0x7)+p64(0)
linkmap+=p64(0)
linkmap+=p64(0)+p64(known_offset_addr-8)
linkmap+='/bin/sh\x00'#for system offset 0x48
linkmap = linkmap.ljust(0x68,'A')
linkmap+=p64(linkmap_addr)
linkmap+=p64(linkmap_addr+0x38)
linkmap = linkmap.ljust(0xf8,'A')
linkmap+=p64(linkmap_addr+8)

resolve_call = p64(plt0+6)+p64(linkmap_addr)+p64(0)
return (linkmap,resolve_call)

有了这个函数,就大大缩减了我的做题时间,不需要我再重新学dlresolve了。

来解释一下这个函数:

  1. ELF_obj也就是通过pwntools的ELF打开的对象
  2. known_offset_addr参数是需要我们找一个地址,满足下面的条件:assert *(known_offset_addr-8) & 0x0000ff0000000000 != 0,一般就是找got表,比如read_got
  3. two_offset参数,比较我最终想执行的是system函数,那么该参数就是libc中system-read的值
  4. linkmap_addr也就是我们会把linkmap写哪,比如写到地址A上,一般就是bss段找地方写
  5. 写入地址A的数据就是返回值[0]
  6. 调用的rop就是返回值[1]

不过这种函数只适用于能修改link_map地址的利用情况。

newpad2

这题想了好久,还找到可以绕过0x100长度的方法,但是没屁用,最后问了主办方,确认了这题就是无解的,白通宵了,哎。。。

期间想了house of romen,但是发现edit每次会有\0结尾,特别致命,如果没有,感觉还可以做。

祥云杯2020

Beauty_Of_ChangChun

本题情况

  1. 2.31的libc
  2. 只有一次malloc机会,大小不可控
  3. 存在2free
  4. 存在UAF
  5. 可以任意alloc,但是大小控制在0x80-0x100之间,fastbin默认最大为0x80,所以这题不考fastbin,考smallbin的利用

利用思路

  1. alloc不会从tcache申请堆
  2. malloc先从tcache申请堆
  3. 在从smallbin申请内存后,如果smallbin还有值,tcache没满,这个时候会把smallbin移动到tcache里面,详细代码如下:
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
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
  1. 通过上述情况,可以修改mmap里面的随机数变成一个arena区域的地址,这样就能通过功能5获取flag了

影流之主

本题情况

  1. 每个功能有次数限制
  2. libc2.23
  3. 正常malloc,大小不可控,fastbin
  4. 考点是普通的fastbin的2free
  5. 难点是泄漏libc地址

利用

  1. 虽然每个功能有次数限制,但是存在下溢,在0的时候空跑一次,就相当于没限制了
  2. scanf输入的字符大于等于0x400的时候会malloc一个堆,在fastbin和smallbin共有的大小区间,当申请一个大于fastbin的堆,会把fastbin转换成smallbin
  3. 通过上述思路,让scanf申请一个大堆,把fastbin转换成smallbin,就能泄漏出libc地址了。
  4. 然后就是正常的2free流程

garden

本题情况

  1. libc2.29
  2. 正常malloc,不过只能同时存在9个堆,大小不可控为0x100,正常free,不存在漏洞
  3. 只有在malloc的时候才能写堆,之后不能编辑堆的内容
  4. 强行造了一个函数能让堆发生2free,只能执行一次
  5. 强行造了一个不能free的malloc,但是大小比正常malloc的小,为0x20

利用

  1. 考点应该是tcache的2free的利用,但是2.29的tcache存在检测,在bk位置有一个标志位。
  2. 本题思路就是想办法把tcache的bk标志位修改或清空,但是在malloc后不能编辑堆,导致增加了难度。
  3. 但是因为堆的数量有限制,所以也没办法通过fastbin进行利用。
  4. 所以现在的思路是使用该题强行加的malloc功能使堆发生偏移。
  5. 填满tcache,用到了7个
  6. unsortbin,使用强行造的洞free,这个时候可以泄漏libc地址(堆A)
  7. 再把unsortbin上面的堆free掉,这样就形成了一个0x220的unsortbin
  8. malloc 7次,把tcache清空
  9. malloc 0x20,会从unsortbin中进行分配,unsorbin大小变成0x1f0
  10. malloc 1次,从unsorbin中进行分配,分配到的堆包含到堆A(这个堆块还能正常free一次)(堆B)
  11. 然后释放堆A和B,填充tcache,让下一次malloc获取到的是堆B,并且能修改到堆A的fd字段,之后就是正常的2free流程的思路了。

把嘴闭上

本题情况

  1. libc 2.23
  2. 正常malloc,大小在范围内可控
  3. 正常free,没有漏洞
  4. 没有leak,题目强行帮你leak了一下libc地址
  5. 可以自由控制mallopt函数

利用

  1. mallopt在修改选项的时候都会执行一下malloc_consolidate
  2. 使用mallopt(M_MXFAST, 1)可以把global_max_fast修改为0
  3. malloc_consolidate函数中,当global_max_fast=0的时候,会被认为堆未初始化,会对arena的bins和top字段进行初始化,把top设置为arena区域的地址。
  4. 正常情况下malloc初始化的时候也会把top设置为arena区域的地址,但是随后还会执行sysmalloc,把top修改为正常的堆地址。
  5. 通过上述思路,可以把top chunk修改为arena区域的地址,然后算好和freehook之前的距离,多次malloc,可以成功申请到freehook的地址,修改freehook地址,然后执行/bin/sh

引用

  1. https://veritas501.space/2017/10/07/ret2dl_resolve%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
Author

Hcamael

Posted on

2020-06-22

Updated on

2020-11-25

Licensed under