文章开始前,增加一个辅助视频以帮助了解基本堆栈结构和C语言程序运行时候的状态。
同时,补充一些基本知识
GOT和PLT
- GOT:全局偏移量表,位于程序的数据段,每个条目8字节,用来存储外部函数(动态链接过来的那些函数)在内存里面的地址,可以在程序运行中被修改,这是因为默认情况下程序只有在需要调用时才导入函数,而不是在一开始就把所有需要用到的函数全部放进表里面。
- PLT:过程链接表:位于程序的代码段,每个条目16字节,前两条是特殊条目,分别用于跳转到动态链接器和记录系统启动函数,其余的条目负责调用一个具体的函数(存储外部函数的入口点,也就是这个函数在GOT表的位置)
- 攻击流程:
- 确定printf函数在GOT表中的位置:程序在正常执行调用printf函数时通过PLT表跳转到GOT表的指定条目,此时可以通过对应的汇编指令看到这个位置(当然这样一来printf函数在内存中的实际地址也能知道)
- 确定system函数在内存中的位置:开启堆栈随机化(ASLR)之后动态链接库在内存的位置是随机的,但是一个函数在其所在的链接库内部的相对地址是不变的,如果printf函数和system函数位于同一个动态链接库,那就可以通过printf在内存中的地址+两个函数在动态链接库内的偏移得到system函数在内存中的实际位置
- 把system函数在内存中的位置值写入printf函数在GOT表中的位置
- 程序调用printf函数时,通过PLT表跳转到GOT表的对应条目,但是那里已经被覆写,调用的函数实际上是system而不是printf
RELRO
让重定位表只读,避免GOT被恶意修改。RERLO有两种形式:
- 部分RELRO:.init_array、.fini_array、.jcr、.dynamic这些表一次性加载,然后变成只读;.got只读;.got.plt依然可写
- 前部RELRO:全都不准写!这样会导致所有符号在程序开始的时候导入,性能和时间开销大
Stack Canary
函数执行之前在栈的EBP附近插入一个值,如果被溢出攻击,那这个值就有可能被覆盖
NX/DEP
数据段不可执行,数据段可以执行,那么就可以将自己的代码随便放在数据段的某个变量里,然后再用溢出使得函数返回到那个数据段的起始地址。开启NX之后,如果程序发现自己返回到了数据段就会终止运行。因此衍生出了ROP攻击,即利用代码段自带的片段拼凑出攻击逻辑。
PIE
地址无关的可执行文件。
正常情况下,内存是这样子的:
- 内核虚拟地址空间(所有进程共享,但是用户空间代码不可见)
- 栈:函数
- 动态映射段(动态库在这里)
- 堆:动态申请的变量
- BSS段:未初始化的变量
- 数据段:静态/全局变量
- 代码段:汇编代码,还有一些字面值变量
开启PIE之后,这些段全部打散,通过GOT来登记其实际位置。
多种ROP利用
ret2text
从ctfwiki上下载ret2text二进制程序,链接
使用checksec
检查是否开启相关保护,可以看出,无地址随机化,无canary,仅仅开启了不可执行栈。该程序为32位程序,拖入IDA 32反汇编查看。
IDA反汇编后,查看main函数,F5生成源代码,gets()函数存在堆溢出
View->Open subviews->strings查询关键字(也可以shift+F12)可以看到,数据资源段有调用/bin/sh
具体位置在secure函数中(空格键查看文本视图),地址0x0804863A
目前需要控制gets()函数,使得返回地址直接跳转到system(“/bin/sh”)处,因此需要计算偏移量。使用gdb运行中调试,在gets()函数处下断点
b *0x080486AE
将断点打在gets()函数处,查询此处栈内情况
可以看出esp:0xffffd3d0
,gets()函数写入的参数地址为[esp+1Ch],计算可得其地址为0xffffd3ec
,相较于ebp的偏移为0xffffd458-0xffffd3ec=0x6c
,因此返回地址相较于ebp的偏移为[0x6c+4]。
编写payload,其中p32(target)使用+进行字符拼接,需要转换成可编码的形式,因此使用.decode("iso-8859-1")
。==补充,其实直接b”A”*(0x6c+4)就行==
1 | from pwn import * |
最终得到shell
ret2shellcode
直接checksec
可以看出,32位程序,啥也没开,导入IDA
反汇编出的内容可以看出,这次没有system()函数调用了,但仍存在栈溢出点,strncpy函数将字符数组s的内容copy入buf2,产生栈溢出。但是由于该程序并未开启不可执行栈,因此可以在栈上插入我们的shellcode,控制返回地址到我们的shellcode处从而执行。
下表补充ELF文件结构
段名 | 存储属性 | 内存分配 |
---|---|---|
代码段 .text | 存放可执行程序的指令,存储态和运行态都有 | 静态 |
数据段 .data | 存放已初始化(非零初始化的全局变量和静态局部变量)的数据,存储态和运行态都有 | 静态 |
bss段 .bss | 存放未初始化(未初始化或者0初始化的全局变量和静态局部变量)的数据,存储态和运行态都有 | 静态 |
堆 heap | 动态分配内存,需要通过malloc手动申请,free手动释放,适合大块内存。容易造成内存泄漏和内存碎片。运行态才有。 | 动态 |
栈 stack | 存放函数局部变量和参数以及返回值,函数返回后,由操作系统立即回收。栈空间不大,使用不当容易栈溢出。运行态才有 | 静态 |
查找buf2的地址,可以直接在.bss段找,地址为0x0804A080
。
gdb动态调试代码,在main函数处下断点,使用vmmap
查看栈的状态
(gef直接高亮闪烁hhh),stack区域可写可读可执行,ret2shellcode方法
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
从IDA的反汇编结果看,s距离ebp的偏移量为64h,实际得再gdb上调试(这里IDA的是错误的)
换用gdb-peda插件,pattern create 150
,生成150个字符,复制输入,c
运行报错(提前下断点b main
)。
可以看出,ebp被覆盖掉了,覆盖内容为MAAi
,使用命令pattern offset MAAi
,即可计算出真实偏移量,为108。
调用pwn库函数的指令asm(shellcraft.sh())
生成shellcode,查看其长度为44字节
则我们需要填充的垃圾字符长度为108-44+4=68
,(+4为返回地址),所以可以构造脚本:
1 | from pwn import * |
很奇怪,EOF了.
==后记==:后来了解到,原因是strncpy()写入的buf2在.bss段,自从Linux内核5.x之后,内存的BSS段默认没有可执行权限,该段上的权限是==rw==,并不是写入了stack里(stack为rwx,有执行权限),因此返回地址跳转到.bss段中的shellcode并执行,并不会真正的拿到shell。真正getshell方法较为困难,后续再探讨
ret2syscall
直接checksec
,开启了NX,IDA反汇编main函数
同样利用ret2shellcode的方法计算出返回地址距离v4的偏移仍为108+4
这里使用ROP技术进行攻击,ROP的全称为Return-oriented Programming(返回导向编程),这是一种高级的内存攻击技术,攻击者扫描已有的动态链接库和可执行文件,提取出可以利用的指令片段(gadget),这些指令片段均以ret指令结尾,即用ret指令实现指令片段执行流的衔接,从而构建恶意代码。可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。
常见的利用方法是使用系统调用获得shell
1 | execve("/bin/sh",NULL,NULL) |
其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb,即execve()函数的系统调用号
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
==这里待定,使用COMPILER EXPLORER编译结果使用的不是这几个寄存器==
补上vmmap
情况
首先查找汇编指令为pop eax
,后续接ret
的命令
使用0x080bb196
的地址,值得注意的是,该部分代码段有执行权限,因为我们不需要写入权限,仅仅需要ret地址为下一条目标指令即可,顺次拼接。
1 | 0x08048913 : pop ebx ; pop esi ; pop edi ; ret |
为了验证上述黄色猜想,选择这两条分别测试,其中第二条为wp选择
接下来寻找字符串”/bin/sh”所在地址
1 | 0x080be408 : /bin/sh |
最后一条是int 0x80系统调用地址
1 | 0x08049421 : int 0x80 |
将上述地址拼接起来,构成payload
1 | from pwn import * |
最后测试结果,只有pop_edx_ecx_ebx_ret可以getshell,有点奇怪
ret2lib1
惯例反汇编加checksec检查
其中,gets()
函数存在栈溢出
本题情况较好,经过IDA查找,发现即既调用有system()
函数,又有/bin/sh
的字符串,因此可以直接组合两段地址,构造exp
本题与前面不同点在于,调用的system()
函数,不是自己写的,而是libc中加载的函数。
1 | from pwn import * |
其中,sys_addr
后面加的b"A"*4
为填位的返回地址,具体结构参见链接。上下两种payload均可。
ret2libc2
checksec()
和IDA发现,本题和上一题大差不差,但是没有/bin/sh
字符串,有system()
函数。
查看主函数,仍然是gets()
函数栈溢出,
可以考虑覆盖返回地址,再次调用gets()
函数,写入/bin/sh
的字符串到指定位置(这里选择的是.bss段的buf2中),然后再从buf2上加载该字符串,作为system()
函数的参数。
这一块儿需要构造两个ROP,其中gets()函数怎么控制返回地址写入buf2中有点迷糊。具体来说,需要构造如下图样式的链条
gets()函数在plt中的相对地址已有,可以在IDA中查询或直接在gdb中disass gets
,同理system()函数
.bss段中buf2的地址为0x0804A080
gets的返回地址的gadget为0x0804843d
构造exp为:
1 | from pwn import * |
上述的0xdeadbeef
是magic number,该处返回地址无意义,填充跳过
最后getshell。
==补充==,之前对buf2返回地址要找一个pop ebx; ret
的gadget有点迷,后来看了一下。在gets()函数被执行之后,main()的返回地址(被替换成了gets()的地址)以及gets()的返回地址会被RET
指令弹出,但是gets()的参数还留在栈顶,需要清除掉后再放入system()的返回地址和参数(system的地址就是gets()的返回地址)。理论上的方案是现在代码段中找到POP
指令插入到gets()和system()之间,因此插入上述的gadget,但是实际上system()不在乎返回地址,可以直接影响就干脆直接返回system()函数。
1 | from pwn import * |
其中,system_plt
也作为gets()
函数的返回地址,第一个buf2是gets()函数写入的地址,第二个buf2是system()函数的参数,很精妙,也能getshell。
ret2libc3
与前两个差不多,但是该题将system()函数调用也隐去
- 本文作者: Isabella
- 本文链接: https://username.github.io/2023/11/15/basic-pwn/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!