如何通过.fini_array劫持实现PWN疑问?
摘要:1. .fini_array劫持 上周打了一下金盾杯,算是第一次做非栈上格式化字符串的题,之前只在ctfwiki上看过一点。边学边做,比赛最后一个小时做出来了,爽了。 以金盾杯的题为例 1.1 2023金盾杯 sign_format 题目介
1. .fini_array劫持
上周打了一下金盾杯,算是第一次做非栈上格式化字符串的题,之前只在ctfwiki上看过一点。边学边做,比赛最后一个小时做出来了,爽了。
以金盾杯的题为例
1.1 2023金盾杯 sign_format
题目介绍:一道非栈上的格式化字符串,通过修改dl_fini数组里的偏移值,使函数在退出时执行我们写在bss段上的shellcode。
题目保护分析:没开PIE,其他防护都开了。其次开启了沙箱,需要ORW
题目主要流程:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
sub_40135D();
puts("Welcome here!");
puts("It's a simple sign-in question.");
puts("Let's start!");
close(1);
read(0, format, 0x100uLL);
printf(format);
return 0LL;
}
有一个很长的输入,之后是格式化字符串漏洞,但是关闭了标准输出流。
其中format保存在bss段上,所以不能修改返回地址了。
格式化字符串%n解析的是地址,当格式化字符串不写在栈上,我们无法通过访问写在栈上的地址来实现任意地址写。而且不能控制返回地址了。
考虑使用.fini_array劫持
1.2 劫持.fini_array
实现功能:在执行exit函数的时候,执行任意地址上的程序
利用条件:需要修改一个在ld.so段上的值。
猜测这个值会在进行动态链接的时候过程中残留在栈上(存疑)
1.2.1 原理分析
下面是程序执行流程图:
根据上图流程,在函数退出执行的时候会调用exit函数,如果我们控制exit函数的finiarray,可以实现任意代码执行,下面我们具体分析一下finiarray这个参数是什么:
1.2.2 dl_fini函数
exit函数执行的时候会调用dl_fini函数。
本来l->l_addr为0,而l->l_info[DT_FINI_ARRAY]->d_un.d_ptr指针指向程序中的fini_array段的地址,也就是l->l_info[DT_FINI_ARRAY]->d_un.d_ptr的值为0x0000000000403D98
如果我们能控制linkmap->l_addr指针,就可以将程序偏移到我们写的位置,执行shellcode。
需要注意这里存的是一个程序地址:0x401200,所以我们伪造l_addr的时候将偏移后的值改为shellcode的地址。
那么如何通过格式化字符串控制l_addr呢?这个就需要用到在栈上留存的一个ld.so上的指针了
1.3 gdb动态分析利用过程
首先,在main函数执行完毕之后会跳转到exit函数执行:
然后我们步进到exit函数中:
可见exit函数会调用一个叫__run_exit_handlers的函数
继续步进,直到这个函数调用了__dl_fini函数,然后dl_fini函数会执行call rax地址:
具体查看rax地址是0x40406b,指向0x404073,其实这里已经是被我们修改了。原本rax应该是0x0403D98 -> 0x401200,原本是要去执行这个代码的:
dl_fini函数:
当我们修改了l_addr之后,array[i]就可以被控制了。
而我们只需要计算0x40406B - 0x403D98 = 723,就知道我们应该把l_addr改成多少了。那么我们怎么改这个值呢?
1.4 修改ElfW中l->l_addr劫持fini_array执行
前面我们说过了,在栈上有一个ld.so的地址残留,猜测是在动态链接的过程中残留在栈上的,对应这道题:
就是那个末尾是0x2e0的粉色的地址。里面存的值0x2d3就是723,这里是执行完格式化字符串漏洞,已经被我们修改过了。原本里面存的应该是0,对应的就是l->l_addr = 0
具体在dl_fini函数中:
执行 add rax,qword ptr [r15] ,这个r15此时保存的就是栈上ld.so保存的那个数据,那么只要利用格式化字符串漏洞修改这个ld.so地址中存的值,就可以控制让rax中保存的值从0x401200+0x0 改为访问我们希望执行的shellcode的地址。
