[C & C++] Search a file - 파일 탐색
인자로 지정한 경로에 존재하는 파일을 탐색하는 코드이다. 만약 폴더를 탐색하면 재귀를 통해 다시 그 안에 파일들까지 탐색한다. 컴파일러에 따라 "_CRT_SECURE_NO_WARNINGS"를 추가해주어야 하거나, "char * 형식의 인수가 LPCWSTR 형식의 매개 변수와 호환되지 않습니다" 라는 오류로 인해 유니코드 문자 집합 사용 설정을 해제해주어야 한다.
2016.08.24
no image
Assembly로 보는 코드, strcmp 문자열 비교
* 컴파일한 어셈블리 코드는 컴파일러나 속성 등 여러 요인에 따라 많이 상이 할 수 있습니다.우선 문자열 비교를 위한 코드는 아래와 같다. buf1 에는 "Compare" 라는 문자열을 저장해놓고 buf2 에는 사용자로부터 입력을 받도록 하였다. 그리고 strcmp 를 통해 두 값을 비교하는 코드이다. 위 코드에서 strcmp 부분의 어셈블리 코드는 아래와 같다. 여기서 buf2 에 "Comparz" 를 넣어주므로 ARG.1 에는 "Compare", ARG.2 에는 "Comparz" 가 위치하게 된다. 눈으로 한번 살펴본 뒤 이에 대한 자세한 설명을 읽어보자. 우선 strcmp(buf1, buf2) 에서 buf1 은 "Compare" 이고 buf2는 "Comparz" 라는 문자열을 입력해주었다. 스택에 ..
2016.08.08
no image
공인인증서 탈취 악성코드
개요랜섬웨어나 게임 계정, 금융 정보 탈취 등으로 인한 피해를 끊이지 않고 있다. 이런 악성코드를 제작하는 공격자의 목적은 결국 금전을 획득하는 것이다. 우리는 많은 매체들을 통해 이런 사건에 대한 피해 소식을 접할 수 있다. 랜섬웨어의 경우 악성코드에 감염되면 파일이 암호화가 되어 공격자에게 금액을 지불해야 한다. 하지만 사용자 PC 에서 금융 정보를 탈취할 때, 공격자는 사용자의 보안 카드 번호 등을 알 수 없으므로 이에 대해 사용자가 입력하도록 한다. 따라서 이번 보고서에서는 금융 정보 탈취 악성코드에 감염된 경우 어떠한 증상이 있는지 알아보자. 동작악성 프로세스가 실행되었더라도 사용자가 Internet Explorer 자체를 실행시키지 않을 수가 있다. 때문에 공격자는 사용자 PC 에서 악성코드를..
2016.08.06
no image
Windows Event Message
Windows Application일반적으로 프로그램이란 실행하면 작성한 코드의 순서대로 동작하는 것으로 생각할 수 있다. 하지만 Windows GUI 응용프로그램은 실행된 후에 단지 윈도우를 출력할 뿐이며, 일반적으로 아무것도 하지 않는다. 이런 프로그램은 사용자가 키보드 입력이나 마우스 버튼 클릭으로 ‘이벤트’가 발생되면 그때마다 대응되는 처리를 하는 방식으로 동작한다.이러한 ‘이벤트 반응형’ 프로그램이 동작하기 위해서는 우선 이벤트가 발생했음을 프로그램에 알리는 구조가 필요하다. Windows에서는 각각의 응용프로그램이 마우스나 키보드로부터 직접 입력을 받지 않는다. 대신 이러한 이벤트가 발생하는지 Windows가 확인한 뒤, 이벤트가 발생했을 때 응용프로그램에 통지한다.[그림 1] 이벤트 전달 과..
2016.07.13
no image
Windows Multi Task
개요 우리는 학교 과제를 하기 위해 HWP나 Word, Excel, PPT 등의 응용프로그램을 실행해야 한다. 하지만 단순히 이러한 동작만을 하는 것이 아니라 노래를 듣는 동시에 코드를 짜거나, 이에 더해 PC 톡으로 친구들에게 물어보기도 한다. 우리가 이러한 프로그램들을 실행하면 결과적으로 CPU에서 명령어를 처리하게 된다. 그렇다면 어떻게 CPU가 하나뿐이더라도 동시에 여러 작업이 가능해지는 것일까? 이를 위해 프로세스와 스레드가 어떻게 동작하는지에 대하여 알아보자. 프로그램은 일반적으로 하드 디스크 등에 저장되어 있는 실행코드를 뜻하고, 프로세스는 프로그램을 구동하여 프로그램 자체와 프로그램의 상태가 메모리 상에서 실행되는 작업 단위를 지칭한다. 예를 들어, 하나의 프로그램을 여러 번 구동하면 여러..
2016.07.08
Windows Service
Introdution윈도우 운영체제에 있어 우리가 흔히 직접 실행할 수 있는 프로그램 이외에 우리가 직접 실행하지 않아도 실행되는 프로세스가 있다. 이 중 서비스는 보통 시스템의 시작과 함께 시작되어 윈도우의 핵심 프로세스의 기능을 수행한다. 중요한 만큼 사용자의 개입을 최소화하기 위해 백그라운드에서 동작한다. 프로세스에 대한 제어권을 우리가 갖지 않고 윈도우의 서비스 제어 관리자가 가지고 있기 때문에 일반적인 방법으로 서비스를 분석할 수 없다. 이러한 요인들로 인해 악성코드 제작자는 서비스를 통해 백그라운드라는 점, 사용자 개입의 최소화 등의 이점을 누릴 수 있다. 따라서 이번 문서에서는 서비스가 어떻게 동작하는지, 그리고 이를 분석하기 위해서는 어떠한 방식을 사용해야 하는지에 대하여 알아보자. Ser..
2016.06.21
WFP 무력화
WFP (Windows File Protection)WFP는 중요한 Windows 시스템 파일이 대체 또는 변경되는 것을 방지하기 위해 Windows에서 기본적으로 제공하는 기능(Vista부터는 WRP로 대체)으로, 프로그램들이 Windows 시스템의 중요한 파일들을 덮어씌울 수 없게 하여 프로그램과 운영체제로부터 발생할 수 있는 문제를 사전에 방지한다. WFP는 보호하고자 하는 시스템 파일이 올바른지 확인하기 위해 코드 서명에 의해 생성된 카탈로그와 파일 시그니쳐를 사용하여 확인한다. 그렇다면 정상적인 경우라도 이러한 파일의 변경이 일어날 수 없을까? 시스템 파일에 치명적인 취약점이 발견되었을 경우 WFP에 의해 해당 파일을 대체하지 못한다면 이는 위험을 품고 있는 OS가 되어버릴 것이다. 따라서 보호..
2016.06.21
no image
DLL이란?
DLL ( Dynamic Link Library ) DLL은 동적 링크 라이브러리(Dynamic Link Library)의 약자로 일반적으로 확장자가 DLL인 파일이다. 라이브러리라는 말에서 알 수 있듯이 다른 프로그램에서 이용하는 함수들을 모아둔 것이다. 하지만 표준 C 라이브러리 같은 일반 라이브러리의 파일과는 구조나 사용법이 다소 다르다. 일반 라이브러리는 소스코드를 컴파일한 결과로 생성되는 객체 파일(.OBJ)을 그대로 모아둔 것이다. 링커는 이 중에서 필요한 함수가 포함된 객체 파일을 꺼내서 실행 파일에 결합하는 '정적 링크' 방식이다. 아래 그림은 정적 링크를 나타내는 것으로 C/C++ 프로그램의 소스 코드를 기계어 코드로 변환하는 컴파일 단계를 거치게 된다. 여기서 C/C++에는 수많은 표준..
2016.05.29

인자로 지정한 경로에 존재하는 파일을 탐색하는 코드이다. 만약 폴더를 탐색하면 재귀를 통해 다시 그 안에 파일들까지 탐색한다. 컴파일러에 따라 "_CRT_SECURE_NO_WARNINGS"를 추가해주어야 하거나, "char * 형식의 인수가 LPCWSTR 형식의 매개 변수와 호환되지 않습니다" 라는 오류로 인해 유니코드 문자 집합 사용 설정을 해제해주어야 한다.



* 컴파일한 어셈블리 코드는 컴파일러나 속성 등 여러 요인에 따라 많이 상이 할 수 있습니다.

우선 문자열 비교를 위한 코드는 아래와 같다. buf1 에는 "Compare" 라는 문자열을 저장해놓고 buf2 에는 사용자로부터 입력을 받도록 하였다. 그리고 strcmp 를 통해 두 값을 비교하는 코드이다. 

위 코드에서 strcmp 부분의 어셈블리 코드는 아래와 같다. 여기서 buf2 에 "Comparz" 를 넣어주므로 ARG.1 에는 "Compare", ARG.2 에는 "Comparz" 가 위치하게 된다. 눈으로 한번 살펴본 뒤 이에 대한 자세한 설명을 읽어보자.

우선 strcmp(buf1, buf2) 에서 buf1 은 "Compare" 이고 buf2는 "Comparz" 라는 문자열을 입력해주었다. 스택에 들어있는 각 문자열을 EDX 와 ECX 레지스터에 넣어준다.

첫 줄에서 EDX 에 있는 기준 문자열 4 Bytes 를 EAX 로 복사한다. 이를 통해 EAX 에는 0x706D6F43 이 오게 된다. 얼핏 보면 함수의 주소 같지만 "Comp" 라는 문자에 해당하는 ASCII 값이다. 두 번째 줄부터 CMP 명령어가 나타난다. CMP 명령어는 두 값을 비교할 때 사용되는 명령어이다. AL 은 EAX 의 하위 1 Byte 로 0x43("C") 가 된다. 이 "C" 와 사용자가 입력한 문자열("Comparz") 의 첫 번째 문자를 비교한다.


AL 과 ECX 의 첫 글자가 일치한다면 ZF 는 1로 설정되어 JNE 에서 점프하지 않는다. 그리고 TEST AL, AL 은 비교한 두 문자가 0 인지 확인하는 명령어로, 두 문자 모두 0 이라면 ZF 가 1로 설정되어 JZ 에서 점프하여 RETN 으로 간다.

위에서 AL, AH 를 통해 두 문자씩 비교를 하였다. 그렇다면 세 번째 문자부터는 SHR EAX, 10 을 통해 EAX 값을 정리해준다. SHR EAX, 10 는 EAX 레지스터의 값을 0x10 bit 만큼 우측으로 이동시키는 것으로 상위 2 Bytes 의 값이 하위 2 Bytes 에 자리 잡게 된다. 이 경우 "0x706D6F43" 이 "0x0000706D" 가 된다. 그리고 다시 AL 과 AH 를 통해 나머지 문자들도 비교한다.

기준 문자열이 4 Bytes 보다 크기 때문에 주소를 옮겨 주어야 한다. 따라서 ECX 와 EDX 에 있는 주소 값에 각각 4 를 더해 다섯 번째 문자를 가리키도록 한다. 그 후 다시 문자 비교를 시작하는 위치 0x62BEF950 으로 이동한다.

반복문을 돌면서 한 문자씩 비교하는 것을 확인하였다. 이번에는 반환 값에 대하여 알아보자. 아래 두 그림은 0 을 반환하는 경우와, 0 이 아닌 값을 반환하는 경우이다. 아래 첫 번째 그림은 XOR EAX, EAX 를 통해 EAX 의 값을 0 으로 만든다. strcmp 는 두 값이 같은 경우 0 이 반환되는 것으로 바로 그 부분이다.

SBB EAX, EAX 는 SUB EAX, EAX 와 유사하다. 다만 이에 Carry Flag(CF)의 값을 다시 빼준다. 따라서 EAX 에는 어떤 값이 있더라도 자기 자신을 소거한 뒤 CF 의 값에 따라 1을 더 빼주는 셈이 된다. SBB 명령어를 통해 0 또는 0xFFFFFFFF 가 된 EAX 에 OR EAX, 0x1 을 해주므로 결국 EAX 에는 1 또는 -1 이 반환된다.

좀 더 구체적으로 1 또는 -1 은 마지막 CMP 명령어에서 결정된다. 기준 문자열의 마지막 문자 'e' 와 비교 문자열의 'z' 를 비교했을 때, 각각의 ASCII 값은 0x65 와 0x7A 이다. '기준 문자 < 비교 문자' 인 경우 CF 는 borrow 가 발생하여 1로 설정된다. 이 경우 최종적으로 반환되는 값은 -1 이 된다. 반대로 비교 문자열이 "Compara" 라 할 때, 마지막 문자 'a' 는 'e' 보다 작은(기준 문자 > 비교 문자) 값이므로 borrow 가 발생하지 않아 CF 가 0으로 설정되어 최종 반환 값은 이에 OR 1 을 하여 1 이 된다.

'Reversing > Theory' 카테고리의 다른 글

암호학 기초 개념  (2) 2016.11.23
Memory Detection(메모리 진단)  (0) 2016.09.26
WFP 무력화  (0) 2016.06.21
DLL이란?  (4) 2016.05.29
PE구조의 이해  (0) 2016.05.04
개요

랜섬웨어나 게임 계정, 금융 정보 탈취 등으로 인한 피해를 끊이지 않고 있다. 이런 악성코드를 제작하는 공격자의 목적은 결국 금전을 획득하는 것이다. 우리는 많은 매체들을 통해 이런 사건에 대한 피해 소식을 접할 수 있다. 랜섬웨어의 경우 악성코드에 감염되면 파일이 암호화가 되어 공격자에게 금액을 지불해야 한다. 하지만 사용자 PC 에서 금융 정보를 탈취할 때, 공격자는 사용자의 보안 카드 번호 등을 알 수 없으므로 이에 대해 사용자가 입력하도록 한다. 따라서 이번 보고서에서는 금융 정보 탈취 악성코드에 감염된 경우 어떠한 증상이 있는지 알아보자. 


동작

악성 프로세스가 실행되었더라도 사용자가 Internet Explorer 자체를 실행시키지 않을 수가 있다. 때문에 공격자는 사용자 PC 에서 악성코드를 지속시키기 위해 자동 실행 레지스트리에 등록한다.

[그림1] 자동 실행 등록 


사용자의 PC 에 공인인증서가 존재하고 있는지 확인한다. 공인인증서를 찾은 경우 임시 폴더 하위에 이를 복제하는 것을 확인할 수가 있다. 그리고 복제한 공인인증서 파일을 압축하여 임의의 ZIP 파일로 저장해 놓는다.

[그림 2] 공인인증서 복사 및 압축


PAC (Proxy Auto Config)

실행된 악성코드는 Internet Explorer 의 시작 페이지를 국내 유명 포털 사이트로 바꾼다. 그리고 AutoConfigURL 에 값을 등록해준다. 이를 통해 자동 구성 스크립트가 사용되며, 사용자가 URL 입력 시 연결할 IP 정보를 알아 오기 위해 레지스트리 값에 등록된 주소에 질의한다.

[그림 3] 시작 페이지 변경 및 PAC 설정


등록된 주소는 "127.0.0.1:1171" 로 자신 PC 의 1171 번 포트에 질의하게 된다. 아래 그림을 보면 악성 프로세스인 b.exe 가 1171 번 포트에서 LISTENING 중인 것을 확인할 수 있다. 이를 통해 URL 에 따른 IP 정보를 b.exe 에서 받아오게 된다. 사용자는 일반 웹 사이트에 접속하더라도 b.exe 에게 질의하여 공격자가 원하는 사이트로 접속하게 된다.

[그림 4] 연결 대기


네트워크

악성코드는 공격자의 서버와 통신을 시도한다. 공격자의 서버는 QQ 사이트에 등록되어 있어, 네트워크 패킷을 보면 아래와 같이 QQ 사이트 공격자 ID(338366585)를 통해 공격자의 IP 를 받아온다.

[그림 5] QQ 에 사용자 주소 요청 (QQ : 103.7.30.86, 공격자 : 103.20.193.205)


공격자 서버의 주소를 정상적으로 받아 왔다면, 이전에 압축한 사용자 공인인증서를 탈취한다. 아래 패킷과 같이 ZIP 파일의 PK 헤더와 공인인증서 정보가 있는 것을 확인할 수 있다.

[그림 6] 공인인증서 전송


파밍 사이트

감염된 후 Internet Explorer 를 실행하면 아래와 같은 팝업 창이 나타나 다른 행동을 할 수 없게 된다. 은행 배너 중 하나를 클릭하면 공격자가 지정한 파밍 사이트로 이동하게 된다.

[그림 7] 인터넷 접속 시 팝업 창


IBK 기업은행 배너를 클릭한 결과 실제 IBK 기업은행과 같은 페이지로 이동한 것을 확인할 수 있다. 여기까지는 일반 은행 업무를 볼 때와 같은 상황이다. 하지만 은행 사이트에 접속하여 업무를 보기 위해 임의의 버튼을 클릭하면 아래와 같은 메시지 창이 나타난다. 메시지 창이 나타난 이후 가짜 본인인증 사이트로 이동된다.

[그림 8] 클릭 시 나타나는 팝업창


이동한 웹 사이트에는 한국 인터넷 진흥원 KISA 를 볼 수 있으며, 이용자 정보 입력을 유도한다. 하지만 이 페이지 역시 가짜 사이트로 이용자 정보 입력을 제외한 다른 버튼을 클릭할 경우 페이지 이동이 이루어지지 않는다. 이용자 정보 또한 임의의 정보를 기입하면 다음 페이지로 넘어가진다.

[그림 9] 개인정보 입력 유도


임의로 개인 정보를 입력하고 진행을 하면 URL 에 자신이 기입한 이름, 주민번호, 계좌번호 등이 URL 에 노출되는 것을 확인할 수 있다.

[그림 10] 입력한 임의의 개인정보

[그림 11] URL 에 노출되는 개인정보


마지막으로 이체 비밀번호와 보안카드 정보 입력을 유도한다. 정상적인 인증과 달리 보안 카드의 모든 번호를 입력하도록 한다.

[그림 12] 보안카드 정보 입력 유도


결론

사용자 PC 에 저장된 공인인증서를 탈취 후 PAC 를 통해 파밍 사이트로 연결하는 악성코드에 대하여 알아보았다. PC 에 인증서를 저장해놓은 경우 금융 정보 탈취 악성코드가 접근하기 쉬워진다. 따라서 인증서를 USB 와 같은 이동식 매체에 저장하여 필요할 때만 연결하여 사용하는 것이 하나의 예방법이 될 수 있다. 또한 파밍 사이트로 연결되는 방법은 위에서 언급한 PAC 외에도 존재하므로 사용자가 속을 수 있다. 그러므로 위와 같이 과도한 개인 및 금융 정보를 요구한다면 의심을 해보아야 한다. 의심되는 증상이 있을 경우 정보 기입을 멈추고 백신을 통해 PC 감염 여부를 확인하여야 한다.


Windows Event Message

Kail-KM
|2016. 7. 13. 09:51

 

Windows Application

일반적으로 프로그램이란 실행하면 작성한 코드의 순서대로 동작하는 것으로 생각할 수 있다. 하지만 Windows GUI 응용프로그램은 실행된 후에 단지 윈도우를 출력할 뿐이며, 일반적으로 아무것도 하지 않는다. 이런 프로그램은 사용자가 키보드 입력이나 마우스 버튼 클릭으로 이벤트가 발생되면 그때마다 대응되는 처리를 하는 방식으로 동작한다.

이러한 이벤트 반응형프로그램이 동작하기 위해서는 우선 이벤트가 발생했음을 프로그램에 알리는 구조가 필요하다. Windows에서는 각각의 응용프로그램이 마우스나 키보드로부터 직접 입력을 받지 않는다. 대신 이러한 이벤트가 발생하는지 Windows가 확인한 뒤, 이벤트가 발생했을 때 응용프로그램에 통지한다.

[그림 1] 이벤트 전달 과정

좀 더 자세히 알아보면, 이러한 각 이벤트는 하드웨어의 장치 드라이버가 처리해야 할 일이다. 장치 드라이버는 이벤트를 감지했을 경우 이를 Windows에 알린다. 하지만 Windows가 각 하드웨어의 모든 부분까지 알지는 못하므로 이벤트를 좀 더 단일화된 형태로 변환한 뒤 시스템에 이를 넣는다. 시스템 큐에 추가 된 각 이벤트는 선입선출 방식으로 Windows가 하나씩 꺼내어 해당 응용프로그램에 이벤트를 넘겨준다.

 

Windows Message

사용자로부터 발생한 이벤트에 대해 각 응용프로그램의 큐에 전달된다고 하였다. 좀더 정확하게는 응용프로그램보다도 응용프로그램의 특정 윈도우에 이벤트를 알려준다고 해야한다. 이는 아래 그림과 같이, 하나의 응용프로그램이지만 여러 윈도우를 가진 경우를 생각하면 된다. 사용자가 입력한 키보드 이벤트(WM_KEYDOWN)‘Proc.exe’이란 프로세스에 도착하더라도, 어떤 윈도우가 이를 받아야하는지 나타나지 않는다면 처리가 복잡해진다.

[그림 2] 여러 윈도우를 가진 응용프로그램

이 때문에 사용자 입력 등의 이벤트는 특정 윈도우와 연관된다. Windows 에서 각각의 윈도우에 이벤트를 알리기 위해선 “Window Message”라는 구조체를 이용한다. 구조체의 첫 번째 인자를 보면 각 메시지가 윈도우 핸들을 담고 있다. 이 값을 통해 조작 대상 윈도우를 구별할 수 있다. 그리고 두 번째 인자 Message ID를 통해 어떤 이벤트가 발생했는지 알 수 있다.

typedef struct tagMSG {

  HWND        hwnd;          // Window Handle

  UINT          message;       // Message ID

  WPARAM      wParam;        // Additional information about the message

  LPARAM       lParam;         // Additional information about the message

  DWORD       time;          

  POINT         pt;             // Cursor position

} MSG, *PMSG, *LPMSG;

[코드 1] 윈도우 메시지 구조체

그렇다면 자신에게 온 메시지를 어떻게 확인할까? 위에서 말했다시피 이벤트가 발생하면 해당 응용프로그램 큐(Application Queue)에 메시지가 추가된다고 하였다. 큐에 들어가 있는 메시지는 GetMessage API를 통해 꺼내어 온다. 여기서 주목해야할 인자는 바로 첫 번째 인자로, 인자로 넘겨준 주소에 메시지 정보들을 담아 반환해준다.

BOOL WINAPI GetMessage(

  _Out_    LPMSG  lpMsg,

  _In_opt_  HWND  hWnd,

  _In_      UINT   wMsgFilterMin,

  _In_      UINT   wMsgFilterMax

);

[코드 2] GetMessage API

OllyDBG를 통해 GetMessage API 호출 부분을 확인해보자. 아래의 코드를 보면 함수를 호출하기 위해 각 인자를 Stack에 넣어주는 것을 확인할 수 있다. 여기서 확인해야 할 것은 바로 밑에서 두 번째 줄에 위치한 ‘PUSH EAX’이다. EAX에는 메시지에 대한 정보가 저장될 주소를 담고 있다. 아래 코드에서는 0x4FF8A0인 것을 직접 확인할 수 있다.

Address       Command                             Comments

00B418A1  | PUSH 0                               ; |MsgFilterMax = 0

00B418A3  | PUSH 0                               ; |MsgFilterMin = 0

00B418A5  | PUSH 0                               ; |hWnd = NULL

00B418A7  | LEA EAX,[LOCAL.24]                    ; |

00B418AA  | PUSH EAX                            ; |pMsg : 0x4FF8A0

00B418AB  | CALL DWORD PTR DS:[GetMessage]     ; \USER32.GetMessageW

[코드 3] Disassembly GetMessage

메시지 정보가 반환될 주소를 확인하고 GetMessage API를 호출해보자. 아래의 두 덤프 중 상단의 내용이 하단의 내용으로 바뀌는 것을 확인할 수 있다. 여기서 가장 중요한 것은 바로 메시지 ID0x4FF8A40x100 (WM_KEYDOWN) 메시지가 있는 것을 확인할 수 있다.

Address   Hex dump                                         ASCII

004FF8A0  CC CC CC CC|CC CC CC CC|CC CC CC CC|CC CC CC CC| ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌ

004FF8B0  CC CC CC CC|CC CC CC CC|CC CC CC CC|CC CC CC CC| ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌ

Address   Hex dump                                         ASCII

004FF8A0  52 0B 35 00| 00 01 00 00 | 11 00 00 00|01 00 1D 00|   R5       

004FF8B0  68 3B 3B 0D | 7E 03 00 00 |81 01 00 00|CC CC CC CC|    h;;

~ÌÌÌÌ

[코드 4] GetMessage Dump

Hex                 Decimal           Symbolic

0000                0                    WM_NULL

0001                1                    WM_CREATE

0002                2                    WM_DESTROY

0003                3                    WM_MOVE

0100                256                 WM_KEYDOWN

0201                513                 WM_LBUTTONDOWN

020a                522                 WM_MOUSEWHEEL

[ 1] 주요 Message Numbers

결국 메시지 큐에서 꺼내온 메시지는 Window Handle이 가리키는 윈도우에 전송되어 윈도우 프로시저에서 처리한다. 윈도우 프로시저는 Windows에 의해 직접 호출되기 때문에 “CALLBACK” 이라는 타입을 지정해주어야 한다. 첫 번째 인자를 통해 어떠한 Window Handle인지 알 수 있고, 두 번째 인자는 GetMessage를 통해 받은 메시지 ID가 인자로 들어오게 된다..

LRESULT CALLBACK WindowProc(

    HWND   hWnd,        // Window Handle

    UINT     uMsg,        // Message ID

    WPARAM wParam,      // argv 1

    LPARAM  lParam       // argv 2

);

[코드 5] Window Procedure

윈도우 프로시저 처리는 비교문을 통해 프로그래머가 지정한 번호의 메시지가 오는지 확인한다. 아래의 코드를 보면 CMP 명령어를 통해 GetMessage로 받은 Message ID(Local.49)0x100 (WM_KEYDOWN)인지 확인한다. 맞을 경우 사용자가 지정해 놓은 MessageBox 출력을 수행한다.

Address        Command                                  Comments

00B44ED0  |.  CMP DWORD PTR SS:[LOCAL.49], 100        ; WM_KEYDOWN

00B44EDA  |.  JE SHORT 00B44EFF

00B44EFF  |   MOV ESI,ESP

00B44F01  |.  PUSH 40

00B44F03  |.  PUSH OFFSET 00B46C08                  ; |Caption = "Message"

00B44F08  |.  PUSH OFFSET 00B46D08                  ; |Text = "Key Down!"

00B44F0D  |.  MOV EAX,DWORD PTR SS:[ARG.1]         ; |

00B44F10  |.  PUSH EAX                               ; |hOwner => [ARG.1]

00B44F11  |.  CALL DWORD PTR DS:[<MessageBox>     ; \USER32.MessageBox

[코드 6] Disassembly Window Procedure

 

'O / S > Window' 카테고리의 다른 글

네트워크 패킷 캡처 (Windows)  (0) 2020.11.25
Windows Multi Task  (0) 2016.07.08
Windows Service  (0) 2016.06.21
Windows Boot Process (Vista 이상ver 부팅 과정)  (0) 2016.04.13
CSIDL 값  (0) 2016.02.20

Windows Multi Task

Kail-KM
|2016. 7. 8. 18:53

 

개요

우리는 학교 과제를 하기 위해 HWP나 Word, Excel, PPT 등의 응용프로그램을 실행해야 한다. 하지만 단순히 이러한 동작만을 하는 것이 아니라 노래를 듣는 동시에 코드를 짜거나, 이에 더해 PC 톡으로 친구들에게 물어보기도 한다. 우리가 이러한 프로그램들을 실행하면 결과적으로 CPU에서 명령어를 처리하게 된다. 그렇다면 어떻게 CPU가 하나뿐이더라도 동시에 여러 작업이 가능해지는 것일까? 이를 위해 프로세스와 스레드가 어떻게 동작하는지에 대하여 알아보자.

프로그램은 일반적으로 하드 디스크 등에 저장되어 있는 실행코드를 뜻하고, 프로세스는 프로그램을 구동하여 프로그램 자체와 프로그램의 상태가 메모리 상에서 실행되는 작업 단위를 지칭한다. 예를 들어, 하나의 프로그램을 여러 번 구동하면 여러 개의 프로세스가 메모리 상에서 실행된다. 프로세스란 단순히 사용자가 실행시킨 실행 파일만을 필요로 하는 것이 아니라 프로그램을 읽어 들일 메모리와 데이터를 보관할 메모리, CPU의 레지스터 그리고 Windows의 경우 윈도우와 파일 등의 리소스도 사용하여 프로그램을 작동시킨다. 이처럼 프로그램을 관리하기 위한 환경을 묶어서 'Context'라고 한다.

 

멀티 태스크

어떻게 여러 작업을 동시에 처리할 수 있을까? 위에서 말했다시피 모든 명령어는 CPU에서 처리해주므로 동작하게 된다. CPU가 두 개일 경우 동시에 두 가지의 작업을 각각의 CPU에서 처리할 수 있겠지만, 하나의 CPU만 쓰는 PC에서는 어떻게 이러한 동시 작업이 가능해지는가에 대답은 바로 우리가 그렇게 느끼도록 하는 것이다. 사실 동시에 작업을 한다고는 하지만 정확히 말하자면 아주 짧은 시간마다 작업을 전환하여 모든 작업이 동시에 처리되는 것마냥 느끼도록 한다.

예를 들어 위의 그림과 같이 몇 개의 프로세스가 메모리에 올라와 있다고 하자. 이때 CPU가 처리할 수 있는 것은 하나의 작업뿐이다. 그렇기에 먼저 프로세스 A를 처리하고, 처리가 끝나면 이를 다시 중지하고 프로세스 B를 처리한다. 마찬가지로 B를 처리하다가 다음엔 C를 처리하는 방식으로 동작하게 된다. 이렇게만 이야기하면 당연히 멀티 태스크라는 말이 안 어울리며 단일 태스크인 것 같지만, 아주 빠른 시간 내에 이런 작업이 이루어진다면 사용자가 느끼기에는 충분히 멀티 태스크가 되는 것이다.

그렇다면 태스크의 전환은 어떻게 이루어지는지에 대하여 프로세스를 예로 알아보자. 우선 응용프로그램이 자발적으로 운영체제에게 제어를 넘기는 것이다. 이를 '비선점형 멀티 태스크'라고 하며 응용프로그램의 처리가 끝나서 함수에서 빠져나오면 Windows로 제어권이 반환되고 이 시점에서 다른 응용프로그램으로 전환하는 경우가 예이다. 하지만 이러한 협조 방식에는 문제가 있다. 바로 하나의 응용프로그램에서 제어를 반환하지 않는다면 다른 모든 응용프로그램은 동작할 기회를 갖지 못하게 된다. 운영체제는 제어권을 응용프로그램으로부터 기다려야 하기 때문에 이는 결국 운영체제가 프로세스의 실행을 제어한다고 하기 어렵다고 할 수 있다.

이러한 단점으로 인해 나온 것이 바로 '선점형 멀티 태스크' 구조로 운영체제는 응용프로그램이 제어를 넘겨줄 때까지 기다릴 필요 없이 강제적으로 실행을 중단시켜 다른 응용프로그램으로 전환 시킬 수 있다. 이는 프로세스 실행의 제어권을 운영체제가 가지고 있다고 할 수 있으며, 강제적으로 실행을 중단시키는 방법에 사용되는 것이 바로 'Interrupt'로 이는 하드웨어로부터 제어신호와 CPU 명령 처리 중에 일어나는 여러 가지 이벤트를 계기로 강제로 특정 코드를 실행하도록 하는 CPU 기능의 일종이다. 인터럽트 처리에도 여러 종류가 있지만 그 중 하드웨어 타이머(클럭)에서 정기적으로 인터럽트를 거는 것이 있다. 운영체제는 이를 설정해서 정기적으로 인터럽트를 받고, 필요에 따라 프로세스 전환에 사용한다.

선점형 태스크 전환은 태스크의 작업이 완료되지 않아도 CPU를 선점하므로 이를 위해선 태스크 스위칭이 일어날 때 기존에 실행중인 태스크의 상태를 보존하고 있다가, 이후 재선점 시에 이를 가지고 올 필요가 있다. 이러한 작업 내용 저장 위치는 작업 단위에 따라 달라지게 된다. 프로세스를 하나의 작업 단위로 볼 경우 PCB(Process Control Block)에 대하여 알아야 한다. PCB에는 프로세스에 대한 다양한 정보들이 저장되어 있는데 스위칭이 일어날 때 바로 이 PCB와 관련된 동작을 수행한다.

만약 현재 Process #A가 CPU에서 실행 중인 상태에서 Process #B로 전환이 일어나면 Process #A가 Ready 상태가 되면서 해당 프로세스의 상태나 레지스터 값 등이 Process #A의 PCB에 저장된다. 반대로 Process #B는 Running 상태가 되면서 Process #B의 PCB에 저장된 내용들을 CPU로 적재 시킨다. 이와 같은 작업을 통해 Context Switching을 진행한다.

하지만 이러한 Process의 Context Switching은 상대적으로 많은 내용을 메모리에서 CPU로 옮기고 다시 CPU에서 메모리로 옮기는 작업을 수행해야 한다. 이에 반해 Thread를 작업 단위로 볼 경우 훨씬 용이한 Context Switching가 가능해진다. Thread는 Process 보다 작은 작업 단위임을 위에서 언급하였다. 이는 Process 마다 하나의 PCB와 주소 공간을 가지는 반면에 Thread의 경우 하나의 Process 안에서 많은 내용을 공유한다. 아래의 그림을 보자.

Case #1의 경우 Process A가 Process B를 생성한 것으로 두 프로세스 간에 전혀 공유되는 요소가 없이 각각의 요소를 서로 갖고 있는 것을 확인할 수 있다. 이에 반해 Case #2의 경우 Process C가 Thread #1과 Thread #2를 생성한 경우로 두 Thread 간에 Code, Data, Heap을 Process 안에서 공유하고 있다. 다시 말해 각각의 Thread는 Stack 영역만을 개별적으로 갖는다. 아래는 실제 프로그램의 메모리를 나타낸 것으로 네 개의 Thread가 각각의 Stack 공간을 갖고 있는 것을 확인할 수 있다.

Memory map

Address Size Owner Section Contains Type Access Initial >Mapped as

006FE000 00002000 > > Stack of main thread Priv >RW RW

00A8C000 00002000 > >

00A8E000 00002000 > > Stack of thread 2. >Priv >RW RW

00B8D000 00002000 > >

00B8F000 00001000 > > Stack of thread 3. >Priv >RW RW

00C8D000 00002000 > >

00C8F000 00001000 > > Stack of thread 4. >Priv >RW RW

 

이에 관련하여 Thread를 생성하는 API인 CreateThread()에도 해당 Thread에게 할당하고자 하는 Stack의 크기를 지정해줄 수가 있다. 이와 같이 Thread릁 통한 태스크 전환은 프로세스 기준 태스크 전환보다 훨씬 교환해야 할 요소가 적어 리소스를 덜 사용하게 된다. Context Switching이 일어나는 동안 CPU는 아무런 일을 하지 못한다는 점을 고려했을 때, 이러한 시간을 줄이는 것은 당연히 중요한 요소가 된다. 따라서 유사한 동작을 하는 두 개의 Task를 만들 경우 굳이 새로운 Process로 하는 것보단 데이터를 공유하는 Thread로 하는 것이 효율적이다.


HANDLE WINAPI CreateThread(

_In_opt_   LPSECURITY_ATTRIBUTES   lpThreadAttributes,

_In_       SIZE_                  dwStackSize,

_In_        LPTHREAD_START_ROUTINE lpStartAddress,

_In_opt_   LPVOID                  lpParameter,

_In_       DWORD                   dwCreationFlags,

_Out_opt_ LPDWORD                 lpThreadId

);

 

우선 순위

마지막으로 스케줄링 우선 순위에 대하여 알아보자. 여러 개의 응용프로그램을 좋은 효율로 동작하게 하려면 어떤 것을 먼저 어느 만큼 실행할 지가 매우 중요하다. 이러한 조절을 '스케줄링'이라고 하며 이는 운영체제에 따라 조금씩 방식이 다르다. Windows의 경우 Thread 우선 순위에 기반한 방식과 라운드-로빈 방식을 조합한 것이다. 우선 순위는 어떤 Thread를 우선해서 실행할 것인가를 나타내는 정수 값으로 0~31까지의 우선 순위가 있으며, Windows 에서는 Process 마다 기본적인 우선순위를 나타내는 Priority Class를 지정하고 있다. 물론 이러한 Thread 우선순위는 Process와 Thread를 생성할 때 지정되며, 실행 도중 SetThreadPriority API를 호출하여 우선순위를 변경할 수 있다. 

 

상수

 

기본 우선 순위

 

REALTIME_PRIORITY_CLASS

 

24

 

HIGH_PRIRITY_CLASS

 

13

 

ABOVE_NORMAL_PRIORITY_CLASS

 

10

 

NORMAL_PRIORITY_CLASS

 

8

 

BELOW_NORMAL_PRIORITY_CLASS

 

6

 

IDLE_PRIORITY_CLASS

 

4

 

Windows는 실행 큐를 우선 순위가 높은 것부터 조사해서 실행 큐의 앞쪽으로 옮긴다. 그리고 CPU에 있는 Thread가 대기 상태로 넘어가게 되면 새로운 우선순위가 높은 실행 큐를 CPU로 가지고 와서 실행한다. 그리고 대기 중인 Thread에 있는 Thread가 실행 준비가 되면 실행 Queue로 옮겨져 CPU에 의해 동작되길 다시 기다린다. 이와 같은 전환은 Thread가 주어진 시간 간격을 사용한 경우 외에도 디스크 접근이나 동기화 등을 위해 자발적인 대가 상태에 들어간 경우와 실행 도중에 우선순위가 더 높은 Thread의 실행 준비가 끝났을 경우 발생한다.

하지만 이처럼 규칙에 충실한 스케줄링이 안 좋을 때가 있다. 예를 들어 우선순위가 높은 Thread가 하나일 경우에는 남겨진 Thread가 영원히 실행되지 않는다. 그래서 실행 준비를 마친 Thread가 일정시간 동안 실행 기회를 얻지 못했을 경우 Windows에서는 그 Thread의 우선순위를 일시적으로 올려주도록 되어 있다. 또한 디스크 접근이 완료되어 실행준비가 되었을 때나 GUI Thread가 동작을 시작했을 때도 그 Thread의 우선순위를 일시적으로 올려준다. 이런 조작을 'Priority Boosts'라고 한다.

 

Reference

+ [API로 배우는 Windows 구조와 원리] p.33 ~ p.38

+ "컨텍스트 스위칭", http://blog.eairship.kr/257

+ "스레드 다루기 (기초편)", http://www.jiniya.net/wp/archives/7194    

+ "x86에서 TSS", http://onestep.tistory.com/31

+ MSDN, " https://msdn.microsoft.com/"

+ "프로세스가 뭐지?", http://bowbowbow.tistory.com/16

'O / S > Window' 카테고리의 다른 글

네트워크 패킷 캡처 (Windows)  (0) 2020.11.25
Windows Event Message  (0) 2016.07.13
Windows Service  (0) 2016.06.21
Windows Boot Process (Vista 이상ver 부팅 과정)  (0) 2016.04.13
CSIDL 값  (0) 2016.02.20

Windows Service

Kail-KM
|2016. 6. 21. 08:59
Introdution

윈도우 운영체제에 있어 우리가 흔히 직접 실행할 수 있는 프로그램 이외에 우리가 직접 실행하지 않아도 실행되는 프로세스가 있다. 이 중 서비스는 보통 시스템의 시작과 함께 시작되어 윈도우의 핵심 프로세스의 기능을 수행한다. 중요한 만큼 사용자의 개입을 최소화하기 위해 백그라운드에서 동작한다. 프로세스에 대한 제어권을 우리가 갖지 않고 윈도우의 서비스 제어 관리자가 가지고 있기 때문에 일반적인 방법으로 서비스를 분석할 수 없다. 이러한 요인들로 인해 악성코드 제작자는 서비스를 통해 백그라운드라는 점, 사용자 개입의 최소화 등의 이점을 누릴 수 있다. 따라서 이번 문서에서는 서비스가 어떻게 동작하는지, 그리고 이를 분석하기 위해서는 어떠한 방식을 사용해야 하는지에 대하여 알아보자.


Service Control Manager(SCM)

Windows에서 서비스를 실행할 때 중요한 역할을 수행하는 것이 SCM이다. SCM은 Boot시에 시작되는 시스템 프로세스의 일종으로, 서비스를 제공하는 프로그램의 등록, 시작, 종료, 삭제 등과 같은 모든 관리를 담당한다.  일반적으로 아래의 그림과 같이 우리는 Controller를 통해 SCM에게 서비스의 실행, 중지 등을 요청하여 서비스 프로그램을 시작하고 종료할 수 있다. 이렇듯 사용자나 다른 프로그램이 Service를 조작할 경우에는 반드시 SCM을 통해야만 한다. 이를 반대로 말하면 서비스 프로그램은 SCM에서 제어할 수 있는 장치를 넣어둔 프로그램이라 할 수 있다.

서비스 프로그램은 SCM에서 제어할 수 있는 장치를 가지고 있다는 것은 서비스 프로그램의 내부에 SCM에 제어를 요청하거나 연결을 요청하는 API가 존재하고 있다는 것이다. 만약 해당 API들이 없다면 서비스 프로그램은 단순히 일반 프로그램과 다를 바가 없다. 아래는 서비스 프로그램의 간략화된 main()으로 일반적인 프로그램의 main()과는 약간 다른 코드를 갖고 있다. 일반적인 프로그램의 경우 Main()에서부터 메인 스레드를 통해 독자적으로 처리하면 되지만, 이와는 다르게 서비스 프로그램의 경우에는  Main 함수에서 프로그램과 SCM을 연결해주는 StartServiceCtrlDispatcher API를 호출해주어야 한다. 여기서 연결이란 서비스의 진입점을 SCM에게 등록해주는 것으로, 해당 API를 통해 SCM은 서비스의 제어를 가지게 되고 서비스가 정지할 때까지 제어를 돌려주지 않는다.


해당 API의 인자로 지정된 services는 배열의 형태를 가지고 있다. 아래의 코드에서 "TestSvc"는 서비스의 이름을 지정해주는 것이고, TestSvcMain은 해당 서비스 프로그램이 SCM에게 실행을 요청할 내용의 코드이다. 이는 다시 말해 서비스 전용 메인 함수로 SCM은 새롭게 스레드를 생성해서 서비스 메인의 함수를 호출한다. 따라서 우리가 이후에 분석을 할 때에 중점적으로 확인해야 할 부분이 바로 TestSvcMain이 위치한 곳이 된다.

#include <Windows.h>
#include <stdio.h>

int main()
{
    SERVICE_TABLE_ENTRY services[] = {{"TestSvc", TestSvcMain}, {NULL, NULL}};
    if(!StartServiceCtrlDispatcher(services))
    {
        printf("서비스 연결 실패");
        return -1;
    }
    return 0;
}


ServiceMain & Control Handler

서비스 프로그램의 전용 메인 함수(위 예의 TestSvcMain)가 SCM과 연결되어 프로세스가 생성되었지만, SERVICE_START_PENDING 상태가 된다. 이는 정식 서비스로 동작하는 것은 아니며 이를 동작시키기 위해선 자신의 서비스 상태를 지정해주어야 한다. 서비스의 상태를 제어하기 위해 서비스의 메인 함수에서는 RegisterServiceCtrlHandler API를 호출해야 한다. 이는 SCM에 제어 핸들러를 등록하기 위한 API로, SCM은 서비스를 제어할 필요가 있을 때 이 핸들러를 호출하는 방법으로 서비스에 통지한다. 이것이 등록되어야 SCM이 서비스 프로그램을 제어할 수 있다. 

SERVICE_STATUS_HANDLE WINAPI RegisterServiceCtrlHandler( 
    _In_ LPCTSTR            lpServiceName,  
    _In_ LPHANDLER_FUNCTION lpHandlerProc
);

제어 핸들러를 등록하고 SetServiceStatus API를 통해 서비스가 시작 상태가 된 것을 SCM에 통지해야 한다. 시작된 것을 알리고자 할 때는 SERVICE_RUNNING을 인자로 해당 API를 호출하면 된다. 이러한 과정은 서비스 프로세스가 생성되고 1초 이내에 완료될 것을 기대학 때문에 그 이상 시간이 걸릴 경우 미리 설정해주어야 한다. 만약 지정한 시간이 지나도 상태가 갱신되지 않을 경우 SCM은 초기화에 실패했다고 판단하고 서비스를 종료한다. 시간 내에 SetServiceStatus를 통해 SERVICE_RUNNIGN을 지정해주었다면 비로소 정상적인 서비스 프로세스로 동작할 수 있게 된다.

BOOL WINAPI SetServiceStatus(  
    In_ SERVICE_STATUS_HANDLE hServiceStatus,  
    In_ LPSERVICE_STATUS      lpServiceStatus
);



Reference


https://ko.wikipedia.org/wiki/윈도우_서비스

[API로 배우는 Windows 구조와 원리]

[리버싱 핵심원리]



'O / S > Window' 카테고리의 다른 글

Windows Event Message  (0) 2016.07.13
Windows Multi Task  (0) 2016.07.08
Windows Boot Process (Vista 이상ver 부팅 과정)  (0) 2016.04.13
CSIDL 값  (0) 2016.02.20
Write Protection - Registry Setting  (0) 2016.01.17

WFP 무력화

Kail-KM
|2016. 6. 21. 08:59
WFP (Windows File Protection)

WFP는 중요한 Windows 시스템 파일이 대체 또는 변경되는 것을 방지하기 위해 Windows에서 기본적으로 제공하는 기능(Vista부터는 WRP로 대체)으로, 프로그램들이 Windows 시스템의 중요한 파일들을 덮어씌울 수 없게 하여 프로그램과 운영체제로부터 발생할 수 있는 문제를 사전에 방지한다. WFP는 보호하고자 하는 시스템 파일이 올바른지 확인하기 위해 코드 서명에 의해 생성된 카탈로그와 파일 시그니쳐를 사용하여 확인한다. 그렇다면 정상적인 경우라도 이러한 파일의 변경이 일어날 수 없을까? 시스템 파일에 치명적인 취약점이 발견되었을 경우 WFP에 의해 해당 파일을 대체하지 못한다면 이는 위험을 품고 있는 OS가 되어버릴 것이다. 따라서 보호되고 있는 파일을 대체하기 위한 방법들이 존재하고 있으며, 오직 아래의 방법들을 통해서만 대체가 가능하다.

- Update.exs를 통한 Windows 서비스 팩 설치
- Update.exe나 Hotfix.exe를 통한 Hotfixes 설치
- Winnt32.exe를 통한 운영체제 업그레이드
- Windows 업데이트

만약 프로그램이 다른 방법으로 보호되고 있는 파일을 대체하고자 한다면 WFP는 원래의 파일로 복구하고자 한다. Windows Installer는 중요한 시스템 파일을 설치할 때 WFP를 준수하고, 보호된 파일 자체를 설치하거나 교체하는 대신 보호된 파일을 교체하라는 요청과 함께 WFP를 호출하게 된다.


How the WFP feature works

WFP는 두 가지 메커니즘을 통해 시스템 파일 보호 기능을 제공한다. 첫 번째 방법은 백그라운드에서 동작하는 것으로 보호되고 있는 디렉터리에서 변경이 일어난다면 변경에 대한 알림을 받은 후 동작하게 된다. 이러한 알림을 받은 WFP는 어떤 파일이 변경되었는지 결정하며 만약 그 파일이 보호되고 있다면 WFP는 파일 시그니쳐를 통해 해당 파일이 올바른 파일인지 확인하는 작업을 거치게 된다. 만약 파일이 올바르지 않다면 WFP는 새로운 파일을 Cache 폴더나 원본 설치 파일에 존재하고 있는 정상적인 파일로 바꾼다. 

1. Cache 폴더(Default : %SystemRoot%\system32\dllcache)
2. 네트워크 설치 경로(네트워크 설치를 사용하여 설치한 경우)
3. Windows CD-ROM(시스템이 CD-ROM으로부터 설치된 경우)

위의 표는 WFP가 정상 파일을 탐색하는 경로로 파일이 변조된 경우 해당 위치로부터 정상 파일을 찾아 복원한 뒤 다음과 같은 시스템 로그를 기록한다. 해당 로그에선 보호되고 있는 파일을 대체하고자 시도했다는 기록을 볼 수가 있다.

Event ID: 64001 
Source: Windows File Protection 
Description: File replacement was attempted on the protected system file c:\winnt\system32\file_name. This file was restored to the original version to maintain system stability. The file version of the system file is x.x:x.x.


How to bypass the WFP

WFP는 두 개의 DLL(SFC.DLL과 SFC_OS.DLL)을 통해 구현되며 ReadDirectoryChacnge API를 사용하여  주요 폴더의 변경 여부를 검사한다. Windows 시스템의 중요 프로세스인 winlogon.exe는 시스템 부팅시 SFC_OS.DLL을 로드시키고, 해당 라이브러리의 Ordinal#1(SfcInitProt)를 호출한다. 해당 함수는 새로운 스레드('SFC Watcher Thread)를 하나 생성하며 이 스레드는 보호 대상 파일이 존재하고 있는 폴더에 대한 Directory Change Notification 이벤트를 생성한다. 이러한 보호폴더 이벤트는 WaitForMultipleObjects 함수에 의해 동기화되며, 만약 보호 대상 파일이 변경 또는 삭제된 경우 Cache 폴더에서 해당 파일을 원상 복구하도록 한다. 만약 백업 폴더에 대상 파일이 존재하지 않다면 사용자에게 윈도우 CD를 삽입하라는 메시지를 출력하게 된다.


SFC.DLL : Windows 2000에서는 WFP의 핵심기능을 담당하지만 XP부터는 SFC_OS.DL의 보조 역할

SFC_OS.DLL : XP부터 WFP의 핵심 기능을 담당

SFCFILES.DLL : 현재 보호되고 있는 파일의 리스트를 관리

SFC.EXE : System File Checker Utility


이러한 WFP는 주요 Windows 파일을 보호하는 메커니즘이니 만큼 악성코드에 의한 타깃이 되고 있다. 최근에도 주요 시스템 파일을 교체 또는 패치하는 형태의 악성코드가 다수 등장하고 있으며, 이로 인한 시스템 불안정, BSOD 등이 발생하고 있다. 그렇다면 이제 WFP를 무력화하는 방법에 대하여 알아보자.


Method #1 


WFP를 무력화시키는 첫 번째 방법은 winlogon이 갖고 있는 Direcotry change notification handle을 종료시키는 방법으로 이 방법을 사용하면 시스템이 재부팅하기 전까지 특정 폴더에 대한 파일 보호가 이루어지지 않는다. 해당 핸들을 종료하기 위해서는 ntdll.NtDuplicateHandle이나 kernel32.DuplicateHandle을 통해 해당 핸들을 복제한 다음 CloseHandle을 통해 핸들을 닫으면 된다.

BOOL WINAPI DuplicateHandle(        // Duplicates an onject handle
  _In_  HANDLE   hSourceProcessHandle,
  _In_  HANDLE   hSourceHandle,
  _In_  HANDLE   hTargetProcessHandle,
  _Out_ LPHANDLE lpTargetHandle,
  _In_  DWORD    dwDesiredAccess,
  _In_  BOOL     bInheritHandle,
  _In_  DWORD    dwOptions
);


Method #2


WFP를 무력화하는 두 번째 방법은 Winlogin.exe가 SFC_OS.DLL을 로드한 뒤 생성하였던 'SFC Watcher Thread'를 종료시키는 것이다. 해당 스레드를 종료시키기 위해서는 SFC_OS.DLL이 export 하고 있는 #2(SfcTermintaeWatcherThread)를 이용하는 것으로 해당 API는 파라미터를 필요로 하지 않는다. winlogon.exe에서 해당 API를 호출하면 해당 스레드는 종료되고, 이로 인해 재부팅 전까지 WFP 기능은 무력화된다.


SfcTerminateWatcherThread를 호출하기 위해서는 SeDebugPrivilege 권한이 필요하며 해당 스레드를 생성한 프로세스인 winlogon.exe에서 실행되어야 한다. 그러므로 이를 위해 Injection의 기법을 사용하는 경우가 많다는 것을 알 수 있다.


Method #3


세 번째 방법은 SFC API를 이용하여 특정 파일에 대한 WFP를 1분 동안 무력화하는 방법으로, 실제로 악성코드가 주로 사용하는 방법이다. sfc_os.dll의 ordinal #5 : SfcFileException을 이용하는 방법으로, 해당 API는 특정 파일에 대하여 1분 동안 WFP 기능을 무력화시킨다. 이 방법을 사용하기 위해서는 LoadLibrary를 통해 sfc_os.dll을 로드한 뒤 GetProcAddress에 "#5"를 넘겨주어 호출하는 코드를 확인할 수 있을 것이다.

DWORD WINAPI SfcFileException(DWORD ?, PWCHAR pwszFileName, DWORD ?);

위 구조와 같이 두 번째 인자에 해당 파일의 이름을 넣어주면 되고 첫 번째 인자와 세 번째인자는 알 수 없는 인자지만 첫 번째에는 0을, 세 번째 인자에는 -1을 넣어주어야 한다. 해당 API가 성공할 경우 return value는 0이며 만약 성공하지 못한 경우에는 1이 반환된다.


Method #4


네 번째 방법은 Undocumented 레지스트리 값을 이용하는 방법으로 Windows 2000 SP1 이전 버전까지만 가능한 방법이다. 해당 레지스트리를 설정하면 WFP는 영구적으로 무력화된다. 

KEY : HKLM\Software\Policies\Microsoft\Windows NT\Windows File Protection 
Value Name : SFCDisable
Value : 0xFFFFFF9D


Method #5


마지막 방법은 Protected File List를 패치하여 특정 파일에 대한 WFP를 영구적으로 무력화하는 방법이다. WFP가 보호하고자 하는 대상은 SFCFILES.DLL에 정의되어 있음을 언급했다시피 해당 대상이 되는 파일의 내용을 패치하여 특정 파일에 대한 WFP를 영구적으로 무력화할 수 있다.


이를 위해선 SFCFILES.DLL을 복사하여 무력화하고자 하는 파일을 찾은 다음 해당 이름의 첫 바이트를 \x00으로 바꾸어 준다. 이렇게 내용을 수정한 뒤 'PEChkSum'를 이용하여 체크섬 문제를 해결하고, 'MoveLatr'를 통해 부팅 시에 원본 파일을 대신해 수정한 파일을 로드하도록 설정해주면 된다. 이러한 준비를 끝냈다면 프로세스를 완료하기 위해 재부팅해주어야 한다.



Reference


https://support.microsoft.com/en-us/kb/222193

https://bitsum.com/aboutwfp.asp

http://sinun.tistory.com/144


'Reversing > Theory' 카테고리의 다른 글

Memory Detection(메모리 진단)  (0) 2016.09.26
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
DLL이란?  (4) 2016.05.29
PE구조의 이해  (0) 2016.05.04
윈도우 후킹 원리 [PDF]  (1) 2016.04.23

DLL이란?

Kail-KM
|2016. 5. 29. 22:55

DLL ( Dynamic Link Library )

DLL은 동적 링크 라이브러리(Dynamic Link Library)의 약자로 일반적으로 확장자가 DLL인 파일이다. 라이브러리라는 말에서 알 수 있듯이 다른 프로그램에서 이용하는 함수들을 모아둔 것이다. 하지만 표준 C 라이브러리 같은 일반 라이브러리의 파일과는 구조나 사용법이 다소 다르다. 일반 라이브러리는 소스코드를 컴파일한 결과로 생성되는 객체 파일(.OBJ)을 그대로 모아둔 것이다. 링커는 이 중에서 필요한 함수가 포함된 객체 파일을 꺼내서 실행 파일에 결합하는 '정적 링크' 방식이다.

아래 그림은 정적 링크를 나타내는 것으로 C/C++ 프로그램의 소스 코드를 기계어 코드로 변환하는 컴파일 단계를 거치게 된다. 여기서 C/C++에는 수많은 표준함수들이 존재하고 있는데 이들은 표준 라이브러리 파일 안에 어셈블리 코드의 형태로 담겨 있다. 소스 코드는 하나 이상 존재할 수가 있는데, 링크 단계는 이 여러 개의 소스 파일들이 하나의 실행 파일로 구성해낸다. 이때 각각의 소스파일에서 호출한 표준 함수들을 표준 라이브러리에서 가져와 실행파일에 붙여준다. 이러한 과정을 링크 과정이라 한다. 그리고 이러한 방식이 바로 '정적 링크'이다. 

하지만 많은 표준 함수를 사용할수록 프로그램의 크기가 커지게 되며, 똑같은 함수를 사용한다고 하더라도 이러한 정적 링크 방식은 각 프로그램마다 링크 과정에서 라이브러리를 가져와 프로그램 안에 저장하기 때문에 이는 비효율적이라 할 수 있다.

정적 링크 방식과는 다르게 DLL은 '동적 링크'에 방식으로, 이는 링크 시에 실행 파일에 결합되는 것이 아니라 프로그램 실행 시에 DLL도 함께 프로그램의 메모리 공간으로 읽어와 호출될 주소 등을 적절하게 바꾸는 것을 말한다. 일단 읽어온 DLL 함수는 프로그램 내부 함수처럼 호출할 수 있다. DLL은 실행 파일과 다른 파일이므로 필요한 시점에 메모리로 읽어오고 불필요하면 메모리에서 내릴 수 있다.

이러한 방식으로 인해 DLL은 여러 장점을 갖게 된다. 우선 여러 프로그램에서 동시에 사용할 수 있다는 것이다. 정적 링크 방식은 자신이 가진 코드를 자기 혼자만 사용하지만, 동적 링크 방식은 하나의 DLL로 존재하여 다른 프로그램에게 라이브러리를 제공해준다. 실행 파일은 DLL에 있는 함수를 Import하게 되는 것이고, DLL은 실행파일에게 함수를 Export 해주게 되는 것이다.

* Export Name Table

Offset Data Ordinal Value

==============================================

0x47C4 0xD9E1 0x01 HEEnterWindowLoop

0x47C8 0xD9F3 0x02 HEEnterWindowLoopInNewThread

0x47CC 0xDA10 0x03 HESpecifySettings

단, 함수나 변수가 실행 파일 안에 포함되지 않았기 때문에, 사용하고자 하는 함수나 변수를 컴파일러나 링커에게 알려주어야 한다. 만약 Export에 대한 정보가 없다면 실행파일은 DLL의 함수를 Import 할 수 없게 된다. 그러므로 위와 같이 Export 하는 함수에 대한 정보가 DLL에 기록되어 있어야 한다.

 

DLL Binding

EXE 파일은 사용하고자 하는 DLL의 함수를 메모리에 같은 메모리상에 올리게 되는데 이때 IAT에는 실제 사용하고자 함수들의 주소가 오게 된다. 다시 말해, 파일에서 IAT는 실제 함수의 주소를 가리키고 있지 않다. 왜냐하면 사용하고자 하는 함수의 주소를 아직 알 수 없기 때문이다. 하지만 메모리에 올라오면서 PE 로더는 IAT에 사용하고자 하는 함수의 실제 주소를 올려주므로 우리는 아무런 의심 없이 사용할 수 있다. 하지만 이러한 작업은 프로그램의 초기화 시간을 지연시키므로 MS는 이러한 제약을 피할 수 있도록 하나의 기능을 제공한다. 바로 IAT에 함수의 주소를 기록하는 작업을 미리 수행하여 로딩 시의 속도 향상을 도모하도록 한다. 이 과정을 바로 DLL 바인딩이라고 하며, 바인드 된 실행 파일의 IAT는 실제 함수의 주소를 가리키고 있게 된다.

* Import Name Table

Offset Data Name

==============================================

0xA3C 0x307C GetDriveTypeA

0xA40 0x308C ExitProcess

--------------------------------------KERNEL32.dll

0xA48 0x309A MessageBoxA

--------------------------------------USER32.dll

* Import Address Table

Offset RVA Data Value

==============================================

0xA50 0x3050 0x307C GetDriveTypeA

0xA54 0x3054 0x308C ExitProcess

--------------------------------------KERNEL32.dll

0xA5C 0x305C 0x309A MessageBoxA

--------------------------------------USER32.dll

우선 위의 표는 일반적인 파일의 Import Section 정보로 INT와 IAT가 같은 곳을 가리키고 있는 것을 확인할 수 있다. INT의 첫 번째 함수인 GetDriveTypeA인 0x307C의 이름의 위치를 IAT에서도 똑같이 가리키고 있다. 메모리에 올라오면서 IAT에 실제 주소가 기록되어 변경된다.

* Import Name Table

Offset Data Name

==============================================

0xA3C 0x307C GetDriveTypeA

0xA40 0x308C ExitProcess

--------------------------------------KERNEL32.dll

0xA48 0x309A MessageBoxA

--------------------------------------USER32.dll

* Import Address Table

Offset RVA Data Value

==============================================

0xA50 0x3050 0x6B826A00

0xA54 0x3054 0x6B827B30

--------------------------------------KERNEL32.dll

0xA5C 0x305C 0x6BAFFF90

--------------------------------------USER32.dll

위의 표는 같은 파일에 바인딩을 실시한 후의 모습으로 INT는 기존과 동일한 모습을 하고 있지만 IAT의 경우 Data에 실제 함수의 주소가 위치한 것을 확인할 수 있다. 이처럼 바인딩을 하게 되면 실제 함수의 주소를 가지게 되는 것을 알 수 있다.


DLL Relocation

DLL 재배치에 대하여 알아보기 전에 먼저 ImageBase에 대하여 알아보자. ImageBase란 PE 구조에서 해당 PE 파일이 PE 로더에 의해 메모리에 로드될 때 로드시키고자 하는 메모리의 주소가 된다. 보통 EXE 파일의 경우 0x4000000이며 DLL 파일의 경우 0x10000000이다.

typedef struct _IMAGE_OPTIONAL_HEADER {

WORD Magic;

BYTE MajorLinkerVersion;

BYTE MinorLinkerVersion;

….

DWORD AddressOfEntryPoint;

….

DWORD ImageBase;

….

하지만 위에서 말한 것과 같이 하나의 EXE 파일은 여러 라이브러리를 필요로 하는 경우가 일반적이기 때문에, 여러 DLL을 메모리에 올리고자 한다. 이 경우 DLL들의 ImageBase가 중첩된다면 하나의 메모리 주소에 여러 DLL이 존재할 수 없으므로 사용할 수 없게 된다. 다행히 DLL 재배치를 통해 원하는 ImageBase에 이미 다른 DLL이 올라와 있다면, 다른 주소에 맵핑될 수가 있다.

하지만 이러한 재배치 작업이 일어나면 PE 로더는 부차적인 작업을 수행해야 한다. DLL의 주소를 바꾸어 올리는 것뿐만 아니라, 해당 DLL의 Code Section의 일부 내용을 수정해야만 한다. 이에 대해 아래의 표를 보자. 아래의 표는 Relocation Section의 내용으로 하단 두 줄에 RVA가 있는 것을 확인할 수 있다. 일반적으로 이 주소가 가리키는 부분은 0x????????과 같은 4 Bytes의 주소를 나타내는 것으로 PE 구조에 기록되어 있는 ImageBase에 맞게 주소가 설정되어 있다. 하지만 ImageBase와 다른 곳에 로드되면 이 주소들은 ImageBase에 로드된 다른 DLL을 가리키게 되는 문제가 발생하므로 값을 수정해주어야 한다.

[+] Relocation Section --------------------

Base Address : 0x1000

Size of Block : 0x1069 (Num : 0x830)

Type Value : 0x5708 --- RVA : 0x1708 (Offset : 0xB08)

Type Value : 0x106C --- RVA : 0x106C (Offset : 0x46C)

다행히 이러한 주소 값의 수정을 사용자가 직접 하나하나 하는 것이 아니라 PE 로더가 재배치 섹션을 확인하여 알아서 수정해준다. 하지만 여기서 몇 가지 문제점이 존재하게 된다. 우선 재배치 정보가 가리키고 있는 값들은 대개 어떤 주소에 관한 값으로, 이러한 값들이 대개 코드 섹션에 위치하고 있다는 것이다. 따라서 이 값을 PE 로더가 수정하기 위해선 해당 섹션에 Write 속성을 추가한 뒤 수정을 하고, 수정을 마치면 다시 원래의 속성으로 되돌려야 한다는 것이다. 이에 더해 위 예에서는 2개의 값만 나타냈지만, 실제로는 더 많은 경우가 많기 때문에 PE 로더는 그 많은 주소의 값들을 직접 찾아 수정해주어야 한다. 만약 하나의 EXE 파일에 여러 DLL에 대하여 이러한 작업을 수행해야 한다면, 프로그램을 실행하기 위한 초기화 시간이 길어질 수 있다.

  

DLL Delay Loading & DLL Forwarding

DLL Delay Loading

상기의 이유들로 초기화 시간이 길어질 수 있다는 것에 대하여 알 수 있었다. 사실 하드웨어의 성능이 상향된 요즘은 별 상관이 없지만, 윈도우는 이러한 초기화 시간을 줄이기 위한 또 다른 방안을 구비해놓았다. 바로 DLL 지연 로딩으로, 단어에서와 같이 DLL을 프로그램 실행 시에 로드하는 것이 아니라 지연하여 로딩하는 것이다.

지연 로딩은 암시적 로딩에서의 간편함과 명시적 로딩에서의 유연함, 이 두 장점을 취하고자 하는 방식으로 EXE 작성에서 DLL 링크 시에는 암시적인 방식으로, 실제 런타임에서 사용 할 때는 명시적인 방식으로 작동하도록 한 것이다. 쉽게 말해 프로그램을 실행 시에 메모리에 매핑되는 것이 아니라 해당 DLL의 Export 함수들 중 하나가 최초로 실행될 때 그 시점에 해당 DLL을 로드해서 가상 주소 공간에 매핑한다는 것이다.

 

DLL Forwarding

DLL의 Export Function Forwarding이란 Export하고자 하는 함수를, 그 기능을 대신하는 다른 DLL 내에 정의된 함수의 호출로 대체하는 것이다. 글로 설명하는 것보다는 직접 코드를 확인 것이 더 좋으므로 일반적인 경우의 DLL의 Export 함수의 주소를 확인해보자. 아래의 표와 같이 Export하는 함수의 주소로 이동을 하면 해당 함수의 내용이 존재하고 있는 것을 확인할 수 있다. 즉, 자신의 DLL 안에 해당 코드를 그대로 잘 가지고 있는 것이다.

text:5F923F4B mov edi, edi

.text:5F923F4D push ebp

.text:5F923F4E mov ebp, esp

.text:5F923F50 sub esp, 1Ch

.text:5F923F53 mov eax, [ebp+arg_0]

.text:5F923F56 push ebx

.text:5F923F57 push esi

.text:5F923F58 push edi ; struct CApplnMgr *

.text:5F923F59 mov edi, [ebp+arg_4]

.text:5F923F5C mov [ebp+var_C], eax

이번에는 DLL Forwarding이 적용된 DLL의 내용을 확인해보자. 위 표와는 다른 DLL이기는 하지만 Export 하는 함수의 주소로 이동하여 확인해보면 심히 코드가 짧다는 것을 알 수 있다. Export 하고자 하는 함수의 이름 "LpkEditControl"과 함께 0x1000261C를 호출하는 것을 확인할 수 있다.

.text:10002BC8 MemCode_LpkEditControl proc near ; DATA XREF: .rdata:off_1001E148

.text:10002BC8 ; .data:LpkEditControl

.text:10002BC8 push offset aLpkeditcontr_0 ; "LpkEditControl"

.text:10002BCD call sub_1000261C

.text:10002BD2 jmp dword ptr [eax]

0x1000261C에는 다시 아래와 같은 내용이 있으며, 10002634를 호출한 다음, 이전에 인자로 전달 받았던 Export하고자 하는 함수의 이름과 함께 GetProcAddress를 통해 주소를 구하고자 하는 것이다.

.text:1000261C call sub_10002634

.text:10002621 push [esp+lpProcName] ; lpProcName : LpkEditControl

.text:10002625 push hModule ; hModule : lpk.dll

.text:1000262B call ds:GetProcAddress

.text:10002631 retn 4

해당 10002634를 따라가다 보면 시스템 디렉터리의 경로를 구한 뒤, 해당 Export 함수를 가진 대상(포워딩 대상)을 LoadLibarary API를 통해 로드하는 것을 확인할 수 있다. 이를 통해 해당 DLL이 로드되고 위의 과정에서와 같이 GetProcAddress를 통해 해당 함수가 로드되는 것이다.

.text:1000265B push esi ; uSize

.text:1000265C push eax ; lpBuffer

.text:1000265D call ds:GetSystemDirectoryA

.text:10002663 lea eax, [ebp+Buffer]

.text:10002669 push offset String2 ; "\\lpk.dll"

.text:1000266E push eax ; lpString1

.text:1000266F call ds:lstrcatA

.text:10002675 cmp hModule, 0

.text:1000267C pop esi

.text:1000267D jnz short loc_10002691

.text:1000267F lea eax, [ebp+Buffer]

.text:10002685 push eax ; lpLibFileName

.text:10002686 call ds:LoadLibraryA ; lpk.dll

DLL 포워딩이 그렇다면 어느 곳에 사용될 수 있을까? 필자는 악성코드에 관심이 많으므로 악성코드를 대상으로 설명하겠다. 악성코드는 문서의 형태로 존재할 수도 있고 실행파일의 형태로 존재할 수도 있다. 하지만 이 글의 취지에 맞게 DLL로 구성된 경우 악성코드 제작자는 매우 유용하게 DLL 포워딩을 사용할 수 있다. 악성코드 제작자 그 누구도 자신이 만든 애지중지한 파일이 누가 보아도 '악성'으로 보이고 싶지는 않을 것이다. 그렇기에 정상 파일인 것처럼 위장을 하게 되는데, DLL의 경우 실제 Export 하는 함수의 내용을 구현할 수 있어야 한다는 것이다. 물론 실제 DLL의 내용을 Ctrl+C/Ctrl+V를 통해 사용할 수는 있겠지만 이는 결코 좋은 방법이 아니다.

이 기능을 사용하면 해당 포워딩 설정을 실제 DLL 파일로 해놓으면 너무나 쉽게 정상적인 기능을 모두 구현할 수 있게 된다. 결국 정상적인 기능을 수행하면서, DllMain()에는 자신이 원하는 기능을 수행하도록 하면 이는 완벽한 위장이 된다. 물론 실제 시스템 DLL이 먼저 로드되는 상황이 발생되면 안되므로 DLL 로딩 순서를 변경하여 실행파일과 같은 디렉터리에 있는 악성 DLL을 먼저 로드하도록 하여 이를 로드시키면 모든 것은 끝이 난다.

 

Conclusion

단순하게 DLL을 이론만 공부했던 때에는 위의 기능들이 어떻게 사용되는지 별로 관심이 없었다. 하지만 실제로 악성 DLL을 분석해보고 난 뒤 너무나 부족한 지식을 가지고 있다고 다시 한 번 느끼게 되어 이렇게 기억하고자 정리해보았다. 이외에도 분석을 통해 어떠한 부분이 부족한지 더 자세히 알게 되어 너무나 유익하였다.

약간의 후기를 공유하자면 이론으로 알고 있던 내용이 어떻게 코드나 어셈블리어로 적용되는지 반드시 확인을 해보자. 이론으로는 자세히 알고 있더라도 그 부분을 무심코 지나칠 수 있다는 것을 뼈저리게 느끼게 되었다.


Reference

http://www.sck.pe.kr/c-cgi/whatisdll.htm

Windows 시스템 실행파일의 구조와 원리 [한빛미디어]

'Reversing > Theory' 카테고리의 다른 글

Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
WFP 무력화  (0) 2016.06.21
PE구조의 이해  (0) 2016.05.04
윈도우 후킹 원리 [PDF]  (1) 2016.04.23
윈도우 후킹 원리 (3) - Kernel [SSDT]  (0) 2016.04.23