首页
论坛
课程
招聘
[原创]CTF-PWN常规题个人实战笔记(持续更新)
2021-2-24 12:18 3040

[原创]CTF-PWN常规题个人实战笔记(持续更新)

2021-2-24 12:18
3040

目录

 

本文主要记录一些个人在CTF PWN(主要是linux用户态)中遇到的一些坑,以及一些方法/套路的总结。

CTF PWN调试技巧

带源码调试glibc

https://xuanxuanblingbling.github.io/ctf/tools/2020/03/20/gdb/

gdb参数

https://blog.csdn.net/gatieme/article/details/51671430

如何使用patchelf换上题目的libc.so.6

https://www.jianshu.com/p/febaae0c410d

1
patchelf --set-rpath /root/roarctf/ ./2a1

其中 --set-rpath 后面跟题目给的 libc.so.6 路径,然后最后是文件名

 

效果如下

 

修改前:

 

root@ubuntu:~/roarctf# ldd 2+1
linux-vdso.so.1 => (0x00007fff2b9d9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f37286c1000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3728a8b000)

 

修改后:

 

root@ubuntu:~/roarctf# ldd 2a1
linux-vdso.so.1 => (0x00007fff12b47000)
libc.so.6 => /root/roarctf/libc.so.6 (0x00007fca605af000)
/lib64/ld-linux-x86-64.so.2 (0x00007fca60979000)

 

可以看到路径已经变化。

 

有时候光换libc是不够的,大概率ld也要换。

在gdb中dump当前进程的memory

https://blog.csdn.net/caiqiiqi/article/details/72807952

 

https://blog.csdn.net/luozhaotian/article/details/79609077

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> print getpid()
 
Program received signal SIGALRM, Alarm clock.
$1 = 101499
 
pwndbg> shell cat /proc/101499/maps | grep vdso
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
 
pwndbg> dump memory ./linux-vdso.so.1 0x7ffff7ffa000 0x7ffff7ffc000    # 这一步dump内存生成linux-vdso.so.1
pwndbg> q
root@ubuntu:~/roarctf# ls
2a1  core  exp.py  libc.so.6  linux-vdso.so.1
root@ubuntu:~/roarctf# file li
libc.so.6        linux-vdso.so.1 
root@ubuntu:~/roarctf# file linux-vdso.so.1             # 查看dump出来的
linux-vdso.so.1: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=4cd5c52e24bb8daba3f767be4ca9084487a64e78, stripped
root@ubuntu:~/roarctf#

编写gdbscript自动化操作的一些例子

1.可以直接在.gdb文件中写。然后source进去

 

2.利用python import gdb然后通过gdb.execute(cmd)来执行命令,同样也是source进去

 

例子1:https://www.cnblogs.com/dylancao/p/9252756.html

 

例子2:

 

在前一段时间的sudo cve中,我们使用gdb脚本编写了一个基于gdb的fuzz程序,也拿上来给大家做一个参考。

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import gdb
import random
#from pwn import *
# POC
# sudo gdb --args sudoedit -s '\' `perl -e 'print "A" x 550'`
 
# 1.sudo gdb sudoedit
# 2.source gdb.py
 
 
 
def set_up():
    gdb.execute("start")
    gdb.execute("set pagination off")
    gdb.execute("set logging on ./gdb.output")
 
def over():
    gdb.execute("set logging off")
    #gdb.execute("quit")
 
def fuzz():
    set_up()
    cmd = "set env LC_ALL = %s" %("A"*4792)
    #print("fuzz1_test:",i)
    gdb.execute(cmd)
 
    cmd = "run -s '\\' `perl -e 'print \"A\" x %s'`" %(4792)
    print("fuzz1_test:",cmd)
    gdb.execute(cmd)
    gdb.execute("bt")
    over()
def fuzz1(i,j):
    set_up()
    cmd = "set env LC_ALL = %s" %("A"*i)
    print("fuzz1_test:",i)
    gdb.execute(cmd)
 
    cmd = "run -s '\\' `perl -e 'print \"B\" x %s'`" %(j)
    print("fuzz1_test:",cmd)
    gdb.execute(cmd)
    gdb.execute("bt")
    over()
 
def fuzz2(i,j,k):
    set_up()
    cmd = "set env LC_ALL = %s" %("C"*i)
    print("fuzz2_test:",i)
    gdb.execute(cmd)
    cmd = "set env LOCPATH = %s" %("D"*j)
    print("fuzz2_test:",j)
    gdb.execute(cmd)
    cmd = "run -s '\\' `perl -e 'print \"E\" x %s'`" %(k)
    print("fuzz2_test:",cmd)
    gdb.execute(cmd)
    gdb.execute("bt")
    over()
 
def fuzz3(i,j,k):
    set_up()
    cmd = "set env LC_ALL = %s" %("F"*i)
    print("fuzz3_test:",i)
    gdb.execute(cmd)
    cmd = "set env LC_MESSAGES = %s" %("G"*j)
    print("fuzz3_test:",j)
    gdb.execute(cmd)
    cmd = "run -s '\\' `perl -e 'print \"H\" x %s'`" %(k)
    print("fuzz3_test:",cmd)
    gdb.execute(cmd)
    gdb.execute("bt")
    over()
 
def fuzz4(i,j,k):
    set_up()
    cmd = "set env LOCPATH = %s" %("I"*i)
    print("fuzz4_test:",i)
    gdb.execute(cmd)
    cmd = "set env LC_MESSAGES = %s" %("J"*j)
    print("fuzz4_test:",j)
    gdb.execute(cmd)
    cmd = "run -s '\\' `perl -e 'print \"K\" x %s'`" %(k)
    print("fuzz4_test:",cmd)
    gdb.execute(cmd)
    gdb.execute("bt")
    over()
 
def fuzz5(i,j,k,h,l,p):
    set_up()
    cmd = "set env LC_ALL = %s" %("F"*p)
    print("fuzz5_test:",cmd)
    gdb.execute(cmd)
    cmd = "set env LC_NUMERIC C.UTF-8@%s"%("A"*i)
    print("fuzz5_test:",cmd)
    gdb.execute(cmd)
    cmd = "set env LC_MESSAGES = %s" %("J"*l)
    print("fuzz5_test:",cmd)
    gdb.execute(cmd)
    cmd = "set env LC_MEASUREMENT C.UTF-8@%s"%("B"*j)
    print("fuzz5_test:",cmd)
    gdb.execute(cmd)
    cmd = "run -s '%s\\' `perl -e 'print \"K\" x %s'`" %("C"*k,h)
    print("fuzz5_test:",cmd)
    gdb.execute(cmd)
    gdb.execute("bt")
    over()
 
 
 
 
def main():
    begin=200
    end=1000
    count=800
 
    for i in range(2000):
        fuzz5(  random.randint(5,100),
            random.randint(5,100),
            random.randint(5,100),
            random.randint(200,800),
            random.randint(5,100),
            random.randint(5,100),
            )
    gdb.execute("quit")
 
main()

1.通过 gdb.execute(cmd)来执行gdb命令。

 

2.关闭分页并打开log输出,指定输出到./gdb.output

1
2
gdb.execute("set pagination off")
gdb.execute("set logging on ./gdb.output")

3.在gdb中通过执行set env来调整环境变量的值。

 

4.每次crash完都运行backtrace导出bt信息到log文件。

栈利用tricks

栈迁移相关

  • 当A中调用B时,两个函数的栈是相邻的,可以覆盖返回地址为ret直接滑到A的栈里,调用A的栈里布置的payload
  • 有时候会崩溃在system+1094这个位置,往往是rsp对齐的问题,只需要在调用payload前加一个/多个ret即可,抬高一下栈

堆利用tricks

堆利用的一些小trick总结

关于几种bins的进入,退出方式

  • fastbin:头插头取,FILO
  • unsortedbin:头插尾取,FIFO。一开始通过unortedbin->bk找到最后一个chunk
  • tcachebin:LIFO,因为要做缓存

关于top chunk的合并

  • topchunk不合并fastbin里的
  • 当topchunk前面的chunk为空闲且不为fastbin里的chunk时进行合并

关于申请/切割大小

  • 有时候直接对unsortbin做切割时不会返回切割后刚好的大小,而是整个chunk返回——因为64位下最小可用chunk大小为0x20,如果切割后chunk小于这个大小,那么就会返回整个chunk,不会切割。

关于fd和bk

  • fd:point to the next free chunk

  • bk:point to the preovious free chunk

当存在uaf,但是清空了table中的指针,size时,如何泄露地址/利用?(无法做到直接连续的tcahce dup或连续free)

  • 如果有堆溢出,考虑overlap掉下面的,然后通过上面的chunk做溢出劫持下方的fd、bk、next指针。

  • 虽然清空了table中的指针,但是我们可以先free一次放进unsortbin,然后再申请出来,若使用的是malloc而不是calloc,或者没有特定的清楚堆块内容的操作时,fd和bk指针会留在里面(相当于进bins里溜了一圈带了俩指针出来。。),然后直接show出来。

  • 注意检查add、edit、del函数中index的合法性(是否为unsigned int?是否可以越界?)

  • 注意映射位置,heap的mapped到的位置很接近bss段,看是否可以越界过去。

  • 当有off-by-one时考虑,做overlap,假如可以修改pre_in_use位,那么比如说先申请三个chunk,0,1,2(其中0号为大chunk可直接进入unsortbin,1号为0x68小chunk,2号为大chunk可直接进入unsortbin),先del(0),再del,add1号,对2号使用off-by-one,同时伪造pre_size位为0+1号的总size合(注意申请1号chunk大小以0x8结尾,方便off-by-one),此时指示0,1号chunk为free状态,再del(2),此时触发unsortbin的前向合并,0、1、2三个chunk被合并放入unsortbin,然后再单独del(1)使其进入tcahce(或fastbin),此时1号chunk被overlap,他既在unsortbin又再tcache。

    然后对于合并放入unsortbin的chunk做切割,将fd、bk指针下放进入1号chunk(既在unsort又在tcache),注意切割的时候要错开,防止把tcache里的1返回,比如1大小为0x68,0大小为0x500,那么先申请0x500,然后申请0x78(注意不是0x68,此时要求从unsortbin中返回而不是tcahce)

    以上操作结束以后,1号chunk此时在tache中,但是被下放了指向main_arena的指针,在申请0x78的时候同时做edit,则可以修改fd、bk指针的位置,比如让其指向stdout。这一方法的目的是在tcache中踩出指向main_arena的指针

  • 有的题在add和edit的时候都让用户输入了size,但是只对一个做了范围限制,比如只限制了add时的size,但是edit时输入的size可是任意值(或负值),造成堆溢出可任意大小读入。这种存在堆溢出的情况下我们比如申请4个chunk(0,1,2,3)大小分别是(0x68,0x68,0xf0,0x68)然后通过0溢出到1修改1的size使1包含一部分2,接着放掉1然后再申请回来(大小为1+包含的2的一部分),此时free掉2,2中出现fd、bk指针,然后再show(1),因为把1重新申请回来的时候size已经变大了,这时候就会把1+2被overlap的那一部分一起打印出来,此时2被overlap的那一部分中存在fd、bk指针,至此泄露成功(0ctf2017_babyheap )

a

  • 关于进阶版,只有off-by-null,对于chunk(0,1,2)大小分别为(0x68,0x38,0x420),首先删掉#0。然后用#1做off-by-null,使#2的size:0x421→0x400,同时伪造#2的pre_size为size(#0 + #1)的和(这样unsortbin前向合并的时候才知道前面的大小到底是多少)。之后在#2的末尾造一个0x31的fake_chunk(否则由于chunk2的size的减小,堆结构会崩溃)。

    然后del(2),此时#0→#2合并,#1被overlap

    然后切割unsotbin中的大chunk。造成fd、bk下放到原#1的位置,由于#1是被直接overlap的,所以table仍存储了#1的ptr和size,然后show(1)泄露libc。

info_struct类型的题

此种类型的题中,bss段上的全局变量并不直接储存chunk的地址,而是储存一个info struct的地址(一般与chunk一同被申请出来),而info struct记录了chunk的一些信息,一般是大小,地址等。题目通过先查找info struct再去操作具体的chunk

  • 看info struct是否可能共用?(比如内容输入一样时只申请info struct不申请new note)

  • 看是否可以直接show出fd、bk指针?

  • 注意strcmp类似的函数!注意if/else分支中malloc的区别!

关于劫持stdout

  • stdout/stderr/stdin在got表中存有真实的地址,如果是tcache pwn,可以考虑dup然后从got表劫持stdout

  • stdout结构:

    图片描述

​ 改_flags为0xfbad1800,然后p64(0)*3,然后改_IO_write_base/ptr/end可控制输出的位置和内容,一般直接输出自己就行。

  • 2.24以下可以劫持vtable,伪造IO结构体,注意fclose函数会调用vtable中的函数指针,可以伪造vtable+IO结构体让vtable中的函数指针指向system

关于top chunk的利用

  • 劫持top chunk的size的时候记得addr(top chunk)+size必须是页对齐的。
  • 如果一开始top chunk的大小太大,没法劫持(超过了能申请的最大大小),那么就做多次申请,对topchunk进行切割,等top chunk的size变小了再申请。

关于size被卡死时

  • 看能不能劫持到大小为0x290的 pthread_tcache_struct 这样就能拿一个0x290的chunk,扔进ub里。

关于UAF与地址泄露

  • 如果程序有且只有一个UAF时,此时泄露地址一般不是特别困难,尤其注意能不能直接造一个大chunk扔进ub里,然后重新拉回来(切割回来)然后直接show出来残留的指针。(没有memset或者calloc的话)

关于memset(chunk_addr,0,size)清空残留指针

  • 注意64位下最小分配单元为0x20,那么我们通过malloc(1),这样拉回来0x20的chunk,此时size=1,memset无法清空全部的残留指针。

关于tcache取chunk(glibc2.27,glibc2.29)

查看一下tcache_get

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)      // 从tcache中取  
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);            //保证tc_idx下标合法
  assert (tcache->entries[tc_idx] > 0);         //保证对应的entry的指针存在。所以这里产生了一个问题,tcache_get的时候没有验证对应的tcache->counts[tc_idx]的计数。只要entry指针存在就往出嗯取
  tcache->entries[tc_idx] = e->next;            //将e->next挂上entry
  --(tcache->counts[tc_idx]);                   //entry计数 -1
  e->key = NULL;                                //清空key
  return (void *) e;
}

你会发现,当tcache取chunk的时候会出现即使计数为0,但只要next指针合法,也能取出来的情况。。。并且此时tcache计数为-1(255)

 

此漏洞在glibc2.31中被修复。

 

图片描述

高版本堆利用(glibc2.29,glibc2.31)

Tcahce stashing unlink

简述:在高版本下对于tcache有一种stashing机制,大体就是说,当从smallbin中取大小为size的chunk时,如果发现取出来一个chunk,但是smallbin中还有剩余的chunk,那么会将剩余的chunk插入对应size的tcahce_entry中。称为:“stashing”

 

源码:

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
/*
     If a small request, check regular bin.  Since these "smallbins"
     hold one size each, no searching within bins is necessary.
     (For a large request, we need to wait until unsorted chunks are
     processed to find best fit. But for small ones, fits are exact
     anyway, so we can check now, which is faster.)
   */
 
  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);        //拿smallbin的idx
      bin = bin_at (av, idx);
 
      if ((victim = last (bin)) != bin)    //smallbin不空时
        {
          bck = victim->bk;                //先拿倒数第二个chunk
      if (__glibc_unlikely (bck->fd != victim))    //分支预测优化,保证倒数第二个chunk的fd指向最后一个chunk(victim)
        malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;                //bin的最后一个chunk为倒数第二个,准备最后一个chunk脱链
          bck->fd = bin;                //bck的fd重新指向bin,victim脱链
 
          if (av != &main_arena)
        set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE                            //如果开启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);    //遍历tcache,获取相同size的tc_idx
      if (tcache && tc_idx < mp_.tcache_bins)
        {
          mchunkptr tc_victim;
 
          /* While bin not empty and tcache not full, copy chunks over.  */
          //#define first(b)     ((b)->fd)
          //#define last(b)      ((b)->bk)
          //stashing 机制
         while (tcache->counts[tc_idx] < mp_.tcache_count
             && (tc_victim = last (bin)) != bin)//当对应的tcache不满时,取出smallbin末尾的chunk=tc_victim
        {
              //如果成功获取了chunk
          if (tc_victim != 0)
            {
              bck = tc_victim->bk;        //bck为smallbin中倒数第二个chunk
              set_inuse_bit_at_offset (tc_victim, nb);
              if (av != &main_arena)
            set_non_main_arena (tc_victim);
              bin->bk = bck;          //最后一个chunk脱链
              bck->fd = bin;
 
              tcache_put (tc_victim, tc_idx);//放到tcache
                }
        }
        }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

stashing 打任意地址写一个bin的地址(任意地址大数写)

启动条件(假设chunk大小0x110):

 

1.(0x110)smallbin 中两个chunk:#1 -> #2

 

2.tcache_entry[0x110] 中不满(not full)

 

过程与结果:

  1. 劫持 #1的bk指针指向 evil_addr-0x10 ,并 保证 #1的fd指针不变

  2. 越过tcache,从smallbin中取0x110的chunk(一般是用calloc实现),此时 #2 从free状态被取出变成used#1 被tcahce_put放入对应的不满的tcache_entry中evil_addr被写入bin地址(0x7f的大数)

stashing 打任意地址分配 与 任意地址写一个bin的地址(任意地址大数写)

启动条件(假设chunk大小0x110,假设我们想分配到 evil_addr):

 

1.(0x110)smallbin 中两个chunk:#1 -> #2

 

2.tcache_entry[0x110] 中不满(not full)

 

过程与结果:

  1. 劫持 #2 的bk指向 evil_addr
  2. 保证 evil_addr+0x10处存#2的地址
  3. 此时bin的bk变为 evil_addr , evil_addr+0x10被写上bin的地址。
  4. 保证 *(evil_addr+0x8)+0x10 可写(会被写上一个bin地址)
  5. tcache_put将evil_addr 放入tcahcebin中

House_of_botcake系列

house_of_botcake : 假如我们有 #1 #2 两个chunk,那么通过将#1 #2 合并放入ub,然后再单独free #2,就实现了#2既在ub又在tc中,之后对#1 进行合适大小的错位切割,可以劫持#2的next指针实现任意地址申请。

 

House_of_botcake+ : 假如我们有 #1 #2 两个chunk(0x110),假设只能申请最大malloc 0x100,那么通过将#1 #2 合并放入ub,然后再单独free #2,就实现了#2既在ub又在tc中,但是此时做不出特别合适的切割大小来一步到位劫持 #2指针,可以考虑先切一个小的chunk,比如0x20,之后ub中的chunk会被切割,导致当我们再次add一个大chunk(0x100)时,返回给我们的chunk是 #1+0x20 ,此时如果再add 0x100,就实现了申请一个地址在:[#1+0x20,#2+0x20] 的chunk #3,然后把#3 free进tc中与#2连接,就可以劫持#2的next指针实现任意地址申请。

高版本栈迁移的问题,使用了一些特有的gadgets

2.29 2.31的gadget,用来拉栈。

 

https://blog.csdn.net/carol2358/article/details/108351308

其他

关于字节对齐

举个例子:

 

malloc(0xf8),返回0x100

 

malloc(0xf9),返回0x110


第五届安全开发者峰会(SDC 2021)议题征集正式开启!

收藏
点赞5
打赏
分享
打赏 + 2.00
打赏次数 1 金额 + 2.00
 
赞赏  kanxue   +2.00 2021/02/25 感谢分享~
最新回复 (4)
雪    币: 6705
活跃值: 活跃值 (4922)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2021-2-24 14:44
2
0
感谢分享
雪    币: 1852
活跃值: 活跃值 (4821)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 活跃值 8 2021-2-25 14:59
3
0
辛苦了!
雪    币: 253
活跃值: 活跃值 (949)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Zard_ 活跃值 2021-3-2 11:10
4
0
感谢分享
雪    币: 9675
活跃值: 活跃值 (7160)
能力值: (RANK:600 )
在线值:
发帖
回帖
粉丝
有毒 活跃值 9 2021-3-5 14:08
5
0
支持长期更新
游客
登录 | 注册 方可回帖
返回