OZ1NG의 뽀나블(Pwnable)

[Tips][CS] Linux(ELF) - 메모리에 맵핑된 공유 라이브러리는 왜 항상 같은 순서로 맵핑 될까? 본문

Tips

[Tips][CS] Linux(ELF) - 메모리에 맵핑된 공유 라이브러리는 왜 항상 같은 순서로 맵핑 될까?

OZ1NG 2023. 1. 10. 04:02

[*] 목차

- 첫 번째 궁금증: 메모리에 맵핑된 공유 라이브러리의 순서는 실행하는 환경에 상관 없이 언제나 동일한 이유는?

- 두 번째 궁금증: 동적 링커는 필요한 공유 라이브러리의 이름을 어떻게 알아내는 것일까?

- 세 번째 궁금증: ELF의 DYNAMIC 섹션에 명시되어있지 않은 공유 라이브러리는 어디서 왜 불러온 것일까?

- 마무리

- 참고

 

 

[*] 첫 번째 궁금증: 메모리에 맵핑된 공유 라이브러리의 순서는 실행하는 환경에 상관 없이 언제나 동일한 이유는?

리눅스에서 동적 링킹으로 컴파일된 ELF 파일을 실행시키면, dynamic linker(ld.so)에 의해 실행에 필요한 공유 라이브러리들을 읽어와 메모리(라이브러리 영역)에 맵핑 시킨다.

그런데 이때 메모리에 맵핑된 공유 라이브러리의 순서는 실행하는 환경에 상관 없이 언제나 동일하다.

 

왜 그럴까?

[그림1] 메모리에 맵핑된 공유 라이브러리들 (libc --> libpthread --> libstdc++ --> libgcc_s --> libm 순서로 맵핑됨.)

 

이유는 ELF 파일의 헤더부분에서 찾을 수 있었다.

ELF 파일의 헤더부에는 다양한 섹션이 존재한다. 이 중 공유 라이브러리 맵핑에 영향을 끼치는 섹션은 DYNAMIC 섹션이다.

[그림2] ELF 파일 헤더 섹션 확인 (readelf -l)

DYNAMIC 섹션을 좀 더 상세히 확인해보면 다음과 같다.

[그림3] DYNAMIC 섹션 상세 정보 (readelf -d)

[그림3]을 보면 NEEDED라는 부가 설명과 함께 어떤 공유 라이브러리가 필요한지 바이너리 내부에서 명시하고 있는 것을 확인할 수 있다.

[그림4] DYNAMIC 섹션 상세 정보 2

해당 부분을 [그림2]에서 알아낸 DYNAMIC 섹션의 Offset에서 데이터를 확인해보면 위와같이 데이터가 명시되어 있는 것을 확인 할 수 있다.

 

ELF 파일 내부의 DYNAMIC 섹션에는 DT_NEEDED라는 키워드로 필요한 라이브러리가 무엇인지를 명시하여 동적 링커(ld.so)가 명시한 공유 라이브러리를 맵핑하도록 한다.

동적 링커(ld.so)는 ELF 프로그램 실행 직후 공유 라이브러리와 ELF를 연결하기 위해 ELF의 DYNAMIC 섹션을 파싱하여 위에서부터 순서대로 Elf64_Dyn 구조체(64bit 바이너리의 경우)를 참고하여 명령?을 실행한다. (DT_NEEDED는 이러한 명령 중 하나인 것이다.)

즉, 제목이자 내가 궁금했던 메모리에 맵핑된 공유 라이브러리는 왜 항상 같은 순서로 맵핑 될까?에 대한 답동적 링커는 DYNAMIC 섹션의 위에 존재하는 DT_NEEDED 키워드에 명시된 라이브러리 순서대로 맵핑을 하기 때문이다.라고 정리를 할 수 있다.

 

 

[*] 두 번째 궁금증: 동적 링커는 필요한 공유 라이브러리의 이름을 어떻게 알아내는 것일까?

그런데 좀 더 보다보니 이러한 추가적인 궁금증이 생겼다.

[그림4]에 보이는 Elf64_Dyn 구조체에는 라이브러리의 이름 자체를 명시하는 부분은 보이지 않는데

그렇다면 동적 링커는 필요한 공유 라이브러리의 이름을 어떻게 알아내는 것일까?

 

먼저 Elf32_Dyn와 Elf64_Dyn 구조체는 다음과 같이 생겼다.

typedef struct {
        Elf32_Sword d_tag;
        union {
                Elf32_Word      d_val;
                Elf32_Addr      d_ptr;
                Elf32_Off       d_off;
        } d_un;
} Elf32_Dyn;

typedef struct {
        Elf64_Xword d_tag;
        union {
                Elf64_Xword     d_val;
                Elf64_Addr      d_ptr;
        } d_un;
} Elf64_Dyn;

[그림4]의 경우 64bit 바이너리이므로 Elf64_Dyn 구조체를 사용하는데 d_val과 d_ptr로만 이루어진 것을 확인할 수 있다.

이때, d_val에 해당하는 것이 키워드의 고유 값이고, d_ptr에 해당하는 것이 일종의 파라미터이자 주소 값이다.

 

이로써 [그림4]를 보면 알 수 있듯이 DT_NEEDED는 고유 값으로 1을 갖는다는 것을 알 수 있다. ([참고] 3번째 링크 참고)

하지만 DT_NEEDED의 d_ptr의 값을 보면 주소 값이라고 하기에는 너무나도 작은 크기의 값을 갖고 있는 것을 확인 할 수 있다. 이 값은 ELF String Table을 통해 확인할 수 있다.

(ELF String Table: ELF 파일 내에서 사용되는 문자열들을 모아둔 테이블)

[그림5] ELF String Table의 상단부

[그림5]를 보면 ELF String Table의 상단에는 공유 라이브러리들의 이름이 있다는 것을 알 수 있다.

그리고, [그림4]의 DT_NEEDED 구조체의 d_ptr에 해당하는 값과 위 ELF String Table의 시작주소와 ELF String Table에서의 맵핑 하고자 하는 공유 라이브러리의 문자열 값의 주소의 Offset을 구하면 정확히 같은 것을 알 수 있다.

즉, 동적 링커는 필요한 공유 라이브러리의 이름을 어떻게 알아내는 것일까?에 대한 답은 ELF String Table에서 필요한 공유 라이브러리 문자열의 Offset으로 실제 공유 라이브러리의 이름을 알아낸다.고 결론을 내릴 수 있다.

(주의!: 오해하면 안되는게 DT_NEEDED의 경우 d_ptr의 값이 ELF String Table로부터의 오프셋인 것이고 DT_NEEDED 바로 밑에 있는 DT_INIT의 경우 ELF의 init 섹션의 virtual offset 값이 들어가 있다. 즉, 모두가 어떤 특정 섹션을 기점으로 offset 값이 들어가는 것은 아니니 오해 없길 바란다.)

 

 

[*] 세 번째 궁금증: ELF의 DYNAMIC 섹션에 명시되어있지 않은 공유 라이브러리 libm은 어디서 왜 불러온 것일까?

근데 아직 한가지 더 궁금증이 남았다.

[그림1]을 보면 분명 맵핑한 공유 라이브러리는 libm, libgcc, libstdc++, libpthread, libc 이렇게 5개인데, [그림3]이나 [그림4], [그림5]를 보면 DYNAMIC 섹션에는 libgcc, libstdc++, libpthread, libc 이렇게 4가지 밖에 명시가 안돼있다.

그렇다면 나머지 하나의 공유 라이브러리 libm은 어디서 왜 불러온 것일까?

 

이 궁금증에 대한 답은 LD_DEBUG=files 환경 변수를 통해 알 수 있었다.

(LD_DEBUG란?: 동적 링커의 실행 과정에서 발생하는 것들을 디버깅 할 수 있는 환경변수이다. 뒤에 value 값으로 붙는 키워드는 LD_DEBUG=help를 통해 자세히 알아 볼 수 있으니 참고하길 바란다.)

[그림6] LD_DEBUG=files를 통해 공유 라이브러리 맵핑 순서 확인

[그림6]과 같이 LD_DEBUG=files와 함께 ELF 파일을 실행시키면 ELF 파일을 실제로 사용하면서, 사용하는 모든 파일들에 대한 정보를 볼 수 있다. 이때, 맨 처음으로 나오는 것이 공유 라이브러리의 맵핑 정보이며, 정확히 위 순서대로 공유 라이브러리의 메모리 맵핑이 이루어진다.

 

[그림6]을 보면 우리가 궁금했던 libm의 경우 실행하고자 하던 ELF 파일이 아닌 libstdc++.so.6 공유 라이브러리에서 필요하여 맵핑을 시켰다는 것을 알 수 있다.

즉, 이 것으로 나머지 하나의 공유 라이브러리 libm은 어디서 왜 불러온 것일까?에 대한 답은 libstdc++.so.6 공유 라이브러리에서 필요하여 맵핑을 시켰다.고 결론을 지을 수 있을 것이다.

 

그리고 이를 통해 한 가지 더 알 수 있는데, 필요한 공유 라이브러리를 맵핑하는 순서ELF 파일의 DT_NEEDED 순서 이후 각 맵핑한 공유 라이브러리의 DT_NEEDED 순서가 된다는 것이다.

 

 

[*] 마무리

이렇게 공유 라이브러리의 맵핑 순서에 대한 궁금증을 해결해보았다.

혹자에게는 이러한 정보가 크게 의미 없다고 느껴질 수 있겠지만 바이너리를 익스하는 사람의 관점에 있어서는 꽤나 의미있는 궁금증이라고 생각한다.

왜냐면 [그림1]을 보면, 보다시피 공유 라이브러리는 메모리 상에 선형적으로 맵핑 된다는 것을 알 수 있는데, 이는 즉 공유 라이브러리 영역의 아무 부분의 주소를 알아낸다면, 항상 메모리에 맵핑되는 순서가 동일하고 맵핑된 라이브러리의 크기는 언제나 동일하므로, 알아낸 다른 라이브러리의 주소를 통해 다른 라이브러리의 base 주소를 쉽게 알아 낼 수 있다 뜻이기 때문이다. 

 

무튼 궁금해하기 시작한 이후로 대충 ELF 헤더 어딘가 순서가 명시되어 있겠지라고 추측만 하고 있다가 이렇게 제대로 확인해서 궁금증을 해결하니 꽤 흥미로운 경험이 되었다. 다음엔 이걸 응용해서 익스를 할때 100% 확신을 가지고 할 수 있을 것 같아 기분이 좋다. ㅎㅎ

 

 

[*] 참고

- https://www.gabriel.urdhr.fr/2015/01/22/elf-linking/#library-resolution

- https://wariua.github.io/man-pages-ko/ld-linux.so%288%29/#description

- DT_x 키워드 및 d_val 값 참고: https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-42444.html

 

 

 

Comments