반응형

시스템 해킹을 공부하다 보면 PLT와 GOT를 한 번쯤은 보거나 듣게 된다.

 

나도 여러 번 PLT와 GOT를 보거나 들었고, 해커스쿨 LOB 18번 문제를 풀면서 PLT와 GOT를 다시 한 번 접하게 됐는데

 

이참에 이 글로 PLT와 GOT 그리고 Dynamic Linking 과정을 정리한다.


[plt & got를 공부하면서 참고한 블로그 글]

1. plt & got 개념 : https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

2. plt & got 개념 및 실습 그리고 GOT overwrite : https://0secusik0.tistory.com/67

3. 실제 해킹 문제로 알아보는 plt & got : https://bpsecblog.wordpress.com/2016/03/09/about_got_plt_2/

4. 위의 3번 글의 내용을 핵심만 간략히 적어둔 블로그 글 : http://cr3denza.blogspot.com/2015/04/plt-got.html

(위의 hackerz on the Ship의 글 2개 중 "실제 해킹 문제로 알아보는 plt & got" 글은 plt & got를 분석함에 있어서 절대적으로 도움이 많이 됐다.)

(4번 블로그는 각 함수에서 구한 값들을 어디에 저장해두는지 더블 체크할 때 유용했다.)

 

STRTAB : https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/strtab_8cxx.html#a75d1e7e61739ba0d0211b3293e5ba9d3

elf32_Rel : https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/structElf32__Rel.html#addcf5ef67ababeb4940889e912c11eff

elf32_Sym : https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/structElf32__Sym.html

symtab : https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/classwhirl2xaif_1_1xlate__ST__TAB.html#a61fad846a94093d7830f7510ef1c86f9


PLT와 GOT 개념

plt : 외부 함수를 연결해주는 테이블로, plt를 통해 다른 라이브러리에 있는 프로시저를 호출해 사용할 수 있다.
plt도 테이블이기 때문에 어떠한 정보들이 저장되어 있는데
이 정보들은 GOT 테이블 내의 주소와 runtime resolve 과정에 필요한 정보들이다.
GOT : PLT가 참조하는 테이블로, 실제 함수들의 주소가 들어있다

링커란

PLT와 GOT를 이해하려면 먼저 링커라는 것을 알아야 한다.

 

함수의 정의가 외부 라이브러리에 있는 함수를 호출하는 코드가 있을 때 실행 파일로 컴파일 하는 과정을 보면 아래와 같다.

 

 

위의 사진을 보면 어셈블러가 오브젝트 파일을 생성하고 나면 링커가 링킹 과정을 거쳐 실행 파일로 만들게 되는데, 이때 링킹 과정이란 컴파일 하는 오브젝트 파일과 라이브러리를 연결하는 것이고, 링킹 방법에는 static과 dynamic 방식이 있다.

 

참고)

라이브러리란

함수 정의 코드를 컴파일하여 나온 결과물인 오브젝트 파일을 해당 함수의 실행 코드 또는 실행 코드 파일이라고 부르는데

여러 함수의 실행 코드 또는 실행 코드 파일들이 모인 것을 라이브러리라고 하며, 즉 라이브러리에는 여러 함수들의 실행 코드가 들어있다.

 

각 링킹 방식의 장점

[static]
실행 파일 안에 모든 코드가 포함되기 때문에 라이브러리 연동 과정이 필요없고

한 번 생성한 파일은 따로 필요한 라이브러리를 관리하지 않아도 된다.


[dynamic]
공유 라이브러리를 사용하는데 공유 라이브러리는 라이브러리를 하나의 메모리 공간에 매핑하고
해당 메모리 공간 안에 있는 라이브러리 내용을 여러 프로그램에서 공유해서 사용한다.

모든 코드를 파일 안에 포함시키는 게 아니므로 파일의 크기가 상대적으로 작다.

 

각 링킹 방식의 단점

[static]
모든 코드가 포함되므로 파일의 크기가 상대적으로 크고
동일한 라이브러리를 사용한다고 해도 프로그램을 새로 실행하면 같은 라이브러리 내용을 메모리에 또 올린다.

[dynamic]
라이브러리 의존도가 높기 때문에 필요한 라이브러리를 관리해주지 않으면 프로그램을 실행할 수 없다.

 

plt와 got는 리눅스 환경에서 정적 라이브러리일 때는 상관없고, 실행 코드가 외부 라이브러리에 있는 동적 라이브러리일 때 함수의 주소를 알아오기 위해 사용하는 개념이다.

(기본적으로 컴파일할 때는 동적 라이브러리 방식으로 컴파일 된다.)


PLT와 GOT의 동작 방식 이론

dynamic link 방식으로 컴파일된 프로그램에서 함수를 호출하게 되면 최초로 호출하느냐 혹은 이전에 호출한 적이 있느냐에 따라 약간의 차이가 있다.

 

https://bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

 

plt를 가장 먼저 참조하고, 이어서 GOT로 점프하는데, 이때 GOT에 기록되어 있는 주소는 실제 함수의 주소가 아니기 때문에

PLT로 다시 이동하여 PLT에 있는 정보들과 _dl_runtime_resolve 함수를 이용하여 함수의 실제 주소를 구하는

runtime resolve 과정을 걸쳐 GOT에 실제 주소를 저장한 후 실제 함수 주소로 점프하여 함수가 동작하게 된다.

 

먼저, 어떠한 함수를 최초로 호출하는 거라면 위의 과정을 따른다.

 

어떠한 함수를 최초로 호출할 때 got에는 함수의 실제 주소가 아닌 다른 주소가 있다.

 

먼저 got에 적혀있는 해당 주소로 점프하여 명령을 실행하다가 _dl_runtime_resolve 함수를 호출하게 되는데, 이 함수가 수행되면서 내부에서 함수의 실제 주소를 구하고 GOT에 저장한다.

 

참고)

바이너리가 실행되면 ASLR 기능에 의해 라이브러리의 주소가 매번 다른 주소에 매핑된다.

이 상태에서 라이브러리 함수를 호출하면, 함수의 이름을 이용해 라이브러리에서 심볼들을 탐색하고

해당 함수의 정의를 발견했을 때 그 주소로 실행 흐름을 옮기는데

이렇게 함수의 이름을 이용해 해당 함수의 정의로 실행 흐름을 옮기는 과정을 runtime resolve이라고 한다.

 

plt를 가장 먼저 참조하고, plt에서 got로 점프하는데

got에 기록되어 있는 실제 함수 주소(라이브러리에서의 주소)를 호출하여 함수가 동작하게 된다.

 

그리고 어떠한 함수를 이전에 호출한 적이 있고,  해당 함수를 다시 호출할 때는 got에 해당 함수의 실제 주소가 저장되어 있기 때문에 위의 과정을 따른다.

 

https://bnzn2426.tistory.com/27

 

어떤 함수를 최초로 호출했을 때와 두 번 이상 호출할 때를 그림으로 나타내면 위와 같다.

 

참고)

함수 호출 명령과 GOT 사이에 PLT를 두어 사용하는 이유는 아래와 같다.

실제로 사용하는 함수의 주소만 GOT에 저장해둠으로써 라이브러리를 로드할 때
사용하지 않는 함수들의 주소들까지 전부 알아와서 GOT에 주소를 저장해두는 것을 방지하기 위함이다.

GOT overwrite

 

어떤 함수를 최초로 호출할 때 runtime resolve 과정을 거쳐 함수의 실제 주소를 GOT에 기록해둔다.

 

그리고 나서 재호출할 때는 PLT에서 GOT를 참조하여 기록되어 있는 실제 함수 주소로 점프하는데, 이때 GOT에 기록되어 있는 실제 함수 주소값을 검증하지 않는다.

 

그러므로 특정 함수를 처음 호출하여 해당함수의 실제 주소를 GOT에 저장한 다음 저장된 값을 변조한다면, 해당 함수를 다시 호출할 때 변조된 값에 해당하는 주소에있는 코드를 수행하게 될 것이고, 이렇게 공격하는 기법이 GOT overwrite이다.


이 글의 목적

위에서 첫 번째 함수 호출 때와 두 번째 함수 호출 때의 GOT에 담겨있는 주소가 다르다고 했는데, 그 이유는 첫 번째 호출 때는 함수의 실제 주소를 알아오는 과정이 필요하기 때문이고, 두 번째 이상 호출 때는 이미 첫 번째 호출 때 실제 주소를 알아내어 GOT에 기록해뒀기 때문이다.

 

그 첫 번째와 두 번째 이상 호출 때의 과정을 조금 더 자세히 보면 아래와 같다.

[첫 번째 함수 호출 때]
함수 호출 -> PLT -> GOT -> PLT -> _dl_runtime_resolve -> _dl_fixup -> _dl_lookup_symbol_x
-> _dl_fixup -> _dl_runtime_resolve -> puts 함수

[두 번 이상 함수 호출 때]
함수 호출 -> PLT -> GOT -> puts 함

 

이론적으로 설명된 첫 번째 호출 때와 두번 이상 호출 때의 과정을 실제로 gdb를 이용해 파헤쳐 본다.

 

환경은 두 개의 환경에서 실습을 진행할 것인데, 하나는 hackerschool의 lob 환경이고, 다른 하나는 ubuntu 16.04.07 버전의 환경이다.

 

hackerschool의 lob 환경에서 진행하는 내용은 옛날 환경에서 진행하는 것이므로 눈으로만 보면서 어떤 과정을 거쳐서 외부 라이브러리에 있는 함수의 실행 코드로 넘어가는지만 알아도 되지만

 

ubuntu 환경에서 진행하는 내용은 같이 따라해보면서 실습해보는 걸 추천한다.

 

사실 hackerschool의 lob 환경에서 진행하는 내용을 잘 이해만 한다면 혼자서도 할 수 있게 설명해뒀다.


각 환경에서 진행하는 과정 미리보기 목록

[hackerschool lob 환경에서 진행하는 과정 목록]
PLT
GOT
Dynamic Linking 시작(PLT+6(reloc_offset), PLT+11)
  - link_map 주소를 스택에 넣고 _dl_runtime_resolve 함수로 점프
    - _dl_runtime_resolve 함수 디스어셈블
    - fixup함수 호출 직전 스택 확인
     - fixup 함수 내부로 진입
      - link_map을 이용해 STRTAB 테이블 주소 구하기
       - link_map을 이용해 STRTAB 테이블 주소 구하는 과정
      - link_map을 이용해 재배치 정보를 담고 있는 재배치 테이블의 시작점인 JMPREL의 주소를 얻어오기
       - JMPREL 테이블에서 puts에 해당하는 Elf32_Rel 구조체 형식의 값에 있는 .dynsym 테이블에서의 index 값을 이용해 수동으로 직접 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_Sym 형식의 구조체 값 찾아보기
      - 위에서는 수동으로 직접 .dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체의 주소를 찾았지만, 코드 상에서 해당 구조체의 주소를 찾기 위해 Elf32_Rel 구조체 형식의 내용 중 .dynsym에서의 index와 재배치 정보가 들어있는 곳의 주소를 edi 레지스터에 저장
   - 여기서 한번 내용 정리 - 
      - STRTAB에서 puts 문자열의 주소를 구하는 부분
       - JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값에서 .dynsym에서의 index 값을 가져와 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값에서 STRTAB 에서의 puts 문자열 offset 값을 구하기
       - GOT 주소 알아와서 EBP-12 주소에 기록하고 재배치 타입을 비교해 조건 분기 및 esi 레지스터에 link_map 주소 넣기
       - 실제로 STRTAB의 주소와 STRTAB에서 puts 문자열의 offset 값과 더해 STRTAB에서 puts 문자열의 주소를 구하는 부분
       - _dl_lookup_symbol 호출 전 레지스터 및 스택 정리
     - _dl_lookup_versioned_symbol 호출 및 디스어셈블
       - _dl_lookup_symbol() 함수가 없는지 확인
     - _dl_lookup_versioned_symbol 함수 반환값 확인
     - _dl_lookup_versioned_symbol() 함수를 끝내고 fixup 함수로 복귀하여 _dl_lookup_versioned_symbol() 함수에서 알아낸 SYMTAB 주소 edx 레지스터에 저장
      - SYMTAB 주소를 구했는지 확인하고 라이브러리의 주소와 SYMTAB에서 찾아낸 puts 함수의 offset 값을 더해 실제 puts 함수의 주소 알아내 eax 레지스터에 저장
      - fixup 함수 내에서 구해 저장해둔 puts 함수의 GOT 주소를 가져와 GOT 주소에 puts 함수의 실제 주소를 넣기
fixup 함수 종료 후 _dl_runtime_resolve 함수 종료
2번째 puts 함수 호출 부분에서 GOT 주소의 값 확인


[ubuntu 16.04.07 환경에서 진행하는 과정 목록]
PLT & GOT 부분 및 Dynamic Linking 시작(PLT+6(reloc_offset), PLT+11)
  link_map 주소를 스택에 넣고 _dl_runtime_resolve 함수로 점프 및 _dl_runtime_resolve 함수 디스어셈블
    fixup함수 호출 직전 스택 확인

      _dl_fixup 함수 내부로 진입 및 _dl_fixup 함수 디스어셈블
	link_map을 이용해 STRTAB의 주소, JMPREL의 주소, GOT의 주소, .dynsym에서의 index 값과 재배치 타입의 값 구하는 부분
		link_map을 이용해 STRTAB의 주소, JMPREL의 주소, GOT의 주소, .dynsym에서의 index 값과 재배치 타입의 값 구하는 과정
			link_map을 이용해 재배치 정보를 담고 있는 재배치 테이블의 시작점인 JMPREL의 주소를 구함과 동시에 reloc_offset 값을 이용해 puts에 해당하는 Elf32_Rel의 주소 구하는 부분
				JMPREL 테이블에서 puts에 해당하는 Elf32_Rel 구조체 형식의 값에 있는 .dynsym 테이블에서의 index 값을 이용해 수동으로 직접 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_Sym 형식의 구조체 값 찾아보기
			link_map을 이용해 STRTAB 테이블 주소 구하기
				STRTAB의 주소를 esp+0xc 주소에 저장
			edx 레지스터에 .dynsym에서의 index 값과 재배치 타입 값을 저장
			JMPREL 테이블에 Elf32_Rel 형식의 값의 내용 중 GOT 주소를 가져와 eax 레지스터에 저장
			.dynsym에서의 index 값과 재배치 타입 값을 esi 레지스터에 저장
			JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값에서 .dynsym에서의 index 값을 가져와 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값이 있는 주소 구하기
				재배치 타입 검사 및 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소를 esp+0x1c 주소에 저장
			GOT 주소를 ebp로 옮기고, 조건 분기
			esp+0xc 주소에 저장해뒀던 STRTAB의 주소와 ebx 레지스터에 있던 0x80481dc 주소에 있는 puts 문자열 offset 값을 더해 STRTAB에서 실제 puts 문자열의 주소 구하기
			esp+0x1c 주소를 ecx 레지스터로 옮기기

_dl_lookup_symbol_x 함수 호출 전 스택에 값 넣고 _dl_lookup_symbol_x 함수 호출
	_dl_lookup_symbol_x 함수 디스어셈블
_dl_lookup_symbol_x 함수를 끝내고 fixup 함수로 복귀
	_dl_lookup_symbol_x 함수의 반환값 확인
	esp+0x1c 주소에 구해둔 SYMTAB의 주소를 ebx 레지스터에 저장
	libc 주소를 담고 있는 주소에서 libc 주소를 가져와 eax 레지스터에 저장
	ebx 레지스터의 값을 이용해 라이브러리에서 puts 함수의 offset 값을 가져와 라이브러리 주소와 더하여 puts 함수의 실제 주소를 구해 eax 레지스터에 넣기
두 번째 puts 함수 호출 부분

해커스쿨 LOB 환경에서 puts() 함수를 두 번 호출하는 프로그램을 만들어 gdb로 실습

vi plt_got.c
#include <stdio.h>

int main(void)
{
        puts("hello");
        puts("world");

        return 0;
}
gcc -o plt_got plt_got.c

 

먼저 위와 같이 plt_got.c 파일에 puts() 함수를 두 번 호출하는 코드를 적고, gcc을 이용해 컴파일하면 기본적으로 동적 라이브러리 방식으로 컴파일이 된다.

 

gdb -q ./plt_got

set disassembly-flavor intel

 

gdb로 컴파일 한 바이너리 plt_got를 연다.

 

어셈블리어 문법은 intel 문법을 사용하도록 설정한다.

 

disas main

 

main() 함수를 디스어셈블 해보면 위와 같은데, puts() 함수를 두 번 호출하는 것 이외에는 별 다른 게 없다.

 

b * 0x80483d8

b * 0x80483e5

 

puts() 함수를 호출하는 두 곳에 BP를 설정한다.

 

puts() 함수를 호출하는 두 곳에 BP를 설정하는 이유는

 

첫 번째 puts() 함수를 호출하는 부분에서는 최초로 puts() 함수를 호출하는 것이므로 puts() 함수의 GOT에 puts() 함수의 실제 주소가 들어가 있지 않을 것이고

 

두 번째 puts() 함수를 호출하는 부분에서는 puts() 함수의 GOT에 puts() 함수의 실제 주소가 들어가 있을 것인데, 이를 확인해보기 위함이다.

 

r

x/i $eip

 

plt_got 프로그램을 실행시키고, 현재 EIP가 가리키는 주소의 명령을 보면 puts() 함수를 호출하는 첫 번째 부분이다.

 

PLT

 

puts() 함수를 호출하기 위해 콜하는 주소 0x80482e8을 보면 위와 같이 plt 부분이 나오고, 0x80482e8 주소부터 3개의 명령을 출력해보면 위와 같이 명령어들이 보이는데

 

plt에서는 got를 참조하기 때문에 0x80482e8 주소에서 점프하는 주소 0x804948c는 puts() 함수의 실제 주소가 들어있는 GOT 주소이다.

(왜 3개의 명령을 출력했는지는 이후에 자연스럽게 이해가 될 것이다.)

 

GOT

 

0x804948c 주소를 보면 정말 puts() 함수의 실제 주소가 저장될 puts() 함수의 GOT이고

 

아직 puts() 함수를 최초로 호출하는 단계에서 runtime resolve 과정을 거치지 않았기 때문에 puts() 함수의 GOT에는 puts() 함수의 실제 주소가 아닌 이전에 PLT 부분에서 봤던 PLT+6의 주소인 0x80482ee가 있다.

 

여기까지 정리해보면, puts() 함수를 최초로 호출 -> plt를 참조 -> plt+0에서는 got를 참조 -> puts 함수에 해당하는 got를 가보니 다시 plt+6 부분으로 점프하도록 되어 있다.

 

즉, 함수를 최초로 호출했을 때는 plt+6에 있는 명령을 수행하게 되어있다.

 


Dynamic Linking 시작(PLT+6(reloc_offset), PLT+11)

 

plt+6에서는 0x8을 넣고, plt+11에서 0x80482c8 주소로 점프한다.

 

여기서부터 동적 링킹(Dynamic Linking) 과정이 시작되는데, 0x8은 reloc_offset이다.

(reloc_offset 값은 이후에 puts 문자열의 주소를 찾을 때 사용되므로 기억해둔다.)


link_map 주소를 스택에 넣고 _dl_runtime_resolve 함수로 점프

 

plt+11에서 점프하는 주소 0x80482c8를 보면 0x8049480 주소에 있는 값 0x40013ed0을 스택에 넣고, 0x8049484 주소로 점프한다.

 

여기서 스택에 넣는 0x40013ed0은 link_map 구조체 포인터이다.

 

그리고 점프하는 곳(0x8049484)은 _dl_runtime_resolve 함수이다.

 

낮은 주소
link_map : 0x40013ed0
reloc_offset : 0x8
높은 주소

 

지금까지의 스택을 보면 위와 같다.

 

참고)

link_map 구조체는 ld loader가 참조하는 링크 지도로, 라이브러리의 정보를 담고 있는데
이 구조체를 통해 여러가지 테이블의 주소를 구할 수 있다.

 

_dl_runtime_resolve 함수 디스어셈블

 

_dl_runtime_resolve 함수를 디스어셈블 해보면 위와 같다.

 

_dl_runtime_resolve 함수 내부에서 fixup 함수를 호출하는 것을 확인할 수 있다.

 

fixup함수 호출 직전 스택 확인

낮은 주소
edx : 0x40109098
ecx : 0x080483d0
eax : 0x401073f8
link_map 구조체 포인터 : 0x40013ed0
reloc_offset : 0x08
높은 주소

 

fixup 함수를 호출하는 0x4000a96b 주소에 BP를 걸고 실행하면 fixup 함수를 호출하기 직전에 멈추게 되는데, 이때 edx와 eax 레지스터에 저장하는 값을 보면 0x8과 0x40013ed0으로 reloc_offset 값과 link_map 구조체 포인터이다.

 

참고)

reloc_offset과 link_map 값을 _dl_runtime_resolve 함수를 호출하기 전에 스택에 넣었으므로
_dl_runtime_resolve 함수의 RET 부분과 SFP 부분이 쌓이고 나서 eax, ecx, edx 값이 쌓여야 하는 게 아닌가 싶을 수 있는데, _dl_runtime_resolve 함수를 실행할 때 call 명령으로 호출한 것이 아니라 jmp 명령으로 실행하는 것임을 주의해야 한다.

call 명령으로 호출했다면 RET 부분을 쌓고 SFP 부분을 쌓지만 jmp 명령을 사용했으므로 RET 부분과 SFP 부분은 없다.

fixup 함수 내부로 진입

(gdb) disas 0x4000a740
Dump of assembler code for function fixup:
0x4000a740 <fixup>:	push   %ebp
0x4000a741 <fixup+1>:	mov    %ebp,%esp
0x4000a743 <fixup+3>:	sub    %esp,20
0x4000a746 <fixup+6>:	push   %edi
0x4000a747 <fixup+7>:	push   %esi
0x4000a748 <fixup+8>:	push   %ebx
0x4000a749 <fixup+9>:	call   0x4000a74e <fixup+14>
0x4000a74e <fixup+14>:	pop    %ebx
0x4000a74f <fixup+15>:	add    %ebx,0x911a
0x4000a755 <fixup+21>:	mov    DWORD PTR [%ebp-20],%eax
0x4000a758 <fixup+24>:	mov    %ecx,%edx
0x4000a75a <fixup+26>:	mov    %eax,DWORD PTR [%eax+48]
0x4000a75d <fixup+29>:	mov    %esi,DWORD PTR [%ebp-20]
0x4000a760 <fixup+32>:	mov    %edx,DWORD PTR [%esi+44]
0x4000a763 <fixup+35>:	mov    %edx,DWORD PTR [%edx+4]
0x4000a766 <fixup+38>:	mov    DWORD PTR [%ebp-8],%edx
0x4000a769 <fixup+41>:	mov    %edx,DWORD PTR [%esi+116]
0x4000a76c <fixup+44>:	add    %ecx,DWORD PTR [%edx+4]
0x4000a76f <fixup+47>:	mov    %edi,DWORD PTR [%ecx+4]
0x4000a772 <fixup+50>:	shr    %edi,0x8
0x4000a775 <fixup+53>:	shl    %edi,0x4
0x4000a778 <fixup+56>:	mov    %eax,DWORD PTR [%eax+4]
---Type <return> to continue, or q <return> to quit---
0x4000a77b <fixup+59>:	add    %edi,%eax
0x4000a77d <fixup+61>:	mov    DWORD PTR [%ebp-16],%edi
0x4000a780 <fixup+64>:	mov    DWORD PTR [%ebp-4],%edi
0x4000a783 <fixup+67>:	mov    %esi,DWORD PTR [%ecx]
0x4000a785 <fixup+69>:	mov    %edi,DWORD PTR [%ebp-20]
0x4000a788 <fixup+72>:	mov    %edi,DWORD PTR [%edi]
0x4000a78a <fixup+74>:	add    %esi,%edi
0x4000a78c <fixup+76>:	mov    DWORD PTR [%ebp-12],%esi
0x4000a78f <fixup+79>:	add    %esp,-4
0x4000a792 <fixup+82>:	mov    %edx,DWORD PTR [%ecx+4]
0x4000a795 <fixup+85>:	cmp    %dl,0x7
0x4000a798 <fixup+88>:	je     0x4000a7b6 <fixup+118>
0x4000a79a <fixup+90>:	lea    %eax,[%ebx-6274]
0x4000a7a0 <fixup+96>:	push   %eax
0x4000a7a1 <fixup+97>:	push   67
0x4000a7a3 <fixup+99>:	lea    %eax,[%ebx-6268]
0x4000a7a9 <fixup+105>:	push   %eax
0x4000a7aa <fixup+106>:	lea    %eax,[%ebx-6255]
0x4000a7b0 <fixup+112>:	push   %eax
0x4000a7b1 <fixup+113>:	call   0x4000d9dc <__assert_fail>
0x4000a7b6 <fixup+118>:	mov    %esi,DWORD PTR [%ebp-20]
0x4000a7b9 <fixup+121>:	mov    %eax,DWORD PTR [%esi+400]
0x4000a7bf <fixup+127>:	test   %eax,%eax
---Type <return> to continue, or q <return> to quit---
0x4000a7c1 <fixup+129>:	je     0x4000a800 <fixup+192>
0x4000a7c3 <fixup+131>:	mov    %eax,DWORD PTR [%eax+4]
0x4000a7c6 <fixup+134>:	shr    %edx,0x8
0x4000a7c9 <fixup+137>:	movzx  %eax,WORD PTR [%eax+%edx]
0x4000a7cd <fixup+141>:	shl    %eax,0x4
0x4000a7d0 <fixup+144>:	add    %eax,DWORD PTR [%esi+488]
0x4000a7d6 <fixup+150>:	cmp    DWORD PTR [%eax+4],0
0x4000a7da <fixup+154>:	je     0x4000a800 <fixup+192>
0x4000a7dc <fixup+156>:	lea    %edx,[%ebp-4]
0x4000a7df <fixup+159>:	mov    %edi,DWORD PTR [%ebp-16]
0x4000a7e2 <fixup+162>:	mov    %edi,DWORD PTR [%edi]
0x4000a7e4 <fixup+164>:	add    DWORD PTR [%ebp-8],%edi
0x4000a7e7 <fixup+167>:	push   7
0x4000a7e9 <fixup+169>:	push   %eax
0x4000a7ea <fixup+170>:	push   DWORD PTR [%esi+4]
0x4000a7ed <fixup+173>:	mov    %ecx,%esi
0x4000a7ef <fixup+175>:	add    %ecx,0x204
0x4000a7f5 <fixup+181>:	mov    %eax,DWORD PTR [%ebp-8]
0x4000a7f8 <fixup+184>:	call   0x40001840
0x4000a7fd <fixup+189>:	jmp    0x4000a823 <fixup+227>
0x4000a7ff <fixup+191>:	nop
0x4000a800 <fixup+192>:	lea    %edx,[%ebp-4]
0x4000a803 <fixup+195>:	mov    %eax,DWORD PTR [%ebp-4]
---Type <return> to continue, or q <return> to quit---
0x4000a806 <fixup+198>:	mov    %eax,DWORD PTR [%eax]
0x4000a808 <fixup+200>:	add    DWORD PTR [%ebp-8],%eax
0x4000a80b <fixup+203>:	push   7
0x4000a80d <fixup+205>:	mov    %esi,DWORD PTR [%ebp-20]
0x4000a810 <fixup+208>:	push   DWORD PTR [%esi+4]
0x4000a813 <fixup+211>:	mov    %ecx,%esi
0x4000a815 <fixup+213>:	add    %ecx,0x204
0x4000a81b <fixup+219>:	mov    %eax,DWORD PTR [%ebp-8]
0x4000a81e <fixup+222>:	call   0x40001850
0x4000a823 <fixup+227>:	mov    %edx,DWORD PTR [%ebp-4]
0x4000a826 <fixup+230>:	test   %edx,%edx
0x4000a828 <fixup+232>:	je     0x4000a830 <fixup+240>
0x4000a82a <fixup+234>:	add    %eax,DWORD PTR [%edx+4]
0x4000a82d <fixup+237>:	jmp    0x4000a832 <fixup+242>
0x4000a82f <fixup+239>:	nop
0x4000a830 <fixup+240>:	xor    %eax,%eax
0x4000a832 <fixup+242>:	mov    %edi,DWORD PTR [%ebp-12]
0x4000a835 <fixup+245>:	mov    DWORD PTR [%edi],%eax
0x4000a837 <fixup+247>:	lea    %esp,[%ebp-32]
0x4000a83a <fixup+250>:	pop    %ebx
0x4000a83b <fixup+251>:	pop    %esi
0x4000a83c <fixup+252>:	pop    %edi
0x4000a83d <fixup+253>:	leave
---Type <return> to continue, or q <return> to quit---
0x4000a83e <fixup+254>:	ret
End of assembler dump.

 

 

edx에는 reloc_offset(0x08), eax에는 link_map(0x40013ed0)을 담은 채로

 

fixup 함수를 분석해보기 위해 fixup 함수 내부로 들어가고, fixup() 함수를 디스어셈블 해보면 위와 같다.

 

fixup 함수 내에서는 link_map 구조체를 이용해 문자열 테이블인 STRTAB의 주소를 알아오고, STRTAB의 주소와 호출하는 함수의 이름의 offset 값을 인자로 하여 _dl_lookup_symbol 함수를 호출한다.

 

참고)

STRTAB에는 현재 프로그램에서 사용되는 함수들의 이름이 있다.


link_map을 이용해 STRTAB 테이블 주소 구하기

 

먼저 fixup+35 라인(0x4000a763)에 BP를 걸고 실행한 후 edx+4 주소에 있는 값을 보면 0x080481c8인데, 이는 STRTAB의 주소이다.

(사용하는 라이브러리에 따라서 STRTAB 주소를 레지스터에 담는 라인의 위치는 달라질 수 있다.)

 

참고)

link_map 구조체는 ld loader가 참조하는 링크 지도로, 라이브러리의 정보를 담고 있는데
이 구조체를 통해 여러가지 테이블의 주소를 구할 수 있다.

 

shell readelf -S plt_got | grep STRTAB | awk '{print $1, $2, $3, $4, $5}'

 

0x080481c8이 STRTAB의 주소인지 확인해보기 위해 위의 명령을 이용해보면 STRTAB 타입으로 테이블이 총 4개 있는데, 그 중 .dynstr 테이블을 보면 0x80481c8인 것을 확인할 수 있다.

 

참고) STRTAB 관련 참고 https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/strtab_8cxx.html#a75d1e7e61739ba0d0211b3293e5ba9d3

 

link_map을 이용해 STRTAB 테이블 주소 구하는 과정

 

fixup 함수 내에서 0x4000a763 주소의 명령을 수행하면 edx에 STRTAB의 주소가 담기게 될 것이라는 것을 확인했다.

(아직 0x4000a763 주소를 실행하진 않았지만, x 명령으로 조사하는 것으로 확인했다.)

 

즉, 위에서 언급했듯이 link_map 구조체를 이용해 문자열 테이블인 STRTAB의 주소를 알아온다는 결과는 확인했는데, 그 과정을 자세히 알아본다.

 

위의 사진을 참고하면 현재 eip가 0x4000a763 주소를 가리킬 때의 스택과 위의 첫 번째 사진 속 빨간 색 박스 부분의 명령들이 중요하다.

(참고로 0x4000a758 주소에서 edx 레지스터에 있는 값을 ecx 레지스터로 옮기는데, 이는 reloc_offset 값을 ecx 레지스터에 담는 것이다.)

 

먼저, 스택에서 주황색 박스인 부분은 이전에 fixup 함수를 호출하기 전에 구성된 스택이다.

 

그리고  파란색 박스는 fixup 함수를 호출하면서 스택에 쌓인 RET 부분이고, 분홍색 박스는 SFP 부분이다.

 

이어서 하얀색 박스와 빨간색 박스를 합쳐 20byte는 fixup 함수 내에서 지역 변수를 위해 확보하는 스택 공간이고,

 

나머지 보라색 박스는 오른쪽에서부터 차례대로 edi, esi, ebx의 값이다.

 

이렇게 현재 스택의 구성 설명은 끝났다.

 

 

그리고 위의 빨간색 박스 부분의 명령들을 보면 ebp-20의 주소에 eax의 값을 넣는 것을 볼 수 있는데, eax에는 fixup 함수를 호출하기 전에 0x40013ed0(link_map) 주소가 들어있었으므로 위의 스택 사진에서 빨간색 박스 부분에 해당하는 ebp-20(0xbffffc80) 주소에 0x40013ed0(link_map) 주소가 들어있는 것을 확인할 수 있다.

 

이어서 ebp-20에 있는 값 0x40013ed0(link_map)을 esi 레지스터에 옮기므로 esi 레지스터에는 link_map의 주소가 들어있는 것이다.

 

 

이 상태에서 esi + 44(0x2c) 주소는 0x40013ed0(link_map) + 0x2c를 연산한 주소 0x40013efc이므로, 0x40013efc 주소에 있는 값을 edx에 옮긴다는 것인데, 위와 같이 0x40013efc 주소에는 0x80494bc 라는 값이 있고, 0x80494bc 값이 edx에 담긴다.

 

 

그리고 나서 edx + 4의 주소에 있는 값을 edx에 옮기는데, 이는 0x80494bc + 4를 연산한 주소 0x80494c0이고, 0x80494c0 주소에 있는 값은 위와 같이 0x80481c8(STRTAB) 주소이다.

 

 

마지막으로, 구한 0x80481c8(STRTAB) 주소를 ebp-8(0xbffffc8c)에 옮긴다.

(ebp-8(0xbffffc8c)에 0x80481c8(STRTAB) 주소가 담기는게 맞는지 확인해보기 위해 si 명령으로 0x4000a766 <fixup+38> 주소의 명령을 수행하고, 스택을 보면 맞게 잘 들어갔다는것을 확인할 수 있다.)

 

정리해보면, fixup 함수를 호출하기 전 eax 레지스터를 통해 넘긴 link_map(0x40013ed0)의 주소와 0x2c와 0x4라는 offset을 이용해 STRTAB(0x80481c8)의 주소를 구하고, ebp-8(0xbffffc8c)에 저장해둔다.


link_map을 이용해 재배치 정보를 담고 있는 재배치 테이블의 시작점인 JMPREL의 주소를 얻어오기

참고)

https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/structElf32__Rel.html#addcf5ef67ababeb4940889e912c11eff

JMPREL은 재배치 정보를 담고 있는 테이블로, Elf32_Rel이라는 형식의 구조체들로 이루어져 있다.

Elf32_Rel 구조체는 8byte로 되어 있고, 처음 4byte는 GOT의 주소이며
다음 4byte에서 처음 1byte는 재배치 타입이고, 나머지 3byte는 .DYNSYM 테이블에서의 index를 나타낸다.

JMPREL 테이블에 있는 Elf32_Rel 형식의 구조체들 중 호출하는 함수에 해당하는 Elf32_Rel 형식의 구조체를 찾을려면
JMPREL의 시작 주소에 reloc_offset 값을 더하면 된다.

호출하는 함수에 해당하는 Elf32_Rel 형식의 구조체가 맞는지 확인하는 방법은
Elf32_Rel 형식의 구조체에서 맨 처음 4byte 값이 GOT의 주소이므로 해당 4byte 값이
호출하는 함수에 해당하는 GOT 주소인지를 대조해봄으로써 확인할 수 있다.

 

 

현재 esi 레지스터에는 0x40013ed0(link_map) 주소가 들어있고, 0x40013ed0 + 116(0x74)은 0x40013f44인데, 0x40013f44 주소 안에 있는 값은 0x80494fc이므로 edx에는 0x80494fc가 담기게 된다.

 

 

이어서 edx(0x80494fc) + 4는 0x8049500인데, 0x8049500 주소에 있는 값은 0x8048278이고, 0x8048278은 JMPREL주소이다.

 

JMPREL의 시작 주소인 0x8048278 주소에서 8byte씩 출력해보면 처음 4byte는 GOT 주소이고, 그 다음 4byte의 처음 1byte는 0x07로 재배치 타입을 의미하며, 나머지 3byte는 0x000001로 .DYNSYM 테이블에서의 index를 의미한다.

 

 

그리고 이전에 언급했듯이 fixup 함수를 호출하기 전 edx에 담아뒀던 reloc_offset(0x8) 값을 ecx 레지스터에 옮겼었으므로 ecx 레지스터에는 reloc_offset(0x8) 값이 담겨있다.

 

그래서 edx+4 주소의 값(0x8048278) + ecx(0x8)는 JMPREL + reloc_offset 연산이고, 이는 위에서 말했듯이 puts() 함수에 해당하는 Elf32_Rel 구조체의 주소로 0x8048280이다.

 

그리고 이 0x8048280 주소를 ecx 레지스터에 담는다.

 

0x8048280 주소의 Elf32_Rel 형식의 구조체가 puts() 함수에 해당하는 구조체가 맞는지는 Elf32_Rel 구조체 내용에서 GOT 주소를 보면 0x804948c로 이는 처음에 확인했던 puts() 함수를 호출할 때 PLT에서 참조하는 GOT 주소랑 일치하다는 것으로 판단할 수 있다.

(.dynsym 테이블에서 puts() 함수의 index는 0x02이다.)

 

shell readelf -S plt_got | grep .dynsym

 

.dynsym 테이블의 주소는 위의 명령으로 알아낼 수 있는데, 0x8048158이다.

 

참고)

https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/structElf32__Sym.html

.dynsym 테이블은 동적 심볼 테이블로, import 및 export하는 모든 심볼의 정보가 담겨있고, Elf32_Sym 구조체로 이루어져 있다.

 

JMPREL 테이블에서 puts에 해당하는 Elf32_Rel 구조체 형식의 값에 있는 .dynsym 테이블에서의 index 값을 이용해 수동으로 직접 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_Sym 형식의 구조체 값 찾아보기

 

 

.dynsym 테이블에서 위에서 구한 index를 이용해 puts 함수에 해당하는 Elf32_Sym 형식의 구조체를 찾았다면, 구조체의 내용 중 맨 처음 4byte와 14번째 1byte의 값이 중요하다.

 

처음 4byte 값은 STRTAB의 주소로부터의 offset 값이고, 14번째 1byte의 값은 호출하는 함수의 .dynsym 테이블에서의 index(2) 값과 and 연산을 해서 0이냐 아니냐로 이미 로딩이 된 함수인지 아닌지를 판단하는데, 만약 결과값이 0이라면 이전에 로딩된 적이 없다는 것을 의미하고, 0이 아니라면 이전에 이미 로딩된 적이 있다고 판단하고 puts 함수를 바로 호출해버린다.

 

위의 사진을 보면 STRTAB의 주소로부터의 offset 값은 0x1a이고, 14번째 1byte 값은 0이다.

 

14번째 1byte 값 0x00과 0x02를 and 연산하면 0이므로 이전에 로딩된 적이 없다는 것을 의미하고

 

 

STRTAB(0x080481c8)의 주소에서 0x1a를 더하니 STRTAB에서 "puts" 문자열의 주소이다.

 

위에서는 수동으로 직접 .dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체의 주소를 찾았지만, 코드 상에서 해당 구조체의 주소를 찾기 위해 Elf32_Rel 구조체 형식의 내용 중 .dynsym에서의 index와 재배치 정보가 들어있는 곳의 주소를 edi 레지스터에 저장

 

edi 레지스터에 0x8048284(JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값의 주소에 4를 더한 주소)를 저장한다.

 

 

정리해보면, 0x40000a76c 주소의 add    %ecx,DWORD PTR [%edx+4] 명령을 수행하면 edx+4의 주소 안에 있는 값(0x8048278)은 Elf32_Rel 구조체 형식의 값으로 구성된 재배치 테이블인 JMPREL의 시작 주소이고, ecx에는 reloc_offset 값(0x8)이 있으니 이 둘을 더하면 ecx에는 JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값의 주소가 담기게 된다.

 

그리고 이 Elf32_Rel 구조체의 내용 중 맨 처음 4byte 값이 GOT 주소이므로 이 값을 이용해 puts 함수에 해당하는게 맞는지 확인할 수 있고, 다음 4byte 값 중 .dynsym 테이블에서의 index인 값을 이용해 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_sym 구조체의 주소를 찾을 수 있다.

 

이어서 이 Elf32_sym 구조체의 내용 중 맨 처음 4byte 값은 STRTAB에서의 offset이므로 STRTAB의 시작 주소에 이 offset 값을 더하면 STRTAB 테이블에서 호출하는 함수의 이름에 해당하는 주소를 얻을 수 있다.


STRTAB에서 puts 문자열의 주소를 구하는 부분

 

위에서는 fixup 함수 내부에서 link_map(ld loader가 참조하는 링크 지도로, 라이브러리의 정보를 담고 있는데, 이 구조체를 통해 여러 테이블의 주소를 구할 수 있다.)을 이용해 STRTAB 테이블(현재 프로그램에서 사용되는 함수들의 이름이 있다.)의 주소를 구하고

link_map을 이용해 JMPREL(재배치 정보를 담고 있는 재배치 테이블)의 시작 주소를 구한 후 JMPREL의 시작 주소에 reloc_offset 값을 더해 JMPREL 테이블에서 puts 함수에 해당하는 Elf32_Rel 구조체의 주소를 구하는데, 이 Elf32_Rel에는 puts 함수의 GOT 주소와 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_sym 구조체의 주소를 알아낼 수 있는 index가 담겨져있다.
(해당 Elf32_Rel 구조체가 puts의 함수에 해당하는 것이 맞는지는 GOT 주소 값을 보면 된다.)

그리고 .dynsym 테이블(동적 심볼 테이블, import 및 export하는 모든 심볼의 정보가 있다.)의 Elf32_sym 구조체에는 STRTAB에서 puts 문자열의 주소를 알아낼 수 있는 offset 값이 있고, 최초 호출인지 재호출인지 and 연산하여 알 수 있는 값이 들어있는 것을 확인했다.

 

이제 STRTAB에서 puts 문자열의 주소를 구하기 위해 필요한 것들을 알아냈기 때문에 실제로 STRTAB에서 puts 문자열의 주소를 구하는 과정을 수행하는 명령 부분을 살펴본다.

 

위의 코드 부분이 STRTAB에서 puts 문자열의 주소를 eax 레지스터에 넣고 _dl_lookup_versioned_symbol 함수를 호출하는 부분이다.

 

그 과정을 하나하나 따라가본다.

 

JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값에서 .dynsym에서의 index 값을 가져와 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값에서 STRTAB 에서의 puts 문자열 offset 값을 구하기

 

가장 먼저, ecx+4(0x8048284) 주소에 있는 값 0x207을 edi에 넣는데, 0x8048284는 이전에 나왔듯이 JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체의 내용 중 .dynsym 테이블에서의 index와 재배치 타입 정보가 들어있는 4byte의 시작 주소이다.

(0x8048280 주소에는 puts 함수의 GOT 주소가 들어있다.)

 

 

0x207 값에 shr 0x8 연산과 shl 0x4 연산을 거치면 edi 레지스터에는 0x20 값이 저장된다.

 

 

이어서 eax+4(0x80494c8) 주소에 있는 값 0x8048158을 eax 레지스터에 담는데, 0x8048158 주소는 .dynsym 테이블의 주소이다.

 

 

.dynsym 테이블의 주소(0x8048158)에 이전에 연산한 0x20 값을 더하면 0x8048178 주소가 되는데

 

이 주소는 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_Sym 구조체의 주소로, STRTAB 테이블에서 "puts" 문자열의 offset 값(0x1a)이 들어있다.

 

그러므로 edi 레지스터에는 STRTAB에서의 puts 문자열 offset 값이 들어있는 주소(0x8048178)가 담긴다.

 

 

STRTAB 테이블에서의 "puts" 문자열 offset 값이 들어있는 주소 0x8048178을 ebp-16(0xbffffc84) 주소와 ebp-4(0xbffffc90) 주소에 넣는다.

 

결론적으로 0x8048178 주소(STRTAB 테이블에서의 "puts" 문자열 offset 값이 들어있는 주소)를 ebp-16(0xbffffc84) 주소와 ebp-4(0xbffffc90) 주소에 넣는다.

 

GOT 주소 알아와서 EBP-12 주소에 기록하고 재배치 타입을 비교해 조건 분기 및 esi 레지스터에 link_map 주소 넣기

 

STRTAB에서의 puts 문자열 주소를 구하는 부분을 살펴보기 전 ecx 레지스터에는 0x8048280 주소가 들어있었는데, 이 주소는 JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체의 주소이다.

 

0x8048280 주소에 있는 값 0x804948c를 esi 레지스터에 넣는데, 0x804948c는 puts 함수의 GOT 주소이다.

 

 

ebp-20(0xbffffc80) 주소에는 이전에 link_map(0x40013ed0)의 주소를 넣뒀는데, link_map(0x40013ed0)의 주소에 있는 값 0x0을 edi 레지스터로 옮기고

 

 

ebp-12(0xbffffc88) 주소에 puts 함수의 GOT 주소(0x804948c)를 넣는다.

 

 

그리고 ecx+4의 주소(0x8048284)는 JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체의 내용에서 .dynsym 테이블에서의 index와 재배치 타입 정보가 들어있는 곳으로, 0x207이라는 값이 들어있었으므로 edx에는 0x207이 담긴다.

 

 

이어서 edx 레지스터에 있는 값에서 8bit 값만 비교하여 재배치 타입이 뭔지 비교 후 0x4000a7b6 주소로 점프한 후 link_map의 주소(0x40013ed0)를 esi 레지스터에 저장한 다음 

 

 

esi+400 주소(0x40014060)에 있는 값 0x804952c 주소를 eax에 넣고, eax의 값이 0인지 검사하여 0이면 0x4000a800 주소로 점프하지만 eax의 값이 0이 아니므로 조건 분기를 수행하지 않는다.

 

 

조건 분기를 하지 않았으므로 바로 다음 줄의 명령을 수행하는데

 

eax+4 주소(0x8049530)의 값 0x8048240 주소를 eax로 옮기고, edx 레지스터의 값 0x207을 0x2로 만든 다음 eax 레지스터의 값(0x8048240)에 0x2를 더한 0x8048242 주소에 있는 2byte 값 0x0002을 eax로 옮긴다.

 

그러면 eax에는 0x02가 들어가게 되고, 0x4와 shl 연산을 해 0x20으로 만든 다음 esi+488 주소(0x400140b8)에 있는 값 0x40014660에 더하면, eax에는 0x40014680이 들어가게 된다.

 

실제로 STRTAB의 주소와 STRTAB에서 puts 문자열의 offset 값과 더해 STRTAB에서 puts 문자열의 주소를 구하는 부분

 

eax+4 주소(0x40014684)에 있는 값 0x0d696910과 0을 비교하여 같으면 0x4000a800 주소로 점프하는데, 0x0d696910과 0은 다르므로 조건 분기를 수행하지 않는다.

(0x4000a800 주소는 이전에도 0x4000a7c1 주소에서 조건 분기를 할 때 점프하는 주소였다.)

 

ebp-4의 주소(0xbffffc90=0x8048178(.dynsym 테이블에서 puts 함수의 Elf32_Sym 구조체 형식의 값의 주소))를 edx 레지스터에 옮기고

 

ebp-16 주소(0xbffffc84)에 있는 값 0x8048178(.dynsym 테이블에서 puts 함수의 Elf32_Sym 구조체 형식의 값의 주소)을 edi 레지스터에 옮기는데, 0x8048178 주소에는 STRTAB에서 puts 문자열의 offset이 담겨있다.

이어서 0x8048178 주소에 있는 값 0x1a를 edi로 옮기므로 edi에는 0x1a가 들어가게 된다.

 

그리고 ebp-8 주소(0xbffffc8c) 주소에 있는 값 0x80481c8에 edi의 값 0x1a를 더하는데, 0x80481c8은 STRTAB의 주소이고, 0x1a는 STRTAB에서 puts 문자열의 offset 값이므로 STRTAB에서 puts 문자열의 주소(0x80481e2)를 구해 ebp-8 주소(0xbffffc8c)에 넣는 것이다.

 

이 부분이 실제로 STRTAB에서 puts 문자열의 주소(0x80481e2)를 구하는 곳이다.

 

 

이어서 스택에 0x7, 0x40014680, 0x40010c27을 넣고, esi 레지스터의 값 0x40013ed0(link_map)을 ecx 레지스터에 넣는다.

 

ecx 레지스터의 값을 0x400140d4로 만들고, ebp-8 주소(0xbffffc8c)에 있는 값 0x80481e2(STRTAB에서 puts 문자열의 주소)를 eax 레지스터에 옮기므로 eax 레지스터에는 STRTAB에서 puts 문자열의 주소(0x80481e2)가 들어있다.

 

_dl_lookup_symbol 호출 전 레지스터 및 스택 정리

 

여기까지 정리해보면 eax 레지스터에는 0x80481e2(STRTAB에서 puts 문자열의 주소)

edx 레지스터에는 0x8048178(.dynsym에서 puts 함수의 Elf32_Sym 주소) 주소가 있는 스택의 주소

esi 레지스터에는 0x40013ed0(link_map) 주소

edi 레지스터에는 0x1a(STRTAB 에서의 puts 문자열 offset)

 

그리고 ebp-16, ebp-4에는 0x8048178(.dynsym에서 puts 함수의 Elf32_Sym 주소), ebp-8에는 0x80481e2(STRTAB에서 puts 문자열의 주소)가 있다.

 

_dl_lookup_versioned_symbol 호출 및 디스어셈블

 

그 다음 _dl_lookup_versioned_symbol() 함수를 호출한다.

 

_dl_lookup_versioned_symbol() 함수는 너무 길기 때문에 라이브러리의 주소와 SYMTAB의 주소를 얻어오는 함수라고 이론적으로만 알고 넘어간다.

 

_dl_lookup_symbol() 함수가 없는지 확인

더보기

 

gdb 내에서  info func 명령으로 모든 함수를 출력하고

 

 

검색 기능으로 _dl_lookup 문자열을 찾아보니 위와 같이 나왔다.

 

_dl_lookup_versioned_symbol 함수 반환값 확인

 

_dl_lookup_versioned_symbol() 함수 끝 부분에 bp를 걸고 실행한 후 fixup 함수로 반환 직전 레지스터의 값들을 출력해보면 함수의 반환값이 담기는 eax 레지스터에 0x40018000 값이 있고, 이는 라이브러리의 주소이다.

 

_dl_lookup_versioned_symbol() 함수를 끝내고 fixup 함수로 복귀하여 _dl_lookup_versioned_symbol() 함수에서 알아낸 SYMTAB 주소 edx 레지스터에 저장

 

fixup 함수로 복귀하여 이어서 진행하면 ebp-4 주소(0xbffffc90)에 있는 값 0x4001c7d0을 edx에 옮기는데, 이 값은 SYMTAB의 주소이고, SYMTAB 주소(0x4001c7d0)로부터 4byte 뒤에 있는 값 0x53570은 라이브러리에서 puts 함수의 offset 값이다.

 

실제로 puts 함수의 offset 값이 맞는지 확인해본다.

 

참고)

https://www.mcs.anl.gov/OpenAD/OpenADFortTkExtendedDox/classwhirl2xaif_1_1xlate__ST__TAB.html#a61fad846a94093d7830f7510ef1c86f9

 

SYMTAB 주소를 구했는지 확인하고 라이브러리의 주소와 SYMTAB에서 찾아낸 puts 함수의 offset 값을 더해 실제 puts 함수의 주소 알아내 eax 레지스터에 저장

 

SYMTAB의 주소를 구해왔는지 확인하기 위해 edx 레지스터의 값이 0인지 확인해서 0이면 0x4000a830 주소로 점프하지만, SYMTAB의 주소(0x4001c7d0)를 구해왔기 때문에 다음 줄의 명령이 수행된다.

 

edx+4 주소(0x4001c7d4)에 있는 값 0x53570(라이브러리에서 puts 함수의 offset)을 라이브러리의 주소(0x40018000)에 더하면 실제 puts 함수의 주소(0x4006b570)가 나온다.

 

fixup 함수 내에서 구해 저장해둔 puts 함수의 GOT 주소를 가져와 GOT 주소에 puts 함수의 실제 주소를 넣기

 

0x4000a832 주소로 점프한 후 fixup 함수 내에서 구해 스택에 저장해둔 puts 함수의 GOT 주소를 edi 레지스터에 담고

 

eax 레지스터의 값 0x4006b570(puts 함수의 실제 주소)을 GOT에 덮어쓴다.

 

이로써 put함수의 GOT 주소에는 puts 함수의 실제 주소가 들어가게 된다.

 

fixup 함수 종료 후 _dl_runtime_resolve 함수 종료

 

나머지 명령들을 실행하면서 fixup 함수가 끝나고 _dl_runtime_resolve 함수로 반환한 후 _dl_runtime_resolve 함수도 끝난다.

 

과정에서는 생략됐지만 _dl_runtime_resolve 함수로 복귀한 뒤 실제 puts 함수의 주소로 넘어간다.

 

2번째 puts 함수 호출 부분에서 GOT 주소의 값 확인

 

위에서 첫 번째 puts 함수 호출에서 runtime resolve 과정을 통해 GOT 주소에 puts 함수의 실제 주소를 기록해뒀기 때문에 두 번째 puts함수 호출부터는 GOT 주소에 PLT+6의 주소가 아닌 puts 함수의 실제 주소가 있다.


우분투 32bit 환경에서 puts() 함수를 두 번 호출하는 프로그램을 만들어서 gdb로 실습

 

위의 실습에서는 hackerschool의 lob라는 오래된 환경에서 진행했기 때문에
이번에는 다른 환경인 ubuntu 16.04.07 32bit에서 진행해본다.
https://releases.ubuntu.com/16.04/)

ubuntu 16.04.07 32bit를 선택한 이유는 ubuntu 17 버전부터는 32bit 버전을 지원하지 않기 때문이다.

ubuntu 17 버전 이상부터 32bit 버전을 사용하려면 ubuntu mate와 같이 아직 32bit 버전을 지원하는
다른 비슷한 환경을 사용해야 한다.

ubuntu에서 진행하는 내용도 위의 내용을 이해했다면 금방 이해할 것이다.

 

lob 환경에서 분석한 내용을 다시 봐보면 아래와 같다.

 

STRTAB의 주소를 구해서 ebp-8에 넣고, JMPREL의 주소를 구한 후 곧바로 reloc_offset과 더해 puts에 해당하는 Elf32_Rel 구조체 형식의 값의 주소를 구해 ecx에 담은 다음 edi 레지스터에 ecx+4의 주소를 담아 연산을 거친 후 .dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체 형식의 값의 주소를 구한다.

이어서 Elf32_Sym 구조체 형식의 값의 내용 중 STRTAB에서의 puts 문자열 offset(0x1a)이 들어있는 주소를 ebp-16과 ebp-4에 넣어두고

Elf32_Sym 구조체 형식의 값의 내용에서 GOT 주소를 구해 ebp-12 주소에 넣어둔 다음 esi 레지스터에 link_map의주소를 넣어둔다.

그 다음 ebp-4에 있는 값(0x8048178)을 edx 레지스터에 넣고, edi 레지스터에 STRTAB에서의 puts 문자열 offset 값인 0x1a를 넣은 후 ebp-8 주소에 넣어둔 STRTAB 주소에 edi 레지스터의 값 0x1a를 더해 실제 STRTAB에서 puts 문자열의 주소(0x80481e2)를 구해 ebp-8과 eax 레지스터에 넣는다.

최종적으로 eax 레지스터에 STRTAB에서의 실제 puts 문자열의 주소(0x80481e2, edx에 .dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x8048178)가 들어있는 스택의 주소(0xbffffc90), esi에 link_map의 주소(0x40013ed0), edi에 STRTAB에서의 puts 문자열 offset(0x1a), ebp-16과 ebp-4에 .dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x8048178), ebp-8에 STRTAB에서 puts 문자열의 주소(0x80481e2)가 들어있는 상태에서 _dl_lookup_versioned_symbol()함수를 호출한다.

 

#include <stdio.h>
int main(void)
{
	puts("hello\n");
    puts("world\n");
    
    return 0;
}

 

우분투에서 위의 코드를 컴파일하여 실행 가능한 바이너리 파일로 만든다.

 

 

gdb로 바이너리 파일을 연 후 intel 문법으로 출력되게 설정하고 main 함수를 디스어셈블 해보면 위와 같이 나오는데 0x8048424 주소와 0x8048434 주소에서 puts() 함수를 호출하는 것을 볼 수 있다.

 

위 두 주소 모두 BP를 걸고 실행한다.

 

PLT & GOT 부분 및 Dynamic Linking 시작(PLT+6(reloc_offset), PLT+11)

 

puts() 함수를 호출하니 plt를 참조하고, plt에서는 got를 참조하는데, 현재 puts 함수의 GOT 주소에는 plt+6의 주소가 있다.

 

plt+6 주소로 점프한 후 reloc_offset 값으로 0x0을 스택에 넣고, 0x80482d0 주소로 점프한다.

 

link_map 주소를 스택에 넣고 _dl_runtime_resolve 함수로 점프 및 _dl_runtime_resolve 함수 디스어셈블

 

0x80482d0 주소에서는 link_map의 주소 0xb7fff918을 스택에 넣고 0xb7ff0000 주소로 점프하는데, 해당 주소는  _dl_runtime_resolve 함수의 주소이다.

 

_dl_runtime_resolve 함수 내에서는 eax, ecx, edx 레지스터의값을 스택에 넣고, 위에서 스택에 넣어둔 reloc_offset 값과 link_map 주소를 각각 edx, eax 레지스터에 넣는다.

 

fixup함수 호출 직전 스택 확인

낮은 주소
edx
ecx
eax
link_map
reloc_offset
높은 주소

 

실제로 해당 값들이 저장되어 있는지 확인해보기 위해 _dl_fixup 함수를 호출하기 직전인 0xb7ff000b 주소에 BP를 걸고 진행한 다음 edx 레지스터와 eax 레지스터의 값을 보면 reloc_offset 값과 link_map의 주소가 들어있다.

 

그리고 스택을 보면 빨간색 박스에 reloc_offset, 주황색 박스에 link_map의 주소, 노란색 박스에 오른쪽부터 eax, ecx, edx 값이 들어있다.

 

_dl_fixup 함수 내부로 진입 및 _dl_fixup 함수 디스어셈블

 

_dl_fixup 함수로 들어가서 _dl_fixup 함수를 디스어셈블 해보면 위와 같다.

 

현재 edx 레지스터와 eax 레지스터에는 reloc_offset 값과 link_map의 주소가 들어있다.

 

link_map을 이용해 STRTAB의 주소, JMPREL의 주소, GOT의 주소, .dynsym에서의 index 값과 재배치 타입의 값 구하는 부분

 

이전에 lob 환경에서는 STRTAB의 주소를 구하고, JMPREL의 주소를 구한 후 puts에 해당하는 Elf32_Rel 구조체 형식의 값의 주소를 구하고 나서 .dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체 형식의 값의 주소를 구하고 puts 문자열의 offset 값을 구한 다음 GOT 주소를 구하고 마지막으로 PUTS 문자열의 주소를 구하는 순서로 진행됐었다.

 

하지만 ubuntu 16.04.07 32bit 버전에서는 좀 다르게 값을 구하는 순서와 구하는 방법에 변화가 있다.

 

위의 사진을 보면 빨간색 화살표로 되어 있는 것과 초록색 화살표로 되어 있는 것 그리고 하얀색 화살표로 되어 있는 것과 노란색 화살표로 되어 있는게 있는데 각각 아래와 같다.

 

빨간색 화살표 : JMPREL의 주소를 구하는 부분

0xb7fe97e2: mov edi, eax
0xb7fe97f4: mov ecx, [edi+0x7c]
0xb7fe97fe: add edx, [ecx+0x4]

초록색 화살표 : STRTAB의 주소를 구하는 부분

0xb7fe97f7: mov eax, [eax+0x34]
0xb7fe9801: mov eax, [eax+0x4]
0xb7fe9807: mov [esp+0xc], eax

하얀색 화살표 : puts 함수의 GOT 주소를 구하는 부분

0xb7fe980b: mov ebp, edx
0xb7fe9810: mov eax, [ebp+0x0]

노란색 화살표 : .dynsym에서의 index 값과 재배치 타입 값을 구하는 부분

0xb7fe980d: mov edx, [edx+0x4]
0xb7fe9813: mov esi, edx

 

그리고 0xb7fe9804 주소에서 edi+0x38 주소에 있는 값을 ecx 레지스터에 넣는다.

 

link_map을 이용해 STRTAB의 주소, JMPREL의 주소, GOT의 주소, .dynsym에서의 index 값과 재배치 타입의 값 구하는 과정

 

실제로 명령 한 줄씩 따라가보며 값을 알아본다.

 

순서가 뒤죽박죽이니 잘 따라가야 한다.(기록하며 따라가는 걸 추천한다.)

 

 

먼저 eax 레지스터에 있는 link_map의 주소(0xb7fff918)를 edi 레지스터에 넣는다.

 

 

link_map의 주소(0xb7fff918)에 0x7c를 더한 주소(0xb7fff994)에 있는 값(0x8049f94)을 ecx 레지스터에 넣는다.

 

 

link_map의 주소(0xb7fff918)에 0x34를 더한 주소(0xb7fff94c)에 있는 값(0x8049f54)을 eax 레지스터에 넣는다.

 

link_map을 이용해 재배치 정보를 담고 있는 재배치 테이블의 시작점인 JMPREL의 주소를 구함과 동시에 reloc_offset 값을 이용해 puts에 해당하는 Elf32_Rel의 주소 구하는 부분

 

esi 레지스터에 들어있던 값(0xb7fff000)을 esp+0x8 주소(0xbffff000)에 넣고

 

ecx+0x4에 있던 값(0x8048298)과 edx에 있던 reloc_offset 값(0x0)을 더해 edx 레지스터에 넣는다.

 

0x8048298 주소는 JMPREL의 시작 주소이고, reloc_offset 값이 0x0이기 때문에 0x8048298 주소의 Elf32_Rel 구조체 형식의 값은 puts() 함수의 값이다.

 

이 부분에서 JMPREL의 주소를 구함과 동시에 JMPREL 주소에서 puts에 해당하는 Elf32_Rel 구조체 형식의 값의 주소를 구해 edx 레지스터에 넣는 것을 확인할 수 있다.

 

추가적으로 .dynsym에서의 index 값은 0x1인 것을 확인할 수 있으니 .dynsym에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값에 있는 STRTAB 주소로부터의 puts 문자열 offset 값을 알아와 수동으로 직접 확인해본다.

 

JMPREL 테이블에서 puts에 해당하는 Elf32_Rel 구조체 형식의 값에 있는 .dynsym 테이블에서의 index 값을 이용해 수동으로 직접 .dynsym 테이블에서 puts 함수에 해당하는 Elf32_Sym 형식의 구조체 값 찾아보기

 

 

.dynsym의 주소는 0x80481cc이고, 해당 주소로부터 index 1번째 값을 보면 위와 같이 나오는데, 맨 처음 4byte는 STRTAB 주소로부터의 puts 문자열 offset 값이고, 14번째 1byte는 최초 호출인지 재호출인지 판단하기 위한 값이다.

 

STRTAB의 주소는 바로 다음에 구할 것이지만 미리 언급하자면 0x804821c이다.

 

0x804821c 주소에 offset 0x1a를 더하니 정말 puts 문자열이 출력되는 것을 확인할 수 있다.

 

link_map을 이용해 STRTAB 테이블 주소 구하기

 

바로 이어서 eax+0x4 주소(0x8049f58)에 있는 값(0x804821c)을 eax로 옮긴다.

 

0x804821c는 STRTAB의 주소이다.

 

이 부분에서 STRTAB의 주소를 구해 eax 레지스터에 넣는 것을 확인할 수 있다.

 

 

edi+0x38 주소(0xb7fff950)에 있는 값을 ecx 레지스터에 옮김으로써 ecx 레지스터의 값은 0x8049f5c로 바뀐다.

 

STRTAB의 주소를 esp+0xc 주소에 저장

 

구했던 STRTAB의 주소를 esp+0xc(0xbffff004) 주소에 저장해둔다.

 

edx 레지스터에 .dynsym에서의 index 값과 재배치 타입 값을 저장

 

edx 레지스터에는 JMPREL에서 puts에 해당하는 Elf32_Rel 구조체 형식의 값의 주소(0x8048298)가 들어있는데, 해당 주소를 ebp 레지스터에 옮긴 후 

 

puts에 해당하는 Elf32_Rel 구조체 형식의 값에서 .dynsym에서의 index 값과 재배치 타입 값이 포함된 4byte 값을 edx 레지스터에 담는다.

 

그러면 최종적으로 edx 레지스터에는 0x107이 들어간다.

 

JMPREL 테이블에 Elf32_Rel 형식의 값의 내용 중 GOT 주소를 가져와 eax 레지스터에 저장

 

ebp+0x0 주소(0x8048298)에 들어있는 puts 함수의 GOT 주소를 eax 레지스터에 넣는다.

 

.dynsym에서의 index 값과 재배치 타입 값을 esi 레지스터에 저장

 

그리고 edx 레지스터에 담겨 있던 .dynsym에서의 index 값과 재배치 타입 값을 esi 레지스터에 옮김으로써 esi, edx 모두 0x107이 들어있다.

 

 

여기까지 정리해보면

eax 레지스터에는 GOT 주소(0x804a00c)

edx와 esi에는 .dynsym에서의 index값과 재배치 타입 값이 포함된 4byte 값(0x107)

ebp에는 JMPREL의 주소(0x8048298)

edi에는 link_map의 주소(0xb7fff918)

esp+0xc에는 STRTAB의 주소(0x804821c)가 들어있다.

 

JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값에서 .dynsym에서의 index 값을 가져와 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값이 있는 주소 구하기

 

.dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x80481dc)를 구해 ebx 레지스터에 담는다.

 

 

link_map의 주소(0xb7fff918)에 있는 값 0x0을 eax 레지스터에 들어있는 값(0x804a00c)에 더해 eax 레지스터에 넣는다.

 

이는 0x0을 더하는 것이므로 별 의미 없는 것 같다.

 

재배치 타입 검사 및 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소를 esp+0x1c 주소에 저장

 

edx 레지스터에는 .dynsym 테이블에서의 index와 재배치 타입값이 들어있는데, 이 값에서 재배치 타입 값이 0x7인지 확인한다.

 

그리고 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x80481d)를 esp+0x1c(0xbfffefb4) 주소에 저장해두고

 

재배치 타입 값 0x7과 0x7을 비교했을 때 같았으므로 0xb7fe996f 주소로 점프하지 않는다.

 

GOT 주소를 ebp로 옮기고, 조건 분기

 

eax 레지스터에 들어있는 GOT 주소(0x804a00c)를 ebp로 옮긴다.

 

ebx+0xd(0x80481e9) 주소에 있는 값 0x00과 0x03을 and 연산하면 당연히 0이 나온다.

 

test 명령은 단순히 and 연산을 통해 0인지 아닌지만 검사하는데, 현재 test 명령의 결과는 0이고, 이는 ZF 플래그를 1로 설정하는데, jne 명령은 ZF 플래그가 0일 때 점프하므로 점프하지 않고 다음 라인의 명령을 수행한다.

 

이어서 edx 레지스터의 값이 0인지 and 연산을 하는데 같은 값을 and 연산하므로 1이 나오고 이는 ZF 플래그를 0로 설정하는데, je 명령은 ZF가 1일 때 점프하므로 점프하지 않고 다음 라인의 명령을 수행한다.

 

다음 라인에서는 edx+0x4(0x8049fc8)에 있는 값 0x8048266을 edx 레지스터에 넣는다.

 

 

 

이어서 edx+esi*2(0x8048268)에 있는 값 0x2를 edx에 넣고 0x20으로 만든 다음 edi+0x170(0xb7fffa88)에 있는 값 0xb7fd5480과 더해 0xb7fd54a0으로 만든 다음 edx 레지스터에 넣고, 0xb7fd54a0에 0x4를 더한 값 0xb7fd54a4 주소에 있는 0xd696910을 ecx에 넣고

 

ecx 레지스터의 값이 0인지 test 명령으로 and 연산을 한 결과 같은 값을 and 연산했으므로 1이 되고, 이는 ZF 플래그를 0으로 만든다.

 

그리고 cmove라는 명령어가 나오는데 https://nightohl.tistory.com/entry/CMOV-assembly-CMOV-관련-모든-명령어-정리https://sdosj.tistory.com/entry/무분기-로직의 글을 참고하면 Cmp Move equal(==Zero)이다.

 

즉, test 명령을 수행할 때 결과가 0이면 ecx의 값을 edx로 옮기는 것인데, test 명령 수행 결과가 1이므로 cmove 명령은 수행되지 않는다.

 

그리고는 ecx에 담긴 값을 test 명령으로 and 연산을 하는데, ecx 레지스터에는 0x0이 담겨있으므로 and 연산 결과 0이고, 이는 ZF 플래그를 1로 설정하는데 jne 명령은 ZF 플래그가 0으로 설정될 때 점프이므로 점프하지 않고 다음 라인의 명령을 수행한다.

 

esp+0xc 주소에 저장해뒀던 STRTAB의 주소와 ebx 레지스터에 있던 0x80481dc 주소에 있는 puts 문자열 offset 값을 더해 STRTAB에서 실제 puts 문자열의 주소 구하기

 

STRTAB의 주소(0x804821c)를 eax 레지스터에 옮긴 후 ebx 레지스터에 담긴 puts 함수의 offset 값과 더해 실제 STRTAB에서 puts 문자열의 주소(0x8048236)를 eax 레지스터에 넣는다.

 

여기서 STRTAB에서 실제 puts 문자열의 주소를 구한다.

 

esp+0x1c 주소를 ecx 레지스터로 옮기기

 

위에서 esp+0x1c 주소(0xbfffefb4)에 .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x80481dc)를 저장해뒀었는데 0x80481dc 주소가 담긴 esp+0x1c 주소를 ecx 레지스터로 옮긴다.

 

_dl_lookup_symbol_x 함수 호출 전 스택에 값 넣고 _dl_lookup_symbol_x 함수 호출

 

그리고는 스택에 0x0, 0x1, 0x1, 0xb7fd54a0를 넣고

 

link_map의 주소(0xb7fff918)를 edx 레지스터에 넣고

 

link_map의 주소로부터 0x1cc offset만큼 떨어진 곳에 있는 값 0xb7fffad0을 스택에 넣는다.

 

그리고는 _dl_lookup_symbol_x 함수를 호출한다.

 

 

_dl_lookup_symbol_x 함수를 호출하기 직전에 레지스터와 스택을 보면 위와 같다.

 

eax = STRTAB에서 puts 문자열의 실제 주소(0x8048236)
ebx = .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x80481dc)
ecx = .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소가 들어있는 스택 주소(0xbfffefb4)
edx, edi = link_map의 주소(0xb7fff918)
ebp = GOT 주소(0x804a00c)
esi = 0x1

 

esp+0x2c(0xbfffefa4) = 0x804821c(STRTAB의 주소)

esp+0x3c(0xbfffefb4) = 0x80481dc(.dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소로 puts 문자열의 offset 값이 있다.)

 

_dl_lookup_symbol_x 함수 디스어셈블

 

_dl_lookup_symbol_x 함수는 lob 환경에서와 비교했을 때 함수 이름도 다를 뿐더러 라인 수도 더 많다.

 

_dl_lookup_symbol_x 함수는 lob 환경에서 호출했던 _dl_lookup_versioned_symbol 함수와는 이름이 다르지만 구해오는 값은 같다.

 

symtab의 주소와 lib의 주소를 얻어온다.

 

_dl_lookup_symbol_x 함수를 끝내고 fixup 함수로 복귀

 

_dl_lookup_symbol_x 함수를 끝내고 _dl_fixup 함수로 복귀한다.

 

_dl_lookup_symbol_x 함수의 반환값 확인 

 

_dl_lookup_symbol_x 함수의 반환값을 edi 레지스터로 옮기는데 반환값은 lib의 주소(0xb7e05000)가 들어있는 주소 0xb7fd51b0이다.

 

libc의 주소를 출력해보니 ASLR 기능으로 인해 실행 할 때마다 변한다.

 

esp+0x1c 주소에 구해둔 SYMTAB의 주소를 ebx 레지스터에 저장

 

그리고 나서 esp+0x1c(0xbfffefb4)에 있는 값 0xb7e0aa48을 ebx 레지스터에 옮긴 후 0인지 아닌지 검사 후 조건 분기를 하는데, 0xb7e0aa48 주소는 SYMTAB의 주소이다.

 

물론 같은 값을 and 연산하므로 결과는 1이 되고, 이로인해 ZF 플래그는 설정되지 않으며 je 명령은 ZF 플래그가 1이여야 점프하므로 je 명령은 수행되지 않는다.

 

이어서 edi 레지스터 역시 값이 잘 구해졌는지 검사하여 점프를 하는데, ZF 플래그가 설정되지 않으므로 je 명령은 수행되지 않는다.

 

libc 주소를 담고 있는 주소에서 libc 주소를 가져와 eax 레지스터에 저장

 

이어서 edi 레지스터에는 libc의 주소가 담긴 주소가 있었는데, 해당 주소에서 libc의 주소를 가져와 eax 레지스터에 넣는다.

 

 

ebx+0xc의 주소에 있는 값 0x22를 edx 레지스터에 넣고

 

SYMTAB에 있는 라이브러리에서의 puts 함수 offset 값을 가져와 라이브러리 주소와 더하여 puts 함수의 실제 주소를 구해 eax 레지스터에 넣기

 

ebx 레지스터에 있는 값은 SYMTAB의 주소이고, ebx+0x4 주소에는 0x5fcb0이라는 값이 있는데 이 값은 라이브러리에서 puts 함수의 offset 값이다.

 

라이브러리 주소(0xb7e05000)에 offset 값(0x5fcb0)을 더하면 0xb7e64cb0라는 puts 함수의 실제 주소가 구해진다.

 

그리고 si 명령으로 진행하다가 0xb7fe98e0 주소의 명령에 의해 puts 함수의 실제 주소 0xb7e64cb0이 GOT 주소(0x804a00c)에 덮어씌워진다.

 

두 번째 puts 함수 호출 부분

 

_dl_fixup 함수가 끝나고 _dl_runtime_resolve 함수로 복귀한뒤 _dl_runtime_resolve 함수도 종료되고 main 함수로 돌아와서 두 번째 puts 함수를 호출하는 부분을 보면 GOT 주소에 puts 함수의 실제 주소가 적혀있다.

 


결론

dynamic 링커는 호출할 함수 이름이 있는 메모리의 주소를 구하고 이를 이용해 공유 라이브러리에 있는 실제 함수의 주소를 구해온다.

 

과정 요약(ubuntu 환경 기준)

plt
got
plt(plt+6 : reloc_offset(0x0), plt+11 : jmp 0x80482d0[dynamic linking])
dynamic 링킹 시작(push link_map(0xb7fff918), jmp _dl_runtime_resolve(0xb7ff0000))

_dl_runtime_resolve
	edx = esp+0xc(0xbffff044) = reloc_offset(0x0)
	eax = esp+0x10(0xbffff048) = link_map(0xb7fff918)
	
    _dl_fixup
        strtab 주소 : 0x804821c
        jmprel 주소 : 0x8048298
        elf32_rel의 내용을 이용해 .dynsym에서의 elf32_sym의 내용 찾기
        elf32_sym의 내용을 이용해 STRTAB에서의 puts 문자열의 주소 찾기
            STRTAB에서의 puts 문자열 offset 값(0x1a) 찾기
            GOT 주소 알아오기
            실제로 STRTAB 주소와 puts offset을 더해 주소를 찾음
	
	eax = STRTAB에서 puts 문자열의 실제 주소(0x8048236)
	ebx = .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소(0x80481dc)
	ecx = .dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소가 들어있는 스택 주소(0xbfffefb4)
	edx, edi = link_map의 주소(0xb7fff918)
	ebp = GOT 주소(0x804a00c)
	esi = 0x1
	esp+0x2c(0xbfffefa4) = 0x804821c(STRTAB의 주소)
	esp+0x3c(0xbfffefb4) = 0x80481dc(.dynsym 테이블에서 puts에 해당하는 Elf32_Sym 구조체 형식의 값의 주소로 puts 문자열의 offset 값이 있다.)
        _dl_lookup_symbol_x 진입
        
    _dl_lookup_symbol_x
	eax = lib의 주소(0xb7e05000)가 담긴 주소(0xb7fd51b0)
        esp+0x1c = SYMTAB의 주소(0xb7e0aa48)
        
    _dl_fixup
        라이브러리의 주소와 SYMTAB에서 알아낸 puts 함수의 offset 값을 더해 실제 주소를 알아와 eax 레지스터에 저장
        GOT에 puts 함수의 실제 주소 넣기

	ebx = STMTAB의 주소(0xb7e0aa48)
	ebx+0x4 = SYMTAB에 있는 라이브러리에서의 puts 함수 offset 값(0x5fcb0)
	eax = puts 함수의 실제 주소
	
        
_dl_runtime_resolve
	puts함수의 실제 주소로 넘어가 puts 함수 실행


GOT 주소 : 0x804a00c
reloc_offset : 0x0
link_map : 0xb7fff918
_dl_runtime_resolve : 0xb7ff0000
strtab 주소 : 0x804821c
jmprel 주소 : 0x8048298
JMPREL에서 puts 함수에 해당하는 Elf32_Rel 구조체 형식의 값의 주소 : 0x8048298
.dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체 형식의 값이 있는 index : 0x1
재배치 타입 : 0x7
.dynsym에서 puts 함수에 해당하는 Elf32_Sym 구조체 형식의 값의 주소 : 0x80481dc
STRTAB에서의 puts 함수 offset : 0x1a
lib의 주소 : 0xb7e05000
라이브러리에서의 puts 함수 offset : 0x5fcb0
실제 puts 함수 주소 : 0xb7e64cb0

 

GOT Overwrite

_dl_lookup_symbol_x 함수의 인자로 넘어가는 함수 이름을 조작한다면 공유 라이브러리에 있는 다른 함수들도 호출할 수 있다.

 

함수 이름은 STRTAB 테이블에 있는데, STRTAB 테이블은 쓰기 권한이 없기 때문에 문자열을 직접 조작할 수 없지만, STRTAB을 가리키는 포인터를 쓰기 권한이 있는 다른 메모리 주소를 가리키도록 하고 그 곳에 원하는 함수 이름이 있다면 exploit이 가능하다.


https://nightohl.tistory.com/entry/CMOV-assembly-CMOV-관련-모든-명령어-정리 

https://sdosj.tistory.com/entry/무분기-로직

반응형

+ Recent posts