Security

Shared Library Injection (1)

Jest 2016. 7. 18. 22:40


Library란?


라이브러리란 보통 실행파일과 같이 실행되는 서브 루틴들을 별도로 분리해 놓은 것이라고 할 수 있습니다.

실행 파일과 라이브러리 파일을 분리하는 이유는 유지 보수가 쉽고, 공동 개발 시 개발 시간 단축이 가능한 것과 같은 여러가지 장점이 있기 때문입니다.


이런 라이브러리들은 크게 두 종류로 나뉘어집니다.


1. Static Library


정적 라이브러리라고 하며, 컴파일 시에 컴파일러에 의해 참조되어 실행파일 생성 시에 복사됩니다.

즉 해당 라이브러리의 프로그램을 실행하기 위한 코드가 실행 프로그램 안에 존재하게 됩니다.


보통 정적 라이브러리를 사용할 경우 라이브러리 코드가 실행 파일 안에 포함되기 때문에 프로그램의 크기는 커지지만 실행 속도는 빨라지게 됩니다.

윈도우는 .lib 확장자를 가지며 리눅스 계열은 .a 확장자를 사용합니다.



2. Shared or Dynamic Library


동적 라이브러리라고 하며, 프로그램 실행 시에 메모리에 로딩되어 사용됩니다.

동적 라이브러리를 사용할 경우 라이브러리 코드가 실행 파일과 분리되기 때문에 프로그램 크기가 작아지는 대신 실행 시에 메모리에 로딩되기 때문에 실행 속도는 정적 라이브러리 방식에 비해 조금 느려집니다.


또한 정적 라이브러리를 사용할 경우 라이브러리 업데이트가 발생하면 실행 파일을 다시 빌드해서 배포해야 하지만, 동적 라이브러리는 실행파일과 상관없이 동적 라이브러리 자체만 배포하면 되기 때문에 유지 보수가 쉬워 일반적으로 많이 쓰이는 방식입니다.

윈도우는 .dll 확장자를 가지며 리눅스 계열은 보통 .so 확장자를 사용합니다. 



Shared Library or DLL Injection


동적 라이브러리가 프로그램 실행 시간에 로딩된다는 점을 이용하여 수정된 라이브러리를 로딩하게 하거나, 임의의 라이브러리를 로딩하게 하는 등의 공격 방법입니다.



일반적으로 공유 라이브러리를 삽입하는 공격 방법은 아래의 3가지로 볼 수 있습니다.


  1. PTRACE를 사용한 shellcode injection
  2. VDSO 수정
  3. __libc_dlopen을 사용하는 방법



각 공격 방법은 다음 포스트에서 다루도록 하고 이번 포스트에서는 각 공격법들을 이해하기 위해 필수인 ELF 파일 포맷에 대해 간략하게 알아보고 shared library가 inject된 상태를 보도록 하겠습니다.


ELF(Executable and Linkable Format) File Format


ELF는 리눅스 계열 운영체제에서 주로 사용되는 파일 포맷입니다. 윈도우 계열은 FAT, NTFS 파일 포맷을 이용합니다.


아래는 ELF 파일 포맷을 나타내는 그림입니다.


(http://www.sw-at.com/blog/wp-content/uploads/2011/04/elf.png 에서 발췌)



오른쪽 그림을 기준으로 설명하도록 하겠습니다.

ELF 파일 포맷은 Program Header Table을 가지는데, 이것은 0개 이상의 세그먼트들에 대한 정의를 가지고 있습니다.

또, Section Header Table도 가지는데, 이것은 0개 이상의 섹션들에 대한 정의를 가지고 있습니다.

그리고 이들 헤더들에 의해 참조되는 데이터를 가지고 있습니다.

섹션은 링킹, 재배치에 필요한 정보를 포함하며 세그먼트는 해당 파일의 런타임 실행에 필요한 정보를 포함하고 있습니다.

(위키피디아 참조. https://ko.wikipedia.org/wiki/ELF_%ED%8C%8C%EC%9D%BC_%ED%98%95%EC%8B%9D)



readelf  유틸리티를 사용하면 ELF 파일 포맷 구조를 자세히 살펴볼 수 있습니다.


아래는 -l 옵션을 이용해서 /usr/bin/id 프로그램의 프로그램 헤더를 살펴보는 화면입니다.



위 화면에서 INTERP는 dynamic linker의 경로를 보여주는데 위의 화면에서는 /lib64/ld-linux-x86-64.so.2 로 표시되어 있습니다.

그리고 아래 두 개의 LOAD는 text segment, data segment를 표시합니다.

특히 구조체 배열로 저장된 데이터들을 나타내는 DYNAMIC을 눈여겨 보셔야 하는데요, 이 segment는 data segment 범위 안에 존재하며 두 번째 LOAD 다음 메모리에 적재됩니다.


위에서 dynamic segment는 구조체 배열의 형태로 저장되어 있다고 언급했는데, 이 구조체는 바로 Elfn_Dyn 구조체입니다.

Elfn_Dyn 구조체는 아래와 같습니다.



typedef struct {
    Elf64_Sxword    d_tag;
    union {
        Elf64_Xword    d_val;
        Elf64_Addr      d_ptr;
    } d_un;
} Elf64_Dyn;
extern Elf64_Dyn _DYNAMIC[];


dynamic segment는 dynamic linker가 필요로 하는 모든 정보를 가지고 있으며, 의존성 해결을 위해 런타임에 dynamic linker에 의해 파싱됩니다. 이후 runtime patching을 이용해 복잡한 재배치 작업을 거쳐 비로소 프로세스 안으로 링크됩니다.

dynamic linker는 그 자체로써 dynamic library object입니다. 


Elf64_Dyn 구조체의 멤버인 d_tag는 구조체에 저장된 데이터의 타입을 dynamic linker에게 알려주는 역할을 합니다. elf(5)를 보면 d_tag 값으로 가능한 모든 리스트를 알 수 있습니다.


가장 중요한 내용으로 dynamic linker는 d_tag 값이 DT_NEED인 엔트리들을 찾아 dynamic object를 프로세스 안에 링크 시킵니다.


-d 옵션을 사용하면 아래와 같이 dynamic segment의 정보를 볼 수 있습니다.



위 화면에서 볼 수 있듯이 (NEEDED)로 표시된 두 개의 shared library가 바로 Elf64_Dyn 구조체의 d_tag값이 DT_NEED인 엔트리들입니다.



ELF File Format에 관한 더 자세한 정보는 아래 자료를 참고하시기 바랍니다.

http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf



Inject된 Dynamic Library 살펴보기



그럼 실제로 dynamic library가 inject 되었을 때를 한번 살펴보도록 하겠습니다.


먼저 inject할 프로세스를 찾아볼까요?

저는 현재 로그인 한 계정인 ubuntu 로 실행되고 있는 프로세스들 중 하나를 선택하였습니다.

(mint 리눅스가 좀 이상해서 ubuntu로 변경했습니다 ㅠ_-)




저는 /usr/bin/prldnd 라는 프로세스를 선택했습니다. 무슨 일을 하는 프로세스인지는 모르겠습니다.

그냥 아무 이유없이 선택했습니다. ^^;



linux-injects 라는 ptrace를 이용하는 오픈소스 툴을 사용해서 inject를 해보겠습니다.

먼저 inject할 shared library를 만들어볼까요?



아래와 같이 fPIC, shared 옵션을 이용해 컴파일 하면 so 파일을 생성할 수 있습니다.



이제 위에서 만들어진 shared library를 이용해 /usr/bin/prldnd 프로세스에 inject 해 보도록 하겠습니다.

해당 프로세스의 PID는 2537입니다.

ptrace를 사용하기 때문에 inject를 성공시키기 위해서는 반드시 sudo 권한이 필요합니다.




위와 같이 inject에 성공하면 inject에 성공했다는 메세지를 볼 수 있습니다.


그럼 제대로 injection이 되었는지 확인해 볼까요?

/proc 디렉토리의 maps 파일을 읽어 해당 프로세스의 프로세스 맵을 보도록 하겠습니다.



위와 같이 testlib.so 가 해당 프로세스의 주소 공간에 제대로 적재되어 있는 것을 확인할 수 있습니다.



그럼 사용자의 입장에서 실행중인 특정 프로세스에 shared library가 삽입되었는지 어떻게 발견할 수 있을까요?

일반적으로 정상적으로 로딩되는 shared library는 인접한 주소 공간에 자리잡게 됩니다.

또한 바이너리 파일의 DT_NEEDED인 object와의 비교를 통해서도 알 수 있습니다.


아래는 실행중인 prldnd 프로세스에 로딩된 모든 shared library 목록들 중 중복을 제거한 결과입니다.

(이 글을 쓰는 시점에서 리눅스 재부팅으로 프로세스 ID의 변경이 있었습니다.)




ldd 명령으로도 확인이 가능하구요,




GDB로도 확인이 가능합니다.




아래는 /usr/bin/prldnd의 DT_NEED object를 확인해 본 결과입니다.




아쉽게도 DT_NEEDED인 뿐만 아니라 다른 shared object도 프로세스 주소 공간 내에 로딩되어 있어서 DT_NEEDED를 통한 단순 비교는 어렵습니다.


인접한 주소 공간에 로딩되었는지 다시 살펴볼까요?

프로세스 맵을 다시 한번 살펴보겠습니다.



보시다시피 우리가 삽입한 testlib.so 파일이 다른 so 파일과는 동떨어진 주소 공간에 로딩되어 있는 걸 볼 수 있습니다.


위와 같이 프로세스 맵을 보면 의심되는 shared library를 확인하실 수 있습니다. ^^




이번 포스팅은 여기까지 입니다.

다음 포스팅에서는 shared library injection의 가장 기초적인 방법으로 LD_PRELOAD를 이용한 injection 방법에 대해 알아보도록 하겠습니다.


읽어 주셔서 감사합니다.



참고자료

http://backtrace.io/blog/blog/2016/04/22/elf-shared-library-injection-forensics/