0. EAT
Export Address Table
이건 exe 가 아니라 dll이나 lib 구조 안에(라이브러리들)서 확인할 수 있는 구조라고 생각하면 되는 것 같다. EAT는 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 사용할 수 있도록 익스포트하는 함수의 시작 주소를 구할 수 있게 해준다.
음.. 그래서 dll은 자신의 함수를 다른 프로그램들에서 가져다 쓸 수 있게 해주는 라이브러리이기 때문에 첫 문장처럼 얘기를 써봤다
그래서 IAT는 한 exe가 여러 라이브러리에서 함수를 가져다 쓸 수 있기 때문에 구조체 배열의 형태를 가지지만(자기가 임포트 하는 라이브러리 개수만큼?) EAT는 한개의 구조체 배열로 이루어져 있다. 다음 제목에서 설명해야징
아무튼 큰 틀에서 보면 IAT와 큰 차이는 없는 것 같다. IAT는 _IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[1]
에서 참조할 수 있었는데, EAT는 _IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[0]
에서 참조할 수 있다.
위 사진은 IAT를 설명할 때 캡처해둔 바이너리인데(Easy Keygen.exe), 얘는 DataDirectory[0]
에 아무값이 안들어있다. 앞에서 말한것처럼 exe 파일은 자신의 함수를 익스포트 하는것이 아니기 때문에 그렇다.
1. IMAGE_EXPORT_DIRECTORY
1-0. 구조체 설명
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // library file name
DWORD Base; // ordinal base
DWORD NumberOfFunctions; // 실제 export 함수 개수
DWORD NumberOfNames; // 이름을 가지는 함수 개수
DWORD AddressOfFunctions; // export 함수 주소(RVA)
DWORD AddressOfNames; // 함수 이름 주소(RVA)
DWORD AddressOfNameOrdinals; // 순서(인덱스)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY
필드 | 뜻 |
---|---|
Name | 라이브러리명 |
Base | 베이스 주소 |
NumberOfFunctions | 실제 Export 함수 개수 |
NumberOfNames | 이름이 있는 함수의 개수 |
AddressOfFunctions | 함수 주소 배열 |
AddressOfNames | 함수 이름의 주소 배열 |
AddressOfNameOrdinals | AddressOfFunctions의 몇번째 인덱스인지 나타냄 |
음.. 여기 뭔가 설명을 더 써야할 것 같은데 IAT 글과 통일성을 위해 뒤에서 같이 설명해야지 ㅎ 암튼 중요한 필드는 이게 다이다.
1-1. GetProcAddress()
라이브러리에서 함수 주소를 가져오는 api는 GetProcAddress()
이다. 이 함수를 통해 함수 주소를 가져오는 과정이 EAT를 통해 이루어진다. 여기서는 글로 간단히 적어두고 다음 단계에서 찾는 방법을 자세히 적어야겠다
AddressOfNames
필드를 참조하여 함수 이름들을 저장하는 배열을 확인한다.
해당 배열에는 함수 이름에 해당하는 문자열의 주소가 저장되어 있다. 여기서 찾고자 하는 함수의 이름을 찾는다. 그리고 이 함수의 이름 주소를 기억하는 것이 아니라 해당 함수 이름이 들어있는 배열의 인덱스를 기억해야한다. (함수 주소가 배열 형태로 저장이 되어있음)
AddressOfNameOrdinals
배열에서 위에 찾은 인덱스를 참조한다.(인덱스 번호에 있는 값==ordinal
).AddressOfFunctions
필드를 참조하여 EAT(함수 주소 배열)로 간다.
EAT
에서 구해둔 ordinal
을 인덱스로 해서 함수의 실제 시작 주소를 찾을 수 있다.
줄줄 적으니 감이 잘 안잡히니 직접 해보는게 빠르다
2. 실습
kernel32.dll 바이너리이다. IAT 볼때랑 똑같이 DataDirectory를 먼저 찾아가야했고, 붉은색이 _IMAGE_NT_HEADERS.Signature
(4byte, \x50\x45\x00\x00)
초록색이 _IMAGE_NT_HEADERS.FileHeader
노란색과 초록색 사이의 빈곳이 _IMAGE_NT_HEADERS.OptionalHeader
이며, 노란색 부분이 _IMAGE_NT_HEADERS.OptionalHeader.DataDirectory
이다. 그중에 파랑색이 이번에 확인해볼 DataDirectory[0]
이다. (IAT때와는 다르게 내가 본 kernel32.dll은 PE64라 OptionalHeader의 크기가 더 큼)
2-0. DataDirectory[0]
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA of the data
DWORD Size; // Size of the data
};
구조체는 위와 같다. 앞의 캡처에서 볼 수 있듯이 DataDirectory[0]
은
VirtualAddress: 9A1E0 (RVA)
Size: DF0C
RVA만 알고있으니까 File Offset을 구해보자..
.rdata RVA: 80000
.rdata size: 33000
.rdata RawDataToPinter: 7EA00
즉 DataDirectory[0]
이 가리키는 곳은 .rdata 섹션 안에 존재한다.
File Offset = 9A1E0 - 80000 + 7EA00 = 98BE0 이다. 거기로 가보자
2-1. IMAGE_IMPORT_DIRECTORY
앞서 말했듯이 exe에서 외부 라이브러리에서 함수를 가져다 쓸 때 해당 라이브러리의 EAT를 참조한다. 이제 kernel32.dll
의 AcquireSRWLockExclusive
함수의 주소를 찾아보도록 하겠다.
주소에 찾아왔다. 한번 자세히 봐보자 이쁘게 색칠해왔다
필드 | 값(hex) | RAW |
---|---|---|
Name(파랑) | 9E1D2 | 9CBD2 |
NumberOfFunctions(갈색) | 661 | - |
NumberOfNames(회색) | 661 | - |
AddressOfFunctions(두번째 빨강) | 9A208 | 98C08 |
AddressOfNames(두번째 초록) | 9BB8C | 9A58C |
AddressOfNameOrdinals(두번째 노랑) | 9D510 | 9BF10 |
- Name
- kernel32.dll 이라는 라이브러리 이름을 확인할 수 있다.
- NumberOfFunctions
- 함수 개수는 0x661 == 1633개
- NumberOfNames
- 함수 이름 개수는 0x661로 함수 개수만큼 이름이 모두 식별이 됨
- AddressOfNames
- 아까 위에서 간단히 적었던 것 처럼,
GetProcAddress()
가 함수의 주소를 찾기 위해선AddressOfNames
필드를 먼저 참조한다. - AddressOfName은 RAW > 얘가 가리키는것이 실제 함수 이름이 저장된 배열의 주소 > 이걸 따라가면 실제 함수 이름이 저장된 배열로 갈 수 있음
- AddressOfName의 RAW: 9A58C >> 9E1DF에 실제 함수 이름 배열이 저장되어 있다
- 실제 함수 이름 배열의 RAW = 9E1DF - 80000 + 7EA00 = 9CBDF
- 해당 주소로 가보면 실제 함수 이름들이 배열 형태로 저장이 되어 있는 것을 확인할 수 있다.
- 이제 여기서 함수 이름 배열 원소 하나가 끝나면 NULL 값이 있으니까 앞에서부터 확인하면서 자기가 사용하려는 함수의 배열 인덱스를 찾아야한다(그래서 원래는
CreateThread
주소 찾기를 해보려했는데 인덱스 찾기가 귀찮아서 그냥 맨 앞에걸로 정했다 ㅋㅋ) - 우리가 확인하려는
AcquireSRWLockExclusive
함수는 인덱스 0인 것을 확인할 수 있다.
- 아까 위에서 간단히 적었던 것 처럼,
- AddressOfNameOrdinals
- RAW: 9BF10
- 앞에서
AcquireSRWLockExclusive
함수의 인덱스가 0인 것을 확인했고, Ordinals 배열에서 인덱스 0인 부분은 0000으로 확인할 수 있다(2byte씩임). (index: 0, ordinal: 0)
- AddressOfFunctions
- RAW: 98C08 >> 9E1F7에 실제 함수의 주소가 저장되어 있다(여기부터 배열인 것).
AddressOfFunctions
의 RAW가 가리키는 값이 실제로 함수 주소를 저장하고 있다.- 앞에서 Ordinals가 0임을 확인했으니 위 주소 배열의 0번째인
9E1F7
을 구할 수 있고,kernel32.dll
의ImageBase
는0x180000000
이다. 이건 Optional Header 부분이다. 회색부분이 ImageBase 부분이다. - 즉
0x180000000
+0x9E1F7
이 실제 메모리에 로딩되었을 때의AcquireSRWLockExclusive
주소이다. CFF Explorer를 통해 확인해봐도 해당 함수의 RVA는9E1F7
이다.
2-2. 확인
실행 환경1: Windows 10 64bit [Version 10.0.19043.1348]
처음엔 그냥 크롬이 kenel32.dll을 사용하길래 windbg로 붙어서 확인을 하려했다.
음.. 막상 프로그램이 실행되고 실제 kernel32.dll이 올라간 주소가 ImageBase와 달랐다.
0:000> !dh 00007ffd`ce3a0000
File Type: DLL
FILE HEADER VALUES
8664 machine (X64)
7 number of sections
38B369C4 time date stamp Wed Feb 23 14:01:56 2000
...
OPTIONAL HEADER VALUES
...
----- new -----
00007ffdce3a0000 image base
1000 section alignment
200 file alignment
3 subsystem (Windows CUI)
...
4160 DLL characteristics
High entropy VA supported
Dynamic base
NX compatible
Guard
그래서 windbg에서 pe 구조를 확인해보았더니 ImageBase가 다른값으로 바뀌어있다. 바뀐걸까.. 내가 뭔가 생각 못한 부분이 있는걸까.. 그리고 그 바로 위에 new라고 써있는 이유도 모르겠다.
그래서 좀 더 보니까 DLLcharacteristics 부분에 Dynamic base라고 되어있는것을 볼 수 있다.
CFF에서도 Optional Header를 다시 보니까 DLL can move에 체크가 되어있다.
이미 0x180000000에 뭐가 올라가 있거나 ASLR 관련된 이유일 수 있을 것 같다.
실행환경2: Windows 7 [Version 6.1.7601]
왠지 윈7에서는 될 것 같아서 환경을 바꾸어서 해봤다. 여기선 귀찮아서 직접 계산은 안했다.
- kernel32.dll ImageBase: 0x78D20000
- AcquireSRWLockExclusive RVA: 0xAA1EE
근데 얘도 실제 메모리에 올라간 값을 보면.. ImageBase랑 차이는 난다. 아무튼 찾고자 하는 함수의 RVA를 실제 dll이 올라간 베이스 주소에 더해준 곳에 함수가 있는 것을 볼 수 있다.
얘도 보니까 win10하고 똑같이 세팅이 되어 있다. 아무튼 이런식으로 dll에서 EAT를 참조하여 함수의 주소를 알아내는 것을 확인할 수 있다. 실제 함수가 올라갈 때 ImageBase와 다른 주소에 올라갈 수 있음을 알았으니 함수의 주소를 직접 계산하고자 할때는 ImageBase만 믿지 말고 직접 dll이 올라간 위치를 알아야 할 것 같기도 하다.
결국은 추측이여서 확실하지 않으니 지나가다 이 글을 봤는데 이유를 아시는 분은 알려주시면 감사하겠습니다.
'Reversing' 카테고리의 다른 글
PE Structure - IAT (0) | 2021.11.30 |
---|---|
PE Structure - Header (0) | 2021.11.28 |