안드로이드 Relocation Table 분석
구글은 AOSP(Android Open Source Project)라고 해서 안드로이드 소스 코드를 오픈 소스로 관리하고 있습니다. 그러다 보니 libc, libdl 등 다양한 라이브러리 코드를 확인할 수 있는데요. 저희는 이 코드를 통해 dlopen() 소스코드를 확인하고 GOT가 어떻게 쓰여지는지 확인해보겠습니다. 다음 URL (https://android.googlesource.com/platform/bionic/+refs)을 통해 각각의 버전에 맞는 소스 코드를 확인할 수 있습니다.
본격적으로 설명드리기에 앞서 ELF 구조에 대해서 어느정도 지식이 있어야 이해가 가실겁니다. 구글에 검색만 하시더라도 이미 많은 분들께서 ELF 구조에 대해 아주 자세하게 설명을 해놓았기 때문에 해당 글들을 보고 오신 다음에 제 글을 읽으시면 될듯합니다.
Relocation Table 재배치 과정
ELF에는 많은 섹션들중에 relocation 섹션이 존재합니다. 해당 섹션은 심볼릭 정의를 참조하는 테이블인데요. ELF 내 심볼이 가리키는 위치(r_offset)를 실행시에 재배치해주는 역할을 합니다. 저희가 C언어로 코딩할때 각종 시스템 함수(printf, memcpy, memset 등)들을 사용하는데요. 이러한 함수들은 libc.so 라이브러리를 동적 링킹하여 사용하는 구조입니다. 컴파일 과정에서 libc.so 를 동적 링킹하여 사용하겠다 명시하게 되는거죠. 빌드 과정을 거치면 최종 라이브러리 파일이 생성이 되는데, 이때 라이브러리 파일을 열어서 확인해보면 dynamic section에서 "DT_NEEDED libc.so" 라고 명시가 되어 있는것을 확인할 수 있습니다. 또한 심볼 정보를 보시면 printf, memcpy 등의 함수들이 있는것을 확인할 수 있습니다. 그런데 여기서 해당 심볼을 확인하시면 r_offset 값이 0으로 지정되어 있습니다. 그렇다면 실제 동작할때에는 어떻게 libc.so를 동적 링킹하여 사용할 수 있는걸까요? 이때 방금 말씀드린 Relocation Table을 확인하여 시스템 함수의 GOT 주소에 실제 로드된 libc.so의 함수 주소를 Write 하게 됩니다.
즉, Relocation Table은 "프로그램 동작 시에 해당 주소를 재배치 해줘" 라고 알려주는 것입니다.
내부 text section에 존재하는 코드를 보시면 bl memcpy 등의 시스템 함수 분기문을 확인할 수 있는데, 이때 memcpy plt 주소로 분기하고 plt는 GOT 주소를 읽고 PC가 해당 주소로 바뀌는 어셈블리어 코드를 확인할 수 있습니다. 이러한 과정을 거쳐 실제 함수의 위치로 분기할 수 있는것이죠.
자 그림을 보면서 하나하나씩 따라가 봅시다. 방금 말씀드린것 처럼 두번째 필드(st_value)가 실제 프로그램 내 Text Section 에 존재하지 않으니 0으로 할당되어 있는것을 확인할 수 있습니다.
Relocation Table에는 다음과 같이 정의가 되어 있습니다. 21번째 인덱스인 심볼(memcpy)이 재배치할 주소는 0x1AEA4라고 알려주고 있습니다.
0x1AEA4를 따라가보면 GOT 주소를 가리키고 있는것을 확인할 수 있죠?
여기서 R_ARM_JUMP_SLOT은 함수 주소의 재배치를 뜻합니다. ELF는 이러한 정보를 명시해놓은채로 dlopen()함수를 통해 메모리로 로드되는데요. dloepn()소스코드를 확인하시면 어떻게 재배치를 하는지 정확히 확인할 수 있습니다.
dlopen() 전체 소스 코드를 보기에는 너무 방대합니다. 그래서 Relcaton Section을 확인하고 어떻게 실제 주소를 Write 하는지 과정만 요약하여 살펴보도록 하겠습니다. 우선 Dynamic Section에서 "DT_NEEDED ???.so"라고 명시해놓았던 라이브러리를 모두 dlopen()함수를 호출하여 메모리로 로드시킵니다.
그다음 plt_rel 변수를 Relocation Section 주소로 할당하고 soinfo_relocate() 함수를 호출합니다.
Relocation Table에서 type과 symbol index를 파싱한 뒤 symbol index가 있을 경우, 심볼 이름을 인자로 soinfo_do_lookup() 호출합니다.
이때 자기 자신의 심볼부터 탐색해보고 없다면 needed로 로드했던 라이브러리에서 심볼을 탐색하게 되는것이죠.
그림으로 확인하셨다시피 GOT에 실제 로드되어 있는 라이브러리내 함수의 주소를 GOT에 Write하는 과정을 살펴보았습니다. 모든 동적 링킹된 함수들은 이러한 시퀀스로 모두 주소가 바뀌고 DT_INIT부터 시작하게 되는것이죠.
그렇다면 PLT는 어떻게 GOT를 가리킬까요? 이 과정도 살펴봅시다.
plt 시작 위치인 0x3DF8에서 IP에 PC 값을 넣게되는데, 이때 PC라면 현재 위치인 0x3DF8을 넣어줘야 되는데 0x3E00으로 바꿔놓은것을 확인할 수 있습니다. 그 이유는 armv7의 경우 pipe line(fetch -> decode -> execute)과정을 거치기 때문에 실제 실행시인 execute과정에서는 0x3E00 주소가 IP에 들어가게 됩니다. 그런 뒤 GOT 주소를 read하고 PC인 현재 위치를 바꾸는것까지 확인할 수 있습니다.