CTF PWN特点:入门难、进阶难、精通难
**PWN(溢出):**PWN在黑客俚语中代表着攻破,取得权限,在CTF比赛中它代表着溢出类的题目,其中常见类型溢出漏洞有栈溢出、堆溢出。在CTF比赛中,线上比赛会有,但是比例不会太重,进入线下比赛,逆向和溢出则是战队实力的关键。主要考察参数选手漏洞挖掘和利用能力。
ELF文件格式
objdump -d ELF
Gdb elf
vmmap
+====================+
- ELF header + // 包含了整个文件的基本属性,如:文件版本,目标机器型号,入口地址。
+====================+
+Program header table+ // 程序标头表是一组程序标头,它们定义了运行时程序的内存布局。对于.obj文件可选的
+====================+
- .interp + // 可执行文件所需要的动态链接器的位置。
+====================+
- .note.ABI-tag + // 用于声明ELF的预期运行时ABI。包括操作系统名称及其运行时版本。
+====================+
- .note.gnu.build-id + // 表示唯一的构建ID位串。
+====================+
- .gnu.hash + // 符号hash表。若段名是.hash,则使用的是SYSV hash,其比gnu hash性能差。
+====================+
- .dynsym + // 动态符号表用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。
+====================+
- .dynstr + // 动态符号字符串表,用于保存符号名的字符串表。静态链接时为.strtab。
+====================+
- .gnu.version + // 表中条目与.dynsym动态符号表相同。每个条目指定了相应动态符号定义或版本要求。
+====================+
- .gnu.version_r + // 版本定义。
+====================+
- .rela.dyn + // 包含共享库(PLT除外)所有部分的RELA类型重定位信息。
+====================+
- .rela.plt + // 包含共享库或动态链接的应用程序的PLT节的RELA类型重定位信息。
+====================+
- .init + // 程序初始化段。
+====================+
- .plt + // 过程链接表(Procedure Linkage Table),用来实现延迟绑定。
+====================+
- .plt.got + // 暂无。。。。。
+====================+
- .text + // 代码段
+====================+
- .fini + // 程序结束段
+====================+
- .rodata + // 只读****变量(const修饰的)和字符串变量。
+====================+
- .rodata1 + // 据我所知,.rodata和.rodata1是相同的。一些编译器会.rodata分为2个部分。
+====================+
- .eh_frame_hdr + // 包含指针和二分查找表,(一般在C++)运行时可以有效地从eh_frame中检索信息。
+====================+
- .eh_frame + // 它包含异常解除和源语言信息。此部分中每个条目都由单个CFI(呼叫帧信息)表示。
+====================+
- .init_array + // 包含指针指向了一些初始化代码。初始化代码一般是在main函数之前执行的。
+====================+
- .fini_array + // 包含指针指向了一些结束代码。结束代码一般是在main函数之后执行的。
+====================+
- .dynamic + // 保存动态链接器所需的基本信息。
+====================+
- .got + // 全局偏移表,存放所有对于外部变量引用的地址。
+====================+
- .got.plt + // 保存所有对于外部函数引用的地址。延迟绑定主要使用.got.plt表。
+====================+
- .data + // 全局变量和静态局部变量。
+====================+
- .data1 + // 据我所知,.data和.data1是相同的。一些编译器会.data分为2个部分。
+====================+
- .bss + // 未初始化的全局变量和局部局部变量。
+====================+
- .comment + // 存放编译器版本信息
+====================+
- .debug_aranges + // 内存地址和编译之间的映射
+====================+
- .debug_info + // 包含DWARF调试信息项(DIE)的核心DWARF数据
+====================+
- .debug_abbrev + // .debug_info部分中使用的缩写
+====================+
- .debug_line + // 程序行号
+====================+
- .debug_str + // .debug_info使用的字符串表
+====================+
- .symtab + // 静态链接时的符号表,保存了所有关于该目标文件的符号的定义和引用。
+====================+
- .strtab + // 默认字符串表。
+====================+
- .shstrtab + // 字符串表。
+====================+
+Section header table+ // 用于引用Sections的位置和大小,并且主要用于链接和调试目的。对于Exec文件可选
+====================+
延迟绑定实现 PLT->GOT->PLT->…->GOT
使用延迟绑定是基于这样一个前提:在动态链接下,程序加载的模块中包含了大量的函数调用,因此动态链接器会耗费很多的时间用于解决模块之间的函数引用的符号查找以及重定位,而实际上只有很少的一部分符号会被立刻访问。延迟绑定通过将函数地址的绑定推迟到第一次调用这个函数时,从而避免动态链接器在加载时处理大量函数引用的重定位。延迟绑定的实现使用了两个特殊的数据结构:全局偏移表(Global Offset Table,GOT)和过程链接表(Procedure Linkage Table,PLT)。
- 全局偏移表 GOT
全局偏移表在ELF文件中以独立的节区存在,共包含两类,对应的节区名为.got和.got.plt,其中,.got存放所有对于外部变量引用的地址;.got.plt保存所有对于外部函数引用的地址,对于延迟绑定主要使用.got.plt表。.got.plt表的基本结构如下图所示:
其中,.got.plt的前三项存放着特殊的地址引用:
GOT[0]:保存.dynamic段的地址,动态链接器利用该地址提取动态链接相关的信息;
GOT[1]:保存本模块的ID;
GOT[2]:存放了指向动态链接器_dl_runtime_resolve函数的地址,该函数用来解析共享库函数的实际符号地址
- 过程链接表 PLT
为了实现延迟绑定,当调用外部模块的函数时,程序并不会直接通过GOT跳转,而是通过存储在PLT表中的特定表项进行跳转。对于所有的外部函数,在PLT表中都会有一个相应的项,其中每个表项都保存了16字节的代码,用于调用一个具体的函数。过程链接表的通用结构如下:
过程链接表中除了包含编译器为调用的外部函数单独创建的PLT表项外,还有一个特殊的表项,对应于PLT[0],它用于跳转到动态链接器,进行实际的符号解析和重定位工作:
考虑经典Hello world
的实现:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
默认情况下,编译器通过动态链接的方式来使用C标准库,因此printf函数实际上就是一个存在于外部动态库的实现,通过观察printf的执行,即可以了解到在程序中延迟绑定的运作过程。我们将上述代码编译后进行反汇编:
第一次还没有执行过的printf函数,观察got表并没有存入函数的真实地址。
执行过一次,发现got表已经存入函数的真实地址。
- main函数对printf的调用流程如下:
main函数不会直接调用printf函数,而是调用puts@plt。注意,这里编译器会优化对printf的调用为对库函数puts的调用;
puts@plt的第一条指令通过GOT[3]进行跳转。由于每个GOT表项初始化时都指向对应PLT条目的第二条指令,因此这个间接跳转会将控制转移到puts@plt的第二条指令继续执行;
puts@plt的第二条指令会将puts的ID压入栈中之后,然后跳转到PLT[0]中的指令;
.plt中指令继续压入全局偏移表表中第二个表项所存放的地址,即本模块ID,最后跳转到动态链接器的入口**_dl_runtime_resolve**,执行符号解析;
完成符号解析后,_dl_runtime_resolve会将解析出来的puts函数的地址,填入GOT[3]中,到达这一步后,对puts函数的符号绑定工作就完成了。
**符号表(.dynsym和.symtab)**记录了目标文件中所用到的所有符号信息,通常分为.dynsym和.symtab,前者是后者的子集。.dynsym保存了引用自外部文件的符号,只能在运行时被解析,而.symtab还保存了本地符号,用于调试和链接。目标文件通过一个符号在表中的索引值来使用该符号。索引值从0开始计数,但值为0的表项不具有实际的意义,它表示未定义的符号。每个符号都有一个符号值(symbol value),对于变量和函数,该值就是符号的地址。
什么是return address
通过第1点中描述,在执行call sum的时候,会将sum 的下一条命令的执行地址0x8048438压入栈中,然后程序进入sum片段进行执行,此时,0x8048438地址就是sum函数的返回地址,即 return address。
0x8048455 push ebp
0x8048456 mov ebp, esp
0x8048458 mov edx, [ebp+arg_0]
0x804845B mov eax, [ebp+arg_4]
0x804845E add eax, edx
0x8048460 pop ebp
0x8048461 ret
当执行到ret时,栈空间如下
栈地址值0xffffceac0x8048438 # sum的下一条指令0xffffceb01 # a 的值0xffffceb42 # b 的值
当执行完ret后,栈空间为
栈地址值0xffffceb01 # a 的值0xffffceb42 # b 的值
程序指定的位置为:
0x0804842A sub esp, 8
0x0804842D push [ebp+var_C]
0x08048430 push [ebp+var_10]
0x08048433 call sum
0x08048438 add esp, 10h # 程序执行ret后跳转到这个位置
0x0804843B sub esp, 8
0x0804843E push eax
关于函数泄露的食用方法:
[泄露函数地址:](https://jue-xian.gitee.io/diazang/2021/02/25/无libc库的利用puts函数泄露libc基址/#:~:text=利用程序自带的函数进行泄露libc (常见的有 puts write,read setbuf函数,其中注意puts函数获取其泄露的puts地址时虽然地址只有6位,但是puts是不受控的输出的,并且以x00结尾,还要补上8个x00,详见exp) 公式 libc基址%3D函数地址-libc偏移值)
当函数被调用过之后,GOT表中存放的函数地址就是函数的实际地址。而这个地址是通过以下方式确定的:函数的实际地址 = libc基址 + 函数在libc中偏移量。因此利用GOT泄露的函数实际地址,和函数在libc中的偏移量就可以计算出libc的基址
利用程序自带的函数进行泄露libc (常见的有 puts write read setbuf函数,其中注意puts函数获取其泄露的puts地址时虽然地址只有6位,但是puts是不受控的输出的,并且以x00结尾,还要补上8个x00,详见exp) 公式 libc基址=函数地址-libc偏移值
最近做了两个libc的题型,发现对于不同的函数,泄露真实地址的手法都是不同,并且在32位和64位的方法也是不同的。
关于puts函数泄露地址
最简单的就是puts函数,在32位的中
payload1 = b'a'*13 + p32(puts_plt) + p32(main_addr) + p32(puts_got)
在64位由于传参方式不同会将参数压入到寄存器中,所以构造rop链方法不同
payload1 = b'a'*13 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
关于printf函数泄露地址
在64位:(我猜一般会有 %s %p %x 来进行地址的泄露
实战libc3连接 printf
payload = cyclic(0x28) + p64(pop_rdi_ret) + p64(f_str) + p64(pop_rsi_r15) + p64(read_got) + p64(0) + p64(printf_plt) + p64(main_addr)
得到的地址:read_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
puts函数获取其泄露的puts地址时虽然地址只有6位,但是puts是不受控的输出的,并且以\x00结尾,还要补上8个\x00 64位的printf函数也是要补上8个\x00
关于write函数泄露地址
在64位,如果使用write函数泄露地址的话,需要挺多的寄存器的,在存放我们的参数,因为write函数关系,
ssize_t write(int fd, const void *buf, size_t count);
其中,第一个参数fd是文件描述符(012输入输出错误),第二个参数buf是要写入的数据缓冲区,第三个参数count是要写入的字节数。write函数返回实际写入的字节数,如果出现错误则返回-1
paylaod = b'a'offest + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15) + p64(a_got) + p64(8) + p64(main_plt) + p64(main_addr)
pwn中 64位与32位,增加堆栈平衡
栈是从高地址向低地址生长的
1)如果要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在RET这条指令之前,ESP指向的是我们压入栈中的地址。 2)如果通过堆栈传递参数了,那么在函数执行完毕后,要平衡参数导致的堆栈变化。含义就是 当函数在一步步执行的时候 一直到ret执行之前,堆栈栈顶的地址 一定要是call指令的下一个地址。也就是说函数执行前一直到函数执行结束,函数里面的堆栈是要保持不变的。
如果堆栈变化了,那么,要在ret执行前将堆栈恢复成原来的样子。
栈溢出漏洞原理
缓冲区溢出分为栈溢出和堆溢出。栈溢出是由于在栈的空间内,放入大于栈空间的数据,导致栈空间以外有用的内存单元被改写,这种现象就称为栈溢出。普通的溢出不会有太大危害,但是如果向溢出的内存中写入的是精心准够着的数据(payload),就可能使得程序流程被劫持,使得危险的代码被执行,最终造成重大危害。
栈溢出的总结
利用一些危险的函数,确定程序是否有溢出及其位置,一些常见的危险函数如下:
**input:**gets(直接读取一行,忽略’\x00’)、scanf、vscanf
**output:**sprintf
**string:**strcpy、strcat、bcopy
strcpy:字符串复制,遇到’\x00’停止
strcat:字符串拼接,遇到’\x00’停止
传 参 方 式| X86 与 X86_64的不同
32位函数参数直接将就近将参数压入栈中(X86 参数自低地址向高地址存放)
#char[88] ebp write函数地址 write函数返回地址(返回到main函数) write函数参数一(1) write函数参数二(write_got地址) write函数参数三(写4字节)
payload=0x88*'a'+p32(0xdeadbeef)+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4)
p.sendline(payload)
#获取write在got中的地址
write_got_addr=u32(p.recv())
print hex(write_got_addr)
64位要将参数压入寄存器中
pop_rdi = 0x0000000000400C83 #一个万能的gadget,x64程序基本都存在,pop rdi;ret;
payload = '1'*0x58+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.sendline(encrypt(payload))
64位调用函数时要用寄存器
随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在**栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。**所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
基本****ROP
ROP 是啥呢
ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。相当于让程序反复横跳的一种“艺术”
这类题目一般开启NX保护
ROP攻击一般需要满足一些条件:
1、程序存在溢出,并且考研控制返回地址
2、可以找到满足条件的gadgets以及相应gadgets地址
栈转移
想必大家都知道用栈迁移技术来解决的问题了吧———溢出的长度不够,只能覆盖到返回地址,至于后面需要构造的rop链的长度显然是不够的。
- 在栈空间不够存放paylaod的情况下,需要一个新的地址空间来存放payload。
- 开启了 PIE保护,栈地址未知,我们可以将栈劫持到已知的区域。
概念
劫持栈的rsp(esp),使其指向其他位置,形成一个伪造的栈。这样栈也就被劫持到攻击者控制的内存上去,然后在该位置做ROP。
必要的Gadget
ROPgadget --binary filename --only “pop|ret”
ROPgadget --binary test --only “leave|ret”
pop EBP;ret
: 释放EBP,并链接伪造的栈
leave;ret
: 更改ESP,指向后续的payload
-
等价于
mov ESP,EBP;
-
pop EBP;
ret;
原理
栈转移的原理就是以 pop ebp;ret + 伪造的栈
让程序直接跳转到伪造的栈里面,然后为了保持栈平衡,从而执行leave; ret
,最后继续执行伪造的栈内的payload
过程
- 使用**输入函数(如read)**将后续的Payload加载到bss段内,也就是伪造的栈
- 通过
pop ebp;ret
来调整EBP寄存器pop ebp;ret
可以使用pop EBX;ret
来代替
- 通过
leave;ret
来更改ESP
,使其指向伪造的栈(bss) - 然后在伪造的栈中执行下一段ROP
注意
- 使用bss作为stack发动ROP攻击可能会失败
- 这是因为(后续ROP使用的GOT等)必要的变量被破坏,以及跳到stack等原因
- 因为
read/write
时,系统内的dl_fixup函数对stack做了很好的保护,使用的bss段建议在中间区域,如bss+0x800
左右