0. 개요

1. C/C++

... 1.1 General

... 1.2 Optimization

... 1.3 Code Generation

... 1.4 Language

... 1.5 Advanced

2. Linker

... 2.1 Manifest File

... 2.2 Debugging

... 2.3 Advanced





0. 개요

  이 문서에서는 VC++의 옵션들에 대해서 다룰 것이며 특히 리버싱과 관련된 즉 생성되는 어셈블리 코드들을 변화시키는 옵션들 위주로 정리한다. 





1. C/C++

1.1 General

1.1.1 Debug Information Format [ 디버그 정보 형식 ] :  /Z7  /Zi  /Zl

  " /Zi "를 설정하면 디버거에서 사용할 형식 정보와 기호 디버깅 정보를 포함하는 프로그램 데이터베이스인 .pdb 파일이 생성된다. 그리고 자동으로 링커 옵션의 /debug가 설정된다. 빌드 후 확인해 보면 실행 파일과 같은 폴더에 pdb 파일이 생성된 것을 알 수 있다.


* PDB

  심볼 파일(.pdb)은 함수, 변수 이름 등의 정보를 갖으며, 디버깅 시에 어셈블리와 함께 이러한 정보들을 같이 볼 수 있음으로서 디버깅을 훨씬 쉽게 해준다. 즉 클래스, 메서드 및 기타 코드의 소스 파일을 만드는 식별자를 프로젝트의 컴파일된 실행 파일에 사용되는 식별자에 매핑(소스 코드의 문을 실행 파일의 실행 명령에 매핑)한다.



1.1.2 SDL checks :  /sdl  /sdl-

  권장되는 Security Development Lifecycle 검사를 추가한다. 추가되는 것은 2가지가 있는데 첫 째는 오류와 같은 추가 보안 관련 경고이고 둘 째는 추가 보안 코드 생성 기능이다. 즉 첫번째는 컴파일 시 여러 경고를 오류로 활성화 한다. 예를들면 위험한 CRT 함수를 사용한 경우에 경고가 뜨면서 컴파일을 할 수 없을 때가 있는데 (C4996 경고) 이것은 이 옵션 때문이며, 프로젝트 생성시에 SDL 검사를 해제하면 된다. 


  다른 하나는 런타임 검사의 경우 여러 검사를 런타임에 수행하기 위한 코드를 생성한다. MSDN에 다르면 다음과 같다.


- /GS로 컴파일할 때와 동일한 #pragma strict_gs_check(push, on) 런타임 버퍼 오버런 검색에 대해 strict 모드를 활성화합니다.

- 제한된 포인터 삭제를 수행합니다. 역참조를 포함하지 않으며 사용자 정의된 소멸자가 없는 식에서 포인터 참조는 delete에 대한 호출 이후 유효하지 않은 주소로 설정됩니다. 이렇게 하면 오래된 포인터 참조가 재사용되지 않습니다.

- 클래스 멤버 초기화를 수행합니다. 개체 인스턴스화 시(생성자 실행 전) 모든 클래스 멤버를 자동으로 0으로 초기화합니다. 이렇게 하면 생성자가 명시적으로 초기화하지 않는 클래스 멤버와 연관된 초기화되지 않은 데이터를 사용하지 않도록 방지할 수 있습니다.




1.2 Optimization

1.2.1 Optimization [ 최적화 ] :  /Od  /O1  /O2  /Ox

  /Od의 경우 최적화 기능을 사용하지 않으면 디폴트 옵션이다. 참고로 디버깅 시에 어셈블리 명령어들의 양이 늘어나 있는 것을 볼 수 있다. 어떤 면에서는 최적화되지 않고 풀어써져 있어서 이해하기 어려운 코드들이 줄어들지만 또 어떤 면에서는 왜 삽입되어 있는지 모를 명령어들이 보이기도 한다. 더 분석해봐야 겠다.


  /O1의 경우 크기 최적화로서 다음 옵션들(/Og /Os /Oy /Ob2 /Gs /GF /GY)이 자동으로 포함된다. 일반적으로 가장 작은 크기의 코드를 생성한다. /O2의 경우 속도 최적화로서 다음 옵션들(/Og /Oi /Ot /Oy /Ob2 /Gs /GF /GY)이 자동으로 포함된다. Release 빌드의 디폴트 설정이며 일반적으로 가장 속도가 빠른 코드를 생성한다.


  /Ox의 경우 최대 최적화로서 다음 옵션들(/Ob2 /Og /Oi /Ot /Oy)이 자동으로 포함된다. 일반적으로 /Ox 대신 /O2를 사용한다고 한다. 



1.2.2 Inline Function Expansion [ 인라인 함수 확장 ] :  /Ob0  /Ob1  /Ob2

  기본적으로 컴파일러는 자신의 판단에 따라 함수를 인라인 확장할지를 결정한다. 함수를 호출하는 경우에는 인자를 push하고 함수를 call하는 등 함수 호출 오버헤드가 존재하는데 만약 함수의 크기가 작거나 하는 경우 따로 함수를 호출하는 대신 코드를 통합함으로써 이러한 오버헤드를 줄일 수 잇는 것이다.


  /Ob0은 인라인 확장을 사용하지 않도록 설정한다. /Ob1은 inline, __inline, __forceinline으로 표시된 함수나 클래스 선언된 C++ 멤버 함수만 확장한다. /Ob2는 디폴트 옵션으로서 inline, __inline, __forceinline으로 표시된 함수 외에도 컴파일러가 판단하여 선택한 기타 함수들을 인라인 확장한다. 또한 위에서 살펴보았듯이 /O1, /O2, /Ox를 사용할 때도 적용된다. 



1.2.3 Enable Intrinsic Functions [ 내장 함수 사용 ] :  /Oi

  응용 프로그램이 더 빨리 실행될 수 있도록 일부 함수 호출을 내장 함수나 특정한 형태의 함수로 교체한다. 즉 특정한 함수 호출을 컴파일러의 내장 함수로 대체하는 것이다. 참고로 인라인 함수와 헷갈릴 수 있는데 인라인 함수의 경우 특정 함수를 호출하는 형태에서 호출 대신 자체적으로 통합시키는 것이며 이것은 컴파일러가 특정 함수를 대체시킨다는 면에서는 비슷하다. 하지만 대체하는 내장 함수는 컴파일러가 자체적으로 그것에 대한 지식이 있으므로 상응하는 내장 함수가 존재한다면 그 함수 호출을 더 나은 방식으로 통합시킬 수 있다. 



1.2.4 Favor Size Or Speed [ 크기 또는 속도 ] :  /Os  /Ot

  /Os는 코드 크기 우선으로서 속도보다 크기를 우선적으로 처리하도록 컴파일러에 지시하며 /Ot는 코드 속도 우선으로서 크기보다는 속도를 우선적으로 처리하도록 컴파일러에 지시한다. 이 옵션들도 각각 /O1과 /O2에 포함된다.



1.2.5 Omit Frame Pointers [ 프레임 포인터 생략 ] :  /Oy  /Oy-

  호출 스택에서 프레임 포인터를 생성하지 않으며 이에 따라 함수 호출 속도가 빨라진다. 이것은 x86 컴파일러에서만 사용할 수 있다. /Oy를 사용하면 프레임 포인터가 생략되며 /Oy-를 사용하면 프레임 포인터 생략이 비활성화된다.


  사실 리버싱을 공부할 때 스택 프레임을 배우면서 EBP를 이용한 방식의 메커니즘을 배우게 된다. 이 옵션을 사용하면 즉 /Oy가 설정되면 이 EBP를 스택 프레임으로서 사용하지 않고 General Purpose로 사용하게 된다. 이렇게 됨으로써 몇 개 되지 않은 범용 목적의 사용 가능한 레지스터가 하나 더 추가될 수 있지만 리버싱하는 입장에서는 문제점이 더 많이 발생하게 된다.


  먼저 Call Stack을 확인할 필요가 있는 경우 디버거의 명령어나 기능을 통해서든 아니면 직접 확인하여 찾든지 간에 이 EBP를 이용해야 할 것인데 이것이 사용되지 않으므로 콜 스택을 구별할 수가 없어진다. 참고로 x64의 경우에도 레지스터를 이용하므로 이런 방식을 통한 콜 스택 추적이 불가능하다. 또한 PDB 형식도 이 EBP를 이용하여 로컬 변수 같은 정보들이 저장되어 있다. 물론 윈도우에서 제공되는 함수들을 확인해 보면 디버깅을 지원하기 위하여 이 옵션을 사용하지 않아서인지 프레임 포인터가 계속 쓰이고 있는것으로 보인다.



1.2.6 Whole Program Optimization [ 전체 프로그램 최적화 ] :  /GL

  전체 프로그램 최적화를 사용하지 않으면 모듈(컴파일)별로 최적화가 수행된다. 




1.3 Code Generation

1.3.1 Enable C++ Exceptions :  /EHa  /EHs  /EHsc

  기본적으로 SEH는 운영체제 차원에서 지원된다. 즉 __try, __except 구문을 사용하면 된다. 아래의 옵션들은 C++ 문법의 catch문에서 SEH나 extern "C"로 선언된 함수의 예외를 catch할 수 있는지의 여부를 판단한다.


  /EHa를 설정하면 C++ EH 뿐만 아니라 SEH도 catch 구문에서 catch할 수 있다. /EHs를 설정하면 catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 있다. /EHsc를 설정하면 catch 구문에서 C++ EH를 catch하지만 SEH는 받을 수 없다. 또한 컴파일러에 extern "C"로 선언된 함수가 예외를 throw할 수 없다.



1.3.2 Runtime Library [ 런타임 라이브러리 ] :  /MT  /MTd  /MD  /MDd  /LD

  /MT는 런타임 라이브러리의 다중 스레드 정적 버전을 사용한다. 즉 런타임 라이브러리의 API 함수를 사용할 경우 실행 파일에 정적으로 링크한다. /MTd는 디버그 버전을 링크한다. /MD는 런타임 라이브러리의 다중 스레드 별 및 DLL 별 버전을 사용한다. 즉 실행 파일이 런타임 라이브러리의 DLL을 임포트하여 필요한 함수를 호출하여 사용하는 방식으로서 이것을 동적 링크라고 한다. /MDd는 디버그 버전을 링크한다. /LD는 DLL 개발 시의 옵션이다.



1.3.3 Security Check [ 보안 검사 ] :  /GS  /GS-

  /GS는 스택 버퍼를 위한 보안 검사를 추가한다. 즉 함수의 반환 주소나 예외 핸들러의 주소에 쿠키를 삽입하여 버퍼가 오버플로우 되었는지를 검사한다.



1.3.4 Control Flow Guard [ 행 가드 제어 ] :  /guard:cf

  제어 흐름 보호 (CFG : Control Flow Guard). vtable을 이용한 가상 함수 호출이나 콜백 함수의 경우 특정 실행 시점에서 실행될 함수가 컴파일 시에 정적으로 결정되는 것이 아니라 런타임 시에 함수 포인터를 이용해 결정된다. 그렇기 때문에 이러한 함수 호출을 간접 호출이라고 한다. 어셈블리 루틴으로 보자면 다음과 같다.


-------------------------------------------------------------------

MOV ESI, DWORD PTR DS:[ESI]

MOV ECX, ESI

PUSH 1

CALL ESI

-------------------------------------------------------------------


  제어 흐름 보호는 이러한 간접 호출을 보호하기 위한 기법으로서 간접 호출 직전에 검사하는 것이다. 생성되는 코드는 다음과 같이 __guard_check_icall_fptr이라는 래퍼 함수 호출이 추가되어 있다.


-------------------------------------------------------------------

MOV ESI, DWORD PTR DS:[ESI]

MOV ECX, ESI

PUSH 1

CALL DWORD PTR DS:[__guard_check_icall_fptr]

CALL ESI

-------------------------------------------------------------------


  이 래퍼 함수는 /guard:cf 옵션이 꺼져 있다면 단지 retn만 존재하는 루틴을 가리키지만 옵션이 켜져 있다면 LdrpValidateUserCallTarget()를 가리킨다. 또한 IMAGE_LOAD_CONFIG_DIRECTORY 구조체에 관련 값들이 추가된다. 컴파일 시에 생성되는 이 값들과 호출할 함수 포인터를 이용해 유효한 함수 포인터인지 비교하여 검사한다.




1.4 Language

1.4.1 Enable Run-Time Type Information [ 런타임 형식 정보 사용 (RTTI) ] :  /GR

  런타임에 개체 형식을 검사하는 코드를 추가한다. 일반적으로 코드에서 dynamic_cast 연산자 또는 typeid를 사용할 경우 이 옵션이 필요하다. 이것을 사용하면 .rdata 섹션 크기가 증가한다. 그러므로 dynamic_cast, typeid를 사용하지 않는 경우 /GR-를 사용해서 이미지 크기를 줄일 수 있다.




1.5 Advanced

1.5.1 Calling Convention [ 호출 규칙 ] :  /Gd  /Gr  /Gz  /Gv

  /Gd는 __cdecl, /Gr는 __fastcall, /Gz는 __stdcall, /Gv는 __vectorcall을 의미한다.



1.5.2 Compile As [ 컴파일 옵션 ] :  /TC  /TP

  /TC는 C 코드로 컴파일하며 /TP는 C++ 코드로 컴파일한다.





2. Linker

2.1 Manifest File

2.1.1 Generate Manifest [ 메니페스트 생성 ] :  /MANIFEST

  xml 파일로 생성된다고 하지만 일반적으로 리소스 섹션에 통합되는 것으로 보인다. 여기서는 UAC 관련된 내용만 다루겠다.



2.1.2 Enable User Account Control (UAC) [ 사용자 계정 컨트롤 사용 ] : /MANIFESTUAC

  프로그램 매니페스트에 UAC 정보를 포함할지 여부를 지정한다.



2.1.3 UAC Execution Level [ UAC 실행 수준 ] :  /level='asInvoker' or 'highestAvailable' or 'requireAdministrator'

  레벨에는 asInvoker (응용 프로그램을 시작한 프로세스와 동일한 권한으로 응용 프로그램 시작), highestAvailable (최대한 높은 권한 수준으로 응용 프로그램 실행), requireAdministrator (관리자 권한으로 실행)이 있다.


* UAC

  리소스 섹션을 보면 xml 형태의 문자열이 보인다. 여기서 requestedExecutionLevel level="highestAvailable" 같은 형태의 문자열을 볼 수 있다. 파일을 실행할 때 참고할 UAC 값이 여기에 존재한다.




2.2 Debugging

2.2.1 Generate Debug Info [ 디버그 정보 생성 ] :  /DEBUG  /DEBUG:FASTLINK

  참고로 /Zi가 설정되어 있으면 자동으로 /DEBUG가 설정되며 실행 파일들에 디버그 정보를 넣는다. 실제로 생성된 실행 파일을 분석해 보면 디버그 섹션이 추가되어 있고 이 섹션에 디렉토리들의 주소가 들어가 있어서 소스 코드라던지 .pdb 파일의 위치라던지 하는 정보가 들어가 있다. 참고로 디버그 섹션은 .rdata 섹션에 통합되는 경우가 많다.


  사족으로 프로그램을 직접 작성한 후 리버싱하려고 할 때 pdb 파일 없이 분석하려고 한다면 이 pdb 파일을 지우거나 이동시키고 바이너리 이름을 변경해야 한다. pdb 파일을 지우거나 이동시킨다는 것은 바이너리 내부에 pdb의 경로가 저장되어 있으므로 실행 파일만 옮긴다고 해서 pdb를 읽어오지 못하는 거이 아니라는 점이다. 아예 그 경로에 pdb 파일이 없어야 읽어올 수 없다. 그리고 바이너리 이름을 변경해야 한다는 것은 해당 바이너리를 처음 읽어올 때 대표적인 디버거들의 경우 이것을 저장해 놓는 경우가 많다. 그래서 pdb가 이미 없더라도 처음 읽어왔을 때 저장한 정보를 가지고 pdb를 이용해 분석된 내용이 계속 존재할 것이다. 그래서 바이너리의 이름을 변경함으로써 이전에 저장된 분석 내용을 불러오지 않게 하는 것이다. 



2.2.2 Strip Private Symbols [ 전용 기호 제거 ] :  /PDBSTRIPPED

  빌드할 때 두 번쨰 PDB가 만들어지는데 여기에는 고객에게 제공하지 않을 기호가 생략된다. 즉 여기에는 공용 기호, 개체 파일의 목록과 개체 파일에서 제공하는 실행 파일의 일부, 스택을 통과시키는데 사용된 FPO(프레임 포인터 최적화) 디버그 레코드가 생략되며 포함되지 않는 것으로는 형식 정보, 줄 번호 정보, 개체 파일별 CodeView 기호(함수, 지역 및 정적 데이터에 대한 기호 등)이 있다.


  참고로 실행 파일 및 원래 pdb 파일이 생성되는 release 폴더가 아니라 프로젝트 폴더에 생성된다. 그리고 비교해 보면 두 번째 PDB의 크기가 훨씬 작다는 것을 확인할 수 있다.



2.2.3 Generate Map File [ 맵 파일 생성 ] :  /MAP

  맵 파일이 생성된다. 이것도 실행 파일 및 원래 pdb 파일이 생성되는 release 폴더가 아니라 프로젝트 폴더에 생성된다. 그리고 pdb와는 달리 텍스트 파일의 형태이다. 개인적으로 pdb의 경우 디버거를 통해 읽혀져 많은 디버깅 정보를 얻을 수 있는 유용한 파일로 알고 있지만 map 파일은 굳이 pdb 파일이 있는데 어디에 필요한지 잘 모르겠다. 어쨌든 pdb가 없는 경우라면 텍스트 파일로 되어 있는 map 파일을 직접 읽어서 유용하게는 사용할 수 있을 것 같다. 


  MSDN에 따르면 맵 파일에 들어있는 정보는 다음과 같다.

- 파일의 기본 이름인 모듈 이름

- 파일 시스템이 아니라 프로그램 파일 헤더의 타임스탬프

- 프로그램의 그룹 목록. 각 그룹의 시작 주소(section:offset), 길이, 그룹 이름 및 클래스가 함- 께 표시됩니다.

- 공용 기호 목록. 각 주소(section:offset), 기호 이름, 플랫 주소, 기호가 정의된 .obj 파일이 함께 표시됩니다.

- 진입점(section:offset)


* 추가

  pdb와 map 파일 간의 차이점에 대해서 찾아보았는데 "John Robbins"가 "Debugging Applications"라는 책에서 한 설명을 보면 맵 파일은 프로그램의 전역 심볼들과 소스 그리고 줄 번호에 관한 정보를 텍스트 형태로 보여주는 파일이라고 한다. 이것은 마이크로소프트가 심볼 테이블 형식을 주기적으로 바꾸게 됨으로써 오래된 프로그램의 경우 오래된 심볼 엔진 버전을 찾기가 힘들 수 있는데 맵 파일이 텍스트 형태로 존재함으로써 분석 시에 더 용이함을 줄 수 있다는 것이다.

[ http://stackoverflow.com/questions/14640676/why-should-we-need-the-map-file-when-pdb-file-is-available-in-windows-platform ]




2.3 Advanced

2.3.1 [ 기준 주소 ] : /BASE

  ASLR을 사용하지 않을 경우 바이너리의 ImageBase 주소는 보통 0x00400000이다. 아래의 "임의 기준 주소" 옵션을 비활성화하고, 이 옵션을 사용하며 주소를 0x01000000으로 설정한다면 바이너리의 ImageBase 주소가 0x01000000으로 설정된 것을 볼 수 있다. 거의 사용되지 않겠지만 가끔은 사용할 수도 있는 옵션으로 보인다.



2.3.2 Randomized Base Address [ 임의 기준 주소 ] :  /DYNAMICBASE

  ASLR 기능을 활성화한다.



2.3.3 Data Execution Prevention (DEP) [ 데이터 실행 방지 ] :  /NXCOMPAT

  데이터 실행 방지 (DEP) 기능 활성화



2.3.4 Image Has Safe Execption Handlers [ 이미지에 안전한 예외 처리기 포함 ] :  /SAFESEH

  SEH 오버플로우 공격을 방어하는 기법으로 /GS 옵션도 있지만(SEH 프레임에 GS 쿠키 사용) /SAFESEH 옵션도 존재한다. 이것은 간략하게 말해서 핸들러가 유효한 핸들러인지를 검사해 준다. 


  /SAFESEH 설정 시에 PE 헤더에 IMAGE_LOAD_CONFIG_DIRECTORY 구조체가 생성된 것을 볼 수 있다. 이 구조체의 여러 멤버 중에서 여기에 사용되는 것은 SEHandlerTable 필드와 SEHandlerCount 필드이다. SEHandlerCount 필드는 프로그램에서 설치되는 SEH 핸들러의 개수이며 SEHandlerTable 필드는 핸들러들의 주소들을 담는 __safe_se_handler_table의 주소이다.


  로더는 PE가 로드될 때 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerTable 필드의 값인 __safe_se_handler_table에 이 프로그램에서 사용되는 핸들러들의 주소를 설정하고 그 개수는 IMAGE_LOAD_CONFIG_DIRECTORY 구조체의 SEHandlerCount 필드에 설정한다. 이후 예외가 발생하면 KiUserExceptionDispatcher()를 지나 RtlDispatchException()에서 호출할 특정 핸들러와 __safe_se_handler_table의 엔트리들을 비교해서 상응하는지를 검사한다.



'악성코드 분석' 카테고리의 다른 글

리눅스 안티바이러스 구현에 관한 정리  (0) 2017.07.04
Windbg, Gdb 명령어 정리  (0) 2017.06.27
다형성 바이러스  (4) 2017.05.16
API Sets  (0) 2017.05.12
윈도우의 예외 처리  (0) 2017.05.09
Posted by SanseoLab



  다형성 바이러스에 대한 개념 및 분류가 너무 헷갈리는것 같아 정리하기로 했다. 물론 이 내용이 모든 것을 포함하지도 않고 틀린 부분도 있을 수 있다. 각 개념에 대한 검색용으로 참고하길.


  기본적으로 바이러스이기 때문에 파일을 감염시키는 개념이고 이것은 File Infection과 관련있다. 특정 행위를 수행하는 루틴이 삽입되는 방식은 다음과 같다.


1. 기존 섹션의 빈 공간에 삽입

2. 기존 섹션의 크기를 늘려서 삽입

3. 새로운 섹션을 추가하여 그곳에 삽입


  AV는 검사 과정에서 감염된 부분이 있는지 여부를 판단하기 위해 파일을 검사할 것이다. 감염된 파일은 정상 파일과 비교해 보자면 먼저 하나 이상의 바이러스 루틴이 삽입되어 있을 것이며 그 루틴으로 이동하는 분기문이 존재할 것이다. 일반적으로 File Infector는 PE 헤더의 EP를 수정하여 감염된 루틴이 있는 주소로 바꾸어 시작할 때부터 감염 루틴에서 시작하게 하거나 EP 부분에 위치한 분기문의 이동 주소(api를 호출하는 call이나 jmp 등)를 감염된 루틴이 존재하는 주소로 변경할 것이다. 바이러스 루틴은 적합하다고 판단된 영역에 삽입되어 있을 것이다.


  시그니처 기반 AV의 경우 바이러스로 인해 감염된 부분의 패턴을 검사할 것이며 일반적인 바이러스의 경우 이 패턴은 항상 같기 때문에 AV에 의해 발견될 수 있다. 이에 따라 다형성 바이러스가 나오게 되었는데 이것은 이러한 시그니처에 잡히지 않게 하기 위한 방식으로 감염 시마다 매번 동일하지 않은 패턴을 생성해낸다.


1. 사용 명령어 변경 : 하는 행위는 동일하지만 어셈블리 루틴은 차이가 난다.

2. Garbage 명령 삽입

3. 사용 레지스터 변경

4. 감염 루틴들의 위치나 순서 바꾸기

5. EPO (EntryPoint Obscuring) : 위에서 언급하였듯이 바이러스 루틴으로 분기하도록 분기문을 수정하는데 이 분기문을 랜덤으로 선택한다. 즉 바이너리에 존재하는 call이나 jmp 명령어 중에서 랜덤으로 골라 감염 루틴으로 분기하는 분기문을 생성하는 것이다.


  이 같은 방식들을 통해 매번 바이러스에 감염될 때 마다 생성되는 감염 루틴은 동일하지 않게 되지만 행위는 같게 유지될 수 있다.


'악성코드 분석' 카테고리의 다른 글

Windbg, Gdb 명령어 정리  (0) 2017.06.27
VC++ 옵션 정리  (0) 2017.06.03
API Sets  (0) 2017.05.12
윈도우의 예외 처리  (0) 2017.05.09
링크 및 책 정리  (0) 2017.04.23
Posted by SanseoLab

2017. 5. 12. 23:56 악성코드 분석

API Sets



1. 개요

  윈도우 7부터는 MinWin 즉 Minimal Windows kernel라는 개념을 통해 커널에 변화가 생겼다. 과거부터 발전되어 감에 따라 그리고 동시에 하위호환을 만족시켜야 함에 따라 kernel32.dll이나 advapi32.dll 같은 DLL은 수 많은 API들을 포함함으로써 크기나 성능, 구조적으로 경제적이지 않게 되었다. 이에 따라 이러한 DLL들을 기능 및 목적에 따라 재구성할 필요가 생긴 것이다.





2. Virtual DLL과 Logical DLL

  예를들어 최신 VC++로 개발한 애플리케이션을 분석해보면 과거와 달리 api-ms-win-crt-heap-l1-1-0.dll 같은 api-ms-win으로 시작하는 DLL들이 보인다. 이러한 DLL들을 Virtual DLL이라고 하며 실제 kernel32.dll이나 advapi32.dll 같은 DLL들을 Logical DLL이라고 한다. 이런식으로 함수들이 기능 및 목적에 따라 각각의 Virtual DLL들로 재구성되어 있다. 실질적으로 Virtual DLL은 실제 함수 루틴이 존재하는 Logical DLL에 대한 스텁 DLL이라고 할 수 있다. api-ms-win-crt로 시작하는 것들은 CRT 라이브러리들의 Virtual DLL이고 또 다른 종류로는 api-ms-win-core로 시작하는 것들이 있는데 이것은 뒤에서 보겠지만 kernelbase.dll 등으로 매핑된다. 


  이렇게 Virtual DLL만 만들어진 것이 아니라 Logical DLL 즉, 실제 시스템 DLL들에 대해서도 변화가 생겼다. 첫 번째는 kernelbase.dll과 sechost.dll인데 kernelbase.dll은 kernel32.dll과 advapi32.dll의 api들 중에서 필요한 부분을 가져온 것이다. 정확히 말하자면 kernelbase.dll은 MinWin을 위한 핵심 서비스를 제공하는 함수들이 포함된 라이브러리이다. 즉 kernel32.dll과 advapi32.dll 같은 무겁고 큰 라이브러리 대신 필요한 핵심 함수들만 제공하는 이 kernelbase.dll만으로 MinWin을 지원할 수 있는 것이다. 참고로 이렇게 가져온 함수들은 kernel32.dll이나 advapi32.dll에서도 여전히 호출할 수 있다. 실제 구현은 kernelbase.dll에 존재하지만 똑같은 이름의 함수가 존재하며 내부적으로 간단한 처리를 거쳐서 kernelbase.dll에 존재하는 함수를 호출하기 때문이다. 이에 따라 레거시 애플리케이션은 계속 kernel32.dll 등의 함수를 호출할 수 있고 최근 VC++로 만들어진 애플리케이션은 직접 kernelbase.dll의 함수를 호출할 것이다. 물론 더 자세히 해보자면 애플리케이션은 api-ms-win-core 같은 Virtual DLL을 임포트할 것이고 앞의 과정을 통해 결국 kernelbase.dll 내부의 함수를 호출할 것이다. 참고로 sechost.dll에는 advapi32.dll 중에서 kernelbase.dll로 이동한 함수들을 제외한 다른 함수들이 이동되어 있다.


  다른 하나는 CRT 라이브러리이다. msvcrt.dll 같은 CRT 라이브러리는 두 개로 나뉘어 졌다. 하나는 vcruntime140.dll로써 프로세스 스타트업이나 예외 핸들링 같은 컴파일러 지원에 요구되는 기능들(함수들)이 존재한다. 다른 하나는 ucrtbase.dll로써 이것은 범용 CRT 즉 순수한 CRT 라이브러리이다. 예를들면 api-ms-win-crt-stdio나 api-ms-win-crt-math 같은 Virtual DLL들이 이것과 매핑된 것이다. 일반적으로 PEview로 보면 vcruntime140.dll은 그대로 보이지만 ucrtbase.dll은 보이지 않고 api-ms-win-crt를 접두사로 갖는 Virtual DLL들만 보일 것이다. 물론 내부적으로 이런 Virtual DLL들은 ucrtbase.dll의 함수를 호출하게 된다.


  정리해 보자면 특정 DLL들의 구조가 Virtual DLL과 Logical DLL로 나뉘게 되었다. Logical DLL은 실제 함수 루틴이 존재하는, 과거부터 있어왔던 DLL들이지만 변화가 생겼는데 kernelbase.dll이 만들어 졌으며 이것은 kernel32.dll과 advapi32.dll의 함수들 중에서 필요한 부분을 가져와서 만들었다. 이 외에도 sechostd.dll도 만들어 졌다. 물론 레거시 애플리케이션을 위해 이 두 DLL에서도 kernelbase.dll로 이동한 함수들을 호출할 수 있는데 같은 이름의 스텁 함수가 존재해서 내부적으로 실제 함수 루틴이 존재하는 kernelbase.dll의 함수를 호출해 주기 때문이다. 이 외에도 CRT 라이브러리에도 변화가 생겼다. vcruntime140.dll과 ucrtbase.dll로 나뉜 것이다. 


  올리디버거로 api들을 분석해 보면 어느 DLL에서 함수 루틴이 진행되는지를 직접 확인할 수 있다. Virtual DLL은 PEview 같은 도구를 통해 임포트 테이블을 확인하면 차이점을 발견할 수 있을 것이다. 확인해 보면 애플리케이션이 익숙치 않은 api-ms-win으로 시작하는 DLL들의 함수들을 임포트하는 것을 볼 수 있다. 이 Virtual DLL들은 실제로는 Logical DLL을 호출한다. 예를들면 api-ms-win-crt-runtime-l1-1-0.dll의 _initterm_e 함수를 호출할 경우 실제로는 ucrtbase.dll의 _iinitterm_e를 호출하게 된다.





3. 원리

  여기서는 Virtual DLL과 Logical DLL이 어떻게 매핑되는지를 알아본다. 부팅 과정 중에 ApiSetSchema.dll이 로드되며 이 DLL의 .apiset 섹션 내의 데이터가 메모리에 로드된다. 이후 이 DLL은 언로드되고 데이터는 PEB의 ApiSetMap에 매핑된다. 이제 DLL을 로드하게 된다면 LoadLibrary() 내부에서 PEB에 저장된 ApiSetMap 구조체를 참조해서 Virtual DLL의 이름을 Logical DLL의 이름으로 리다이렉트 시킨다.





4. 분류

  참고로 아래에서 kernel32.dll 및 advapi32.dll로 설명한 것은 kernelbase.dll도 포함하며 advapi32.dll로 설명한 것은 sechost.dll도 포함할 수 있다. 자주 볼 수 있는 것들 위주로 정리한 것이며 이 외에도 다수 존재한다.


- API-MS-Win-Core : 대부분 kernel32.dll의 함수들이며 advapi32.dll도 약간 존재한다.

- API-MS-Win-Security : kernel32.dll, advapi32.dll, sechost.dll의 함수들.

- API-MS-Win-Service : advapi32.dll 및 sechost.dll의 함수들

- API-MS-Win-CRT : ucrtbase.dll



'악성코드 분석' 카테고리의 다른 글

VC++ 옵션 정리  (0) 2017.06.03
다형성 바이러스  (4) 2017.05.16
윈도우의 예외 처리  (0) 2017.05.09
링크 및 책 정리  (0) 2017.04.23
윈도우의 드라이버 개발과 루트킷 그리고 AV  (0) 2017.04.23
Posted by SanseoLab

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.4
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

최근에 올라온 글

최근에 달린 댓글

글 보관함