Pwnable/Tech

.init_array && .fini_array

Kon4 2023. 2. 20. 14:54

배경지식

ELF는 GOT를 활용하여 반복되는 라이브러리 함수의 호출 비용을 줄인다. 이때 GOT에 값을 쓰게 되는데, 그 중 처음 호출 될떄 함수의 주소를 구하고, 이를 got에 적는 Lazy Binding 방식이 있다.

이때 Lazy Binding의 특성 상 GOT를 실시간 업데이트(실행 중) 해야하므로, GOT에 쓰기 권한이 부여되는데, 이는 GOT를 변조 시킬 수 있는 취약점으로 연계될 수 있다.

.init_array && .fini_array

ELF의 데이터 세그먼트에는 프로세스의 초기화 및 종료와 관련된 .init_array, .fini_array가 존재하고 이 영역들은 프로세스의 시작과 종료에 실행할 함수의 주소를 저장하고 있다. 이때 이 주소값을 임의의 값을로 덮어 쓴다면 어떻게 될까?

답은 공격자가 원하는 프로세스의 실행 흐름으로 조작할 수 있다이다.

RELRO(RELocation Read-Only)

이 보호 기법은 리눅스 개발자들이 함수의 주소값 변조를 방지하기 위해 만든 보호 기법이다.

즉, RELRO는 쓰기 권한이 불필요한 데이터 세그먼트의 쓰기 권한을 제거한다.

구분

NO RELRO : 왼만한 모든것 가능

Partial RELRO : .got.plt, .data, .bss

FULL RELRO: .data, .bss

범위에 따라 위와 같이 나누어 부루고 각각의 특징들이 있으니 참고한다.

( 단, 이는 이론 상의 표준이고 실제 문제에서는 Partial 임에도 쓰기권한이 부여되어 있는 경우가 존재하니, 문제의 의도를 파악하고 gdb로 분석해보길…)

.got ? .got.plt ???

: Partial RELRO 가 적용된 got와 관련된 섹션이 .got와 .got.plt 로 두 개가 존재한다. 이는 전역 변수 중 실행되는 시점(시작 시) 바인딩되는 (NOW Binding) 변수는 .got, 전역 변수 중 실행 중 바인딩 되는(LAZY Binding)되는 변수는 .got.plt에 위치한다.

당연하게도 FULL RELRO 는  .got만 존재한다.

우회

이를 우회하기 위해서 여러 방법이 있지만 다음과 같은 방법을 사용할 수도 있다.

NO RELRO : Whatever — .init_array, .fini_array Overwrite

Partial RELRO :.got.plt, .data, .bss —> GOT over Write

FULL RELRO: .data, .bss hook Overwrite(malloc hook, free hook)

.fini_array Overwrite

.text:0000000000402960 sub_402960      proc near               ; DATA XREF: start+F↑o
.text:0000000000402960 ; __unwind {
.text:0000000000402960                 push    rbp
.text:0000000000402961                 lea     rax, unk_4B4100
.text:0000000000402968                 lea     rbp, fini_array
.text:000000000040296F                 push    rbx
.text:0000000000402970                 sub     rax, rbp
.text:0000000000402973                 sub     rsp, 8
.text:0000000000402977                 sar     rax, 3
.text:000000000040297B                 jz      short loc_402996
.text:000000000040297D                 lea     rbx, [rax-1]
.text:0000000000402981                 nop     dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988:                             ; CODE XREF: sub_402960+34↓j
.text:0000000000402988                 call    ss:(fini_array - 4B40F0h)[rbp+rbx*8]
.text:000000000040298C                 sub     rbx, 1
.text:0000000000402990                 cmp     rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994                 jnz     short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996:                             ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996                 add     rsp, 8
.text:000000000040299A                 pop     rbx
.text:000000000040299B                 pop     rbp
.text:000000000040299C                 jmp     _term_proc
.text:000000000040299C ; } // starts at 402960
.text:000000000040299C sub_402960      endp
__int64 fini()
{
  signed __int64 v0; // rbx

  if ( (&unk_4B4100 - (_UNKNOWN *)fini_array) >> 3 )
  {
    v0 = ((&unk_4B4100 - (_UNKNOWN *)fini_array) >> 3) - 1;
    do
      fini_array[v0--]();
    while ( v0 != -1 );
  }
  return term_proc();
}

fini_arrayv0--; 으로 보아 fini_array+8 값에 해당하는 함수를 먼저 실행하고 fini_array 값에 해당하는 함수를 실행한다.

이를 사용하면 fini_array = fini(), fini_array +8 = main() 으로 덮어 쓴다면, main 으로 무한 루프가 가능하다.

이때 leave_ret 가젯이 존재한다면, fini_array +16 부터 rop 체인을 걸수 있다.

에필로그 (leave, ret)

(lea     rbp, fini_array)

1. leave:

mov esp, ebp
pop ebp # arg(fini_array+8)

2. ret:

pop eip (fini_array + 16)
jmp eip