ctf笔记

随心情笔记,不定期更新

one_gadgets笔记:

1.改malloc_hook为one_gadgets,一般malloc触发的方式,one_gadgets由于限制条件不满足,执行都不会成功,可以考虑free两次造成double free,调用malloc_printerr触发,恰好[esp+0x50]=0(当向非标准IO函数向缓冲流输出或输入的数据过大时,往往会先预先给数据分配内存。比如printf/scanf-打印/输入字符串过长时会触发malloc)

2.在地址上malloc_hook与realloc_hook是相邻的,在攻击malloc_hook我们没有能够成功执行one_gadgets,但是我们可以通过将malloc_hook更改为_libc_realloc+0x14,将realloc_hook更该为one_gadgets。 这样的好处在于,我们能够控制__malloc_hook指向的代码的内容,规避掉_libc_realloc中部分指令,从而更改在执行one_gadgets时的占空间,创建能够成功执行one_gadgets的栈空间。这是一个很巧妙的点

3.虽然__free_hook上方几乎是"\x00",无可用size,但是我们可以先用 unsorted attack 攻击__free_hook上方,在其上方踩出 size,再去劫持 __free_hook。

4.使用tcache stashing attackunsorted_bin_attack,将_IO_2_1_stdin_->_IO_buf_end改成main_arena+x(我这里是+352),从而可以在scanf的时候输入数据到realloc_hookmalloc_hook,改成one_gadget,调节下偏移即可。

5.利用house of husk ,覆写__printf_function_table表为heap地址(让其不为空),覆写__printf_arginfo_table表为heap地址且heap['s']被覆写为了one_gadget,在调用格式化字符带有%sprintf()函数时,即可get shell。

6.写exit函数在ld.so中的_rtld_global._dl_rtld_lock_rescursive 为one_gadget

7.当程序开启full relor时,可写libc中puts函数开头的strlen的got表

无leak函数的利用笔记:

  • 没开PIE的情况

1.可申请或者构造非fastbin chunk情况,能够修改free_got --> puts_plt,下次释放一个unsorted_bin chunk(入链)或者fastbin chunk(入链) ,程序调用链 free->free_got->puts_plt->puts 以此泄露libc地址或者heap地址。

2.只能存在fastbin chunk情况,修改 free_got 为 printf,释放一个有格式化字符串的chunk,利用构造格式化字符串漏洞 打印栈中的 libc 地址。

  • 开启PIE的情况

利用IO_write_base实现leak,详细见https://b0ldfrev.gitbook.io/note/pwn/iofile-li-yong-si-lu-zong-jie#li-yong-iowritebase-shi-xian-leak

  • 无须泄露,全程爆破的方式(不实用)

House_of_Roman

IO_FILE笔记

程序调用exit 后会遍历 _IO_list_all,调用 _IO_2_1_stdout_ 下的vatable中_setbuf 函数.

glibc缺陷

glibc缺陷 : 对于未开启tcache版本来说,只要释放chunk大小在fastbin范围,那就不检查当前释放这个chunk是否已经free(通常检测下一个相邻chunk的prev_size位),就直接将其放入fastbin;

对于开启tcache版本来说,只要tcache中有空,那就不检查当前释放这个chunk是否已经free(通常检测下一个相邻chunk的prev_size位),并直接将其放入tcache中。对于开启tcache这种情况,相对来说就会更危险,我们很容易构造堆风水来实现进一步利用。

malloc_consolidate笔记

malloc_consolidate()函数用于将 fast bins 中的 chunk 合并,并加入 unsorted bin 中。 ptmalloc中会有以下几种情况会调用malloc_consolidate()

  1. _int_malloc的while循环之前,分配的 chunk 属于 small bin,如果 small bin 还没有初始化为双向循环链表,则调用malloc_consolidate()函数将 fast bins中的 chunk 合并.

  2. _int_malloc的while循环之前,分配的 chunk 属于 large bin,判断当前分配区的 fast bins 中是否包含 chunk,如果存在,调用 malloc_consolidate()函数合并 fast bins 中的 chunk

  3. 在分配chunk时 假如最后 top chunk 也不能满足分配要求,就会查看 fast bins 中是否有空闲 chunk ,若存在就调用malloc_consolidate()函数,并重新设置当前 bin 的 index,并转到最外层的循环,尝试重新分 配 chunk。

  4. 在释放chunk时,遇到相邻空闲chunk合并或者与topchunk合并,如果合并后的 chunk 大小大于 64KB,并且 fast bins 中存在空闲 chunk,则会调用malloc_consolidate()函数合并 fast bins 中的空闲 chunk 到 unsorted bin 中

一些能触发malloc_consolidate的 trick

  • scanf时可输入很长一段字符串 "1"*0x1000,这样可以导致scanf内部扩充缓冲区,从而调用init_malloc来分配更大的空间,从而导致malloc_consolidate,合并fast_bin中的空闲chunk。调用栈如图:

  • 如果程序没有setbuf(stdin,0)也就是没有关闭stdin的缓冲区。getchar() 会开辟一个很大的堆块形成缓冲区,也就是申请0x400的chunk,此时fast_bin中存在chunk,就会调用malloc_consolidate合并

程序退出

1.程序会执行到libc里面__GI___call_tls_dtors函数

观察看出,__GI___call_tls_dtors+18也就是rbx不为零时,程序会执行到下面的call rax,且参数是由rbx控制,再看__GI___call_tls_dtors+13,rbx是fs:[-0x40],也就是当前线程栈的TLS结构体的上方0x40处,若能覆盖到此处,就能控制程序执行流程。在最终call rax之前,还有一次ror rax,0x11xor rax, fs:[0x30],需要leak出TLS的pointer_guard成员.

2.程序在执行退出流程时,最终会在ld.so这个动态装载器里面调用_dl_fini函数,这个函数,利用方式见下图:

rbx的值为栈里_dl_init+139上方的0x7ffff7ffe168

calloc绕过 leak

2.23及 以上libc都适用

给fastbin_chunk的size的IS_MAPPED域置1.通过calloc分配到时,不会被清空。

stack_povit

栈迁移到.bss段时,若栈上方(低地址处)有大约0x200字节的空白空间,则执行system函数就不会报错;但我们通常使用onegadget获取shell

fd相关 close(1)

  • 对于有write函数调用的情况下.

write函数直接能够将输出重定位到0或2描述符.

这时能打印123.原因是0,1,2文件描述符都指向同一个tty文件,如下:

  • 无write函数调用情况下.

由于程序只关闭了文件描述符1,却没有关闭文件描述符0,所以我们可以修改stdout的文件描述符_fileno为0或2,则可以使得程序再次拥有了输出的能力,这时再调用printf或者puts就能输出了

  • close(1)后,格式化字符串最多只能写0x2000字节,这种情况下在利用时可修改程序.bss段中的stdout指针地址为stderr指针,由源码分析,在vprintf的check时刚好能通过,这使得printf再次拥有输出能力

  • close(1)时获取服务器端flag,利用重定向"cat flag >&0"

  • 再调用scanf时,会取到_IO_2_1_stdin_结构的fileno,最终汇到底层系统调用read(_IO_2_1_stdin_.fileno,buf,nbytes)。所以有些时候如果我们能够控制IO_stdin结构的fileno为其它fd,再去调用scanf函数时就可以实现从其它fd读数据。

off-by-one 构造思路

  • 方法一

  • 方法二

  • 方法三(非特殊情况不推荐)

原理与方法一类似,在能泄露heap地址前提下,直接构造fake_chunk,填好指针,绕过unlink

realloc

简化版的realloc,非mmapped分配方式

tcache相关

tcache_perthread_struct结构体是用来管理tcache链表: 这个结构体位于heap段的起始位置,且有size:0x251

一个tcache链表的结构,单个tcache bins默认最多包含7个块。tcache_entry: 2.26

2.28存在bk字段所有的bk都指向tcache_perthread_struct的fd

放入tcache bin的情况:

  • 释放时,_int_free中在检查了size合法后(小于0x400),放入fastbin之前,它先尝试将其放入tcache

  • _int_malloc中,若fastbins中取出块则将对应bin中其余chunk填入tcache对应项直到填满(smallbins中也是如此)

  • 当进入unsorted bin(同时发生堆块合并)中找到精确的大小时,并不是直接返回而是先加入tcache中,直到填满:

取tcache bin中的chunk:

  • __libc_malloc_int_malloc之前,如果tcache中存在满足申请需求大小的块,就从对应的tcache中返回chunk

  • 在遍历完unsorted bin(同时发生堆块合并)之后,若是tcache中有对应大小chunk则取出并返回:

  • 在遍历unsorted bin时,大小不匹配的chunk将会被放入对应的bins,若达到tcache_unsorted_limit限制且之前已经存入过chunk则在此时取出(默认无限制):

tcache struct攻击

  • tcache初始化

在程序需要进行动态分配时,如果是使用TCACHE机制的话,会先对tcache进行初始化。跟其他bins不一样的是,tcache是用_int_malloc函数进行分配内存空间的,因此tcache结构体是位于heap段,而不是main_arena。通常 tcache结构体位于堆首的chunk.

tcache的结构是由0x40字节数量数组(每个字节代表对应大小tcache的数量)和0x200(0x40*8)字节的指针数组组成(每8个字节代表相应tache_entry链表的头部指针)。因此整个tcache_perthread_struct结构体大小为0x240。

  • tcache free

在将chunk放入tcahce的时候会检查tcache->counts[tcidx] < mp.tcache_count(无符号比较),也就是表示在tacha_entry链表中的tache数量是否小于7个。但值得注意的是,tcache->counts[tc_idx]是放在堆上的,因此如果可以修改堆上数据,可以将其改为较大的数,这样就不会将chunk放入tache了。

  • tcache malloc

而在tcache分配时,不会检查tcache->counts[tc_idx]的大小是否大于0,会造成下溢。且没有检测entries处chunk的合法性,我们若能伪造tcache->entries[tc_idx]tcache_entry指针,那我们就能实现从tcache任意地址分配chunk。

关于glibc 2.29及以上一些check的绕过

1.在unlink操作前增加了prevsize的检查机制:在合并的时候会判断prev_size和要合并chunk的size是否相同。

这样导致了常规off-by-null的构造方式失效,但可利用残余在 large bin 上的 fd_nextsize / bk_nextsize 指针,smallbin残留的bk指针,以及fastbin的fd指针 来构造出一个天然的chunk链来绕过size检测与双向链表检测。具体见https://bbs.pediy.com/thread-257901.htm

2.增加了tcache_double_free的检测,2.29将每个放入tcache中的chunk->bk(也是tcache entries结构的key位)设置为tcache。

在释放tcache中的chunk时,只根据相应的tc_idx检测重复chunk

绕过方式:可以将同一个tcache_chunk放入不同的tcache_bin或其他bin中来重新实现利用(这种方式见House_of_botcake);也可以篡改chunk->key,使其e->key != tcache来绕过。

也可以利用fastbin的double free,待fastbin形成double_free链后再malloc重分配清空tchache预留位置,最后一次malloc使得剩余fastbin进入tcache,实现堆块复用。详细可参见glibc2.31下的新double free手法/字节跳动pwn题gun题解

3._int_malloc中,使用unsortedbin_attack时,增加了对unsortedbin双向链表的完整性检测,导致unsortedbin_attack不可用.

但有另外的地方可利用,unsortedbin_attack无非就是往一个地址写一个值,如果只是为了改例如global_max_fast,那largebin_attack完全可以替代,只不过写入的是堆地址,只是和largebin_attack配套的house of strom来实现任意地址分配不能用了。

如果要达到写libc地址,也可以,有师傅把它叫做tcache stash unlink attack plus

前置条件是:对应tcache中预留2个chunk位(至少)(除非你能伪造fd,绕过双向链表检测)

small bin中存在2个chunk,我们修改small bin头部chunk的bk为target,fd不变( 不修改small bin尾部chunk是为了绕过分配时的smallbin double linked list corrupted检测 ),且target->bk( target+3*size_t )必须是一个可写地址,记作target->bk = attack_addr

原理是_int_malloc中,当从small bin中申请出chunk时,small bin尾部chunk在经过双向链表检测后会被分配出去,启用tcache会遍历small bin中剩余的chunk放入到对应tcache中,但此时的small bin链表已经被破坏,(tc_victim = last (bin)) != bin 这个条件恒成立直到abort,为了beak那个while循环,我们才在tcache中预留2个chunk位,直到tcache被填满tcache->counts[tc_idx] = mp_.tcache_count以此来跳出循环。

同时在最后一次unlink过程中会往attack_addr -> fd写入一个main_arena的地址,实现任意地址写。(当然这个洞在引入tcache时的glibc版本就已经存在)。

这时tcache已满,且tcache顶部刚好是我们伪造那个target_chunk

4.在使用top chunk的时候增加了检查:size要小于等于system_mems,因为House of Force需要控制top chunk的size为-1,不能通过这项检查,所以House of Force不可用

5.从glibc 2.30开始,常规large bin attack方法也被封堵

查看相关代码,发现其中只增加了对 size 大于最小 size 的时候做了 检查,但是小于最小 size 却没有进行检查,因此我们可以利用这一点来完成 libc2.30 及以上的largebin attack

具体做法是,往 largebin 中放一个堆块,并在 unsorted bin 中放一个比 large bin 中小但是在同一个 index的堆块,利用 uaf 修改 large binbk_nextsize = 目标地址,申请一个比 unsorted bin 中小的 chunk 触发攻击,此时 largebin->bk_nextsize->fd_nextsize 写入堆地址。

tcache相关冷门漏洞(任意地址写与任意地址分配)

1.small bin

前置条件是:对应tcache中预留2个chunk位(至少)(除非你能伪造fd,绕过双向链表检测)

small bin中存在2个chunk,我们修改small bin头部chunk的bk为target,fd不变( 不修改small bin尾部chunk是为了绕过分配时的smallbin double linked list corrupted检测 ),且target->bk( target+3*size_t )必须是一个可写地址,记作target->bk = attack_addr

原理是_int_malloc中,当从small bin中申请出chunk时,small bin尾部chunk在经过双向链表检测后会被分配出去,启用tcache会遍历small bin中剩余的chunk放入到对应tcache中,但此时的small bin链表已经被破坏,(tc_victim = last (bin)) != bin 这个条件恒成立直到abort,为了beak那个while循环,我们才在tcache中预留2个chunk位,直到tcache被填满tcache->counts[tc_idx] = mp_.tcache_count以此来跳出循环。

同时在最后一次unlink过程中会往attack_addr -> fd写入一个main_arena的地址,实现任意地址写。

这时tcache已满,且tcache顶部刚好是我们伪造那个target_chunk

由于smallbin摘链后chunk全部进入tcache,且已满,这时tcache对应idx入口处的chunk是target_chunk。如果再次调用malloc申请chunk,得益于从tcache分配时未仔细检查chunk_head,这时便会从tcache中将这个target_chunk分配出来,实现任意地址分配内存。

demo代码可参考V1me师傅写的

2.fast bin

当从fastbin中分配出chunk时(比如调用calloc->_int_malloc),如果fastbin中还有剩余chunk且相对应idx的tcache有空闲位置,这时就会根据fd指针将剩余的fastbin_chunk链入tcache中,且在这个过程中并没有检查剩余fastbin_chunk的完整性。

如果我们通过UAF能修改fastbin链表尾部chunk的fd指针为一个target_addr,当这个target_chunk最后被滑入tcache中时,target_chunk做为tcache的头部,若tcache中存在其他chunk,则target_chunk -> fd 就被写入一个堆地址,实现任意地址写。

与此同时,如果再次调用malloc申请chunk,得益于从tcache分配时未仔细检查chunk_head,这时便会从tcache中将这个target_chunk分配出来,实现任意地址分配内存。(任意地址分配内存在这种情况下是个鸡肋,因为我们完全可以不清空tcache,利用UAF+calloc也就是fastbin_attck来实现任意地址分配)

glibc 2.28及以上堆利用的栈转移

1.在2.29中vtable是可写的

2.setcontext函数中gadget指令可控寄存器变成了rdx,非rdi

1.利用FSOP控制程序流程

在libc-2.29下_IO_strfile没有了像libc-2.24下的fp->_s._allocate_buffer()这类函数操作,都被修改为了标准函数(malloc...),所以没办法直接直接像libc-2.24那样直接劫持程序流。因此不能使用以前的io_file攻击手法来劫持流程,但是我们看到在_IO_str_overflow函数中有很多函数,并且参数我们都可以控制,因此我们可以利用这一点来完成新版的io_file攻击

_IO_str_overflow对应的汇编代码:

可看到调用malloc之前rdx可控制为[rdi+28h]

攻击方式:

  • 劫持IO_list_all指向我们伪造的io_file

  • 劫持malloc_hook为setcontext+61

  • io_str_overflow里面会调用malloc,调用malloc之前rdx=rdi+0x28 rdi=&fake_IO_FILE ,此时rdi可控,执行srop

还一种情况是不能分配到理想大小的tcache,无法通过常规方式劫持free或malloc_hook项。(参见TCTF2020 duet)

在执行FSOP前布置好一条tcache bin:chunk A-> ptr 构造好三个IO_FILE: X.chain -> Y.chain -> Z.chain

调用malloc前rdx=rdi+0x28 rdi=&fake_IO_FILE ,此时rdi可控。

这样就可以利用FSOP在_IO_flush_all_lockp时三次进入_IO_str_overflow;第一次控制malloc参数将chunk A分配出来;第二次的时候调用malloc就会分配到ptr,而后memcpy(参见_IO_str_overflow C源码),即可进行任意地址写(通常将_malloc_hook写成setcontext+61,用于第三次刷新IO用);第三次调用malloc,即可setcontext实现orw。

2.利用_free_hook控制程序流程

  • 方法 I :

然而在我们控了free_hook以后,我们发现libc-2.29中没有可以利用rdi控制rsp进行迁栈的gadget,所以使用了其它方法。IO_wfile_sync函数可以利用rdi控制rdx,函数setcontext+0x35处可以用rdx控rsp,两个搭配使用就可以进行迁栈。在IO_wfile_sync+0x6d处有call [r12+0x20],这里的r12也是可以用rdi控制的,所以可以利用这条指令调用setcontext+0x35,实现free_hook -> IO_wfile_sync -> setcontext+0x35

  • 方法 II :

利用libc中的这段gadget

free_hook写成这个gadget,可控制rbp寄存器,call时跳转到leave_ret指令实现栈迁移

或者glibc2.31中的这一段

3.利用fclose(特殊条件)

fclose的源码,其核心函数是位于/libio/iofclose.c的_IO_new_fclose函数,其大致流程是:首先检查文件结构体指针,之后使用_IO_un_link将文件结构体从_IO_list_all链表取下,_IO_file_close_it里会最终调用IO_SYSCLOSE(fp)关闭文件描述符,之后返回fclose函数会调用到vtable里面的函数_IO_FINISH(fp),如果并非stdin/stdout/stderr最后调用free(fp)释放结构体指针。

_IO_file_close_it里调用IO_SYSCLOSE(fp),或是在fclose里调用_IO_FINISH(fp)的时候,rdx的寄存器值都是_IO_helper_jumps,所以只要我们能控制_IO_helper_jumps的值就能控制rdx(2.29以上jumps_table可写).

利用方式:

我们可以将IO_file_jumps+0x88(sysclose)(0x10(finish))的位置覆盖为setcontext+53并且在IO_helper_jumps上布置setcontext参数,或者将vtable直接覆盖为IO_helper_jumps,然后直接在IO_helper_jumps布置所有的值。

House-of-Corrosion 任意地址写

  1. 可以分配较大的堆块(size <=0x3b00)

  2. 通过爆破4bit,改写bk进行unsortedbin attack 改写global_max_fast变量

  3. 通过分配释放特定大小的堆块,记为A (chunk size = (offset * 2) + 0x20 ,offset为target_addr与fastbinY的差值)

    pwndbg> p (mfastbinptr (*)[10])target_addr - &main_arena.fastbinsY target_addr为攻击地址

所以我们至少可实现任意地址写null,存在UAF时可写任意value.

seccomp 没禁用架构

大致思路:

  1. 调用mmap申请地址,调用read读入32位shellcode

  2. 同时构造用retfq切换到32位模式,跳转到32位shellcode 位置

  3. 按照32位规则调用fp = open("flag")

  4. 保存open函数返回的fp指针,再次调用retfq切换回64模式,跳转到64位shellcode位置

  5. 执行read,write打印flag

注意点:

cs = 0x23代表32位模式,cs = 0x33代表64位模式,retfq有两步操作,ret以及set cs,所以执行retfq会跳转到rsp同时将cs设置为[rsp+0x8],我们只需要事先在ret位置写入32位的shellcode就可以执行了,但retfq跳转过去的时候程序已经切换成了32位模式,所以地址解析也是以32位的规则来的,所以原先的rsp = 0x7ffe530d01b8会被解析成esp = 0x530d01b8,所以跳过去之后再执行push/pop的指令就会报错,所以在跳转过去后要先平衡好esp的地址,比如执行mov esp,im

Last updated

Was this helpful?