안티바이러스(백신)에서 사용하는 악성코드 탐지 메커니즘은 종류가 너무 많이 있기 때문에 한 가지 방식으로 분류할 수는 없는 것 같다. 참고로 아래에서 말하는 것들 보다 훨씬 많은 종류가 있지만 여기에서는 개인적인 공부를 위해 필요한 부분만 정리하였고 계속 추가해 나갈 생각이다. 또한 여기에서는 악성코드만 다루었지만 이 외에도 패킷 즉 네트워크, 그리고 같은 악성코드 안에서도 랜섬웨어 등 형태에 따라 탐지 방식이 달라질 것이다.


  첫 번째 분류 방식에서는 On-Demand 및 On-Access 방식으로 나눈다. On-Demand 방식은 검사를 위해 안티바이러스에서 버튼을 누른다던지 하는 방식을 통해 진행된다. 물론 주기적으로 특정 시간에 검사를 진행하게 하는 것도 여기에 속할 것이다. On-Access 방식은 안티바이러스의 실시간 검사의 개념이다. 말 그대로 실시간으로 검사하여 컴퓨터 사용 중 파일이 생성되거나 또는 실행될 때 같이 관련 이벤트가 있을 때 마다 감시하는 방식이다. 일반적으로 대부분의 안티바이러스는 항상 실시간 검사를 이용해 감시하며 On-Demand 방식은 정밀 검사 같은 방식을 통해 제공되는 것으로 보인다.


  두 번째 분류 방식으로는 시그니처 기반과 행위 기반 즉 휴리스틱이 있다. 시그니처 기반 방식은 말 그대로 악성코드가 판정나면 이것의 시그니처를 저장해 둔 후 검사 시에 파일과 악성코드 시그니처들을 비교하는 방식이다. 이것도 구현 방식 뿐만 아니라 종류가 매우 많지만 여기서는 이렇게 간단하게만 정리한다. 


  행위 기반은 크게 두 가지로 나뉜다. 하나는 정적 휴리스틱이며 다른 하나는 동적 휴리스틱이다. 정적 휴리스틱의 경우에는 파일을 분석하여 의심스러운 특징을 찾아내어 판단하는 방식이다. 동적 휴리스틱의 경우에는 두 가지 방식으로 나뉘는 것 같다. 하나는 HIPS(Host based Intrusion Prevention System)에서 사용되는 방식이다. 이 기능을 순수하게 휴리스틱 방식으로 사용하는 안티바이러스가 있는가 하면 HIPS로 분리하여 보여주는 안티바이러스도 있는 것으로 보인다. 이것은 API 함수들을 후킹하여 실시간으로 어떤 함수들이 사용되는지를 검사하여 의심스러운 행위로 보일 때 탐지하는 방식이다. 다른 하나는 에뮬레이터를 이용한 방식이다. 안티바이러스에서는 에뮬레이터 외에도 샌드박스라고도 불리는데 그냥 같은 의미로 사용되는 개념인 것 같다. 에뮬레이터는 굳이 동적 휴리스틱 방식 외에도 다양하게 사용된다. 즉 실행 파일 에뮬레이팅 외에도 셸 코드 분석에 그리고 실행 파일 언패킹에도 사용된다. 어쨌든 에뮬레이터 내에서 실행 파일을 실행시키고 그 행위를 분석하는 방식이다.



Posted by SanseoLab



1. 개요

2. 패킹된 바이너리

3. 언패킹

.... 3.0 No option

.... 3.1 API Redirection

.... 3.2 Code Redirection

.... 3.3 Remove OEP

.... 3.4 Debug Blocker

.... 3.5 etc

4. OEP 찾기





1. 개요

  PEspin(http://pespin.w.interiowo.pl/)은 전형적인 프로텍터로 보인다. Themida 같은 코드 가상화 기술은 사용하지 않지만 상당히 많은 안티 디버깅 기술이 사용된다. 물론 압축 알고리즘을 통해 보호 뿐만 아니라 패킹의 기능도 갖는다. 참고로 실행 압축 알고리즘은 aPLib을 사용한다.


  다음은 옵션들이다. 여기에서는 옵션 없이 처리한 바이너리부터 분석한 후에 각각의 옵션을 사용한 바이너리를 분석하여 어떤 방식으로 구현되었는지를 확인하고 정리할 것이다.


- Anti-debugger protection : [ Add Debugger detection ( Exit without any message, Display message and exit ) ]

- Protection : [ API Redirection ], [ Antidump protection ], [ Remove OEP ], [ Code redirection ], [ Debug Blocker ]

- Advanced : [ Compress resources ], [ Strip Overlays ], [ Strip .reloc section if possible ], [ Optimize MS-DOS header size ]

- Other : [ Password protection ], [ Close program after : "" 'minute ], [ Create backup file ]

- Section names : [ User name : ], [ Random known protector, packer ], [ Don't rename ]





2. 패킹된 바이너리

  이 부분은 "Section names" 옵션과 같이 설명하겠다. "Don't rename"을 사용한 경우에는 초기 섹션들 외에 .taz 섹션이 추가된 것을 볼 수 있다. 옵션 중 "Compress resources" 옵션을 사용하지 않았기 때문에 리소스 섹션(.rsrc)의 내용은 그대로이지만 나머지 섹션들은 모두 압축되어 있다. 만약 "User name" 옵션을 사용하여 이름을 짓는 경우 .rsrc 섹션명을 제외하고 .taz 섹션 포함 모든 섹션 이름이 그 이름으로 설정된다. 또한 "Random known protector, packer"로 설정한 경우 앞과 동일한 섹션들의 이름이 모두 .PELOCKnt, .ficken, .peco 등으로 변경된다.


  .taz 섹션은 일반적인 패커들처럼 언패킹 루틴과 압축된 데이터가 존재하며 동시에 Import Table이 존재한다. PEspin은 MessageBoxA(), InitCommonControls(), LoadLibraryA(), GetProcAddress()를 기본으로 임포트한다.





3. 언패킹

  PEspin은 암호화 및 복호화에 다형성 루틴을 사용한다. 이것은 하나의 바이너리를 패킹하더라도 생성될 때 마다 출력된 바이너리는 모두 다르다는 것을 의미한다. 압축된 데이터가 들어간 섹션들은 완전히 다르며 복호화 루틴 및 압축된 데이터가 저장된 .taz 섹션도 차이점이 존재한다. 복호화 루틴이 기본적으로 Self-modifying code인 면도 있다.


  여기서는 32비트 기준 작은 바이너리 두 개(하나는 calc.exe)를 대상으로 하여 분석했다. 그렇기 때문에 큰 바이너리의 경우 인지하지 못한 부분에서 차이가 있을 수 있다. 이 섹션에서는 아무런 옵션을 주지 않은 채로 생성된 바이너리를 언패킹하며 하위 섹션들에서 하나씩 옵션을 설정하여 비교하면서 분석하겠다. 참고로 프로텍터의 특징처럼 난독화 코드가 매우 많다. 기본적으로 garbage 코드를 비롯하여 "Jumping into the middle of an instruction" 또는 "Breaking Code Alignment"로 불리는 안티 디스어셈블러 기법이 사용되어 Ollydbg를 이용하여 분석하는데 많은 방해가 되었다. garbage 코드는 그렇다 치더라도 jmp 및 call을 이용하여 코드를 깨는 저 방식은 한 두개라면 감당하겠지만 저렇게 대량으로 존재하는 경우 Ollydbg를 이용하여 분석하는 것은 굉장히 힘든 일로 보인다. "Analyse Code" 즉 "Ctrl + A"도 이것들을 제대로 분석하지 못하기 때문에 어지간한 경우에는 그냥 분석을 해제하는 편이 낫다. 이것을 극복할 수 있는 방식을 최대한 찾을 것이며 없으면 만들어 보도록 시도라도 할 생각이다. 


  하나씩 옵션을 설정하면서 깨달은 것은 언패킹 루틴이 기본적으로 동일하며 옵션에 따라서 조금씩 기능이 추가되는 경우가 많았다는 점이다. 개인적으로 능력이 부족하여 대충 넘긴 부분이 특정 옵션 사용 시에 사용되어 이 때서야 깨닫는 것을 보며 확신하건데, 아마 이렇게 최종적으로 정리한 글에서도 놓친 부분이 많이 남아있을 것이다. 그렇기 때문에 이 글은 참고 용으로만 보고 정확한 분석은 직접 수행하는 것을 추천한다.



3.0 No option

  앞에서도 언급하였듯이 "Jumping into the middle of an instruction" 기법 때문에 Step Over (F8) 대신 Step Into (F7)를 이용해야 한다. 참고로 분석 대상 바이너리가 다를 수 밖에 없지만 이것을 설명하면서 주소를 적는 것이 참고하는데 많은 도움이 될 것 같아서 주소도 같이 적으려고 한다. 본인의 바이너리는 00406000이 .taz 섹션의 시작이며 EP도 이 섹션의 거의 처음 부분이다. 사족으로 EP의 앞 부분에는 Import Table이 존재한다.


  비록 Ollydbg에서는 우리의 섹션들이 모두 통합되어 보이지만 어쨌든 영역을 구분하는 것은 필요한 일이다. 참고로 왜 Ollydbg에서 통합되어 보이는지에 대한 확실한 이유는 알 수 없지만 추측은 다음과 같다. 원래 일반적인 경우 .text 섹션은 읽고 실행하기 즉 rx, .rdata 섹션은 r, .data 섹션은 rw 등과 같이 섹션 별로 접근 권한이 부여된다. 하지만 PEspin으로 패킹한 경우 전체 섹션들이 모두 rwx 권한을 갖게된다. 아마 Ollydbg는 이 섹션들이 일반적인 이름을 갖고 있지만 일반적인 권한을 갖고 있는 것이 아닌데다가 권한도 모두 같으니 한 뭉텅이로 통합하여 인식시킨 것으로 보인다.


  시작하자 마자 앞에서 언급한 "Jumping into the middle of an instruction" 기법과 garbage 코드들이 나온다. 가장 대표적인 garbage 코드는 분기문으로서 retn을 이용하는 방식이다. jmp와 call을 통한 분기 뿐만 아니라 주소를 push한 후에 retn함으로써 분기하거나 " ADD DWORD PTR SS:[ESP], 0C " 이후 " RETN " 을 이용해 분기하는 방식이 자주 사용된다.



a.

  EP는 004060D4이며 대부분 Step Into (F7)로 진행하였다. 앞에서 00406000 즉 .taz 섹션의 처음 부분은 Import Table이라고 말했다. 정확히 말해서 로드 이후 00406050에 MessageBoxA()의 주소가 들어가 있다. 이 값 즉 주소를 004084C1에 복사한다. 이 외에도 LoadLibraryA()는 00408566에, 0040856B에는 GetProcAddress()를 넣는다. 참고로 이 세 함수들은 모두 Import 하는 API들이다. 일종의 Import Table의 역할을 수행하는데 복잡하기 때문에 뒤에서 따로 정리하도록 하겠다.


  복호화 루틴이 자가 수정 코드 즉 Self-modifying code라고 했듯이 실행 중에 코드를 계속 수정해 가면서 진행한된. 이 수정 즉 복호화는 바로 다음 명령어가 해당되기도 하지만 뒤에서 나올 반복문 처럼 13A8 정도 되는 크기를 복호화하기도 한다. ECX가 13A8을 갖는 반복문인데 004085xx부터 004099xx 부근까지 복호화를 담당한다. 이후 ECX가 BA인 반복문도 있다. 이제 복호화된 부분으로 이동한다.


  참고로 ECX를 이용한 루프의 경우 간단하게 조건부 BP 즉 "Shift + F2"를 누른 후 조건으로 "ecx == 1"을 입력하고 F9 즉 실행을 누르면 해당 명령어 실행 중 ECX가 1이될 때 정지하게 된다. ECX를 이용한 루프는 이렇게 처리하기로 한다.


  다음 부분은 조금 설명이 필요하다. 패커들의 경우 대부분 LoadLibrary()와 GetProcAddress()를 임포트하며 언패킹 후 마지막으로 IAT를 복구하는데 두 함수를 사용한다. 패커는 위의 두 함수를 제외하면 API 함수들이 그다지 필요한 경우가 많이 없지만 프로텍터의 경우에는 원본 바이너리의 IAT 복구 외에도 언패킹 루틴에서 보호에 사용되는 여러 함수들이 필요할 수 있다. 하지만 대상이 어떤 함수를 사용하는지 알 수 있다는 것은 대상을 분석하는데 많은 힌트가 될 수 있으므로 직접 import 하지는 않는다. 대신 언패킹 루틴에서 사용되는 함수들도 이 두 함수를 이용해서 주소를 얻어와 사용하는 경우가 많다.


  뒷 부분에서 나올 때 마다 언급하겠지만 여타 프로텍터들처럼 PEspin도 보호를 위해 여러 함수들이 필요하다. 하지만 PEspin은 이 두 함수를 임포트하고 있어도 이 함수들을 사용하지 않는다. 즉 언패킹 루틴에 필요한 함수들 뿐만 아니라 원본 바이너리의 IAT 복구를 위한 함수들 주소도 이 두 함수를 이용하지 않는 것이다. 필요한 함수들은 Export Table을 이용하여 함수 주소를 얻어온다. 오직 LoadLibrary()만 마지막 즈음에 원본 바이너리의 임포트 테이블을 복구할 때 사용되기도 한다. 이 두 함수를 사용하여 필요한 함수들의 주소를 가져오지 않는 것은 아마 보호의 방식 중 하나로 보인다.


  Export Table을 이용하여 api 함수의 주소를 얻는 방식은 한국어로 되어있는 좋은 자료들이 존재하므로 (예를들면 [ http://ezbeat.tistory.com/283 ]) 자세히 설명하지 않겠다. 참고로 PEspin에서는 이미 LoadLibraryA() 등 kernel32.dll의 함수를 사용하기 때문에 kernel32.dll의 BaseAddress를 구하는데 PEB를 사용하지는 않는다. 그냥 LoadLibraryA()를 이용해서 BaseAddress를 구하며 이후 EAT를 이용한 방식은 비슷하다. 물론 문자열을 비교하는 부분 같이 세세한 부분은 다르지만 기본적으로 Export Table을 이용하는 메커니즘이 사용된다. LoadLibrary()와 GetProcAddress()를 이용하면 매우 간단할 것이지만 이 방식을 사용함으로 인해 익숙하지 않다면 막힐 수도 있다. 이 방식의 특징 상 차례대로 모든 API 함수 이름을 비교하면서 찾아내야 하기 때문이다. 


00408653 CMP BYTE PTR DS:[EDI+2], 69


  EDI에는 API 함수 이름 문자열의 주소가 들어가 있다. 저 명령어는 함수 이름의 세 번째 문자와 특정 알파벳을 비교한다. PEspin 개발자만의 방식으로 보인다. 이후 3번째 글자가 맞다면 나머지 부분도 비교한다.


00408738 MOV DWORD PTR DS:[EDI+1], EAX


  이 명령어는 api 함수 이름이 원하는 것과 같다면 그 주소를 아까 위에서 설명한 004084C1 이후 부분 즉 새로운 임포트 테이블에 쓰는 역할을 한다. 어쨌든 이 루틴을 벗어나는 것을 더 원할 것이다. 아래의 JE가 활성화되면 이 루틴이 종료된다. 그냥 00408740에 BP를 걸면 된다.


00408624 CMP DOWRD PTR DS:[EDI], 0

00408627 JE 00408740


  마지막으로 PEspin이 사용하는 api 함수들을 정리하겠다. 사실 여기서는 아무 옵션도 사용하지 않았기 때문에 아래의 함수들 중 몇 개만 사용된다. 옵션이 추가될 수록 사용되는 함수들이 많아질 것이다.


[ ExitProcess, VirtualProtect, Closehandle, VirtualAlloc, VirtualFree, CreateFileA, ReadFile, GetTickCount, GetModuleHandleA, CreateThread, Sleep, GetCurrentProcessId, OpenProcess, TerminateProcess, GetFileSize, GetModuleFilenameA, CreateMutexA, CreateProcessA, GetCommandLineA, GetLasterror, GetThreadContext, SetThreadContext, VirtualProtectEx, WaitForDebugEvent, ContinueDebugEvent, ReadProcessMemory, WriteProcessMemory, VirtualQueryEx ]


  계속 진행하다 00408467로 가고 또 진행하다 보면 ecx를 이용한 루프가 존재한다. 이것도 조건부 BP를 이용해 넘긴다. 



b.

  이후 다음 명령어들이 존재한다.


0040663A XCHG DWORD PTR FS:[EBX], EAX

; EBX는 0이다. 그래서 EAX에는 SEH를 가리키는 포인터가 들어간다.

0040663D LEA EBX, [EBP+403AFF]

; EBX에는 SEH 핸들러의 주소가 들어간다.

00406643 PUSH EBX

00406644 PUSH EAX

; 둘을 스택에 push함으로써 새로운 SEH 핸들러가 설치된다.

00406645 OR EDI, FFFFFFFF

00406648 OR ECX, EDI

0040664A REPE SCAS BYTE PTR ES:[EDI]

; EDI가 0xFFFFFFFF이므로 예외가 발생한다.


  위의 루틴은 뒤에서도 나오겠지만 전형적인 SEH를 이용한 안티 디버깅이다. 예외 핸들러를 설치하고 예외를 발생시키는 것이다. 우리는 간단하게 예외를 발생시키는 코드 실행 이전에 또는 이미 발생하여 Ollydbg가 아래의 패널에서 "Access violation when reading [FFFFFFFF] - Shift+Run/Step to pass exception to the program" 메시지를 내놓고 중지된 상황에서 스택 패널에 보이는 SEH 핸들러의 주소를 참조하여 핸들러에 BP를 걸고 "Shift + F9"하여 SEH 핸들러로 넘어올 수 있다. 이런 방식은 뒤에도 자주 나오는데 예외를 일으키는 방식만 다르지 메커니즘은 같다.


00406660 REP MOVS BYTE PTR ES:[EDI], BYTE DS:[ESI]


  핸들러에서는 위의 명령을 실행하여 ESI 즉 004085C0에서 EDI 즉 0040664D로 데이터를 옮긴다. 그리고 다시 위 즉 0040664D로 가서 명령어를 실행한다. 그냥 방금 사용한 SEH 핸들러를 제거하는 부분이다.



c.

  VirtualAlloc()으로 메모리를 할당한다. 명령어는 아래와 같은데 앞에서도 설명했듯이 참조하는 주소는 새로운 Import Table이다. 참고로 정확히 004084E4이다.


0040668B CALL DWORD PTR SS:[EBP+405996] ; VirtualAlloc()


  간략하게 정리해서 GetModuleFileNameA()로 이 바이너리의 파일 경로 명을 얻어와서 앞에서 할당한 메모리에 저장한다. 이후 이 경로명을 인자로 CreateFileA()를 이용해 파일의 핸들을 얻고 GetFileSize()로 크기를 얻는다. 그리고 이 크기만큼을 인자로하여 VirtualAlloc()으로 메모리를 할당한 후 ReadFile()로 파일을 읽어서 이 영역에 저장한다. 마지막으로 CloseHandle()로 핸들을 닫는다.


  읽어온 파일 자체의 데이터를 이용한 루틴이 수행된다. 뭔가 무결성 검사처럼 파일 전체 데이터를 이용하여 연산한 후 그 결과를 0040AE62에 저장한다. 이후 VirtualFree()로 할당한 두 메모리를 해제한다. 그리고 SEH 핸들러를 설치한 후 다음 명령어로 예외를 발생시킨다. 참고로 BL은 0이다.


0040684A DIV BL


  이번에 사용되는 핸들러에서는 retn을 이용해 반환하기 때문에 결국 ntdll로 들어가서 NtContinue()까지 가게된다. 예외와 관련하여 정리한 글이 이미 이 블로그에 존재하기 때문에 결과만 말하겠다. 인자로 넣은 pContext의 주소는 0019FBFC이다. 여기에 B8을 더하면 되는데 결과는 0019FCB4가 된다. 스택 패널에서 이 영역을 찾아가면 값이 0040859D인 것을 볼 수 있으며 이곳에 BP를 걸면 된다. 


  이후 계속 복호화는 수행된다. 이 부분은 조금 더 구체적인게 각 섹션 별로 주소를 EDI에 넣고 크기만큼 ECX에 넣고 복호화를 수행하는게 보인다. 참고로 아까 0040AE62에 저장한 값을 이용한다. 그리고 SEH 핸들러를 이용한 안티 디버깅이 존재한다. 조금 특이하기 때문에 설명을 더 하겠다. 인터럽트는 STI 명령어를 이용하는데 핸들러에서 핸들러의 주소를 변경한다. 이후 반환하는데 다시 이 주소로 복귀하여 STI를 실행하고 또 예외가 발생한다. 대신 이번 핸들러의 주소는 이전의 00408F8A가 아닌 변경된 00408F8F가 된다.


0040900B STI


  그리고 또 예외가 발생한다. 물론 다음 명령어 실행 이전에 핸들러 주소를 00408FDA로 변경하였으므로 여기에 BP를 건다.


00408FCF XLAT BYTE PTR DS:[EBX+AL]


  이후 00406C23부터 ECX 11D4 만큼의 자가 수정 부분을 지나 각 섹션에 대해 크기만큼 복호화를 수행한다.



d.

  예외가 발생한다.


00406C00 MOV DWORD PTR DS:[EAX], EBX


  이후 00406C25 부터 11D4만큼 자가 수정을 수행한다. 이것도 ECX를 이용한 루프문이므로 똑같이 처리한다. 다시 이곳으로 이동하여 진행한다. 조금 지나서 VirtualAlloc()을 호출한다.


004097B0 VirtualAlloc()


  여기서는 이제 마지막으로 복호화가 수행된다. 즉 이 과정을 거치고 난 후 코드 섹션 근처로 가보면 원본 바이너리와 거의 같은 형태로 복호화가 완료되었다는 것을 볼 수 있다. 조금 더 자세히 살펴보면 PUSHAD로 시작해 POPAD로 끝나는 루틴이다. 이 부분은 다른 부분과 다르게 "Analyse Code"를 이용하면 깔끔하게 분석된 것을 볼 수 있다.


004071E1 PUSHAD

...

00407288 POPAD

00407289 RETN


  이후 retn을 하면 004097FF로 이동하며 다음 명령어를 통해 할당된 메모리에서 코드 섹션으로 E00의 크기 만큼을 이동시키는 것을 볼 수 있다. 참고로 이 부분도 분석된 코드를 보면 깔끔하게 이해할 수 있다.


00409809 REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]


  참고로 이 예제 바이너리에서 해당 루틴은 .text 섹션과 .rdata 섹션만 해당하는 것으로 보인다. 어쨌든 위에서 언급하였듯이 위의 명령어 수행 후 해당 섹션으로 가보면 원본 바이너리처럼 복호화되어 있는 것을 확인할 수 있다. 물론 다른 프로텍터나 패커처럼 JMP 및 CALL의 주소와 관련해서는 아직 남아있다. 이후 VirtualFree()로 할당한 메모리를 해제한다.


00409866 VirtualFree()



e.

004089E5 VirtualProtect()


  004001F0부터 528의 크기 만큼 PAGE_EXECUTE_READWRITE 권한을 부여한다. 이후 이 부근에 값들을 쓰고 이곳으로 이동하여 진행하다가 예외를 발생시킨다.


004001F7 JMP FAR FWORD PTR DS:[EDX+9090]


  핸들러의 주소인 00408A4D로 이동하여 핸들러를 지우고 GetTickCount()를 호출한다.


00408A8B GetTickCount()


  결과에 대하여 반복적으로 특정 연산을 수행하며 00406000부터 D4의 크기만큼 차례대로 결과값을 집어넣는다. 이후 자가 수정 코드를 진행하다 0040956F로 이동한다.



f.

  이제 Import Table 복구를 시작한다. 일반적인 패커들의 경우 로더가 하는 역할과 같이 LoadLibrary()로 해당 DLL에 대한 핸들을 얻어온 후 GetProcAddress()로 해당 API 함수의 주소를 얻어온 후 IAT에 주소를 복사해 넣는다. 이후 바이너리에서는 보통과 같이 IAT의 주소를 참고해 API 함수를 호출한다. 즉 로더의 역할을 대신해줄 뿐 차이점은 없는 것이다.


  하지만 PEspin의 경우에는 더 복잡하다. 프로텍터로서 언패킹 루틴에서 사용할 함수들이 많이 존재하기 때문이다. 게다가 보호의 목적인지 LoadLibrary(), GetProcAddress() 처럼 기본적인 API를 제외하고는 직접적으로 임포트 하지 않는다. 사실 개인적인 생각은 이 두 함수도 거의 사용되지 않는 것과 섹션 이름을 rename할 수 있는 기능이 있는 것으로 보아 다른 패커 및 프로텍터와 혼동시키게 하려는 목적도 있는 것 같다.


  어쨌든 초반부에 설명했 듯이 원래 임포트하던 MessageBoxA()는 주소 004084C1에 저장한다. 여기서는 사용되지 않지만 이것을 호출할 일이 있다면 이 주소를 IAT로 삼아서 함수를 호출할 것이다. 그 다음으로는 차례대로 00408566에 LoadLibraryA(), 0040856B에 GetProcAddress()를 저장한다. 다음으로는 Export Table을 이용해 ExitProcess()부터 VirtualQueryEx() 까지의 주소를 얻어와서 각각 MessageBoxA() 뒤의 위치에 저장한다. 이 주소들은 MessageBoxA()와 LoadLibraryA() 및 GetProcAddress() 사이에 저장되게 된다.


  지금까지 여러 API 함수들을 호출하였으며 모두 저 주소에 위치한 값을 주소로 사용해 API 함수들을 호출했었던 것을 알 수 있다. 이제부터는 언패킹 이후 사용되는 즉 바이너리 자체에서 실제로 사용되는 함수들을 위한 IAT를 복구할 것이다. 하지만 이것도 설명이 필요하다.


  참고로 현재 예제 바이너리는 Visual Studio 2015로 개발하여 PEspin 1.33에서는 그다지 호환되지 않는 것으로 보인다. 자세한 설명은 이 블로그의 "API Sets" 문서를 보면 될 것이다. 어쨌든 아까처럼 Export Table을 이용하여 kernel32.dll에서 임포트 하는 함수들부터 차례대로 문자열을 비교하며 주소를 얻어오는데 해당하는 경우에는 0040AE67을 시작으로 이곳에 주소를 저장한다. 즉 언패킹 이후 바이너리에서는 GetCurrentProcessId() 같은 kernel32.dll의 함수를 호출할 때 이 영역을 IAT로 생각하고 호출하게 될 것이다. 간단하게 말해서 새로운 IAT의 위치라고 할 수 있다.


  하지만 이 예제 바이너리는 PEspin 1.33에서 지원하지 않는 것으로 보이는 VCRUNTIME140.dll 등의 DLL에 위치한 함수들도 임포트한다. 이 경우 PEspin은 일반적인 바이너리처럼 00402000 영역에 해당 주소를 복구한다. 다시 말해서 패킹한 바이너리가 PEspin에서 지원되는 kernel32.dll 같은 DLL의 함수를 사용할 경우에 0040AE67에 새로운 IAT가 만들어지고 이곳이 사용된다. 지원되지 않는 DLL의 함수들을 사용할 때는 일반적인 경우처럼 원래 IAT에 함수를 복구하여 원래 IAT가 사용되는 것이다. 참고로 GetProcAddress()를 이용하지 않는 것은 앞 부분과 같지만 VCRUNTIME140.dll 같은 DLL의 핸들을 얻을 경우에는 LoadLibrary()가 사용된다. 이것을 보면 지원되는 경우에 PEspin은 LoadLibrary()와 GetProcAddress()를 임포트함에도 불구하고 전혀 사용하지 않는다는 것을 알 수 있다.


  참고할 코드 부분을 설명하겠다. 


00406F4E CMP BYTE PTR DS:[EDI], 5F


  이 부분은 구해온 API 함수 이름 문자열과 특정 알파벳을 비교하는 부분이다. 참고로 이 부분도 수도 없이 진행되기 때문에 더 참고할만한 부분을 설명한다.


00406FBB JBE SHORT 00406FF2


  이곳에 BP를 걸고 실행하다 보면 바이너리에서 사용되는 함수들이 EAX에 차례대로 보이는 것을 볼 수 있다. 어쨌든 넘기기 위해서는 0040708D의 JE 00407125가 활성화되어야 한다. 진행하다 보면 00407A84로 가고 계속 진행하다보면 다음을 볼 수 있다.


00407DA2 JMP 004012EA


  OEP로 가는 점프문이다.



3.1 API Redirection

  위의 [ f ] 과정 즉 IAT를 복구하기 이전에 먼저 VirtualAlloc()으로 메모리를 할당한다. 그리고 IAT 복구를 수행하는데 앞에서 일반적인 경우 0040AE67에 새로운 IAT가 들어가 있고 여기에 함수의 주소가 들어가 있다고 했다. API Redirection 옵션을 사용한 경우 이 주소는 할당한 메모리의 주소가 된다. 그리고 이 메모리의 그 주소에서는 API 함수의 처음 부분이 복사된다. 마지막 부분은 API의 다음 부분으로 분기하는 점프가 추가된다.


  즉 API 호출 시 할당된 메모리로 이동하며 거기서 함수의 처음 부분을 실행하고 이후 실제 함수의 다음 부분으로 분기하여 거기서 다시 실행하는 것이다. 이 예제를 이용해 설명하도록 하겠다. 참고로 "API Sets" 문서에 관련 내용이 정리되어 있다. 이 바이너리에서는 Kernel32.GetVersionExA()를 호출한다. 하지만 실제로 이 함수는 kernelbase.dll로 이동하였고 kernel32.dll에는 kernelbase.dll로 분기시켜 주는 스텁 코드가 들어있다. 즉 다음과 같다.


76FB56D0 MOV EDI, EDI

76FB56D2 PUSH EBP

76FB56D3 MOV EBP, ESP

76FB56D5 POP EBP

76FB56D6 jmp dword ptr ds:[<&api-ms-win-core-sysinfo-l1-2-1.GetVersionExA>]


  이 API를 바이너리에서 사용한 경우 새로운 IAT에는 이 함수에 대한 주소가 76FB56D0가 아닌 할당된 메모리인 001E00xx로 되어 있다. 즉 호출 시 이 주소로 가는데


001E00xx MOV EDI, EDI

001E00xx PUSH EBP

001E00xx MOV EBP, ESP

001E00xx POP EBP

001E00xx jmp 76FB56D6


  위와 같이 실제 kernel32.GetVersionExA()의 처음 부분이 (참고로 이 스텁 함수의 경우 매우 짧기 때문에 대부분이 실행되는 것처럼 보인다) 실행되고 이후 그 다음 주소인 76FB56D6으로 jmp하게 된다.



3.2 Code Redirection

  [ e ] 단계에서 004001F0 영역은 0xFF로 채워졌었다. 이 옵션을 사용하면 IAT가 완성되고 OEP로 이동하기 바로 전에 특정 데이터가 004001F0을 시작으로 해당 영역에 삽입되는 것을 볼 수 있다. 이것은 E9로 시작하는 jmp 문들이다. 다음은 0040AEA3에서 004001F0으로 166 크기의 데이터를 이동하는 명령어이다. 0040AEA3에 해당 내용을 삽입하는 부분은 분석하지 않았다.


00407C9E REP MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]


  이후 언패킹 루틴을 종료하고 OEP로 간 후 실행해 보겠다. 실행 중 일반적인 분기문이 평소와 다르게 004001F0을 시작으로 하는 저 영역으로 가는 jmp 문으로 수정된 것을 볼 수 있다. 명령어를 실행하여 해당 영역으로 이동하면 E9를 시작으로 하는 jmp 문이 존재하는데 이 명령어들은 결국 원래 가려고 했던 곳의 주소를 가지고 있다. 이렇게 Code Redirection 옵션은 API 함수가 아닌 일반적인 분기문을 대상으로 Redirect를 수행하여 준다.



3.3 Remove OEP

  OEP는 004012EA이며 언패킹 루틴에서 OEP로 가는 마지막 분기는 다음과 같은 형태이다.


00407DA2 JMP 004012EA


  그리고 실제 바이너리의 OEP 부분은 다음과 같다.


004012EA CALL 0040155F [ 참고 : __security_init_cookie() ]

004012EF JMP 0040116E


  Remove OEP 옵션을 사용하는 경우에는 마지막 분기문이 다음과 같다.


00407D2F JMP 0040155F

00407D34 JMP 004012EF


  설명해 보자면 바로 OEP로 가지 않고 실제 OEP의 첫 코드 부분을 직접 호출한 후에 복귀하여 다시 그 다음 부분으로 가는 것이다. 실질적인 흐름은 차이가 없지만 OEP인 004012EA로 직접 가지는 않는다는 점이 Remove OEP 옵션의 의미인 것으로 보인다. 참고로 이쪽을 구현한 언패킹 루틴은 분석하지 않았다.



3.4 Debug Blocker

  이 방식은 전형적인 Debug Blocker (또는 Self-Debugging) 방식이다. 기본적인 형태와 관련해서는 많은 자료들이 있고 아직 분석하지 않아서 PEspin만의 특징을 알지 못하지만 추후에 분석한 경우 올리도록 할 것이며 여기서는 기본 구현만 알아보도록 한다. 


  앞에서 설명했듯이 [ a ] 단계에서는 이후에 수행할 영역에 대한 자가 수정 코드가 있으며 언패킹 루틴에서 사용될 함수들을 위한 새로운 Import Table을 생성한다. [ a ] 단계를 지나면 00409D86으로 간다.


  이제부터는 상당히 직관적이다. CreateMutexA()로 뮤텍스를 생성한 후 GetLastError()로 검사한다. 부모 프로세스의 경우에는 성공할 것이고 자식 프로세스의 경우에는 이미 있기 때문에 GetLastError()로 0x000000B7 즉 ERROR_ALREADY_EXISTS를 받을 것이다. 참고로 이 예제의 경우 004099C0에 저장된 URXYTLAH 문자열을 사용하며 이것은 [ a ] 단계의 마지막 루프에서 생성된다. 이후부터는 부모와 자식 프로세스 사이에서 차이가 발생한다. 


  부모 프로세스의 경우 이후 두 번의 VirtualAlloc()을 통해 메모리를 할당한다. GetModuleFileNameA()로 파일 이름을 얻어와 첫 번째 할당한 메모리에 저장하고 GetCommandLineA()으로 커맨드 라인을 받아온 후 이것들을 이용해 CreateProcessA()를 호출한다. 이 때 플래그는 DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS를 사용한다. 이후에는 WaitForDebugEvent(), ContinueDebugEvnet()를 반복적으로 호출하여 자식 프로세스와 통신한다. 부모 프로세스를 더 이상 분석하지 않았고 자식 프로세스도 아예 분석하지 않았다. 



3.5 etc

  옵션 중 눈여결 볼만한 [ Add Debugger detection ( Exit without any message, Display message and exit ) ], [ Antidump protection ], [ Close program after : "" 'minute ]는 다루지 않았다. "Antidump protection"의 경우에는 무 옵션과의 차이를 찾지 못했으며 몇 개는 굳이 시도하지 않았다.





4. OEP 찾기

  OEP를 찾기 위해서 예제에서 사용한 바이너리와 calc.exe를 비교하면서 분석해 보았다. 두 개의 바이너리를 사용하고 API Redirection 옵션을 사용해 4개의 샘플을 이용했으며 각각의 크기는 9KB, 113KB이다. 아래의 주소는 .taz 섹션의 시작 주소를 기반으로 한다.


a. 예제 바이너리 + No option

+ 00001CF3 ADD DWORD PTR SS:[ESP], 0C

+ 00001CF7 RETN

...

+ 00001DA2 JMP OEP


b. 예제 바이너리 + API Redirection 옵션

+ 00001CF3 ADD DWORD PTR SS:[ESP], 0C

+ 00001CF7 RETN

...

+ 00001DAE JMP OEP


c. calc.exe + No option

+ 00001C7D ADD DWORD PTR SS:[ESP], 0C

+ 00001C81 RETN

...

+ 00001D8E JMP OEP


d. calc.exe + API Redirection 옵션

+ 00001C7D ADD DWORD PTR SS:[ESP], 0C

+ 00001C81 RETN

...

+ 00001D8A JMP OEP


  비교해보면 알겠지만 주소가 조금씩 다르다. 하지만 "ADD DWORD PTR SS:[ESP], 0C", "RETN" 명령어 및 "JMP" 명령어는 항상 동일한 것으로 보이므로 이것을 이용해 OEP를 쉽게 구할 수 있을 것으로 보인다. 즉 특정 API에 BP를 설치하거나 예외를 단계로 보고 뒷 부분까지 넘기다가 저 부근의 주소에서 해당 명령어를 찾은 후 OEP로 실행시키면 될 것으로 보인다.



Posted by SanseoLab



차례

1. 개요

.... 1.1 배경지식

.... 1.2 문서 정리

2. 드라이버 기본

.... 2.1 컴퓨터 설정

.... 2.2 드라이버 개발 환경

.... 2.3 도구

.... 2.4 DriverEntry / DriverUnload

3. 자가 보호

4. 스캐닝

.... 4.1 파일 시그니처 기반 스캐닝

.... 4.2 행위 기반 스캐닝

5. 프로세스 / 스레드 생성 보호

6. 레지스트리 보호

7. ELAM 및 Protected Process

8. 유저 모드 애플리케이션과의 통신

9. 기타

.... 9.1 네트워크 보호



[ 소스 코드 : https://github.com/101196/simpleAVdriver ]





1. 개요

1.1 개요

  악성코드 분석을 위주로 공부하고 있지만 개인적으로 안티바이러스가 어떻게 구현되는지에 대해서도 항상 궁금해 하여 왔다. 어떻게 보면 크게 관련이 없는 분야라고 할 수도 있겠지만 적어도 내 분야 중에서 주요한 부분을 차지한다고 생각하기 때문에 메커니즘에 대해서 공부하고 정리하려고 한다. 사실 더 관심이 갔던 이유는 찾기 정말 힘들었기 때문이다. 만약 제대로 정리된 자료들이 많았다면 그것만 정리하면서 공부하고 그 정도로만 알려고 했을 것이다.


  여기서는 배경 지식과 함께 전체적인 방식을 설명한다. 이 문서 외에도 깃허브를 통해 간단한 형태로 구현한 소스 코드도 같이 제공한다. 이미 있는 자료들을 기반으로 최소한 간략하게 만든 것으로서 원본 링크도 같이 제공한다. 실질적인 구현에 관한 것은 소스 코드의 주석을 통해 정리하기로 할 것이고 이 문서에서는 전체적인 방식 및 흐름을 정리하기로 한다. 


  각 내용들은 안티바이러스를 구현하는데 필요한 기능들을 차례대로 설명한다. 참고로 드라이버 개발과 관련해서도 정리할 수 밖에 없는데 순수하게 직접 구현한 안티바이러스와 관련된 내용에 한정해서 정리한다. 디바이스 드라이버와 관련된 자료들은 종종 보임에도 불구하고 순수하게 안티바이러스 관련 드라이버의 내용은 많이 없는 편이다. 물론 아주 상세하게 정리하는 것은 의미도 없고 능력도 되지 않기 때문에 아주 기초적인 것은 안다는 가정 하에 그리고 충분히 MSDN 등의 자료들을 보고도 알 수 있는 내용들은 제외하고 나머지의 내용을 적기로 한다. 이것들은 어차피 공부하면서 보아야 되기도 하고 정리도 잘 되어 있으며 시간을 가장 많이 낭비하는 것은 의도치 않은 내용들일 것이기 때문이다.


  안티바이러스는 물론이고 드라이버 개발 경력도, 주 분야가 개발인 것도 아닌 학생으로서 공부한 자료를 올린다는 것이 맞는 것인지는 모르겠다. 하지만 공부하는 입장에서 그리고 찾아보며 알아가고 구현해 보는데 너무 많은 시간을 보낸 입장에서 혹시나 공부하는 사람들이 적어도 검색에 필요한 키워드라도 하나 찾아갈 수 있지 않을까 하는 생각으로 올려본다. 다시한번 말하지만 전문가가 아닌 공부한 내용을 올린 것이다. 전체적인 수준은 당연하고 심지어 틀린 부분이 있을 수도 있다. 모르는 부분도 많이 있고 그렇게 적어 놓았으므로 읽는데 참고하길 바란다.



1.2 배경 지식

  안티바이러스에 조금의 관심이라도 있다면 인터넷에 널려있는 자료인 파일 시그니처 기반 안티바이러스 소스 코드들을 보면서 우리가 사용하는 안티바이러스가 단순하게 이러한 형태로 구현되어 있을 것이라고는 생각하지 않을 것이다. 아마도 20여 년 전에는 비슷한 형태를 가지고 있지 않았을까 싶다. 윈도우 XP 즈음에는 안티바이러스도 루트킷과 비슷한 형태를 갖었다고 한다. 인터넷에는 커널 모드에서의 후킹을 이용한 루트킷 관련 자료들이 많이 존재한다. 간략하게 구현적인 내용만 보자면 안티바이러스도 이것과 비슷하다고 한다. 그리고 이 정도까지도 인터넷에는 수많은 자료들이 존재한다.


  이제 최신 형태를 알아보겠다. 윈도우 비스타의 x64 버전부터는 많은 부분이 달라지게 되었고 내가 찾던 부분도 이쪽이었지만 아직도 인터넷에서는 관련 자료들을 찾기가 매우 힘든 편이다. 아마 이유가 있을 것이다. 보안 관련 정보는 대부분 공격과 관련된 정보이고 과거에도 보안 프로그램과 관련된 직접적인 내용이 많았다기 보다는 안티바이러스가 루트킷의 형태와 비슷하므로 굳이 찾아보면 어느 정도는 비슷하게라도 찾아볼 수 있었을 것 같다. 하지만 최신 윈도우 버전부터는 많은 부분이 달라져서 공격 입장에서 즉 루트킷이 그렇게 많이 활동하는것 같지는 않아 보인다. 그래서 공격자의 입장과는 다른 순수 보안 프로그램 구현과 관련된 내용이 인터넷에 많이 돌아다닐 일이 없어진 것이 아닌가 싶다.


  어쨌든 이제 비스타 x64 이후부터의 변화에 대해서 알아보려고 한다. 첫 번째는 KPP(Kernel Patch Protection)이다. 우리는 앞에서 루트킷이 커널 영역에서 후킹을 사용한다고 했다. 이것과 관련된 내용은 많이 있을 것이므로 간략하게 말하자면 KPP 다시 말해서 패치가드는 후킹의 대상이 되는 SSDT 같은 부분을 주기적으로 모니터링하여 변경이 일어나지 않게 하는 방식이다. 변경이 인식되는 즉시 BSOD가 뜨기 때문에 루트킷은 물론이고 이 영역을 후킹해서 보안 목적으로 사용하던 안티바이러스도 커널 코드 후킹을 사용할 수 없게 되었다.


  다른 하나는 KMCS(Kernel Mode Code Signing)이다. 이름과 같이 이제부터는 서명된 드라이버만 커널에 로드할 수 있게 되었다. 루트킷도 드라이버이기 때문에 이것을 우회하지 않는 이상 루트킷이 활동하기 어려워졌다. 물론 KPP와 달리 안티바이러스의 입장에서는 큰 상관은 없는 부분이다.


  마지막으로 필터 관리자가 커널에 추가되었는데 안티바이러스의 경우 이것을 사용하는 파일시스템 미니필터 드라이버를 개발함으로써 과거 후킹을 통해 파일들을 검사하던 것보다 더 개선된 방식을 사용할 수 있게되었다.


  지금까지 언급한 것들을 정리하자면 과거에는 커널 모드에서 후킹을 사용해서 악성코드 자체 프로세스 및 레지스트리를 보호하였을 것이고 파일 및 프로세스 모니터링도 수행하였다. 하지만 KPP로 인해서 커널 후킹이 불가능해졌고 마이크로소프트는 대신 파일시스템 미니필터 드라이버 및 여러 콜백 함수들을 제공하여 원래 사용하던 기능들을 제공해 준다. 





2. 드라이버 기본

2.1 컴퓨터 설정

  처음 공부하는 입장에서는 드라이버 개발과 관련해서 시작하는것 부터 까다롭다. 개발 환경에 대해서 설명하기 이전에 실행 대상이 되는 현재 컴퓨터부터 말해보겠다. 이것은 윈도우 10 x64이며 현재 테스트 모드로 실행 중이다. 앞에서도 언급하였듯이 KMCS로 인해서 본인이 개발한 드라이버를 본인 컴퓨터에 로드하는 것도 서명이 되어있어야만 가능하기 때문에 서명 정책을 사용하지 않게 설정한 것이다.


  기본적으로는 관리자로 명령 프롬프트를 열고 이곳에 다음 명령들을 입력하면 된다.

> bcdedit.exe -set loadoptions DDISABLE_INTEGRITY_CHECKS

> bcdedit.exe -set TESTSIGNING ON


  조금 된 컴퓨터의 경우 이 두 명령을 통해 "작업을 완료했습니다."라는 결과를 얻을 수 있다. 이후 컴퓨터를 재부팅(종료하고 다시 켜는 것이 아니다)하면 바탕화면의 오른쪽 아래 부분에 테스트 모드라는 글씨가 삽입되어 있는 것을 볼 수 있다.


  하지만 두 번째 입력에서 "요소 데이터를 설정하는 동안 오류가 발생했습니다. 값은 보안 부팅 정책에 의해 보호되며 수정 또는 삭제할 수 없습니다."라는 오류가 뜰 때가 있다. 일반적으로 최신 컴퓨터의 경우 강화된 보안을 위해 바이오스(BIOS) 펌웨어가 아닌 UEFI 펌웨어를 사용하는데 이것을 통해 Secure Boot(안전 부팅)가 지원된다. 어쨌든 이 설정을 없애주어야 한다. 컴퓨터 부팅 시 "F2" 또는 "DEL" 키를 계속 누르다 보면 UEFI 부팅 메뉴를 볼 수 있다. 이것은 설치된 것마다 다르겠지만 나의 컴퓨터의 경우 "부팅 옵션" 메뉴에서 "Safe Booting"을 OFF시켰다. 이제 아까처럼 명령어를 입력하면 오류가 뜨지 않을 것이고 재부팅 이후 테스트 모드를 확인할 수 있게 된다.


  반대의 경우 원상복구하는 명령들은 다음과 같다. 재부팅을 하고 다음에는 UEFI를 on시키면 된다.

> bcdedit.exe -set loadoptions ENABLE_INTEGRITY_CHECKS

> bcdedit.exe -set TESTSIGNING OFF



2.2 드라이버 개발 환경

  나의 경우 지원 문제로 인해 Visual Studio 2015와 WDK 10을 설치했다. WDK 10의 경우 아직 Visual Studio 2017을 지원하지 않았기 때문이다. 프로젝트의 경우 "템플릿 -> Visual C++ -> Windows Driver"까지 고른다. 여기서는 두 가지를 사용할 것이다. 일반적인 대부분의 것들은 "Legacy -> Empty WDM Driver"를 고른 후에 개발하면 된다. 하지만 미니필터 드라이버의 경우 "Devices -> Filter Driver: Filesystem Mini-filter"를 골라야 한다. 여기서 다루는 예제 중 미니필터 드라이버를 제외하고 다른 것들만 간단하게 테스트해보고 싶다면 WDM으로 간단하게 개발하면 될 것이고 미니필터 드라이버를 포함하여 전체적으로 개발하고자 한다면 미니필터 드라이버를 고른다. 이것은 미니필터 드라이버 뿐만 아니라 다른 추가적인 내용을 넣을 수도 있기 때문이다.


  현재 컴퓨터가 x64이기 때문에 Release 모드 및 x64를 선택했다. 소스 코드의 경우 생성할 때 확장자를 c로 설정한다. 이제부터 여러 설정을 추가하도록 하겠다. 사실 개발하고 로드하고 하면서 여러 개의 에러가 뜨는데 물론 쉽게 검색하여 설정할 수 있지만 여기서는 정리하면서 미리 설정하도록 한다. 


  먼저 "프로젝트 -> 속성" 항목을 연다. "Driver Settings -> Target OS Versiion"은 드라이버가 로드될 컴퓨터의 운영체제를 선택할 수 있다. 내 컴퓨터는 윈도우 10이기 때문에 윈도우 10으로 설정한다. 다른 컴퓨터에서 드라이버를 설치할 경우 윈도우 7 같이 다른 버전을 설정할 수도 있다. 


  다음으로 "inf2Cat -> UseLocalTime"를 "예" 즉 "/uselocaltime"으로 설정한다. 이것을 설정하지 않으면 "Inf2Cat, signability test failed." 에러를 볼 수 있다. 마지막으로 "링커 -> 명령줄"의 추가 옵션 부분에 "/integritycheck"를 입력한다. 뒤에서 사용할 확장 함수들의 경우(Ex가 붙은) 무결성 검사 옵션을 사용하지 않으면 STATUS_ACCESS_DENIED 에러가 발생하여 해당 함수를 사용할 수 없게 된다.


  현재 개발하는 것이 파일시스템 미니필터 드라이버라면 .inf 파일을 수정할 필요가 있다. 대부분 실제 회사에서 개발하는 입장에서야 손댈 것이 많아보이지만 테스트하는 입장에서는 에러가 뜨지 않게만 하면 될 것으로 보인다. 에러 코드는 다른 것과 다르게 직접적인데 Class, ClassGuid 부분을 위의 주석에 보이는 형태로 수정하면 된다. 그대로 복사해서 붙여넣기 해도 테스트하는데에는 지장이 없다.


  마지막으로 "C/C++ -> 일반 -> 경고를 오류로 처리"를 아니요(/WX-)로 설정한다. 능력의 부족으로 최대한 좋게 만드려 해도 코드가 조잡하므로 간단한 경고들은 무시하려고 했다.



2.3 도구

  드라이버 개발 및 테스트를 위해서는 Visual Studio 2015와 WDK 10 외에도 여러 도구들을 알아야 한다. 먼저 드라이버 로더의 경우 개인적으로 다음 링크의 오픈 소스 프로그램을 사용한다. [ https://github.com/tandasat/DrvLoader ] 물론 로드 기능밖에 없긴 하지만 Legacy, WDM 뿐만 아니라 파일 시스템 미니필터 드라이버도 간단하게 로드할 수 있다. 해당 소스 코드를 컴파일하고 사용할 때는 관리자 모드로 명령 프롬프트를 열고 인자로 드라이버 바이너리의 경로를 주면 된다. 파일 시스템 미니필터 드라이버를 설치할 때는 --filter 등의 옵션을 주면 성공적으로 설치해 준다. 참고로 미니필터 드라이버의 경우 소스 코드를 보면 Altitude(고도)가 370000으로 설정되어 있으므로 컴파일 시에 수정하면 된다. 나의 경우에는 굳이 수정하지 않고 테스트했다. 참고로 명령 프롬프트에서 fltmc를 입력하면 현재 설치되어 있는 필터 드라이버들과 Altitude를 볼 수 있다. 


  원래는 sc를 이용해서 옵션으로 create를 주는 방법도 있지만 DvrLoader를 사용하는 것이 훨씬 간단하기 때문에 이것을 사용한다. 대신 다른 명령어는 sc를 사용하기로 한다. DvrLoader를 통해 드라이버를 로드하면 자동으로 sc create 뿐만 아니라 sc start도 사용된다. 즉 드라이버 로드 이후 바로 시작하는 것이다. 우리는 등록한 드라이버를 삭제하거나 잠시 언로드 시킬 필요가 있을 것이다. 다음은 myDriver를 설치한 이후 설치된 드라이버의 이름이 맞는지 확인해 보고 stop, 다시 start 그리고 delete까지 수행해 보겠다.


> sc query myDriver

  myDriver로 설치된 것이 맞다면 기본 정보들이 보일 것이다.

> sc stop myDriver

  드라이버를 중지시킨다.

> sc start myDriver

> sc stop myDriver

  드라이버 재실행 이후 다시 중지시킨다.

> sc delete myDriver

  드라이버를 삭제한다. 참고로 위처럼 드라이버를 stop시킨 후에 삭제해야 한다. 참고로 create 옵션은 다음과 같이 사용한다. 하지만 미니필터 드라이버의 경우 복잡해지기 때문에 그냥 DvrLoader를 사용하기로 한다.

> sc create [drivername] binPath= [path] type= kernel


  지금까지 기본 개발 뿐 아니라 로드와 언로드를 알아보았다. 하지만 우리는 드라이버가 제대로 동작하는지 확인하기 위해서 DbgPrint(), DbgPrintEx() 등의 함수를 이용해 메시지를 받아 볼 것이다. 이 메시지들은 DebugView를 이용해서 확인할 수 있다. 참고로 "Capture" 항목에서 [ Capture Kernel, Enable Verbose Kernel Output, Capture Events ]를 설정해야 메시지를 받아 볼 수 있다. 뒤에서는 DLL을 로드하게 될 것인데 우리는 이것이 제대로 로드되었는지 확인하기 위해 dllmain()에서 OutputDebugStringA()를 이용해 메시지를 출력하게 할 것이다. 이 메시지는 "Capture" 항목에서 [ Capture Win32, Capture Events ]를 설정하면 볼 수 있다.


  마지막으로 Process Explorer를 알아보겠다. 이것은 프로세스 종료 시도 시에나 DLL 인젝션을 수행했을 때 DLL이 제대로 로드되었는지를 확인할 때 사용한다. "View -> Lower Pane View -> DLLs"를 클릭하면 아래에 DLL 창이 뜬다. 이제 특정 프로세스를 클릭하면 아래에 해당 프로세스에서 로드한 DLL들의 목록을 볼 수 있다. 프로세스 종료는 해당 프로세스를 마우스 오른쪽 키로 클릭한 후 "Kill Process"를 클릭하면 된다.



2.4 DriverEntry / DriverUnload

  앞에서도 설명했듯이 아주 기초적인 내용은 적지 않기로 한다. 물론 개인적인 정리를 포함하기도 하기 때문에 뜬금없는 내용은 있을 것이다. 프로그램 작성 시 인자를 선언만 하고 참조를 하지 않는 경우에는 C4100 에러가 발생한다. 물론 앞에서 "경고를 오류로 처리"를 아니오로 설정하였기 때문에 컴파일하는데는 지장이 없겠지만 UNREFERENCED_PARAMETER 매크로를 이용해서 참조하지 않을 경우에도 warning을 없앨 수 있다.


  이것 외에도 여기에는 보이지 않지만 다른 곳에서 자주 사용되는 내용을 정리하겠다. 먼저 NTSTATUS는 시스템에서 내부적으로 사용하는 오류 코드이다. 대표적인 값으로 STATUS_SUCCESS가 있는데 이것은 0x00000000 즉 성공을 의미한다. 일반적으로 특정 함수를 실행시키고 반환 값을 NTSTATUS 변수로 받는다. 이후 NT_SUCCESS 매크로에 해당 변수를 인자로 넣음으로써 반환 값이 성공했는지 여부를 확인한다. 이 매크로는 NTSTATUS 값이 STATUS_SUCCESS 같이 성공에 해당하는 값인 경우에는 TRUE를, 실패의 경우에는 FALSE를 반환한다. NTSTATUS 값들 중에 성공을 의미하는 값이 하나가 아니기 때문에 NT_SUCCESS 매크로를 이용해서 간단하게 성공 여부를 TRUE / FALSE로 확인할 수 있다.


  마지막으로 RtlInitUnicodeString() 함수도 자주 사용된다. 유저 모드에서와는 달리 드라이버에서는 이 함수를 이용해서 유니코드 문자열을 초기화해서 사용하는 경우가 많은 것 같다.


  DriverEntry()에서는 먼저 DriverUnload를 등록할 것이다. 그리고 Installxxx 형태의 함수들을 호출하는데 이것은 우리가 다룰 주제들 즉 각 기능들을 설치하는 함수들이다. 반대로 DriverUnload()에서는 이렇게 설치한 기능들을 제거한다. 드라이버 로드 시에 DriverEntry()가 호출되면서 이 기능들이 설치되고, 드라이버 언로드 시에 DriverUnload()가 호출되면서 설치한 기능들을 제거할 것이다. 


  소스 코드는 크게 3개로 나누었다. 하나는 파일시스템 미니필터 드라이버(miniFilter), 두 번째는 행위기반 분석을 위해 후킹 DLL을 삽입하는 방식(BehaviorBased), 마지막으로 프로세스 보호 및 자가 보호 그리고 레지스트리 보호(threeProtection)이다. 3개를 통합하려고 했지만 전혀 안정적으로 보이지 않아서 포기했다. 





3. 자가 보호

  코드 상으로 보면 DriverEntry의 InstallSelfProtect()와 DriverUnload()의 UnInstallSelfProtect()가 관련된 내용이다. 실제 함수는 SelfProtect.h에 구현되어 있다. 여기서 사용하는 함수는 ObRegisterCallbacks()와 ObUnRegisterCallbacks()이다.


  ObRegisterCallbacks() 함수는 스레드, 프로세스 그리고 데스크탑 핸들 오퍼레이션 시에 호출되는 콜백 루틴들을 등록해 준다. 구체적으로 예를들어 보자면 어떤 프로세스가 보호하려는 프로세스를 종료시키고 싶다고 하자. 이 경우에는 특정한 행위를 수행하기 위해 먼저 핸들을 얻을 것이다. 등록된 콜백 함수는 이렇게 핸들을 얻을 때 호출된다. 핸들을 얻을 때는 대상에 대한 접근 권한을 설정하는데 이 콜백 함수는 얻으려는 접근 권한 중에서 특별한 것들을 제거할 수 있다. 이로써 다른 프로세스가 보호받는 프로세스를 종료하기 위해 핸들을 얻고 종료시키려고 하지만 종료할 수 있는 권한이 제거되어 있기 때문에 종료가 불가능하게 된다. 물론 Handle Operation 이전 뿐만 아니라 이후에 호출되는 콜백 루틴도 등록할 수 있다.


  이 예제에서는 구체적으로 이름이 "calc.exe"인 프로세스에 대한 핸들을 얻을 때 PROCESS_TERMINATE, PROCESS_VM_READ, PROCESS_VM_WRITE, PROCESS_VM_WRITE 권한을 제거한다. PROCESS_TERMINATE 권한 제거로 인해 다른 프로세스에서 이것을 종료시킬 수 없게 된다. 나머지 3개는 DLL 인젝션 시에 많이 본 권한일 것이다. DLL 인젝션 시에는 DLL을 삽입할 프로세스의 메모리를 조작할 필요가 있기 때문에 해당 권한이 필요했다. 하지만 이 권한을 제거함으로써 보호받는 프로세스는 다른 프로세스에 의해 종료될 수도, DLL 인젝션 공격을 받을 수도 없게 된다.


  개인적으로 이 함수는 아직까지 완벽하게 이해를 하지 못했다. 먼저 PROCESS_TERMINATE 권한을 제거하더라도 직접 종료가 가능한 것으로 보인다. 그리고 명령 프롬프트에서 taskkill 명령어를 이용한 종료도 가능한 것 같다. Process Explorer를 통해 제거하는 경우에는 종료가 불가능한 것으로 나온다. 그리고 나머지 세 권한도 이해하지 못하는 부분이 있다. "calc.exe" 프로세스를 먼저 실행시킨 후 드라이버를 로드하면 해당 권한을 얻는 것이 불가능한 것은 맞는 것 같다. 하지만 드라이버를 먼저 로드한 이후에 해당 프로세스를 실행하면 실행이 제대로 되지 않는다. Process Explorer를 보면 해당 프로세스가 Suspended 상태로 존재한다. 이것을 Resume 시키면 제대로 실행되며 이후에 종료가 불가능한 것으로 보아 제대로 동작하는 것 같지만 실행 자체가 제대로 되지 않으며 그 이유를 아직도 모르겠다. 뭔가 잘못된 부분이 있든지 아니면 프로세스 실행 시에 위의 세 권한이 필요한 것인지 잘 모르겠다.


  마지막으로 Altitude 즉 고도에 관한 설정도 존재하는데 현재 개발 중인 드라이버가 미니필터 드라이버가 아니라면야 아무 값을 넣어도 상관 없어 보인다. 그리고 미니필터 드라이버라고 하더라도 값이 달라도 큰 차이는 없는 것 같다. 이것과 관련된 강제적인 사항을 아직 찾지는 못했다.





4. 스캐닝

4.1 파일 시그니처 기반 스캐닝

  처음 파일시스템 미니필터 드라이버를 오픈하면 800라인이 넘는 스켈레톤 코드가 보인다. 여기서는 이것 대신 최소화된 내용인 예제 코드를 이용하도록 한다. 기본 방식은 아주 간단하다. minifilter driver registration structure를 설정한 후 FltRegisterFilter()에 인자로 넣고 미니필터 드라이버를 등록한다. 이후 FltStartFiltering()을 이용해 필터링을 시작한다. Registration 구조체에 등록한 것들 중 중요한 부분은 FLT_OPERATION_REGISTRATION 구조체들의 배열이다. 이 예제에서는 IRP_MJ_CREATE, IRP_MJ_SET_INFORMATION의 경우에 PFLT_PRE_OPERATION_CALLBACK 루틴을 등록하였다. 자세히 설명하자면 파일의 쓰기 및 삭제에 해당하는 IRP에 대해 우리가 구현한 콜백 함수를 등록하겠다는 의미이며 PFLT_PRE_OPERATION_CALLBACK이므로 이 행위 이전에 호출되게 된다. 


  일반적으로 백신에서는 필터링을 통해 파일을 검사하고 허용할지 여부를 판단하겠지만 이 예제에서는 단지 DbgPrint()로 어떤 파일에 어떤 행위가 발생하였는지를 출력하고자 한다. 등록한 루틴 즉 PreOperationCallback()을 살펴보면 IRP_MJ_CREATE의 경우 FILE_WRITE_DATA 또는 FILE_APPEND_DATA일 때 process_irp() 함수를 호출하고, IRP_MJ_SET_INFORMATION의 경우 DeleteFile일 때 process_irp()를 호출한다. process_irp()에서는 각 상황에 맞게 어떤 이름의 파일에서 쓰기 또는 삭제 행위가 일어났는지를 DbgPrint()로 보여준다.



4.2 행위 기반 스캐닝

  행위 기반 즉 휴리스틱 스캐닝은 크게 두 가지로 나뉜다. 하나는 정적 휴리스틱이며 다른 하나는 동적 휴리스틱이다. 정적 휴리스틱의 경우에는 파일을 분석하여 의심스러운 특징을 찾아내어 판단하는 방식이다. 동적 휴리스틱의 경우에는 두 가지 방식으로 나뉘는 것 같다. 


  하나는 HIPS(Host based Intrusion Prevention System)에서 사용되는 방식이다. 이 기능을 순수하게 휴리스틱 방식으로 사용하는 안티바이러스가 있는가 하면 HIPS로 분리하여 보여주는 안티바이러스도 있는 것으로 보인다. 이것은 API 함수들을 후킹하여 실시간으로 어떤 함수들이 사용되는지를 검사하여 의심스러운 행위로 보일 때 탐지하는 방식이다. 


  다른 하나는 에뮬레이터를 이용한 방식이다. 안티바이러스에서는 에뮬레이터 외에도 샌드박스라고도 불리는데 그냥 같은 의미로 사용되는 개념인 것 같다. 에뮬레이터는 굳이 동적 휴리스틱 방식 외에도 다양하게 사용된다. 즉 실행 파일 에뮬레이팅 외에도 셸 코드 분석에 그리고 실행 파일 언패킹에도 사용된다. 어쨌든 에뮬레이터 내에서 실행 파일을 실행시키고 그 행위를 분석하는 방식이다.


  여기서는 앞에서 HIPS로 설명한 부분 즉 PsSetLoadImageNotifyRoutine()을 이용한 방식을 설명하도록 한다. PsSetLoadImageNotifyRoutine() 함수는 이미지가 로드될 때 호출되는 콜백 루틴을 등록한다. 이 예제에서는 이미지 로드 시 로드된 이미지가 ntdll.dll인지 먼저 검사한다. ntdll.dll인 경우 Kernel to User land APC injection 방식을 이용한다. 아직 제대로 정리하진 못했지만 유저 모드에서 APC를 이용한 전형적인 DLL 인젝션 방식과 비슷해 보인다. 구체적으로는 ntdll.dll의 LdrLoadDll() 함수를 호출시키는데 인자로 삽입하길 원하는 DLL의 경로를 넣는다. 


  이 예제에서는 x64 DLL의 경우 System32 폴더에, x86 DLL의 경우 SysWOW64 폴더에 각각 이름을 InjectionMitigationDLLx64.dll 그리고 InjectionMitigationDLLx86.dll로 정하고 넣어 놓는다. 개인적으로는 dllmain()에서 OutputDebugStringA()를 이용해 메시지를 출력하게 함으로써 dll이 제대로 로드되었는지를 확인하였다. 또는 Process Explorer로 현재 로드된 DLL들을 확인해 볼 수 있다.


  이것은 예제일 뿐이고 검색하여 얻은 정보에 의하면 안티바이러스에서는 이러한 방식으로 ntdll.dll이 로드될 때 후킹 엔진이 구현된 DLL을 삽입시킨다. 삽입된 DLL은 로드될 때 dllmain()이 실행되는데 악의적인 행위에 사용될 가능성이 있는 API 함수들을 후킹한다. 즉 어떤 API 함수들이 호출되는지에 대하여 보고받기 위해 후킹하는 것이다. 안티바이러스는 각 후킹된 API들이 호출되는 그 행위를 이용해 분석하여 행위 기반 분석을 수행한다. 





5. 프로세스 / 스레드 생성 보호

  코드 상으로 보면 DriverEntry의 InstallPsProtect()와 DriverUnload()의 UnInstallPsProtect()가 관련된 내용이다. 실제 함수는 PsProtect.h에 구현되어 있다. 여기서 사용하는 함수는 PsSetCreateProcessNotifyRoutineEx()이다. 이것은 PsSetCreateprocessNotifyRoutine()의 확장된 버전이다. 이 함수로 등록한 콜백 루틴은 프로세스 생성 직전에 호출된다. 참고로 두 번째 인자의 경우 설치 시에는 FALSE, 제거 시에는 TRUE를 입력한다.


  이 예제에서는 간단하게 프로세스 실행 전에 호출되어 이 프로세스가 특정한 이름을 갖는지 검사한 후에 실행 여부를 판단한다. PsSetCreateThreadNotifyRoutineEx() 및 PsSetCreateThreadNotifyRoutine()도 프로세스가 아니라 스레드라는 것만 빼면 같은 내용이다. 이 예제에서는 이 4가지 함수들 중에서 PsSetCreateProcessNotifyRoutineEx()만 사용하였다.


  일반적으로 안티바이러스에서는 이 함수를 이용해서 (어느 안티바이러스 업체에서는 PsSetCreateThreadNotifyRoutine()만 사용하는 것 같다) 프로세스가 실행될 때 마다 이 콜백 함수를 이용하여 검사한 후에 실행 여부를 판단할 것이다. 물론 여기서 사용하는 예제처럼 검사에 이름을 이용하지는 않을 것이다.





6. 레지스트리 보호

  코드 상으로 보면 DriverEntry의 InstallRegMonitor()와 DriverUnload()의 UnInstallRegMonitor()가 관련된 내용이다. 실제 함수는 RegMonitor.h에 구현되어 있다. 여기서 사용하는 함수는 CmRegisterCallbacksEx()와 CmUnRegisterCallback()이다. 참고로 CmRegisterCallbacksEx()는 CmRegisterCallbacks()의 확장 버전이며 여기서는 이 확장 버전을 사용한다.


  이 함수는 Registry Operation 시에 호출되는 콜백 루틴을 등록시켜 준다. 이 예제에서는 콜백이 호출된 경우 어떤 operation을 수행하려고 하는지 검사한다. 해당 operation이 RegNtPreCreateKeyEx, RegNtPreOpenKeyEx라면 다시 말해서 특정 프로세스에서 해당 키를 생성하거나 읽으려는 operation이 요청되었다면 그 키가 우리가 보호하려는 키인지 검사한 후에 맞다면 STATUS_ACCESS_DENIED를 반환시킨다.


  안티바이러스의 경우 드라이버 뿐만 아니라 서비스도 실행 중일 것인데 악성코드가 이것들의 정보를 담고 있는 레지스트리를 삭제하거나 수정하려는 시도를 할 수 있다. 테스트를 위해서 regedit.exe를 실행한 후에 보호하고 있는 레지스트리 키를 클릭하면 접근이 거부되었다는 것을 확인할 수 있다.





7. ELAM 및 Protected Process

  윈도우 8부터 Secure Boot라는 기능이 도입되었다. 이것은 윈도우 부팅 설정 및 구성 요소들을 보호하고 ELAM(Early Launch Anti-Malware) 드라이버를 로드한다. ELAM 드라이버는 다른 boot-start 드라이버들보다 먼저 실행되어 다른 드라이버들을 검사하는 역할을 한다. 즉 다른 드라이버들의 무결성을 검사하며 부트 드라이버가 수정되었는지 확인하여 부트킷을 방지한다.

  안티바이러스의 유저 모드 서비스는 과거부터 지금까지 악성코드의 공격 대상이 되어 왔다. 윈도우 8.1부터는 Protected Service라는 개념을 통해 유저 모드 서비스를 보호한다. 이것은 System Protected Process로서 실행되는 서비스이다. 안티바이러스 벤더들은 ELAM 드라이버를 이용해 사용자 모드 서비스를 Protected Service로 실행한다. 이를 위해서 ELAM 드라이버의 리소스 섹션에는 해당 바이너리(서비스)를 서명하는데 사용되는 인증서에 대한 정보가 있어야 한다. 부트 타임 시 이 리소스 섹션이 추출되어 인증 정보를 검증하고 사용자 모드 서비스를 등록하면 서비스는 Protected Service로서 실행된다.

  권한 관련 문서에서 설명한 내용이지만 간략하게 언급하자면 관리자 권한인 High Integrity Level을 가진 프로세스의 경우 SeDebugPrivilege를 활성화할 수 있고 이를 통해 System Integrity Level을 가진 프로세스에 코드 인젝션을 수행할 수 있다. 다시 말해서 Protected Process는 관리자 권한으로 실행된 프로세스로부터의 공격을 방어하기 위한 메커니즘이다. Protected Process는 PROCESS_ALL_ACCESS 뿐만 아니라 PROCESS_CREATE_PROCESS, PROCESS_CREATE_THREAD, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE 등의 접근 권한이 거부된다.





8. 유저 모드 애플리케이션과의 통신

  이 내용은 예제 코드의 communication 부분에 정리되어 있다. 기본적인 사항을 정리한 후 예제와 함께 설명할 것이다. 유저 모드 애플리케이션과 커널 모드 드라이버가 통신하는 방법 중에서 IOCTL 명령을 이용한 방식을 설명한다. IOCTL 명령은 둘 사이의 통신을 위해 프로그래머가 정의한 명령이다. IRP는 일종의 데이터 구조체로서 드라이버에서는 IRP의 종류에 맞게 이것을 처리하는 함수가 필요하다. I/O 관리자는 애플리케이션의 요청이 있을 때 IRP를 생성해 드라이버 오브젝트의 MajorFunction[] 필드를 이용해 해당 디스패치 루틴으로 IRP를 전달(dispatch)해 준다. 그래서 Dispatch 루틴으로 불린다. 즉 드라이버가 처리하는 IRP 명령어들을 위한 디스패치 루틴들이다.


  애플리케이션은 드라이버와 통신하기 위해서는 CreateFile()하는데 필요한 핸들을 구해야 한다. 이것을 위해 드라이버는 디바이스 이름, 이것의 심볼릭 링크 그리고 디바이스 오브젝트에 대한 포인터가 필요하며 이것을 전역으로 설정한다. 이후 IoCreateDevice()로 디바이스를 그리고 IoCreateSymbolicLink()를 통해 심볼릭 링크를 생성한다. IoCreateDevice()로 디바이스 생성 이후 IoCreateSymbolicLink()로 디바이스에 심볼릭 링크를 지정하는 것이다. 마지막으로 처리할 IRP에 대한 Major Function 즉 디스패치 루틴을 등록한다. IRP_MJ_CREATE의 경우에는 애플리케이션이 CreateFile()로 디바이스 드라이버의 핸들을 얻을 때 발생한다. IRP_MJ_CLOSE는 애플리케이션이 CloseHandle()을 사용할 때 발생한다. 여기서 중요하게 다룰 것은 IRP_DEVICE_CONTROL이다. 이것은 애플리케이션에서 deviceIoControl()을 이용해서 드라이버와 통신할 때 발생한다. 먼저 CTL_CODE 매크로를 이용해 커스텀 컨트롤 코드를 정의한다. 이것은 애플리케이션에서도 똑같이 정의할 것이며 애플리케이션에서 이 컨트롤 코드를 보낼 시에 특정 행위를 수행한다. 이 예제에서는 애플리케이션에서 커스텀 컨트롤 코드를 받은 경우에 같이 받은 데이터를 DbgPrint()로 보여주고 동시에 데이터를 애플리케이션으로 보낸다.


  애플리케이션의 경우 CreateFilte()을 통해 통신할 디바이스의 핸들을 얻는다. 이후 DeviceIoControl()을 통해 드라이버와 통신한다. 이 함수는 2개의 버퍼를 제공하며 디바이스 드라이버와 양방향으로 통신이 가능하다. 드라이버에서와 같이 CTL_CODE 매크로를 이용해 커스텀 컨트롤 코드를 정의한다. DeviceIoControl()에는 커널에서 받은 데이터를 저장할 버퍼도 갖는다. 이 예제에서는 애플리케이션에서 커스텀 컨트롤 코드와 함께 드라이버로 데이터를 보냄과 동시에 드라이버로부터 데이터를 읽는다.




9. 기타

9.1 네트워크 보호

  안티바이러스에서도 기본적으로 방화벽 기능이 제공된다. 과거에는 NDIS 및 TDI 필터 같은 드라이버를 개발해야 했지만 최근 비스타 부터는 WFP(Windows Filtering Platform)라는 플랫폼을 제공한다. 이것은 네트워크 필터링 애플리케이션을 만들기 위해 제공되는 플랫폼으로서 API와 시스템 서비스들의 집합이다. 참고로 과거의 NDIS / TDI는 다른 필터 드라이버들보다 개발하기 위한 난이도가 있었지만 WFP라는 간단한 개발 플랫폼이 제공됨으로 인해 최근 버전의 윈도우에서 돌아가는 안티바이러스 프로그램들은 이것을 사용한다고 한다.



Posted by SanseoLab

0. 개요

  여기서는 안드로이드 앱의 다운로드에서부터 설치, 시작까지의 과정을 다루기로 한다. 아직 안드로이드에 익숙하지 않고 인터넷에 자료가 거의 없으며 꾸준히 업데이트되고 있음으로 인해 정확한 자료가 아닐 가능성이 포함되어 있을 것으로 예상한다. 하지만 찾아보며 공부하는 사람들의 입장에서 키워드 제공의 의미라도 있길 바라며 정리한 내용을 올린다.





1. 다운로드

  일반적으로 앱은 구글 플레이 스토어에서 설치하는 방식이 대부분일 것이다. 물론 직접 apk 파일을 다운로드 받아서 설치하는 방식도 존재한다. 구글 플레이 스토어에서 악성코드가 발견되었다는 뉴스가 자주 보이기는 하지만 기본적으로 악성코드는 직접 설치 방식을 악용한다. 예를들어 블랙 마켓에서 무료 버전이라고 올려놓지만 내부에 악의적인 코드가 존재하는 경우가 있을 수 있고 성인물 사이트에서 성인 앱을 위장해서 설치하게 유도하는 방법이 있을 수 있다. 마지막으로 가장 대표적인 방식은 SMS/MMS를 이용한 피싱이다.





2. 설치

  변경 및 생성된 결과물을 기반으로 설명해 보겠다. 먼저 apk 파일 부터 보자면 시스템 앱들 즉 사전에 설치된 apk들은 /system/app/에 저장되어 있다. 사용자가 설치한 앱은 /data/app/<appname>/에 base.apk라는 이름으로 복사되며 Native 라이브러리는 /data/app/<appname>/lib/*.so 형태로 저장된다.


[ /data/app/<appname>/base.apk ]

[ /data/app/<appname>/lib/*.so ]


  apk에서 추출된 dex 파일은 /data/dalvik-cache/data@app@<appname>@classes.dex로 복사된다. 그리고 애플리케이션의 데이터는 /data/data/<appname>/과 서브디렉터리에 저장된다. 마지막으로 /data/data/<appname>/lib은 위의 /data/app-lib/<appname>/libapp.so에 대한 심볼릭 링크이다.


[ /data/dalvik-cache/data@app@<appname>@classes.dex ]

[ /data/data/<appname>/ ]

[ /data/data/<appname>/lib ]


  참고로 설치 과정에서 AndroidManifest.xml의 내용이 파싱되어 패키지 정보가 /data/system/packages.xml과 /data/system/packages.list에 추가된다.


[ /data/system/packages.xml ]

[ /data/system/packages.list ]


  이제 dex 파일에 대해서 조금 더 설명하기 위해 odex라는 확장자를 설명하도록 하겠다. 먼저 dexopt라는 유틸리티가 존재하는데 dex 파일을 pre-optimization한 .odex 파일 즉 optimized DEX 파일로 변환해주는 역할을 한다. 이것은 설치 과정에서 일어나는데 저장된 파일을 보면 .dex 확장자를 갖지만 실제로는 .odex 파일이다. Dalvik 가상 머신에서 실행되는 바이트 코드인 것은 같지만 최적화되어 성능을 향상시켜 준다.


  이 개념을 기반으로 하여 설명하겠다. 최근 안드로이드에서는 Dalvik VM이 아니라 ART 즉 Android RunTime이 사용된다. 이 메커니즘은 바이트 코드를 가상 머신에서 실행시키는 방식이이 아니라 설치 시점에 컴파일하여 ELF 파일 포맷을 만들며 이것을 실행하는 것이다. 앞에서 dexopt를 통해 .odex를 만드는 것이 아닌 dex2oat를 통해 oat 파일 포맷의 바이너리를 만든다. 이렇게 기능적으로도 많은 차이가 나지만 호환성을 위해서 이것도 Dalvik VM과 같은 과거의 이름을 확장자로 갖는다. 예를들어 과거에는 /data/dalvik-cache/<arch>/data@app@<packagename>.<appname>-1@base.apk@classes.dex였으며 안드로이드 6.0부터는 /data/app/oat/<pachagename>.<appname>/<arch>/base.odex이 되었다. 즉 어느 방식이든지 내부의 포맷은 달라도 이름은 같은 것이다.


[ /data/dalvik-cache/<arch>/data@app@<packagename>.<appname>-1@base.apk@classes.dex ]

[ /data/app/oat/<pachagename>.<appname>/<arch>/base.odex ]





2. 실행

  앞에서도 언근했듯이 안드로이드 5.0부터는 Dalvik VM 대신 ART를 사용한다. ART는 애플리케이션들과 시스템 서비스들에서 사용되는 런타임으로서 가상 머신을 통해 실행시키는 메커니즘이 아니라 런타임 환경을 제공하는 방식이다. apk의 내용을 보면 dex 파일이 존재하는데 이것은 Dalvik VM에서 실행할 수 있는 바이트 코드가 존재한다. 이것을 통해 Dalvik VM은 JIT 방식을 사용하였다. 하지만 가상 머신에서 실행시킨다는 것은 속도 등의 면에서 많이 부족할 수 밖에 없다. 그래서 앱 설치 시에 dexopt를 통해 .odex 파일로 최적화하지만 가상 머신에서 실행된다는 것 자체는 차이가 없기 때문에 한계가 존재하였다.


  ART 메커니즘은 AOT 즉 ahead-of-time 컴파일을 방식을 사용한다. 즉 apk의 dex 파일을 컴파일하여 oat 포맷 더 정확히는 elf 바이너리 형태로 만든다. 이것은 네이티브 기계어로서 가상 머신에서가 아닌 바이너리로서 동작할 수 있게 된다. 참고로 과거 방식에서는 설치 시에 dexopt를 통해 .odex 파일을 만들었다면 ART는 dex2oat를 이용해 oat 파일 포맷으로 앱을 컴파일한다. 물론 앞에서도 언급하였듯이 하위 호환을 위해 확장자는 oat가 아니라 dex 또는 odex가 사용된다. 참고로 안드로이드 7.0부터는 AOT와 JIT(just-in-time) 컴파일의 조합을 이용한다.


  이런 차이를 기반으로 비록 이제는 사용되지 않지만 Dalvik VM부터 시작하여 ART 런타임 메커니즘을 비교하면서 알아보도록 하겠다. 안드로이드 부팅 후 가장 먼저 시작되는 프로세스인 Init은 /system/core/rootdir/init.rc를 읽고 여기에 적힌 내용을 수행한다. 여러가지가 있겠지만 여기서는 앱의 실행과 관련된 것을 위주로 보겠다. Init은 /system/bin/app_process를 통해 AndroidRuntime 객체를 생성하며 이것은 DalvikVM을 실행시킨다. 그리고 DalvikVM에 Zygote를 실행하도록 요청한다.


  조금 더 진행하기 전에 Zygote에 대해서 설명하도록 하겠다. 이것은 안드로이드 앱에서 공통적으로 필요로 하는 자원, 라이브러리 등을 미리 로드시킨 후 대기하다가 새로운 애플리케이션 실행 시에 간단하게 자신을 fork 하는 메커니즘이다. 만약 애플리케이션 실행 시 마다 메모리 및 자원을 할당하고 로드하고 등의 작업을 한다면 부하가 많이 걸릴 것이다. 대신 Zygote라는 프로세스를 미리 만들어 놓아서 대기시킨 후 애플리케이션 실행 시에 fork를 통해 복사하여 필요한 부분 즉 실행시킬 애플리케이션과 관련된 부분만 처리해 준다면 중복된 행위를 통한 낭비를 줄일 수 있을 것이다.


  앞에서 DalvikVM을 실행시킨 후 Zygote를 실행하도록 요청한다고 하였다. 이 말은 Zygote 자체도 네이티브 바이너리가 아니라 가상 머신 위에서 돌아가는 바이트 코드 형태로 존재한다는 것을 의미한다. 즉 다른 애플리케이션과 차이가 없는 것이다. 그렇기 때문에 Zygote를 fork()한 후 실행시킬 애플리케이션을 로드하여 이 애플리케이션도 DalvkVM 위에서 실행될 수 있게 한다.


  이제부터는 ART 런타임 메커니즘을 알아보겠다. 이것은 가상 머신 방식이 아닌 런타임 환경을 제공하는 방식이다. 하지만 이 메커니즘이 Dalvk VM과 비교해서 완전히 바뀐 것은 아니다. Zygote를 이용하는 방식의 장점에 따라 이것을 수용하였고 단지 차이가 있다면 가상 머신을 이용하는 방식이 아니라는 점이다.


  app_process는 DalvikVM을 실행시키고 이것에 Zygote를 실행하도록 요청하지 않고 단지 Zygote를 실행시킨다. Zygote가 시작되면 ART 즉 libart.so를 로드하고 system/framework/<arch>/boot.oat, system/framework/<arch>/boot.art를 로드한 후 클래스 링킹 과정을 거친다. boot.art는 pre-initialized 클래스 및 객체들의 힙 등을 가진 이미지로서 기본적으로 프레임워크 함수와 실행 가능한 코드의 실제 주소 사이의 매핑 테이블을 제공한다. boot.oat는 pre-compiled 코드를 담고 있는 elf 파일인데 모든 안드로이드 프레임워크 바이트코드의 컴파일된 바이너리를 갖는다. 앱에서 프레임워크 함수를 호출하기 위해서는 boot.art 매핑 테이블에 쿼리하고 boot.oat의 텍스트 섹션에서 실제 코드를 호출하게 된다. 이것은 미리 zygote의 초기화 과정 즉 클래스 링킹 과정을 통해 프레임워크 라이브러리들의 클래스 멤버들에 접근할 수 있게 된다.


  이후부터는 Dalvik VM 방식과 비슷하다. 먼저 SystemServer 프로세스를 fork하는데 이것으로부터 다시 Framework Services, Package manger, Activicty Manager(Launcher) 등이 fork된다. 마지막으로 ActiveManagerService와 상호작용하기 위한 소켓을 생성하고 기다린다.


  이제 앱을 실행해 보도록 하자. 화면에서 클릭함으로써 Launcher의 onClick() Callback이 호출되고 이것은 Binder를 통해 Activity Manager의 StartActivity()를 호출한다. Activity Manager는 이 요청을 받으면 startViaZygote()를 호출하는데 Zygote가 이것을 받은다면 fork()한 후 Application Binding 과정이 시작된다. 이것은 프로세스를 실행할 애플리케이션에 어태치하는 과정이다. 여러 과정을 거쳐서 makeApplication() 메소드가 실행되는데 이것은 앱 관련 클래스를 메모리에 로드해 준다. 이후 Activity Manager는 realStartActivity()를 시작으로 하여 프로세스를 launch 시킨다.


  지금까지의 설명을 보충하자면 ART 메커니즘으로 바뀜으로서 윈도우나 리눅스에서처럼 oat 즉 elf 바이너리를 직접 실행할 수 있는지 궁금할 수 있다. 하지만 Zygote는 초기에 boot.art 및 boot.oat를 통한 초기화, ART 런타임 로드 등의 작업을 미리 하였고 이렇게 대기 중인 프로세스를 fork()한 후 oat 바이너리를 로드하여 실행하는 메커니즘이었다. 그렇기 때문에 이 많은 과정 없이 순수하게 oat 하나만으로 무엇인가를 할 수는 없다.




Posted by SanseoLab

  다음에 언급되는 개념들은 혼동하기 쉬우므로 여기서 정리하기로 한다. 먼저 문학적인 개념을 정리한 후에 논리적인 개념을 정리하기로 한다. 문학적인 개념으로 반어법(Irony), 역설법(Paradox), 모순어법(Oxymoron)을 다루도록 하겠다. 그리고 논리적인 개념으로써 모순(Contradiction), 역설(Paradox), 이율배반(Antinomy)을 다루기로 한다.


  반어법(Irony)은 가치의 반전을 의미하는데 다시 말해 표현에 나타나는 의미와 숨은 의미가 서로 상반되게 함으로써 의미를 강조하는 방식이다. Irony라는 단어는 일상생활에서도 자주 사용되는데 조금 더 자세히 예시를 들어 가면서 설명하겠다. 먼저 Verbal Irony 즉 언어적 반어가 있다. 이것은 겉으로 하는 말이 의도된 뜻과 다르게 사용하는 것이다. 예를들어서 잘못한 경우에 잘했다고 비꼬는 경우가 이것과 같다. 그리고 Dramatic Irony 즉 극적 반어가 있다. 이것은 등장인물은 깨닫지 못하지만 관객은 아는 경우라고 한다. 마지막으로 Situational Irony 즉 상황적 반어가 있다. 이것은 예상했던 상황과 반대의 결과가 만들어지는 것이다. 일상 생활에서 가장 많이 사용되는 방식이 이것일 것이다. 어떠한 것을 의도하였지만 결과가 생각했던 것과 다른 방식으로 흘러갈 때 사용될 수 있다.


  이제 Paradox 즉 역설법을 다루도록 하겠다. 사실 Paradox를 굳이 문학적인 개념과 논리적인 개념으로 나누고 따로 설명한다는 것이 맞는지는 모르겠지만 적어도 한국인의 관점에서는 이렇게 나눌 필요가 있어보인다. 역설법은 모순어법과 함께 설명하기로 하겠다. Oxymoron 즉 모순어법은 양립할 수 없는 두 단어를 의도적으로 연관시켜서 강조하는 기법이다. 예를들어서 "소리없는 아우성"을 보자면 아우성과 소리가 없다는 것은 양립할 수 없지만 여기서는 사용하였다. 비록 이것이 양립할 수는 없지만 문학적으로는 그 의미를 이해할 수 있을 것이다. 이 모순어법은 역설법의 일종인 수사법이라고 할 수 있다. 우리는 고등학교에서 문학을 공부할 때 역설법에 대해 다음과 같은 설명을 들었을 것이다. "언뜻 보기에는 모순되어 보이지만 그 의미는 받아들일 수 있는 즉 사실이라고 생각될 수 있는 표현 방식" 이것은 앞에서 설명하였듯이 모순어법이 역설법의 종류 중 하나인데 가장 대표적이어서인지는 몰라도 이 모순어법을 강조하여 설명한 것으로 보인다.


  문제는 Paradox라는 개념이 문학에서 의미하는 것과는 서로 다르므로 좀 더 제대로 된 설명이 필요하다. 이를 위해 먼저 Contradiction 즉 모순에 대해서 다루도록 하겠다. 모순은 하나가 참이면 반드시 다른 하나는 거짓이어야하는 상태 즉 두 개의 명제가 동시에 참이될 수 없는 것을 의미한다. 예를들면 "A는 B이다"라는 것과 "A는 B가 아니다"라는 두 명제는 동시에 참이될 수 없다. 하지만 이 두 가지를 같이 주장한다면 이것은 옳다고 할 수 없다.


  이 모순이라는 개념을 통해 Paradox라는 개념을 설명해보도록 하겠다. 앞에서도 언급하였듯이 문학에서는 역설법이라는 표현 방식을 가르치고 이것은 실제로는 모순어법의 개념이지만 어쨌든 양립할 수 없는 두 단어를 의도적으로 연관시킴으로써 강조하는 방식이라고 하였다. 그래서 역설법을 겉보기에는 틀리지만 실제로 그 의미상으로는 참이라고 할 수 있는 개념이라고 했다. 하지만 Paradox라는 단어는 다른 방식으로도 자주 사용된다. 참인 전제들을 가지고 합당한 추론을 함으로써 나오는 결론은 당연히 참이어야 하지만 자기 모순적이거나 논리적으로 받아들일 수 없는 결론이 나오는 상황을 Paradox라고 부르는 경우가 많다. 다시 말해서 문학적인 표현과는 반대로 겉보기에는 참인듯 하지만 실제로는 거짓이라는 것이다. 이렇게 Paradox는 철학 등의 개념에서 사용되는 방식과 문학에서 그리고 일상 생활에서 사용되는 방식이 매우 다양하게 사용되는 경향이 있다. 어딘가에서는 역설을 "부정하기 힘든 추론 과정을 거쳐서 받아들이기 힘든 결론에 도달하는 것"이라는 설명을 본 적이 있다. 이 설명을 기반으로 해석해보자면 조금 전에 설명한 방식 즉 참인 전제들을 가지고 합당하게 추론하였음에도 불구하고 모순적인 결론이 나온다는 것을 설명할 수 있을 것 같다. 또한 문학에서의 역설법 또한 설명할 수 있을 것으로 보이는데 양립할 수 없는 두 단어를 사용함으로써 틀린 표현으로 보아야 하지만 그 의미를 우리는 알아챌 수 있기 때문에 이 의미를 참으로 받아들여야 한다는 결론이 나기 때문이다. 물론 이 설명 말고도 더 광범위하게 "모순되는 결론을 낳는 추론"도 충분히 역설이라는 개념을 설명할 수 있을 것으로 보인다.


  마지막으로 자주 사용되는 표현 중 하나인 이율배반 즉 Antinomy에 대해서 알아보자. 모순은 하나가 참이면 다른 하나는 반드시 거짓이어야 하는 즉 두 명제가 양립할 수 없는 상태를 말한다고 하였다. 이율배반도 이것과 비슷하게 서로 모순 또는 대립되는 명제를 의미한다. 하지만 완전히 동일한 개념은 아니고 차이점을 조금 더 찾아보자면 이 명제가 주장이나 규칙 같은 특징을 가지고 있는 상황에서 이 두 가지를 모두 주장함으로써 생기는 오류를 이율배반이라고 부를 때가 많다.


  최대한 정리해 보았지만 아주 깊은 의미를 알지는 못하는 데다가 너무 많은 방식으로 사용되기 때문에 앞에서 정리한 내용이 틀릴 수 있다. 혹시 틀린 내용을 발견한다면 댓글 부탁드립니다.


Posted by SanseoLab


0. 개요

1. 윈도우 사용자 모드 후킹

.... 1.1 인젝션

........ 1.1.1 DLL 인젝션

................ 1.1.1.1 CreateRemoteThread()

................ 1.1.1.2 NtCreateThreadEx() / RtlCreateUserThread()

................ 1.1.1.3 QueueUserAPC()

................ 1.1.1.4 레지스트리 이용 ( AppInit_DLLs, AppCertDlls )

................ 1.1.1.5 SetThreadContext()

................ 1.1.1.6 Reflective DLL Injection

................ 1.1.1.7 Shims

........ 1.1.2 코드 인젝션

................ 1.1.2.1 기본적인 코드 인젝션

................ 1.1.2.2 Atom Bombing

................ 1.1.2.3 EWMI (Extra Window Memory Injection) / Powerloader Injection

................ 1.1.2.4 PROPagate

........ 1.1.3 취약점

........ 1.1.4 윈도우 비스타 이후의 변화

.... 1.2 후킹

........ 1.2.1 IAT 후킹

........ 1.2.2 Inline 후킹

.... 1.3 윈도우에서 제공되는 메시지 후킹

.... 1.4 기타

2. 리눅스 사용자 모드 후킹

.... 2.1 인젝션

........ 2.1.1 Ptrace()

.... 2.2 후킹

........ 2.2.1 PLT / GOT redirection

........ 2.2.2 Inline 후킹

.... 2.3 LD_PRELOAD









0. 개요

  프로그램의 흐름을 빼앗아서 본인이 원하는 코드를 실행시키는 기술을 후킹이라고 한다. 이를 위해서는 먼저 특정한 행위를 수행하는 코드가 존재해야 하기 때문에 이러한 코드를 프로그램에 삽입하는 방법(인젝션)이 필요할 것이며 삽입된 코드에서 실질적으로 프로그램의 흐름을 빼앗는 방법(후킹)이 있을 것이다. 여기서는 크게 윈도우와 리눅스 플랫폼에서의 후킹 및 인젝션 메커니즘을 설명할 것이고 이것은 사용자 모드에 한정한다.





1. 윈도우 사용자 모드 후킹

1.1 인젝션

  위에서 언급하였듯이 전제 조건으로서 먼저 원본 함수를 대체하거나 추가적인 기능을 가진 코드가 프로세스 내에 존재해야 할 것이며 이것 뿐 아니라 이 코드로 흐름이 넘어가도록 후킹을 설치하는 코드도 프로세스 내에 삽입되어 있어야 한다. 이렇게 두 종류의 코드가 존재하면 이후 이 설치 코드를 실행시킴으로써 후킹을 설치하여 궁극적으로 프로그램의 흐름을 빼앗고 원하는 행위를 수행할 수 있다.


  앞의 설명으로 알 수 있듯이 우리가 필요한 것은 저 코드를 삽입하는 것과 삽입된 코드를 실행하는 메커니즘이다. 일반적으로 인젝션은 코드 페이로드를 삽입시키는 코드 인젝션, 그리고 원하는 기능들을 모두 포함하는 DLL을 개발하고 이 파일을 삽입하는 방식인 DLL 인젝션 이렇게 두 가지가 존재한다. 이처럼 해당 프로세스의 가상 주소 공간에서 실행되는 코드들은 프로세스 자체의 권한을 가짐으로써 현 프로세스가 할 수 있는 모든 것을 할 수 있게 된다.


  참고로 지금까지 한 설명은 인젝션을 후킹의 목적으로 여기고 그것과 관련된 부분만 설명하였지만 후킹과 관련 없이 인젝션 자체적으로도 특별한 일을 할 수 있다. 꼭 후킹 함수와 후킹 함수를 설치하는 코드 이외에도 자신이 원하는 코드를 삽입하고 그 코드를 실행시킬 수 있다면 다양한 일을 할 수 있을 것이다. 이 부분은 뒤에서 알아본다.


  코드를 삽입하는 방법들은 뒤에서 차례대로 살펴볼 것이고, 여기서는 상대적으로 설명하기 간단한 코드를 실행시키는 메커니즘을 살펴보겠다. 먼저 DLL 인젝션의 경우 DLL Main에 수행하고자 하는 코드가 추가되어 있다면 DLL 인젝션만으로도 원하는 행위를 수행할 수 있다. DLL Main 부분은 DLL이 로드될 때 자동으로 실행되기 대문이다. DLL이 대상 프로세스 공간에 로드되면 대상 프로세스와 동일한 권한을 가지고 코드가 실행되므로 그 프로세스가 할 수 있는 모든 일을 할 수 있다. 그리고 당연히 가장 대표적인 행위가 후킹이다. 참고로 일반적인 API 후킹에서는 API를 후킹하는 함수를 설치하는 부분이 DLL Main에 존재해서 DLL 인젝션이 되자마자 바로 후킹 함수를 설치한다. 물론 후킹당한 원본 함수를 대체하거나 추가적인 기능은 DLL 내부의 다른 부분에 함수 형태로 존재할 것이다. 


  코드 인젝션인 경우에는 이렇게 자동으로 실행되는 부분은 없지만 WriteProcessMemory()로 페이로드를 써 넣은 후 이 부분을 CreateRemoteThread()를 통해 실행시킬 수 있다.





1.1.1 DLL 인젝션

  여기서는 코드를 삽입하는 방법 중에서 DLL 인젝션 기법을 살펴본다. 궁극적으로 DLL 인젝션은 대상 프로세스가 스스로 LoadLibrary()를 호출하게 만들어서 원하는 DLL을 로드하게 하는 것이다. 이렇게 원격 프로세스에 LoadLibrary()를 호출하게 하는 방식은 당연히 윈도우에서 기본적으로 제공되지는 않을 것이며 아래에서 설명할 여러 종류의 트릭들을 통해 수행된다.



1.1.1.1 CreateRemoteThread()

HANDLE WINAPI CreateRemoteThread (

    __in    HANDLE     hProcess,
    __in    LPSECURITY_ATTRIBUTES   lpThreadAttributes, 
    __in    SIZE_T      dwStackSize, 
    __in    LPTHREAD_START_ROUTINE  lpStartAddress, 
    __in    LPVOID      lpParameter, 
    __in    DWORD       dwCreationFlags, 
    __in    LPDWORD     lpThreadId
);


  이 함수는 임의의 프로세스의 가상 주소 공간에 스레드를 실행시키는 역할을 한다. hProcess는 대상 프로세스의 핸들이고, lpStartAddress는 스레드 함수의 주소이며 lpParameter는 스레드 함수의 파라미터이다. DLL 인젝션을 위해서는 이 함수가 원래 목표처럼 스레드를 생성하는 것이 아니라 실질적으로 LoadLibraryA() 함수를 호출시키게 만들어야 한다. 즉 스레드가 아닌 LoadLibraryA() 함수를 실행 시키는 것이고 이 함수의 인자에 자신이 작성한 DLL의 경로를 적어주는 것이다. 이에 따라 임의의 프로세스가 스스로 LoadLibraryA()를 호출하여 악의적인 DLL을 가상 주소 공간에 로드할 것이다. 


  먼저 OpenProcess()를 통해 대상 프로세스의 핸들을 구해야 할 것이고, 대상 프로세스의 메모리에 삽입할 DLL의 경로를 대상 프로세스에 써야 한다. 이것은 VirtualAllocEx() API를 이용해서 대상 프로세스의 메모리 공간에 버퍼를 할당한 후에 WriteProcessMemory() API를 통해 경로가 적힌 문자열을 써준다. 그리고 LoadLibraryA() API의 주소를 구해야 하는데 이것은 GetModuleHandle() API에 kernel32.dll을 인자로 넣어서 핸들을 구하고 GetProcAddress() API에 이 핸들과 LoadLibraryA 문자열을 넣음으로써 LoadLibraryA() 함수의 주소를 구하게 된다. 이제 처음에 설명했듯이 CreateRemoteThread() 함수를 실행하여 원하는 DLL을 로드한다.


  CreateRemoteThread()를 통해 어떻게 LoadLibraryA()가 호출될 수 있는지 조금 더 설명해 보자면 CreateRemoteThread()의 인자이자 원격에서 실행되는 스레드는 ThreadProc 콜백 함수이다. 이 스레드 프로시저는 인자가 4바이트 하나이며 반환값도 4바이트를 반환하는데 이 형태가 LoadLibraryA()와 같아서 이러한 트릭이 통하는 것이다.



1.1.1.2 NtCreateThreadEx() / RtlCreateUserThread()

  윈도우 7부터는 CreateRemoteThread()를 이용한 기법에 제한이 생겼지만 내부 함수인 NtCreateThreadEx() API를 사용하면 된다. 하지만 NtCreateThreadEx()가 윈도우 버전에 따라 수정될 수 있기 때문에 이것의 래퍼 함수인 RtlCreateUserThread()의 사용이 추천된다.



1.1.1.3 QueueUserAPC()


DWORD WINAPI QueueUserAPC(
  _In_ PAPCFUNC  pfnAPC,
  _In_ HANDLE    hThread,
  _In_ ULONG_PTR dwData
);


  APC(asynchronous procedure call)는 비동기 함수 호출 메커니즘으로써 일반적인 함수 호출인 SPC(synchronous procedure call)와 구별된다. 모든 스레드는 APC Queue를 가지고 있는데 이 Queue에는 APC 함수 및 파라미터를 저장할 수 있다. QueueUserAPC()는 사용자 모드 APC 객체를 APC Queue에 추가해주는 함수이다. 만약 APC Queue에 APC 함수가 추가되어 있고 동시에 해당 스레드가 Alertable State에 놓이면 이 APC 함수가 호출된다.


  이것을 이용하여 DLL 인젝션을 수행하는 방법을 알아보겠다. 먼저 첫 번째 인자인 pfnAPC에는 LoadLibrary()의 주소를 지정한다. 즉 호출함 함수로 LoadLibrary()를 설정하는 것이다. 이것은 CreateRemoteThread()를 이용한 방식과 비슷하다. 그리고 세 번째 인자 dwData도 비슷하게 VirtualAllocEx() 및 WriteProcessMemory()를 이용해 대상 프로세스에 써넣은 DLL 경로를 지정한다. 마지막으로 두 번째 인자인 hThread 즉 대상 쓰레드를 지정하면 된다. 이것은 인젝션할 프로세스의 PID를 이용해 GetMainThreadId로 스레드 ID를 구하고 OpenThread()로 해당 스레드의 핸들을 구한 후 지정하면 된다.


  이후 QueueUserAPC()를 호출하면 APC Queue에 APC 함수로서 LoadLibrary()가 그리고 인자로서 DLL의 경로가 Queue에 추가된다. 마지막으로 WiatForSingleObject()를 통해 대상 스레드를 Alertable State로 놓으면 LoadLibrary()가 호출된다.


  참고로 스레드를 Alertable State로 만들어주는 API로는 SleepEx(), SignalObjectAndWait(), WaitForSingleObjectEx(), WaitForMultipleObjectsEx(), 그리고 MsgWaitForMultipleObjectsEx()가 있다. Alertable State에 대해 조금 더 설명해 보기 전에 일반적으로 스레드들은 Ready, Running, Waiting State가 존재한다는 것을 기억해보자. 실제로는 이 3가지 State 외에도 Alertable State가 존재하는데 이것은 사용자가 조정할 수 있는 유일한 스레드의 상태이다.



1.1.1.4 레지스트리 이용 ( AppInit_DLLs, AppCertDlls )

[ HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs ]

  이 레지스트리 값은 디폴트로 비어있다. 하지만 여기에 원하는 DLL들의 경로를 써주면 USER32.dll을 로드하는 모든 프로세스에서 원하는 DLL들이 강제로 로드된다. 약간의 설명을 덧붙이자면 USER32.dll은 GUI 환경인 윈도우에서 기본적으로 사용하는 DLL로서, 이 DLL의 DLL_PROCESS_ATTACH 과정에서 AppInit DLL들을 LoadLibrary() 함수를 이용해 로드하는 것이다.


[ HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AppCertDLLs ]


  이 외에도 AppCertDlls 레지스트리도 존재한다. 위와의 차이점은 CreateProcess(), CreateProcessAsUser(), CreateProcessWithLogonW(), CreateProcessWithTokenW(), WinExec()를 호출하는 프로세스에만 로드된다는 것이다.



1.1.1.5 SetThreadContext()

  먼저 DLL 삽입 후 다시 복귀하는 역할을 하는 인라인 어셈블리 루틴이 필요하다. 이 루틴은 LoadLibraryA()를 통해 악의적인 DLL을 삽입할 것이고 다시 실행을 재개 즉 복귀해야 한다. 참고로 복귀 주소는 현재 알 수 없기 때문에 뒤에서 추가해야 한다. 인젝션 프로그램은 대상 프로세스에 VirtualAllocEx()로 메모리를 할당하고 이 루틴을 써 넣는다. 이후 SuspendThread()로 대상 프로세스의 스레드를 정지시키고 GetThreadContext()로 Context를 얻는다. 그리고 Context의 EIP를 앞에서 설명한 루틴의 복귀 주소로 설정한다. 이후 EIP를 어셈블리 루틴의 시작 주소로 변경하고 SetThreadContext()로 Context를 저장한 후에 ResumeThread()로 실행을 재개한다. 결론적으로 변경된 EIP를 통해 어셈블리 루틴이 실행될 것이고 이것은 주어진 DLL을 삽입하고 아까 저장했던 원본 EIP의 주소로 ret한다.



1.1.1.6 Reflective DLL Injection

  대상 프로세스를 RWX 권한으로 오픈하고 VirtualAllocEx() 함수를 이용해 DLL이 들어갈 만한 메모리를 할당한다. 일반적으로 DLL은 드로퍼 형태로 리소스 섹션에 저장되어 있는데 이 DLL을 읽어와 할당한 곳에 복사한다. 중요한 것은 이 DLL은 현재 파일 형태로 존재한다는 점이다. 그래서 로더를 통해 로드된 것처럼 현재 파일 오프셋으로 존재하는 각 섹션을 가상 메모리에 올라온 형태로 재배치해야 한다. 이것은 블로그의 SimplePacker 문서에서도 패커를 개발하면서 다루었다. 이 뿐만 아니라 DLL에서 사용하는 함수들의 IAT도 수정해야 한다. 이 함수들의 주소는 LoadLibrary()와 GetProcAddress()를 이용해서 얻어올 수 있다. 참고로 이 함수들은 당연히 대상 프로세스 내에서 호출되기 때문이 이 함수들의 주소는 또 어떻게 얻어야 하냐는 문제가 발생한다. 이것은 대상 프로세스의 PEB를 찾고 이것을 통해 kernel32.dll의 메모리 주소를 찾은 후에 Export Table을 이용해 얻어내는 방식을 사용한다.


  지금까지의 과정을 보면 알겠지만 이 과정들은 모두 DLL을 직접 로드하는 방식이다. PE에 존재하여 실행 시 자동으로 로드된 경우나 도중에 LoadLibrary()를 호출한 경우에는 로더가 모든 과정을 알아서 해주지만 이 방식은 순수하게 로더의 역할을 수행해야 한다. 마지막으로 CreateRemoteThread()를 통해 그 주소를 엔트리 포인트로 하여 실행시킨다. 참고로 일반적인 코드 인젝션에서는 CreateRemoteThread()를 이용해 LoadLibrary()로 DLL을 로드함과 동시에 실행시키지만 여기서는 직접 로더의 역할을 통해 DLL을 로드하고 CreateRemoteThread()로는 직접 주소를 넣어 실행시키는 것이다.


  이것을 응용한 내용을 알게되어 먼저 정리부터 한 후에 추가하도록 하겠다. Reflective DLL Injection 방식은 먼저 대상 프로세스에 메모리를 할당하게 한다. 참고로 VirtualAllocEx() 대신 내부의 NtAllocateVirtualMemory()를 사용할 수도 있다. 이렇게 대신 사용할 수 있는 ntdll 함수들은 많이 존재하므로 단지 저 함수들만 사용해야 한다는 생각은 버리자. 전체는 아니지만 다음 링크에 간략하게 정리된 함수들이 있다.


[ http://sanseolab.tistory.com/57 ]

[ http://sanseolab.tistory.com/58 ]


  참고로 CreateRemoteThread()의 경우는 다음과 같다.

[ CreateRemoteThread() -> CreateRemoteThreadEx() -> NtCreateThreadEx() ]


  어쨌든 할당한 후 로더처럼 섹션에 맞게 써줄 것이고 (WriteProcessMemory / NtWriteVirtualMemory) 이후 CreateRemoteThread()를 이용해 쓰레드를 실행시킨다. 원래 존재하던 쓰레드들은 처음부터 상관 없이 잘 실행되고 있을 것이며 이후 이 인젝션된 DLL의 DllMain() 부분이 새로운 스레드로서 추가되어 실행되는 것이다.


  이것을 응용할 수 있는 방식으로 여러가지가 있을 것이며 개인적으로 정리된 문서를 보게되어 여기에 간략하게 추가하려고 한다. 링크는 다음과 같다.


[ https://zerosum0x0.blogspot.kr/2017/07/threadcontinue-reflective-injection.html ]


  사실 코드나 DLL 인젝션 방식들은 모두 탐지를 회피하기 위해 발전되고 있다. Reflective DLL 인젝션의 경우도 그러한데 문제는 눈치챘겠지만 코드를 실행시킬 때 CreateRemoteThread()를 이용한다는 것 자체가 조금 문제가 있다. 그래서 이것의 내부 함수 즉 앞에서 간략하게 쓴 ntdll 함수를 이용하던지 아니면 APC Queue를 이용한 방식으로서 코드를 실행시키는 방식으로 응용할 것이다. 이 문서에서 나온 방식은 이러한 함수들 대신 ResumeThread()와 NtContinue()를 이용해 실행시키는 방식이다.


  먼저 SuspendThread()로 대상 프로세스의 쓰레드를 Suspend 시킨다. 그리고 프로세스에 인젝션할 DLL의 공간 뿐만 아니라 Context가 저장될 부분도 메모리를 할당한다. 이 Context는 인젝션하는 프로그램 뿐만 아니라 대상 프로세스 내부에서도 추후에 사용될 것이다. 이후 섹션별로 DLL의 내용을 써준 후에 SetThreadContext()로 DllMain()의 위치를 지정한다. 또한 스택을 위해 스택 포인터를 더 낮은 주소로 맞추어 준다. 스택을 맞추는 것이 필요한 이유는 일반적인 경우와 이 방식이 다르기 때문이다. 새로 쓰레드로서 실행되는 경우에야 각 쓰레드 별로 스택이 할당되기 때문에 상관이 없지만 이 경우는 잘 실행되던 쓰레드를 멈추고 제어를 삽입한 DllMain() 부분으로 옮긴 것이기 때문이다.


  이후 ResumeThread()를 실행하면 DllMain() 부분이 실행되며 여기서 추가적인 작업을 수행할 수 있다. 당연히 현재 부분을 새로운 쓰레드로서 실행하기 위해서 CreateThread()를 이용해 작업을 수행한다. 중요한 점은 삽입된 코드는 이제 새로운 쓰레드로서 실행되지만 원본 프로그램의 경우 제어가 강제로 여기까지 (DllMain) 와버렸다. 하지만 우리는 아까 Context를 대상 프로세스의 메모리 공간에 복사해 놓았으며 동시에 스택도 오염되지 않게 스택 포인터를 바꾸어 놓았었다. 이제 DllMain()의 남은 부분에서는 저장된 Context를 인자로 넣고 NtContinue()를 실행하면 원래 진행하던 코드에서 그대로 진행할 수 있게 된다. 혹시 DllMain() 부분을 진행하다 오염되지 않게 스택 포인터를 저 멀리로 설정해 놓았었으므로 스택 메모리도 오염되지 않은 상태로 남아 있을 것이다.




1.1.1.7 Shims

  MS에서는 하위 호환을 위해 "Shim"이라는 개념을 도입했다. 이를 통해 개발자가 해당 애플리케이션을 직접 수정하지 않도록 해주는 편의성이 있지만 보안 문제가 생기게 된다. 이것은 후킹을 이용한 방식으로서 운영체제에게 이 애플리케이션을 어떻게 다루어야 하는지를 설정해줄 수 있다. 예를들면 인자 처리라던지 특정한 동작에 대한 처리 방식이라던지. 


  이러한 설정은 Shim database (sdb) 파일에 저장된다. 즉 소스 코드를 수정하여 다시 만들거나 패치를 적용하는 것 대신 이 파일을 통해 수정 사항을 적용시킬 수 있다. 해당 파일에서 사용할 수 있는 기능들을 보면 충분히 위험하게 사용될 수 있는 것들이 많이 보인다. "InjectDll"부터 시작하여 "DisableNX", "DisableSeh", "ForceAdminAccess", "ShellExecuteXP" 등이 있다.


  어떤 공격에서는 파워셸을 이용하여 sdbinst.exe 유틸리티를 실행시켜 악성 sdb를 등록시켰다. 이 sdb는 시스템 프로세스나 다른 애플리케이션에 대한 패치 또는 DLL 인젝션을 수행하여 공격하는 방식을 사용하였다.





1.1.2 코드 인젝션

1.1.2.1 기본적인 코드 인젝션

  지금까지 DLL 인젝션 방식을 설명했고 이제 코드 인젝션을 알아보겠다. 코드 인젝션은 설명하기는 더 간단하지만 실제로 구현하기는 상당히 까다롭다. 위에서 DLL의 경로를 담은 문자열을 대상 프로세스에 쓸 때 VirtualAllocEx() API를 이용해서 대상 프로세스의 메모리 공간에 버퍼를 할당한 후에 WriteProcessMemory() API를 통해 원격 프로세스에 메모리를 써 넣었다. 코드 인젝션은 문자열이 아니라 페이로드 전체를 VirtualAllocEx()와 WriteProcessMemory()를 통해 써 넣는다. 참고로 페이로드는 Thread Procedure 형태(ThreadProc 콜백 함수)로 만들 수도 있고 직접 어셈블리 루틴의 바이너리를 바이트 배열로 만들 수도 있으며 inline 어셈블리 루틴으로 만들 수도 있다.


  하지만 까다로운 부분이 존재하는데 DLL로 만든 경우에는 자동으로 컴파일되어 세세한 부분을 다룰 일이 없겠지만 이렇게 직접 코드 인젝션으로 페이로드를 삽입하는 경우에는 고려해야할 사항이 존재한다. 먼저 페이로드에서 API 호출이 사용되는 경우에는 그 주소를 호출하는 대상 프로세스에서의 주소가 다를 수 있기 때문에 (DLL 재배치나 ASLR 등으로 인하여) 정확한 주소를 알아낸 후에 추가하여야 한다. 또한 페이로드에서 함수 호출에 사용되는 인자 값 같이 데이터가 필요한 경우도 있는데 이 때에도 데이터 부분 또한 코드 페이로드 처럼 따로 삽입하고 코드 페이로드에서는 주소를 설정해 주어야 한다.


  어쨌든 DLL이 아니어서 자동으로 호출되는 메커니즘이 없기 때문에 위에서 설명했듯이 CreateRemoteThread() 함수를 사용하여 써 넣은 페이로드를 실행시킨다. 참고로 DLL 인젝션에서는 LoadLibrary() 함수를 실행시켰었다.



1.1.2.2 Atom Bombing

  이 방식은 일반적으로 사용되는 VirtualAllocEx(), WriteProcessMemory(), CreateRemoteThread() 등의 함수를 사용하지 않고 Atom Table이라는 메커니즘을 악용하여 코드를 삽입하고 실행시키는 메커니즘이다.


  공격자는 먼저 GlobalAddAtom()을 통해 인젝션할 코드를 Global Atom Table에 추가한다. 이후 ntdll!RtlDispatchAPC() [ QueueUseApc() -> NtQueueApcThread() -> ntdll!RtlDispatchAPC() ]를 통해 공격 대상 프로세스가 GlobalGetAtomName()을 호출하게 함으로써 추가한 코드를 읽게 한다. 이 메커니즘을 통해 WriteProcessMemory() 없이도 코드를 인젝션하게 된다.


  문제는 VirtualAllocEx()로 RWE 메모리를 할당하고 여기에 WriteProcessMemory()로 인젝션할 코드를 삽입한 위의 방식과는 달리 현재 코드는 인젝션되어 있지만 이 영역에는 실행 권한이 없다. 이 코드를 실행시키기 위해 ROP 체인을 이용하는데 사용되는 ROP 체인은 결과적으로 ZwAllocateVirtualMemory()를 호출하여 RWE 메모리를 할당하고 memcpy()로 인젝션한 코드를 복사하는 역할을 한다.


  마지막으로 실행 흐름을 다시 복구해주는 것도 가능하다.


http://www.reversenote.info/atombombing-stage1/ ]

http://www.reversenote.info/atombombing-stage2/ ]

http://www.reversenote.info/atombombing-stage3/ ]



* 서브클래싱

  EWMI와 PROPagate 인젝션을 정리하면서 윈도우의 서브클래싱과 관련된 이해 없이는 완벽히 파악할 수 없을 것 같다는 생각이 들었다.


[ https://docs.microsoft.com/en-us/windows/desktop/controls/subclassing-overview ]


  해당 링크를 보면 과거의 서브클래싱 방식과 현재의 서브클래싱 방식이 정리되어 있다. SetWindowLong() 및 SetWindowLongPtr()을 이용하는 방식은 과거 버전으로 보이며, Win32 GUI 프로그래밍 등에서 흔히 보던 방식은 최신 방식으로서 SetWindowSubclass() 방식이 사용되는 것 같다. 물론 아직도 정확한 이해는 하지 못했다. 추가적으로 SetProp() 함수는 개개의 윈도우에 대해 사용자 데이터를 저장하는데 사용된다.


  어쨌든 정리하자면 서브클래싱은 윈도우 메시지를 가로채는 일종의 후킹이며 분석하는 입장에서는 메시지 후킹 시의 SetWindowsHookEx() 메커니즘을 생각하면 될 것 같다.



1.1.2.3 EWMI (Extra Window Memory Injection) / Powerloader Injection

  이것은 Explorer Tray Winow (Shell_TrayWnd)의 추가적인 윈도우 메모리에 코드를 인젝션하는 방식이다. 윈도우 클래스를 등록할 때 애플리케이션은 메모리의 추가적인 바이트들을 설정할 수 있으며 이것이 Extra Window Memory라고 불린다. 물론 여기에는 충분한 공간이 없기 때문에 악성코드는 explorer.exe의 공유 섹션에 셸코드를 삽입하며 SendNotifyMessage()와 SetWindowLong()을 이용해 해당 셸코드를 가리키는 함수 포인터를 갖고 이것을 실행시킨다.


  GetWindowLong()은 윈도우 클래스 오브젝트의 EWM에 대한 주소 값을 얻어오는 역할을 하며 SetWindowLong()은 여기에 대한 주소 값을 변경해주는 역할을 한다. 이를 통해 악성코드는 윈도우 클래스에서 함수 포인터의 오프셋을 셸코드의 주소로 변경할 수 있다. 셸코드는 OpenSection()을 통해 공유 섹션을 오픈한 후에 직접 써 넣는다. 참고로 공유 섹션에는 RW 권한 밖에 없기 때문에 ROP 방식이 사용된다.


  마지막으로 해당 코드를 실행하기 위해서는 SendNotifyMessage()를 이용한다. 이 함수는 주어진 메시지를 해당 윈도우의 프로시저로 전달하여 다루게 한다. 즉 악성코드가 이것을 호출할 경우 explorer.exe 내부의 윈도우 프로시저가 트리거되는 것이다.


  참고로 Ensilo에서는 PowerLoaderEx라는 추가적인 방식을 소개하기도 했다.



1.1.2.4 PROPagate


[ https://modexp.wordpress.com/2018/08/23/process-injection-propagate/ ]


  위의 링크에 잘 정리되어 있어서 해당 링크를 참고하여 정리한다. 마지막 전체 소스 코드 부분을 보면 먼저 FindWinowEx() 류의 함수를 통해 특정 윈도우에 대한 핸들을 얻고, 이 핸들을 가지고 GetProp()으로 서브클래스 헤더에 대한 핸들을 얻는다. 참고로 이 예제에서는 윈도우7과 윈도우10 모두에 해당하는 "Progam"이라는 부모 윈도우 및 "SHELLDLL_DefView"라는 자식 윈도우를 대상으로 하며, 서브클래스 헤더는 "UxSubclassInfo"를 대상으로 한다.


  이후 explorer.exe를 대상으로 새로운 서브클래스 헤더 및 셸코드를 위한 공간을 할당하고 쓴다. 그리고 서브클래스 헤더의 pfnSubclass 필드를 셸코드의 주소로 변경하고 쓴다. 마지막으로 SetProp()을 이용해 explorer의 "UxSubclassInfo" 서브클래스의 프로시저가 셸코드 주소로 "업데이트"된다. 이제 PostMessage() 함수를 이용해 explorer에 WM_CLOSE 메시지를 보내면 셸코드가 실행된다.





1.1.3 취약점

  인젝션 기법은 아니지만 DLL Hijacking 취약점의 경우 악성 DLL을 로드하게 되며 DllMain()을 통해 로드 시에 코드가 실행되기 때문에 여기에 포함하기로 하였다.





1.1.4 윈도우 비스타 이후의 변화

  간단했던 윈도우 XP와 달리 비스타 부터는 변화가 많이 생겼다. 이를 설명하기 위해 바뀐 부분들을 위주로 더 자세히 설명해 나가도록 하겠다. DLL 인젝션을 수행하기 위해 가장 먼저 필요한 것이 대상 프로세스의 핸들이다. 우리는 인자로 우리가 필요한 접근 권한을 넣고 OpenProcess()를 호출하여 핸들을 얻는다. 이 접근 권한이 중요한데 예를들면 뒤에서 사용될 ReadProcessMemory() 함수를 사용하기 위해서는 프로세스에 대한 PROCESS_VM_READ 접근 권한이 있어야 한다. WriteProcessMemory() 함수는 PROCESS_VM_WRITE, PROCESS_VM_OPERATION 접근 권한이 필요하다. CreateRemoteThread()의 경우에는 PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, PROCESS_VM_READ 권한이 필요하다. 마지막으로 VirtualAllocEx()와 VirtualFreeEx()는 PROCESS_VM_OPERATION 권한이 필요하다. 일반적으로는 이것들을 모두 쓰기 보다는 PROCESS_ALL_ACCESS를 사용해서 OpenProcess()를 호출한다.


  중요한 점은 윈도우 비스타부터는 OpenProcess() 호출 시 호출하는 프로세스와 같거나 더 낮은 Integrity Level을 가진 프로세스에 대해서만 이러한 접근 권한을 얻어올 수 있다는 것이다. 만약 더 높은 Integrity Level을 가진 프로세스에 대해서 PROCESS_ALL_ACCESS 접근 권한을 얻어오려고 한다면 OpenProcess()는 에러를 반환한다. 권한과 관련한 내용은 이 블로그의 다른 글을 참고한다. 


  주제를 바꿔서 이제 원하는 권한을 가진 핸들을 받았다고 치고 위에서 언급한 함수들을 이용해 DLL 인젝션을 수행할 수 있다. 하지만 한가지 더 추가된 것이 있는데 Session Isolation 정책이 그것이다. 이 정책으로 인해 이제 CreateRemoteThread()는 같은 세션에 해당하는 프로세스들에만 DLL 인젝션을 수행할 수 있게 되었다. 참고로 리버싱 핵심원리에 따르면 ReadProcessMemory, WriteProcessMemory, VirtualAllocEx() 같은 함수들은 세션과 관련 없다고 한다.


  참고로 시스템 서비스들은 세션 0을 갖고 각 사용자들은 다른 숫자들을 부여받게 된다. 이에 따라 주로 DLL 인젝션의 대상이 되왔던 시스템 서비스들에게는 더이상 DLL 인젝션이 통하지 않는 것이다. 물론 이것도 우회하는 방법이 있는데 NtCreateThreadEx(), RtlCreateUserThread() 등의 네이티브 api를 사용하면 된다. 이 함수들을 통해 다른 세션의 프로세스에 DLL 인젝션을 수행할 수 있게 되었다.


  비록 이제 세션의 장벽을 넘었다고 해도 위에서 언급하였듯이 Integrity Level이라는 한계가 존재한다. 현재 프로세스가 관리자 권한을 가지고 있다고 해도 시스템 서비스들은 High Integrity Level보다 높은 System Integrity Level을 가지고 있다. 이것을 우회하는 방법은 프로세스에 SeDebugPrivilege 권한을 주는 것이다. 이 디버그 권한을 갖는다면 접근 통제 정책을 우회할 수 있기 때문에 System Integrity Level에 대해 DLL 인젝션을 수행할 수 있다. 하지만 이것도 한계가 있는데 SeDebugPrivilege 권한은 관리자 계정에 주어지기 때문이다. 그래서 관리자 권한 즉 High Integrity Level이어야 이 디버그 권한을 활성화시킬 수 있다. AdjustTokenPrivileges() 호출 시 인자로 상수 SE_DEBUG_NAME 즉 텍스트로 SeDebugPrivilege를 넣는 부분이 이 내용이다.


  예를들어 흔한 상황을 가정해 본다면 현재 프로세스는 관리자 권한이 아니라 표준 사용자 권한으로 실행 중일 것이며 우리는 DLL 인젝션을 수행하기 위해 세션이 같은 것을 찾아야 한다. explorer.exe는 항상 실행 중인 프로세스이며 이 두가지 즉 같은 세션에 같은 Medium 권한을 갖는다. 이에 따라 explorer.exe는 DLL 인젝션의 대상이 되는 경우가 많다. 관리자 권한 없이도 손쉽게 DLL 인젝션의 대상이 될 수 있기 때문이다. 만약 관리자 권한을 갖게 된다면 디버그 권한을 활성화시켜 이제 어디든 DLL 인젝션을 수행할 수 있게 된다.









1.2 후킹

  지금까지 원하는 코드를 DLL의 형태로든 코드 자체로든 삽입하였으며 실행시키는 메커니즘까지 알아보았다. 이제는 프로그램의 흐름을 가로채서 이 코드를 실행시키기는 후킹 방식을 알아보겠다. 후킹의 대상으로는 대표적으로 함수 호출이 있을 것이고 이 외에도 메시지나 이벤트 등이 있을 수 있다. 여기서는 가장 많이 사용되는 API 함수 호출 후킹을 대상으로 하겠다.


  후킹 기법도 여러 종류가 있겠지만 여기서는 가장 많이 사용되는 방식들인 IAT 후킹과 Inline 후킹에 대해서 다룰 것이다. 후킹은 간단하지만 목적에 따라 수많은 다양한 방식들이 나올 수 있으므로 간단하게만 정리한다.


  참고로 보안 솔루션들도 당연히 후킹을 통해 구현되는데 일반적으로 인라인 후킹이 많이 사용된다고 한다. 물론 IAT 후킹을 사용하는 제품들도 존재한다. 또한 DLL 내부에 구현되는데 (즉 대부분 DLL 인젝션을 사용한다) 후킹되어 실행할 루틴들이 각 함수로 구현되어 있으며 dllmain()에 후킹을 설치하는 루틴이 존재하여 DLL 인젝션 시에 dllmain()이 자동으로 실행되어 후킹을 설치한다. 이 DLL을 다른 말로 후킹 엔진이라고도 부른다. 후킹 엔진에는 오픈 소스인 EasyHook, Deviare2 외에도 마이크로소프트의 detours 그리고 상용 제품인 Madcodehook 등이 존재한다. 



1.2.1 IAT 후킹

  리버싱을 하다 보면 API 함수를 호출할 때 call 명령어에 의해 호출되는 주소가 직접적으로 해당 라이브러리에 위치한 API 함수의 시작 주소인 것이 아니라 간접 호출 방식을 사용함으로 인해 IAT 영역을 가리키는 것을 볼 수 있다. 이 IAT 영역의 주소에 그 API의 실제 주소가 들어 있으며 이를 통해 API가 호출되는 것이다. IAT 후킹은 IAT 영역에 위치한 후킹 대상 API 함수의 주소를 후킹 함수 주소로 변경하는 방식이다. 이에 따라 코드 영역에서는 똑같은 IAT 영역을 가리키지만 이 IAT에 들어있는 주소가 후킹 함수로 변경되어 있으므로 설치된 후킹 함수가 호출된다.



1.2.2 Inline 후킹

  기본적으로 Inline 후킹의 특징 상 여러 방식을 통해 코드를 수정하여 후킹 함수를 설치할 수 있다. 가장 대표적으로는 처음 5 바이트( mov edi, edi  /  push ebp  /  mov ebp, esp )를 jmp 문으로 수정하여 후킹 함수로 분기시키는 방식이다. 물론 우리가 수정함으로써 실행되지 못한 위의 명령어는 내부에서 따로 실행시켜 주어야 한다. 따로 실행시켜 주어야 할 5 바이트는 위와 같이 항상 ( mov edi, edi  /  push ebp  /  mov ebp, esp )이다. 또는 다른 방식을 사용할 수도 있다. "mov edi, edi" 명령어가 시작하는 주소의 바로 앞 5바이트를 jmp 문으로 수정한다. 이 부분은 항상 비어있는 공간이므로 안전하게 수정할 수 있다. 그리고 2바이트의 "mov edi, edi"를 해당 jmp 문 즉 jmp $-5로 변경한다. 이것은 short jmp이기 때문에 2바이트 밖에 차지하지 않는다. 


  지금까지는 x86에서의 설명이었고 x64에서는 다른 방식이 필요하다. 주소 공간이 커서 5바이트 jmp 명령어로는 부족할 수 있기 때문이다. x86 뿐만 아니라 x64에서의 인라인 후킹은 다음 문서를 참고한다. [ https://www.blackhat.com/docs/us-16/materials/us-16-Yavo-Captain-Hook-Pirating-AVs-To-Bypass-Exploit-Mitigations.pdf ]





1.3 윈도우에서 제공되는 메시지 후킹

HHOOK SetWindowsHookEx (
   int idHook,
   HOOKPROC lpfn,
   HINSTANCE hMod,
   DWORD dwThreadId
);


  SetWindowsHookEx() 함수는 메시지 후킹을 위해 지원되는 함수이다. Windows는 Event 구동 방식으로 동작하는데 이런 이벤트가 발생할 때 메시지를 통해 해당 어플리케이션에 통보되며 어플리케이션은 메시지를 받아서 분석한 후 해당하는 작업을 수행한다. 메시지 후킹은 중간에서 메시지를 가로채는 것이다. 예를들면 운영체제가 키보드에서 신호를 받아서 처리한 것이 메시지가 되고 이것이 메시지 큐에 있다가 거기에 맞는 응용 프로그램에 메시지를 넘겨준다.


  SetWindowsHookEx()는 인자로 dwThreadId를 받아서 대상 스레드(프로세스보다 더 구체적인)를 정하고 idHook을 통해 후킹할 메시지를 선택하며, lpfn은 그 스레드가 받는 특정 메시지를 후킹하여 대신 받아서 처리할 프로시저를 의미하고 마지막으로 그 프로시저가 들어 있는 DLL을 hMod로 받는다. 즉 특정한 스레드가 받는 특정한 메시지를 처리할 프로시저를 DLL 형태로 개발하고 이 함수를 이용하여 메시지 후킹을 설치하는 것이다.





1.4 기타

  이 항목에서는 여러가지 참고 사항들을 정리하기로 하겠다. 첫 번째 주제는 보안 프로그램 개발 시 사용자 모드 후킹을 어떻게 탐지할 것인가이다. 일반적으로 API 후킹의 경우 후킹 대상이 될만한 함수의 엔트리 포인트를 모니터링하여 방지한다고 한다. 이러한 모니터링도 결국은 미리 후킹을 해서 감시하는 것이다. 또한 코드 섹션의 해시 검사 또는 CRC 검사를 수행하여 변경되었는지를 검사할 수도 있다. 









2. 리눅스 사용자 모드 후킹

2.1 인젝션

2.1.1 Ptrace()

  ptrace()를 이용해 코드 인젝션 또는 .so 인젝션을 수행하는 방식이다. 요약해서 PTRACE_ATTACH를 이용해 대상 프로세스를 어태치하고 PTRACE_POKETEXT를 통해 메모리에 쓴 후 PTRACE_SETREGS로 EIP를 삽입한 코드로 변경한 후에 PTRACE_CONT로 실행을 재개한다. 물론 PTRACE_GETREGS와 PTRACE_SETREGS를 통해 문맥을 다시 복구하는 등의 절차도 필요하다.





2.2 후킹

2.2.1 PLT / GOT redirection

  윈도우에서는 API를 호출할 때 IAT에 있는 주소를 참조하여 간적 호출 방식을 사용하였다. 리눅스에서는 PLT와 GOT를 이용한 메커니즘이 사용된다. PLT/GOT redirection은 GOT에 존재하는 주소를 후킹 함수의 주소로 변경한다.



2.2.2 Inline 후킹

  인라인 후킹의 특징이 그렇듯, 윈도우에서처럼 라이브러리에 있는 함수의 시작 부분을 후킹 함수의 주소로 변경할 수도 있고 앞에서 설명했던 PLT 부분을 수정할 수도 있고 아니면 코드 섹션의 함수 호출 명령어의 주소를 직접 변경할 수도 있을 것이다.





2.3 LD_PRELOAD

  리눅스에서 프로그램을 실행하면, 즉 프로세스 생성 과정에서 로더가 라이브러리를 로딩하는데 이 때 만약 LD_PRELOAD 환경 변수가 설정되어 있다면 로더는 여기에 지정된 라이브러리를 먼저 로딩한다. 중요한 점은 이 중에서 libc 함수와 동일한 이름의 함수가 있다면 이 라이브러리의 함수가 호출된다는 것이다. 그러므로 후킹을 위해서는 후킹할 함수의 이름과 동일한 함수를 구현하여 공유 라이브러리를 개발하고 LD_PRELOAD 환경 변수에 이 파일의 경로를 써주면 이후 프로그램 실행 시 마다 원하는 함수가 후킹된다. 참고로 후킹 함수 내에서 원본 함수로 실행을 리다이렉트시키고 싶다면 dlsym() 함수를 통해 주소를 얻어서 사용할 수 있다. 이 방식은 일반적인 의미의 후킹과 인젝션이라고 불리기 힘들 수 있으나 (일종의 라이브러리 치환의 개념) 실질적으로 후킹과 인젝션 모두의 기능이 가능하다.



Posted by SanseoLab



1. 개념

  일반적인 사람들 입장에서 처음 컴퓨터를 사고 설치할 때 계정을 생성하고 등록할 것이다. 중요한 것은 이 계정이 관리자 그룹(Administrators)에 속한다는 점이다. 참고로 표준 사용자 그룹(Users)에도 동시에 속해 있다. 이후 다른 계정을 생성할 때가 있을텐데 이 때도 관리자 계정을 만들 수도 있고 대신 표준 사용자 계정을 만들 수도 있다. 만약 관리자 계정으로 새로운 계정을 만든다면 이 계정도 Administrators, Users 두 가지에 속해있는 것을 볼 수 있을 것이다. 대신 표준 사용자 계정을 선택하여 만든다면 단지 Users에만 속해있다.


  윈도우는 표준 사용자 계정으로 로그온한 경우 거기에 맞는 적절한 보안 토큰을 준다. 관리자 계정으로 로그온한 경우에는 2개의 토큰을 받는데, 앞서 말한 표준 사용자 계정이 받는 토큰과 비슷한 권한을 가진 보안 토큰 그리고 관리자 토큰 이렇게 2개를 받는다. 


  조금 더 자세히 설명해 보겠다. Integrity 메커니즘은 커널의 SRM(보안 참조 모니터)에 기반한 윈도우 보안 아키텍처이다. SRM은 보안 접근 토큰에 존재하는 (유저와 그룹의) SID를 객체의 보안 디스크립터의 접근 권한과 비교해서 접근 제어를 강제한다. Integriry level에는 System, High, Medium, Low, Untrusted가 있다. 정리하자면 LocalSystem, LocalService 등의 경우 System Integrity level이 할당되며, Administrators의 경우 High, Standard Users의 경우에는 Medium이 할당된다.


  이를 통해 앞의 설명을 보충하자면 표준 사용자(Standard Users Group)로 로그온한 경우에는 표준 사용자 접근 토큰을 받는다. 이 접근 토큰은 Medium Integrity Level이 할당되어 있다. 관리자 계정(Administrators group)으로 로그온한 경우 표준 사용자 접근 토큰과 관리자 접근 토큰 2개를 받는다. 표준 사용자 접근 토큰은 위와 같고 관리자 접근 토큰은 High Integrity Level이 할당되어 있다.


  이제 응용 프로그램을 실행한다고 하자. 현재 계정이 표준 사용자던지 관리자던지 실행은 Medium Integrity Level의 접근 토큰을 가지고 하게된다. 왜냐하면 애플리케이션 실행 시에 explorer.exe의 자식 프로세스로 실행되는데 이 explorer.exe가 Medium 레벨을 가지고 실행 중이기 때문이다. 그래서 대부분의 프로그램은 이렇게 실행될 것이다. 하지만 문제가 있는데 만약 응용 프로그램 내부에 관리자 권한이 필요한 부분이 있다고 하자. 이 경우에는 현재 접근 토큰이 Medium이기 때문에 High Integrity Level의 권한을 갖지 못하여 권한이 필요한 루틴이 에러를 반환하고 프로그램이 제대로 동작할 수 없을 것이다. 이럴때는 보통 마우스 우클릭 후 "관리자 권한으로 실행"을 통해 관리자 권한으로 실행할 수 있다. 가끔 오래된 프로그램의 경우 제대로 실행되지 않을 때 관리자 권한으로 실행하라는 답변을 얻을 수 있는데 이것이 그 때문이다. 일반적으로 UAC 메커니즘이 생기기 전인 Windows XP 시절에 만든 프로그램일 것이다.


  또한 사족으로 관리자 권한으로 실행된 프로그램에는 드래그 앤 드랍이 통하지 않는다는 사실이 있다. 예를들어 CMD를 관리자 권한으로 실행한 후 특정 파일을 실행시키고 싶어서 드래그 앤 드랍으로 CMD에 끌어다 놓으면 경로명이 자동으로 올라오지 않는 것을 볼 수 있다. 이것은 단지 CMD만 해당하는 것이 아니고 OllyDbg를 관리자 권한으로 실행하거나 HxD를 관리자 권한으로 실행한 후 바이너리를 끌어다 놓을 때 통하지 않는 것을 확인할 수 있다. 드래그 앤 드랍은 exeplorer.exe가 관리하는 것으로 추정되며 이것이 Medium 레벨을 가지므로 High 레벨을 갖는 프로세스에 영향을 끼칠 수 없기 때문이다. 그래서 드래그 앤 드랍 방식 외에 직접 파일을 읽어들이거나 최악의 경우 Windows XP에서 해당 작업을 수행하던지, 아니면 explorer.exe를 관리자 권한으로 실행시키는 방식이 사용되기도 한다.


  현재 계정이 표준 사용자라고 하자. 그렇다면 관리자 권한으로 실행시킬 때 UAC가 나타난다. 이 UAC를 보면 관리자의 비밀번호를 입력해야 한다. 즉 당연히 관리자의 비밀번호가 있어야만 관리자 접근 토큰을 받아 관리자 권한으로 프로그램을 실행시킬 수 있다. 하지만 현재 계정이 관리자라고 하더라도 관리자 권한으로 실행시키면 UAC가 뜨는 것을 볼 수 있다. 관리자 계정으로 로그온한 경우에도 explorer.exe가 Medium 권한으로 실행되며 이것의 자식 프로세스로 실행되는 것은 똑같기 때문에 Medium 접근 토큰으로 실행할 것다. 그리고 관리자 권한으로 실행한다면 High Integrity Level의 접근 토큰으로 실행할 것이다. 관리자 계정이 비록 관리자 접근 토큰을 소유하고 있지만 UAC가 뜨는 것은 같다. 물론 이 때는 비밀번호를 입력할 필요 없이 간단하게 확인 버튼만 누르면 된다. 사실 이것이 UAC의 핵심인데 관리자 권한을 가진 관리자 계정이라고 하더라도 평소에는 축소된 표준 사용자 권한을 가지며 지내다가 관리자 권한으로 프로그램을 실행할 때에는 사용자의 확인을 받고 실행하는 것이다. 그래서 비밀번호의 입력 차이만 있을 뿐 관리자 권한을 사용하여 프로그램을 실행할 때에는 UAC가 뜨는 것은 같게 된다. 참고로 이미 관리자 권한으로 실행 중인 프로세스가 관리자 권한을 필요로 하는 작업을 하는 경우에는 당연히 UAC가 뜨지 않을 것이다. UAC는 단지 더 높은 권한을 필요로 할 때에만 뜨는 메커니즘이다.


  윈도우 7부터는 UAC도 설정이 가능한데 당연히 UAC의 설정을 바꾸기 위해서도 관리자 권한이 필요하므로 UAC가 뜨는 것을 볼 수 있다. UAC 뿐만 아니라 다른 중요한 설정을 바꿀 때에도 마찬가지이다. 예를들면 UAC를 우회하여 방화벽의 설정을 바꾸는 악성코드도 존재한다. 언제 관리자 권한이 필요한지에 대해 더 알아보자면 아마 가장 흔한 경우가 응용 프로그램을 설치할 때일 것이다. 아니면 시스템 파일을 수정하거나 디바이스 드라이버를 설치할 때도 그렇다. 또한 속한 그룹에 따라 가질 수 있는 권한도 다른데, SeDebugPrivilege 같은 권한의 경우에는 관리자 그룹에 속한 계정이 가질 수 있다. 참고로 이 권한은 접근 통제를 우회할 수 있게 해준다. 만약 관리자 권한으로 실행 중인 어떤 프로세스가 이 권한을 활성화하였다면 System Integrity Level을 가지고 실행 중인 프로세스에 CreateRemoteThread(), WriteProcessMemory() 같은 함수를 사용할 수 있게 된다. 다시 말해서 (LocalSystem에 의해 생성되어) System Integrity level을 가진 프로세스의 경우 일반적으로는 (관리자 계정에 의해 실행되어) High Integrity level을 가진 프로세스에게 접근 권한이 없지만 비활성화된 SeDebugPrivilege를 활성화시킨다면 이것을 우회하여 System 권한을 가진 프로세스들에 접근할 수 있다는 것이다. 즉 관리자 계정의 경우 해당 과정을 거쳐 실질적으로는 (현재 High Integrity level임에도 불구하고 더 높은 권한인) System Integrity level에도 접근할 수 있는 권한이 존재한다고 볼 수 있다.


  어쨌든 이런 중요한 작업에 관리자 권한이 필요하다는 것은 당연할 것이고 만약 응용 프로그램이 관리자 권한을 필요로 할 경우에 어떻게 할 것인가에 대해서도 알아보자. 앞에서도 언급하였듯이 마우스 우클릭 후 "관리자 권한으로 실행"을 클릭할 수 있다. 또는 속성의 호환성 탭을 누른 후 "관리자 권한으로 이 프로그램 실행"을 선택해도 된다. 


  하지만 이것은 사용자의 입장이고 개발자로서도 개발한 프로그램이 관리자 권한을 필요로할 경우에는 실행 시에 자동으로 관리자 권한으로 실행되도록 프로그램을 개발할 필요가 있다. 이것은 VC++ 프로젝트 옵션에서 링크 -> Manifest file을 클릭하면 설정이 가능하다. level에는 asInvoker (응용 프로그램을 시작한 프로세스와 동일한 권한으로 응용 프로그램 시작), highestAvailable (최대한 높은 권한 수준으로 응용 프로그램 실행), requireAdministrator (관리자 권한으로 실행)이 있다. 이렇게 설정한 후 생성된 응용 프로그램을 보면 리소스 섹션에 xml 형태의 문자열이 설정한 레벨에 써져있는 것을 볼 수 있다. 프로그램 실행 시 이 레벨을 참고하여 실행하는데 만약 레벨이 requireAdministrator로 설정되어 있다면 UAC가 뜨는 것을 볼 수 있을 것이다. 


  참고로 highestAvailable는 조금 더 설명이 필요하다. 만약 현재 표준 사용자 계정으로 로그온한 경우라면 UAC가 뜨지 않을 것이다. Medium이 최대 권한이므로 관리자 권한을 요구하지 않을 것이기 때문이다. 하지만 관리자 계정이라면 최대 권한인 관리자 권한으로 실행되기 때문에 UAC가 뜨는 것을 볼 수 있다. 굳이 이러한 메커니즘을 만든 이유를 알아보자. 이것은 개발한 프로그램이 관리자 권한을 가진 경우에는 모든 기능을 사용할 수 있고 만약 표준 사용자 계정이라도 제한적인 기능을 사용할 수 있게 하려는 경우에 사용된다. 예를들면 regedit.exe (레지스트리 편집기)가 있다. 일반 사용자의 경우에도 HKCU 하이브의 어떤 값들은 수정할 권한이 있기 때문이다. 물론 관리자 권한이 없으므로 HKLM 하이브의 경우에는 손댈 수 없다.





2. UAC 우회

  앞에서도 설명하였듯이 사용자가 관리자 계정을 가지고 있더라도 관리자 권한이 필요한 프로그램을 실행할 경우에는 UAC가 뜨게 된다. UAC Bypass 즉 UAC 우회는 관리자 권한으로 실행 시 UAC가 뜨는 것을 우회하여 사용자가 인지하지 못하게 악성코드를 설치하거나 악의적인 작업을 하기 위해 사용된다. 악성코드 설치 시에 레지스트리 수정 같이 설치 시에 관리자 권한이 필요한 경우가 많기 때문일 것이고 악의적인 작업도 Medium 권한으로는 한계가 있다. UAC 메커니즘이 만들어지기 전인 Windows XP에서는 지금처럼 대부분 관리자 계정을 사용하였을 것이고 UAC 메커니즘도 없었기 때문에 악성코드는 사용자가 인식하지 못하게 관리자 권한을 가지고 악성코드를 설치하는 일이 훨씬 편했을 것이다.


  물론 앞에서도 설명했지만 굳이 관리자 권한 없이도 악의적인 행위를 수행할 수 있다. 어떤 랜섬웨어의 경우에는 자신이 SeDebugPrivilege 권한을 가지고 실행 중인지를 판단한 후 가지고 있지 않다면 단지 사용자 파일을 암호화하며 가지고 있다면 추가적으로 MBR도 암호화시키는 루틴을 가지고 있다. 하지만 자동 시작을 위해 레지스트리의 AutoRun에 값을 집어넣거나 서비스 및 드라이버 설치, 시스템 디렉터리에 손대기, 방화벽 설정 변경 등 악성코드로서 할 수 있는 많은 부분에 제약이 생기기 때문에 많은 악성코드들은 관리자 권한을 원한다.


  이제부터는 UAC bypass를 설명하고자 한다. UAC 우회는 전제 조건으로 먼저 사용자가 관리자 계정이어야 한다. 또한 대부분 UAC 옵션이 디폴트 옵션인 "Notify me only when programs try to make changes to my computer" (앱에서 사용자 모르게 컴퓨터를 변경하려는 경우에만 알림)를 선택한 경우에만 통하는 편이다. 공개된 방법들 중에는 UAC의 가장 강한 옵션인 "Always Notify"로 설정된 경우에는 제대로 동작하지 않는 기법들이 많다. 물론 위에서 언급하였듯이 대부분의 사용자는 관리자 계정을 가지고 컴퓨터를 사용할 것이며 UAC도 디폴트 설정으로 머물러 있을 것이기 때문에 버전에 맞고 취약점이 패치되지 않았다면 대부분 통할 것이다.


  그리고 공통적으로 사용되는 메커니즘이 있다. 윈도우 시스템 파일들은 Microsoft Windows publisher에서 서명한 사인이 존재하는데 (그렇기 때문에 사인받았다고 해도 3rd party 애플리케이션은 이와 관련이 없다) 이 중에서 리소스의 manifest에 <autoElevate>true<autoElevate> 즉 자동 권한 상승 속성이 삽입되어 있는 프로그램이 있다. 이 프로그램들은 당연히 관리자 권한으로 실행되지만 UAC 옵션이 "Notify me only when programs/apps try to make changes to my computer"로 설정되어 있다면(Always Notify와 Never Notify의 중간) 이름처럼 실행 시에 UAC가 뜨지 않는다. Administrators group에 속해 있다면 관리자 권한으로 실행되지만 UAC가 뜨지 않는 것이다. UAC bypass는 이러한 프로그램들을 대상으로 한다. 즉 악성코드가 이것이 설정된 애플리케이션을 실행시킬 때는 UAC가 뜨지 않으며 관리자 권한을 가진 이 프로그램이 악성코드를 실행하게 만들어서 자동으로 상승된 권한을 상속받은 악성코드를 실행하는 것이다.


  UAC Bypass 취약점의 경우 다양한 프로그램들을 이용하지만 기본적인 방식은 비슷한 경우가 많다. 먼저 앞에서도 언급하였지만 auto-elevate 속성을 가지며 취약점을 통해 악성코드를 실행하게 될 프로그램들로는 sysprep.exe, cliconfg.exe, mmc.exe 등이 있으며 이 프로그램들 외에도 최근에는 다른 프로그램들도 많이 발견되고 있는 중이다. 또한 이 프로그램이 가진 즉 UAC bypass에서 사용되는 취약점은 대부분 DLL Hijacking 방식이다. 다시말해 이 프로그램이 취약하여 로드할 DLL을 안전하게 검사하지 않아서 공격자가 만든 DLL을 로드하게 만드는 방식이다. 이제부터는 실제 예를들어서 설명할 것이다. 여기서 언급하는 취약점들은 모두 패치되어 더 이상 사용이 불가능하지만 메커니즘을 공부할 수는 있다.


  먼저 Mcx2Prov.exe를 이용하는 방식을 통해 설명해 보겠다. 이 프로그램은 cryptbase.dll에 대해 DLL Hijacking 취약점을 갖는데 이 취약점은 애플리케이션이 dll을 로드하기 위해 찾을 때 현재 디렉터리에서 가장 먼저 찾기 때문에 벌어진다. 그러므로 만약 우리가 악의적인 행위를 수행하는 dll을 만들어서 cryptbase.dll이라는 이름을 붙이고 그 디렉토리에 교체해 넣으면 이 dll을 로드할 것이다. 하지만 이 프로그램은 C:\Windows\System32에 존재하기 때문에 공격자가 만든 dll을 여기에 옮기는 것부터 문제가 생긴다. 왜냐하면 애초에 해당 디렉토리에 파일을 옮기는 것도 관리자 권한이 필요하기 때문이다. 여기서는 wusa.exe(Windows Update Standalone Installer)를 이용한다. 먼저 악의적인 dll을 만들고 이름을 cryptbase.dll로 짓는다. 이후 makecab 프로그램을 이용해 이 dll을 cabinet 파일로 만든다. 이제 wusa를 사용하여 이 파일을 /extract 옵션을 주고 Mcx2Prov.exe가 존재하는 디렉터리에 풀면 된다. 관리자 권한이 필요한 동작인 C:\Windows\System32에 파일을 이동시키는 행위를 auto-elevate 프로그램인 wusa를 이용하여 가능케 한 것이다. 참고로 wusa.exe의 이러한 문제점 때문에 이후 /extract 옵션이 제거되었다. 이제 Mcx2Prov.exe를 실행하면 변경된 cryptbase.dll을 로드하기 때문에 UAC 없이 dll로 만들었던 내용이 관리자 권한을 가지고 실행되는 것을 볼 수 있다.


  더 흔한 예시를 들어보겠다. 앞에서는 Mcx2Prov.exe를 가지고 설명했지만 사실 가장 많이 사용되었던 프로그램은 sysprep.exe이다. 이것도 위와 비슷하게 DLL Hijacking 취약점을 사용한 방식이고 앞에서 말했던 cryptbase.dll도 해당하는 dll 중 하나였다. 물론 이렇게 간단한 차이를 설명하기 위해 예시를 든 것은 아니고 이번에는 wusa 대신 IFileOperation 방식을 위주로 설명하고자 한다. 또한 다른 프로세스들도 마찬가지이지만 explorer.exe 같은 윈도우 자체 프로세스도 Medium Integrity Level을 가지고 있기 때문에 dll 인젝션이 가능하다는 것을 알 수 있다. CreateRemoteThread() 함수는 XP에서와 달리 제약 사항이 많아졌지만 대상 프로세스가 같은 세션에 존재하고 (explorer.exe 프로세스는 당연히 로그온 시에 생성됨과 동시에 실행할 프로세스의 부모이므로) 같거나 낮은 Integrity Level을 가지고 있다면 과거와 같이 DLL 인젝션에 사용될 수 있다.


  먼저 explorer.exe에 dll을 인젝션한다. 인젝션되는 dll에서는 IFileOperation이라는 COM 인터페이스를 이용하는데 여기에는 파일을 복사하고 삭제하는 메소드가 들어있다. 여기와 관련된 부분은 뒤에 추가하기로 한다. 이를 통해 보호된 디렉터리에(C:\Windows\System32 같은) 존재하는 cryptbase.dll을 지우고 공격자가 제작한 dll로 변경한다. UAC 없이 이 방식이 어떻게 관리자 권한을 갖을 수 있냐면 COM 오브젝트는 MS 서명이 되어있는 프로세스에서 사용되는 경우 UAC 없이 auto-elevated되기 때문이다. 즉 구체적으로 말하자면 IFileOperation 오브젝트는 만약 explorer.exe 같이 MS 서명이 되어있는 프로세스에서 (dll이 인젝션되었으므로 dll의 내용은 explorer와 같은 권한으로 실행된다) 사용되는 경우 auto elevated되어서 실행된다. 그래서 UAC 없이 관리자 권한으로 시스템 디렉터리에 쓸 수 있는 것이다.


  이 방식의 단점은 IFileOperation COM 오브젝트를 사용할 경우 UAC 옵션이 "Always Notify"라면 UAC가 뜬다는 점과, 과정 중 DLL 인젝션이 수반되기 때문에 보안 프로그램에서 탐지될 수 있다는 점이 있다.


  마지막으로 지금까지 봐왔던 것과는 조금 다른 방식을 설명하려고 한다. IFileOperation의 경우에는 dll 파일 및 이것을 프로세스에 인젝션하는 과정이 필요했다. wusa를 이용한 방식도 결국 DLL Hijack 방식이라는 것은 같기 때문에 dll이 필요하며 이것을 시스템 디렉터리로 이동시키는 작업이 필요했다. 이런 방식들은 보안 솔루션을 통해 탐지될 확률이 높다. 여기서는 레지스트리를 이용하는 방식을 다룬다. 먼저 취약점에 이용되는 레지스트리에 대한 배경 지식을 설명하자면 HKEY_CLASSES_ROOT(HKCR)HKEY_LOCAL_MACHINE\SOFTWARE\ClassesHKEY_CURRENT_USER\Software\Classes의 조합이다. 그리고 표준 사용자는(Medium Integrity Level) 관리자 권한이(High Integrity Level) 없어도 자신의 HKCU 레지스트리를 수정할 수 있다.


  여기서 사용되는 eventvwr.exe도 당연히 auto-elevate 속성을 가지고 있다. 이 프로그램은 실행 시 RegOpenKey()를 이용해 HKCU\Software\Classes\mscfile\shell\open\command를 읽지만 “NAME NOT FOUND”라는 결과를 받는다. 실패 이후에는 HKCR\mscfile\shell\open\command를 읽는데 값을 보면 mmc.exe의 경로이며 이것을 실행시키는 것을 볼 수 있다. 중요한 점은 eventvwr.exe가 auto-elevate 속성을 가지므로 UAC 없이 관리자 권한을 통해 실행된다는 점이고 이에 따라 mmc.exe도 관리자 권한으로 실행된다는 것이다. 우리가 이용할 취약점은 eventvwr가 HKCR을 읽기 전에 먼저 HKCU를 읽으며 표준 사용자로서 HKCU의 레지스트리를 조작할 수 있다는 점이다. 만약 우리가 HKCU\Software\Classes\mscfile\shell\open\command에 원하는 악성코드의 경로를 집어넣는다면 이 악성코드는 UAC 없이 관리자 권한으로 실행될 것이다.





3. 참고사항

- UAC는 디지털 서명의 유무에 따라 다르게 보여지는데, 응용 프로그램이 서명된 경우에는 윗 부분이 파랗게, 아닌 경우에는 노랗게 뜬다.


- 디폴트로 비활성화되어 있는 계정 중 Administrator라는 계정이 있다. 헷갈릴 수 있지만 그룹 Administrators에 속하는 이름이 Administrator라는 계정이다. 과거와 달리 현재는 디폴트로 비활성화되어 있기 때문에 볼 일이 없겠지만 굳이 이것을 언급하는 이유는 이 계정을 사용하면 UAC가 디폴트로 비활성화되어 있다는 특징 때문이다. 과거 XP의 자료들과 비교하여 혼동의 여지가 있을 수 있으므로 적어놓는다.


- 실행 도중에 관리자 권한 획득에 대해서 생각해볼 수 있다. 일반적으로 CreateProcess()를 사용하여 가능할 방법을 찾겠지만 불가능하다. 대신 ShellExecute() 또는 ShellExecuteEx()의 인자 또는 구조체에 "runas"를 넣고 호출한 경우에만 가능하다.


- 먼저 COM 등과 관련된 문서의 링크이다. [ http://sanseolab.tistory.com/49 ] 그리고 여기와 관련된 추가적인 내용은 다음과 같다.

  IUnknown 인터페이스는 COM에서 가장 기본적인 인터페이스이다. 그러므로 모든 COM 객체들은 만드시 최소한 이 인터페이스는 구현해야 하며 나아가 모든 COM 인터페이스는 반드시 여기에서 나온다. 그러므로 ActiveX 하에 설계된 컴포넌트들도 반드시 IUnknown 인터페이스를 구현해야 한다.

  IFileOperation 인터페이스는 IUnknown 인터페이스를 상속하며 Shell 아이템들에 대한 복사, 이동, 생성, 삭제 등의 메소드를 제공한다.


- Window XP에서는 Integrity Level 메커니즘이 존재하지 않으므로 당연히 UAC 관련된 기능도 없다. 그래도 비슷한 점이 많이 있는데 현재 관리자 그룹 즉 Administrators Group에 속한 계정이라면 SeDebugPrivilege를 Enable시킬 수 있다는 점이다. XP도 일반적으로는 이 권한이 Disabled되어 있다.

Posted by SanseoLab



목차

0. 개론

1. 경로

2. 실행

.... 2.1 취약점

........ 2.1.1 파일 포맷 취약점

........ 2.1.2 웹 브라우저 관련 취약점

........ 2.1.3 DLL Hijack

........ 2.1.4 운영체제 취약점

.... 2.2 매크로

.... 2.3 스크립트 파일

.... 2.4 Autorun.inf

.... 2.5 확장자 위장

3. 문서 쪽 정리





0. 개론

  개인적으로 악성코드 분석 중에서도 리버스 엔지니어링을 통한 바이너리 분석에만 집중하여 공부해 왔다. 원래 웹 쪽에는 관심이 없었을 뿐더러 취약점과 관련해서도 관련 지식이 많이 부족했기 때문이다. 하지만 악성코드 분석이라는게 순수하게 바이너리를 분석하기만 하는 것이 아니라 어떤 방식으로 유포되어 사용자 컴퓨터를 감염시키는지도 포함된다고 생각한다. 악성코드가 실행되는 그 순간 이후부터는 항상 해오던 것이기 때문에 여기에는 포함시키지 않기로 한다. 여기서는 그 직전까지의 상황을 위주로 설명한다. 웹도 그렇고 취약점도 그렇고 그다지 많이 알지 못하는 분야이기 때문에 많이 부족하며 많은 바이러스 분석 리포트를 읽어보고 추가할 내용이 생기면 반영하기로 하겠다.


  악성코드가 공격 대상의 컴퓨터에서 실행되기 위해서는 먼저 악성코드 바이너리가 공격 대상의 컴퓨터에 존재해야 하며 또한 이것을 실행시킬 메커니즘이 필요하다. 바이너리는 순수하게 그대로 존재할 수도 있고 실행 파일이지만 사용자에게 혼란을 줄 목적으로 다른 형태의 확장자를 가질 수도 있다. 이외에도 다른 파일에 암호화되어 삽입된 형태로 존재하여 드로퍼 형태로 이것을 추출한 후에 실행하는 메커니즘일 수도 있다. 마지막으로 다운로더 형태로 존재하여 실제 악성코드를 다운로드 받은 후에 실행시키는 메커니즘도 존재한다. 사실 가장 많은 방식은 웹 브라우저나 애플리케이션의 취약점을 이용한 방식이다. 임의 코드 실행 취약점은 뒤에서 살펴보겠지만 취약한 입력 등을 이용하여 공격자가 설정한 임의적인 쉘코드를 실행하는 방식이다. 그렇기 때문에 이 쉘코드 내부에 모든 기능을 넣기 힘드므로 드로퍼 방식을 사용해 다른 파일에서 바이너리를 추출하고 실행하던지 아니면 다운로더 방식으로서 악성코드를 다운로드 받고 실행하는 내용의 쉘코드가 사용된다.





1. 경로

  여기서는 악성코드에 감염되는 최초의 경로를 말하고자 한다. 가장 많이 이용되는 경로 중 하나는 스팸 메일일 것이다. 이것은 사회공학적인 방법을 이용해 링크를 클릭하게 할 수도 있고 위장 문서 파일을 다운로드하고 열어보게 만들 수도 있다. 감염된 사이트의 url을 클릭하게 만들어서 취약점을 이용한 공격이 가능하게 하거나 직접 악성코드를 다운로드받게 하는 것이다. 다운로드 받는 경우는 대부분 문서 파일 형태로서 매크로를 이용한 방식일 것이지만 확장자의 특징을 이용해 사용자가 이것이 실행 파일이 아닌 것처럼 생각하게 해 실행하게 만들 수도 있다.


  스팸 메일 외에도 피싱 방식도 감염된 사이트의 url을 클릭하게 만들 수 있다. 물론 많은 사람들이 이용하는 사이트가 취약해 감염된 경우에는 이런 방식 없이도 이 사이트에 접속한 많은 사람들이 직접 감염될 것이다. Drive-By-Download가 이런 방식으로서 특별히 실행 파일을 다운로드한다거나 실행하는 것 없이 이렇게 웹 서핑만 했는데도 악성코드에 감염될 수 있다. 참고로 워터링 홀 공격은 이 DBD 공격과 같은데 차이점이 있다면 불특정 다수가 아닌 특정한 집단을 목표로 한다는 점이다. 즉 APT 공격 중 하나이다. Malvertising 방식은 온라인 광고를 통해 악성코드를 유포시킨다. 사람들은 안전한 사이트에서는 광고도 안전하다고 여기는 경향이 있지만 사실 수 많은 광고를 모두 검증하기 어렵다. 광고 클릭 시 악성코드 다운로드를 유도하거나 취약점을 이용하는 감염된 사이트로 redirect 시킬 수 있다.


  앞의 스팸메일의 경우처럼 꼭 문서 파일이 아니더라도 웹하드나 p2p를 통해 주로 다운로드 받는 mp3나 동영상 같은 미디어 파일도 감염되어 있을 수 있다. 이것은 애플리케이션이 읽는 미디어 파일 포맷에 관한 취약점을 이용한다. 또 다른 방식으로 게임 핵이라는 이름으로 제공되지만 실제로는 악성코드에 감염시키는 내용이 들어있는 실행 파일일 수도 있다.


  최근에는 운영체제 자체의 취약점을 통해 같은 네트워크에 존재하기만 해도 즉 인터넷에 연결만되어 있어도 악성코드에 감염되는 경우가 발생하기도 했다.





2. 실행

2.1 취약점

2.1.1 파일 포맷 취약점

  동영상 또는 음악 파일을 재생하고 자막 파일을 읽어서 보여주는 미디어 플레이어나 문서를 처리하는 워드 프로그램 또는 파일을 압축 및 압축 해제하는 종류의 프로그램들은 공통적으로 파일을 읽어들인 후 특정 행위를 수행한다. 만약 이러한 애플리케이션에서 특정 영역을 읽어들일 때 오버플로가 발생하는 취약점이 존재한다면 파일의 이 취약점을 이용하는 쉘코드를 작성해 임의적인 코드를 실행시킬 수 있다. 이러한 형태의 취약점을 임의 코드 실행 취약점이라고 한다. 이 쉘코드는 악성코드를 다운로드하고 실행시키는 다운로더의 역할을 할 수도 있으며, 파일에 악성코드를 암호화하여 삽입한 경우 다운로드 대신 해당 바이너리를 디코딩하여 추출한 후에 실행시키는 드로퍼의 역할을 할 수도 있다.


   웹 브라우저 취약점 항목에서 설명하겠지만 어도비 플래시 플레이어의 경우 .swf 파일을 읽고 실행하는데 이 때 파싱하는 과정에서 파일 포맷 취약점이 존재할 수 있다. 또한 플레이어가 직접 읽어들이는 .swf 파일 외에도 ActionScript를 통해 읽어들이는 mp4 같은 미디어 파일도 마찬가지이다.


  그러므로 미디어 플레이어 같은 자주 사용되는 애플리케이션을 항상 최신 버전으로 업데이트할 필요가 있으며 출처가 확실치 않은 미디어 파일을 다운로드하지 않는 것도 방법이다. 이것은 웹 브라우저에서 실행되는 애플리케이션인 어도비 플래시 플레이어 같은 프로그램도 마찬가지이다.


2.1.2 웹 브라우저 관련 취약점

  앞에서는 간단하게 감염된 사이트에 접속하여 악성코드에 감염된다고만 설명하였다. 여기서는 조금 더 깊게 들어가서 어떤 방식을 통해 악성코드가 공격 대상의 컴퓨터에 주입되고 이후에 실행되는지에 대한 메커니즘을 다루고자 한다. 사실 요즘은 대부분의 악성코드가 여기서 설명할 Drive-By-Download 방식으로 웹 브라우저를 이용하여 감염시키는 경향이 있다. 


  일반적으로 Drive-By-Download 공격 방식에는 악성코드 경유지와 유포지 등의 단어가 등장한다. 여기서는 경유지란 최초로 방문하게 되는 감염된 페이지를 뜻할 것이고 중계지는 추적을 어렵게 만들기 위하여 유포지로 도착하기 전에 방문하게 되는 페이지들을 뜻할 것이다. 그리고 유포지는 실질적으로 취약점을 이용해 공격을 수행하는 내용이 들어있는 페이지를 뜻하며 마지막으로 악성코드가 실제로 저장된 저장소가 있다.


  공격자는 특정한 웹 사이트의 취약점을 이용하여 웹 서버에 악성 스크립트를 삽입하거나 직접 사이트를 변조할 수 있다. 일반적으로 iframe 태그를 통해 여러 중계지를 거쳐 유포지로 redirect 시키는 형태이다. 즉 공격자는 취약한 웹 페이지를 변조시켜 iframe 태그를 삽입하거나 이러한 역할을 하는 악성 스크립트를 삽입함으로써 경유지를 만든다.


  이제 사용자는 방문하고자 했던 사이트에서 여러 중계지를 거쳐 유포지로 redirect되었다고 가정하겠다. 유포지에서는 사용자의 브라우저와 운영체제 등을 검사하여 취약점을 찾고 실제로 취약점을 이용해 공격하는 역할을 수행한다. 취약점의 경우 조건이 맞아야 그 역할을 할 수 있는데 애초에 관련 프로그램이 설치되어 있지도 않다면 통할리가 만무하기 때문이다. 또한 설치되어 있다고 해도 해당하는 버전에 따라 취약점의 성공 여부도 다르므로 공격자의 입장에서는 사용자의 정확한 환경을 파악하고 이후 이것에 맞는 취약점을 사용할 것이다.


  이제 해당하는 취약점 예를들면 어도비 플래시 플레이어 취약점이 공격 가능하다고 하자. 유포지에서는 해당하는 취약점에 상응하는 악성 파일(이 경우에는 .swf)을 사용자로 유입시킨다. 취약점이 존재하는 사용자의 어도비 플래시 플레이어는 이 악성 파일을 읽어들일 것이고 이 과정에서 파일 내부의 쉘코드가 실행되어 실제 악성코드를 다운로드하고 실행시킨다. 물론 쉘코드는 swf 파일 내부에 인코딩되어 있는 악성코드를 추출하고 실행시키는 내용일 수도 있다. 어도비 플래시 플레이어와 관련된 취약점은 뒷 부분에서 다루도록 하겠다.


  정리해보자면 공격자는 악성코드 유포지로 사용자를 유도하기 위해 취약한 웹 사이트를 감염시켜 경유지로 만든다. 유포 페이지는 사용자가 어떤 취약한 애플리케이션을 사용하는지 검사하고 해당 취약점을 이용하는 곳이다. 만약 사용자가 어도비 플래시 플레이어의 취약한 버전을 사용한다고 판단될 경우 .swf 파일을 사용자의 웹 브라우저로 다운로드시킨다. 이 파일은 ActionScript 언어를 이용해 취약점을 이용하는 루틴으로 개발되었을 것이다. 어쨌든 플레이어는 이 파일을 읽음으로써 공격자가 원하는 임의적인 코드를 실행하게 된다. 이 코드의 내용은 궁극적으로 악성코드를 다운로드 받고 실행시킬 것이다. 이 악성코드는 악성코드 저장소에 존재한다.


  가장 유명한 어도비 플래시 플레이어에 관해서 더 알아보겠다. 이것은 .swf 파일을 실행한다. 이 파일은 플래시 파일로서 ActionScript라는 언어를 이용해서 개발한다. 파일 포맷 취약점 형태를 보면 어도비 플래시 플레이어에서 .swf 파일을 파싱하는 부분에서 발생할 수도 있고 ActionScript를 통해 .mp4 파일을 읽고 파싱하는 부분에서 발생할 수도 있다. 즉 이런 방식들은 파일 포맷 취약점을 이용하는 방식이다. 물론 애플리케이션 자체적인 취약점이 더 많을 것이다. 예를들면 Use-After-Free나 사용하는 라이브러리에서 제공되는 함수가 받은 인자를 제대로 처리하지 못할 때 즉 취약한 함수를 이용할 때에도 발생할 수 있다. 결론적으로 어도비 플래시 플레이어의 취약점을 이용하기 위해서는 악성 swf 파일이 필요한데 이 파일의 내용은 즉 ActionScript의 내용은 mp4 같은 미디어 파일의 파싱 취약점을 실행하는 간단한 내용일 수도 있고 자체적으로 취약점을 공격하는 내용이 들어있을 수도 있다.


  어도비 플래시 플레이어 외에도 이와 비슷한 Java Applet, MS Silverlight가 있다. 자바의 경우 취약한 .jar 파일이 사용되는데 JVM이 포함되어 있는 웹 브라우저가 자바 애플릿이 포함된 웹 사이트 접속 시에 JVM으로 애플릿을 다운로드하여 로컬에서 실행하는데 이 때 보안 관리자를 우회하여 악성코드에 감염시키는 것이다. 또한 이러한 애플리케이션 말고도 브라우저의 취약점 즉 MS IE 취약점을 사용할 수도 있을 것이다. 참고로 자바 취약점을 이용한 방식은 JVM에서 실행되기 때문에 브라우저가 강제로 종료되지 않으며 플래시 플레이어나 브라우저 자체 취약점의 경우에는 대부분 브라우저가 강제로 종료된다고 한다.


  마지막으로 대부분의 악의적인 행위를 수행하는 스크립트는 난독화되어 있다. 즉 경유지부터 중계지들 그리고 유포지까지 관련된 대부분의 사이트들의 악의적인 부분이 대부분 난독화되어 있다고 보면 된다. 스크립트 언어의 특성상 컴파일되지 않아 사람도 쉽게 읽을 수 있기 때문이다. 특히 자바스크립트의 경우에는 난독화 툴(대부분 Exploit Kit에서 제공된다)부터 이것을 해제하는 툴들이 많이 존재하는데 이쪽만 해도 공부할 내용이 많다.


2.1.3 DLL Hijack

  특정 DLL을 로드하는 애플리케이션이 DLL에 대한 검사 없이 로드한다고 하자. 이러한 경우 악의적인 사용자가 이름이 같은 악성 DLL을 만들고 함수 이름도 같게 만들어서 export시키고 실제 DLL 대신 이 DLL을 포함시켜 배포할 수 있다. 아니면 인라인 패치로 정상 DLL 파일에 악의적인 부분을 써 넣을 수도 있다. 물론 함수는 실제적인 기능이 아닌 악의적인 행위를 수행할 것이다. 사용자는 실행 파일이 안전하므로 거리낌없이 사용할 것이고 해당 실행 파일은 실행될 경우 악성 DLL을 로드하고 그 함수를 호출할 것이다. 


2.1.4 운영체제 취약점

  최근에는 윈도우의 SMBv2 원격코드 실행 취약점을 이용해 랜섬웨어가 전파되었다. 이 방식은 자신의 네트워크 대역 IP 및 랜덤으로 생성된 IP 대역을 스캔하여 SMB 취약점이 발견될 경우 랜섬웨어에 감염시키는 방식으로 전파된다.



2.2 매크로

  앞에서 문서 파일을 언급하였다. 이러한 형태의 취약점은 문서 파일 내의 매크로(MS 오피스 프로그램의 경우 VBA) 기능을 이용한 기법으로서 주로 다운로더나 드로퍼 기능을 가지며 이후 다운로드 또는 추출한 악성 바이너리를 실행시킨다. 일반적으로 메일 등의 첨부파일로 전파되는데 사용자가 해당 문서 파일을 열 경우 악의적인 매크로가 실행된다. 최신 오피스 프로그램은 디폴트로 매크로 기능이 꺼져 있지만 궁금증을 유발시켜 매크로 기능을 켜게 유인시키기도 한다. 



2.3 스크립트 파일

  다음 문서에 정리한다. [ http://sanseolab.tistory.com/41 ]




2.4 Autorun.inf

  USB나 외장 하드를 연결한 경우 최상위 디렉토리에 Autorun.inf라는 파일이 있다면 운영 체제는 이 파일을 실행하고 이후 디스크를 열게 된다. 내부적으로 만약 Autorun.inf 파일 내부에 open= 구문으로 지정된 파일이 실행된다.



2.5 확장자 위장

  사용자에게 악성 실행 파일의 확장자를 실행 파일이 아닌 것처럼 인식시키는 방식은 여러가지가 있다. 가장 간단한 방법은 사용자가 확장자 자동 숨김 기능을 사용한다는 가정 하에 " aaa.jpg.exe " 파일을 만들고 아이콘을 사진 처럼 변경하여 사용자 눈에 aaa.jpg로 인식시키는 방식이 있다. 이외에도 디렉토리 이미지도 자주 사용된다. exe 파일의 경우 확장자 자동 숨김을 통해 확장자는 가려져 있는데 아이콘이 디렉터리라면 당연히 디렉터리인줄 알고 더블클릭하여 실행시키는 방식이다. exe 파일은 아니지만 실행 가능한 파일로서 사용자에게 실행 파일이 아니라 데이터 파일로 인식시켜 의심을 사지 않게 하는 방법도 있다. 결론적으로 실행 파일의 이미지를 특정 파일 포맷 예를들면 사진이나 동영상 같은 이미지로 교체하고 확장자도 혼란스럽게 하여 사용자의 부주의를 통해 실행시키는 방식이다. 이러한 방식들은 아래에 설명하기로 한다. 


- 앞에서 언급한 스크립트 파일들. [ .js  .vbs  .hta  .wsf  .chm  .bat  .ps1 ]


- RLO

  먼저 charmap.exe를 실행하면 문자표라는 애플리케이션이 뜬다. 아랫 부분에 "유니코드로 이동"이라는 부분과 오른쪽에 빈 칸이 있다. 이 빈 칸에 202E 입력한다. 맨 왼쪽 맨 위에 아무것도 보이지 않는 빈 칸이 보인다. 마우스로 대보면 "U+202E: Right-To-Left-Override"라고 나온다. 이것을 클릭하고 아랫 부분에 복사를 누른다. (참고로 복사할 문자 부분에서 눈에는 보이지 않으므로 복사된건지 확인하기 힘들다)

  이후 실행 파일의 이름을 " aaa4pm.exe " 로 바꾼 후 aaa 바로 다음 부분에 커서를 두고 붙여넣기를 해보자. 그렇다면 " aaaexe.mp4 " 처럼 변경되는 것을 볼 수 있다. 이제 애플리케이션의 이미지만 바꾸면 동영상 파일로 착각할 수 있게된다. 이름 부분도 충분히 쓸데없이 길게 영어를 섞어서 해주면 잘 모를 것이다.


- .lnk

  바로가기 파일이다. 사용자로서는 바로가기 파일을 악성코드로 인식하기 어려울 수 있다. 바로가기 파일은 내부적으로는 실행 파일의 경로를 저장하고 있다가 더블클릭되면 해당 실행 파일을 실행시켜주는 파일이다. 속성을 눌러서 대상 부분을 보면 실행할 프로그램의 경로가 나와있다. 이것을 다음처럼 수정할 수 있다.


C:\Windows\System32\cmd.exe /c C:\Users\longa\Desktop\EJDbg\example2.exe


  위의 예는 cmd 명령어를 이용한 방식으로서 한계가 존재할 수 밖에 없다. 현재 주로 사용되는 방식을 알아보겠다. 물론 이것들도 모두 원래 실행할 프로그램의 경로가 존재해야 하는 곳에 특별한 값들을 집어넣는 방식이다.


  첫번째는 파워쉘이 있다. 앞에서도 말했듯 기본적인 cmd 명령어는 수행할 수 있는 능력에 한계가 있다. 하지만 파워쉘은 훨씬 많은 기능이 제공되기 때문에 다운로더로서도 그리고 다운로드 받은 악성코드를 실행하는 기능도 사용 가능하다. 


  두번째는 스크립트 언어이다. 해당 폼에는 명령어나 파워쉘 외에도 JavaScript(.js), VBScript(.vbs), VBScript Encoded Script(.vbe) 등의 스크립트 언어를 사용할 수 있다. 


- .scr

  화면 보호기 파일. 실행 가능하다.


- .pif

  오래된 확장자로서 중요한 부분만 설명해 보자면 윈도우의 로더의 경우 이 확장자를 가진 파일이 실행 가능한 파일이라면 실행시켜 준다. 예를들어서 아무 실행 파일의 확장자를 pif로 바꾸어 보면 exe 형태였을 때와 마찬가지로 실행 파일처럼 실행된다.


- .cpl

  dll과 동일하다고 할 수 있다. 차이점은 exe처럼 직접 실행 가능하다는 점이다. 실제로는 rundll32.exe를 통해 실행된다.





3. 문서 쪽 정리

  먼저 MS Office 쪽부터 설명하겠다. 기본적으로 매크로가 있으며 위에서 설명하였다. 그리고 오피스 애플리케이션들의 파일 포맷 취약점을 이용한 익스플로잇이 있을 것이다. 참고로 Office 관련 파일들 외에도 rtf도 많은 공격에 이용되고 있다. 또한 요즘에는 잘 알려지지 않았던 DDE (Dynamic Data Exchange)를 이용한 공격이 많아지고 있다.


  다음으로는 PDF가 있을 것이다. PDF에서는 JavaScript가 사용될 수 있다. 물론 브라우저처럼 어도비 리더도 샌드박스를 구현하여 방어 메커니즘을 가지고 있다. 많은 취약점은 샌드박스 우회 및 코드 실행을 위해 자바스크립트를 이용하여 취약점 공격을 통해 셸코드를 실행한다.



Posted by SanseoLab

2017. 7. 27. 15:21 카테고리 없음

ejdbg


Posted by SanseoLab





0. 개요

  이 문서에서는 리눅스 안티바이러스에 대한 내용을 정리하기로 하겠다. 사실 그나마 흔한 윈도우 기반 안티바이러스에 대해서도 제대로 알지 못하지만 공부하며 찾아본다는 의미로 이 문서를 만들고자 한다. 리눅스도 윈도우와 마찬가지로 안티바이러스와 관련된 대부분의 자료들이 사용자 모드에서 실행되는 on-demand 형태의 시그니처 기반 스캐닝 위주로 존재한다. 여기서는 실제 상용 리눅스 안티바이러스 제품에서 사용될만한 기술들을 최대한 찾아보며 정리하고자 한다.

  먼저 파일 스캐닝과 관련된 내용을 정리하겠다. 앞에서 언급하였듯이 on-demand 형태의 사용자 모드 시그니처 기반 스캐닝은 윈도우와 비슷하기도 하고 이미 자료가 많이 있기 때문에 언급하지 않기로 하고 바로 실시간 파일 스캐닝 기법을 다루기로 한다. 다행히도 리눅스에서는 최근 fanotify라는 메커니즘이 제공되어 파일 실행 및 종료, 수정 등의 이벤트에 대한 알림 및 승인, 거부도 간단한 방식으로 가능해졌다. 윈도우의 경우에는 파일 시스템 미니필터 드라이버가 제공되서 필터 관리자를 통한 메커니즘이 제공된다.

  이후에는 프로세스 생성 감시 메커니즘을 다룬다. 윈도우의 경우에는 PsSetCreateProcessNotifyRoutineEx() 등의 함수들이 제공되어 프로세스나 스레드 생성 시에 콜백 루틴을 등록해줄 수 있으며 생성되는 프로세스의 정보 획득 및 생성의 승인 또는 거부를 선택할 수 있다. 관련 내용을 리눅스에서 찾아보았는데 아직 윈도우에서 제공되는 형태와 같은 메커니즘이 제공되어 있지 않다. 그래서 관련 사항들을 최대한 찾아본 결과 리눅스 보안 모듈 (LSM : Linux Security Module)을 이용한 방식이 가장 적합한 것으로 보인다. 다른 몇몇 방법들이 있긴 하지만 프로세스 생성 이전에 통지받고 동시에 생성을 거부할 수도 있는 방법은 이 방법밖에는 없어보인다.

  마지막으로 자가 보호 방식에 대해서 설명하고자 했지만 적절한 방식을 찾지 못하고 위에서 언급한 LSM을 이용한 방식을 소개한다. 구체적으로 grsecurity의 소스 코드를 분석하여 자가 보호를 제공하는 방식을 공부한다. 이 외에도 실제 안티바이러스 소프트웨어에서 사용되는 방식은 더 많이 존재하고 리눅스 자체적으로 제공하는 것이 한계가 있기 때문에 대부분 직접 구현되어 있을 것이다. 더 자세한 설명 그리고 추가적인 사항을 공부하면서 꾸준히 이 문서를 업데이트할 것이다.





1. 실시간 파일 스캐닝

  파일 스캐닝 기법에는 on-demand 스캐닝과 on-access 스캐닝이 존재한다. on-demand 스캐닝은 사용자가 검사 버튼을 누르거나 주기적으로 검사 스케줄을 정했을 때 수행되는 스캐닝이다. 일반적으로 인터넷에 퍼져 있는 안티바이러스 소스 코드들을 보면 대부분 시그니처 기반에 이러한 on-demand 스캐닝 방식이다. 이 방식은 전체 또는 지정된 범위에 존재하는 파일들을 모두 검사하는 방식으로서 특별한 지식이 필요하거나 난이도가 높거나 하지는 않으며 관련 자료들도 이미 충분히 존재한다. 물론 대부분 윈도우 플랫폼에서의 자료겠지만 리눅스 플랫폼에서도 충분히 존재한다고 생각한다.

  여기서는 on-access 스캐닝을 다룰 것이다. 이 방식은 실시간 검사라고도 불리는데 파일이 여러 이벤트를 통해 생성되었을 때 실시간으로 검사하는 것이다. 예를들어서 취약점을 이용해 악성코드 바이너리를 다운로드시킨다거나 드로퍼가 악성코드를 드롭시켜서 악성코드 바이너리가 생성되었을 때 이 파일을 검사한다. 당연히 이러한 방식을 사용하기 위해서는 파일 생성 이벤트가 발생하였을 때 알려주는 메커니즘이 필요할 것이다. 이 항목에서는 리눅스에서의 관련 메커니즘과 함께 on-access 스캐닝 기법을 다루기로 한다.

  먼저 그나마 조금 더 대중적인 윈도우의 메커니즘을 먼저 예시로서 설명한 후에 리눅스로 나아가기로 하겠다. 윈도우도 앞에서 설명한 on-demand 스캐닝 및 on-access 스캐닝 기법이 나뉜다는 것은 당연한 이야기일 것이다. 윈도우의 경우에는 on-access 스캐닝을 위하여 즉 안티바이러스 소프트웨어를 위하여 제공되는 메커니즘이 존재한다. 먼저 윈도우에서는 과거 루트킷처럼 (파일 생성 등과 관련된) 관련 함수들을 후킹하는 드라이버를 만들어 on-access 스캐닝을 수행하였다. 하지만 최근에는 커널의 구성 요소인 필터 관리자에 의해 관리되는 파일 시스템 미니 필터 드라이버를 개발하여 이러한 기능을 제공할 수 있다. 파일 시스템 미니 필터 드라이버에서는 여러 제공되는 함수들을 통해 파일 생성 등의 이벤트를 통지받고 또한 특정한 행위를 수행할 수 있다. 물론 안티바이러스의 경우에는 이 특정한 행위란 생성되는 파일에 대한 검사일 것이다.

  리눅스의 경우에는 비슷한 메커니즘이 존재한다. 과거 dnotify부터 inotify를 거쳐 현재는 fanotify가 존재한다. 여기서는 dnotify는 제외하고 inotify부터 설명해보도록 하겠다.


1.1 inotify

  inotify는 리눅스 커널에서 제공되는 기능으로서 디렉토리 및 파일을 감시할 수 있다. 참고로 디렉토리를 감시할 경우에는 디렉토리 자체 및 내부의 파일이 변경되는 이벤트에 대한 정보도 받을 수 있다. 이 메커니즘은 리눅스 커널에서 CONFIG_INOTIFY와 CONFIG_INOTIFY_USER를 설정해야 사용할 수 있는 커널 컴포넌트이다. 참고로 특정 디렉토리를 감시 대상에 추가한 경우에 내부에 존재하는 디렉토리까지 감시 대상에 추가되지는 않는다. 즉 recursive하지 않기 때문에 디렉토리 내부에 새로운 디렉토리가 생성되거나 삭제되는 경우를 위한 추가적인 메커니즘이 필요하다.

  다음으로는 inotify를 사용하는 방식 및 구성에 대해서 알아보겠다. 아주 간단한 예제를 통해 설명해 보자면 가장 먼저 inotify_init()을 호출하는데 이 함수는 inotify 인스턴스를 생성하는 역할을 하며 우리는 이 함수에서 반환된 file 디스크립터를 통해 생성된 inotify 인스턴스에 접근한다. 이후 감시할 대상의 경로 및 감시할 이벤트를 인자로 넣고 inotify_add_watch()를 통해 watch list에 추가한다(반대로 삭제하는 함수는 inotify_rm_watch()이다). 이 함수는 유니크한 watch 디스크립터를 반환하는데 이것은 뒤에서 사용될 것이다. 어쨌든 이후 반복문을 사용하여 주기적으로 read()를 통해 file 디스크립터를 읽는다. read()로 inotify_event 구조체를 읽을 수 있고 여기에는 여러 정보들이 들어있다.

  앞에서 inotify_add_watch()의 인자로 감시할 이벤트를 넣는다고 언급하였다. 파일과 관련된 이벤트는 여러 종류가 있지만 많이 사용되는 이벤트들은 다음과 같다. 파일이 read() 등의 함수를 통해 접근된 경우에는 IN_ACCESS 이벤트가, 감시 대상 디렉토리 내에 파일이나 디렉토리가 생성된 경우에는 IN_CREATE, 삭제된 경우에는 IN_DELETE, 감시 대상 파일 또는 디렉토리 자체가 삭제된 경우에는 IN_DELETE_SELF 이벤트가 발생하며 파일이 수정된 경우에는 IN_MODIFY, 파일이 오픈된 경우에는 IN_OPEN 이벤트가 발생한다. IN_ATTRIB 이벤트는 파일의 권한, 소유자, UID, GID 등의 정보가 수정되었을 때 IN_MOVE_SELF는 대상의 이름이 변경되었을 때 (즉 이동하였을 때) 발생한다. 마지막으로 IN_MOVED_FROM과 IN_MOVED_TO 이벤트는 감시 대상 디렉토리 내부의 파일이나 디렉토리 이름이 변경(이동)되었을 때 발생하는데 원본 파일에서는 IN_MOVED_FROM이 발생할 것이고 변경된 이름의(이동 후의) 파일에서는 IN_MOVED_TO가 발생할 것이다.

  위에서는 inotify_add_watch()를 통해 감시할 이벤트를 넣어주었는데 이후 read()를 통해 얻은, 즉 특정 이벤트가 발생한 경우에 얻는 이벤트도 동일하다. read()를 통해 얻는 데이터는 inotify_event라는 구조체인데 이 구조체에는 앞에서 설명했듯이 발생한 이벤트 및 이벤트가 발생한 watch 디스크립터 등의 값이 존재한다. watch 디스크립터를 굳이 알려주는 이유는 동일한 file 디스크립터를 통해 여러 대상을 감시할 때 필요하기 때문이다.


1.2 fanotify

  앞에서는 간략하게 inotify를 살펴보았다. 이것을 통해 우리는 파일에 관련 이벤트가 발생한 경우 이를 통지해 주는 메커니즘을 사용할 수 있다는 것을 알게되었다. inotify는 많은 이벤트들에 대한 모니터링이 가능하지만 단점도 존재하는 것이 recursive한 모니터링이 불가능했고 또한 이벤트가 발생하였다는 통지 이외에는 제공하는 기능이 없었다. inotify와 비교하여 장단점이 존재하는 fanotify를 살펴보겠다.

  fanotify도 inotify와 비슷한 부분이 많다. 이 메커니즘도 리눅스 커널에서 CONFIG_FANOTIFY를 설정해야 사용할 수 있는 구성 요소이다. 또한 뒤에서 나오겠지만 권한과 관련된 처리가 필요할 때가 있는데 이것은 CONFIG_FANOTIFY_ACCESS_PERMISSIONS를 설정해야 한다.

  이번에는 fanotify를 사용하는 방식을 알아보겠다. 이것도 비슷하게 fanotify_init()을 통해 fanotify group을 초기화하고 이 함수는 이 인스턴스와 연관된 file 디스크립터를 반환한다. 이 file 디스크립터는 inotify에서 inotify_add_watch(), inotify_rm_watch()와 비슷한 역할을 하는 fanotify_mark()에서 사용된다. 이 함수는 이것 외에도 flags, mask, 경로명 등의 인자를 받는데, 경로명은 당연히 감시 대상의 경로명을 의미할 것이고 flags는 FAN_MARK_ADD, FAN_MARK_REMOVE, FAN_MARK_FLUSH가 있다. FAN_MARK_ADD는 감시 대상에 대해 감시할 이벤트들을 추가한다는 것이고 FAN_MARK_REMOVE는 삭제, FAN_MARK_FLUSH는 전체 삭제를 의미한다. 이 3가지 중 적어도 하나는 필수이며 OR ( | )를 통해 다른 flag들이 추가될 수 있다. FAN_MARK_IGNORED_MASK는 mask에 설정된 이벤트들을 감시가 아닌 무시할 때 사용된다. 또한 FAN_MARK_MOUNT는 경로명에 지정된 마운트 포인트를 설정한다. 즉 마운트 포인트인 경로명 내의 서브디렉토리들까지 감시에 추가해주는 recursive한 모니터링 메커니즘을 제공한다.

  마지막으로 mask는 inotify와 마찬가지로 앞에서 언급한 감시할 이벤트들이다. FAN_ACCESS는 접근 즉 read된 경우에 발생하며 FAN_MODIFY는 수정 즉 write된 경우에 발생한다. FAN_OPEN 및 FAN_CLOSE는 각각 파일 또는 디렉토리가 open, close되었을 때 발생한다. FAN_OPEN_PERM과 FAN_ACCESS_PERM 이벤트는 파일 및 디렉토리를 각각 open 또는 read할 경우에 권한이 요구될 때 발생한다. 정확히 말하자면 파일이 열리기 전에는 FAN_OPEN_PERM, 닫힌 후에는 FAN_CLOSE_WRITE 이벤트가 발생한다.

  이제 모니터링이 제공되고 감시 대상 파일이나 디렉토리에서 이벤트가 발생할 것이다. 이것도 inotify처럼 read()를 통해 읽는데 정확히는 fanotify_event_metadata 구조체가 생성된다. 이 구조체에는 발생한 이벤트들 외에도 open file descriptor, 이벤트를 발생시킨 프로세스의 pid 등 여러 정보가 존재한다. 

  발생하는 이벤트는 두 종료가 있는데 Notification 이벤트와 Permission 이벤트가 그것이다. Notification 이벤트들은 inotify의 경우처럼 정보를 제공해 주며 Permission 이벤트들은 파일 접근에 관한 권한을 결정해 줘야 한다. 이 이벤트들은 앞에서 설명한 mask와 거의 같다고 보면 된다. 특이사항으로는 Permission 이벤트들 즉 FAN_ACCESS_PERM, FAN_OPEN_PERM 이벤트는 반드시 write()를 통해 응답을 보내야 한다는 것이다. fanotify_response() 구조체를 생성하여 fanotify file 디스크립터에 write()를 통해 응답을 보낸다. 응답은 이 구조체의 response 멤버에 FAN_ALLOW 또는 FAN_DENY 값을 설정하는 것이다.


1.3 정리

  여기서는 inotify와 fanotify를 비교하면서 정리하겠다. 둘을 살펴보면서 확인한 공통점들이 많기 때문에 기본적인 정보는 생략하고 차이 위주로 설명하겠다. 먼저 recursive한 모니터링을 제공한다는 점과 이벤트 통보 이외에도 접근 통제 즉, 접근의 허용 여부를 결정할 수 있다는 점에서 fanotify의 장점이 크다는 것을 알 수 있다. 이 접근 통제는 특히 안티바이러스에서 유용하게 사용될 수 있는 기능이다. 또한 fanotify_event_metadata 구조체를 보면 이벤트를 발생시킨 프로세스의 PID 및 open file descriptor가 제공된다. open fd를 통해 감시 대상 파일의 내용에도 접근할 수 있다.

  하지만 fanotify에서는 제공하지 않는 inotify만의 장점도 존재하는데 fanotify에서는 inotify보다 제한된 이벤트들을 감시할 수 있다. 즉 inotify에서는 지원됬었던 create, delete, move(rename) 이벤트에 대한 감시는 지원되지 않는다. 





2. 프로세스 생성 감시

  앞에서도 설명하였듯이 리눅스에서는 프로세스 생성 및 종료 등 프로세스의 상태와 관련된 모니터링 메커니즘이 존재하지 않는다. inotify를 통해 /proc을 모니터링하는 것은 불가능하고 proc connector 방식도 fork나 exec 같은 프로세스 이벤트를 알려주기만 할 뿐이지 이벤트 실행 이전에 통지함과 동시에 생성을 거부할 수 있는 메커니즘은 아니다. 그래서 결국 리눅스 보안 모듈을 이용한 방식을 선택하고 설명하고자 한다. 하지만 이 방식이 안티바이러스에서 실제로 사용될 수 있는지 즉 오버헤드를 감당할 수 있는지에 대해서는 모르겠다. 물론 다른 여러 방식으로 사용되고 있기 때문에 확실한 자료 없이 정리하기로 한다.


2.1 LSM

  리눅스 보안 모듈(LSM)이라고 하면 일반적인 모듈이라고 생각할 수 있는데 사실 설명이 필요한 부분이다. 과거 리눅스라는 운영체제에서 보안을 구현할 수 있는 방식으로서 선택 사항은 두 가지가 있었다. 하나는 커널 수정 방식이며 다른 하나는 LKM(Loadable Kernel Module) 즉 탑재 가능한 커널 모듈 방식이었다. 보안을 커널에 집적 구현하는 것과 모듈에 구현하는 것은 각각의 장단점이 명확했다. 리눅스 보안 모듈이라고 하면 LKM 형태로 구현된 보안 모듈이라고 생각하기 쉽지만 사실은 조금 다른 개념이다. 

  정확히 설명하자면 LSM은 여러 종류의 보안 정책 모듈들을 위하여 일반화된 framework를 제공하는 방식이다. 즉 리눅스 커널에서는 framework만을 제공하는 방식으로서 커널에 인터페이스를 만들고 이 인터페이스를 통해 써드 파티 보안 모듈들의 접근 제어 메커니즘과 커널을 연결시킨다. 많이 들어봤겠지만 SELinux, AppArmor, Smack, TOMOYO Linux 등이 공식적으로 리눅스 커널에 받아들여진 보안 모듈들이다.

  조금 더 자세히 보자면 리눅스 커널의 소스 코드 중에서 security.c가 LSM을 구현한 것이라고 보면 된다. 우리는 LSM에서 구현된 인터페이스를 통해 보안 정책 모듈을 만들 수 있다. 실질적으로 LSM은 커널이 내부 객체에 접근을 시도하기 직전에 Hook을 걸어 보안 모듈로 우회시킨다. SELinux나 AppArmor 등의 보안 모듈들은 원하는 부분에 콜백을 등록하여 보안 정책을 구현한다.


2.2 LSM을 이용한 프로세스 생성 모니터링

  앞에서는 개념만 설명하였기에 예를들어 설명해보겠다. 다음 링크 [ https://github.com/skx/linux-security-modules ]의 개발자는 LSM을 이용한 간단한 보안 모듈을 만들었다. 이 중에서 whitelist라는 예제를 보면 실행 파일 중에서 특정한 속성을 가진 바이너리만 실행 가능하게 만들려고 한다. 이 경우 여러 설정들은 생략하고 security_add_hooks() 함수로 bprm_check_security에 hook을 설치한다. 참고로 리눅스 커널 버전 4.1부터 security_add_hooks()라는 함수가 사용되고 있으며 그 이전에는 register_security()를 이용한 방식을 사용하였다. 또한 bprm_check_security라는 hook은 자세히 살펴보면 (CONFIG_SECURITY 커널 컴파일 옵션을 사용해 LSM을 활성화시킨 경우에) exec()이 최종적으로 호출하는 함수인 security_bprm_check()에서 사용되는 hook이다. 즉 exec() 호출 시에 호출되는 hook에 security_add_hooks()로 콜백 함수를 등록하는 것이다. 이 콜백 함수는 당연히 바이너리의 속성을 살피고 실행 여부를 판단하는 루틴으로 이뤄어져 있다.

  예를 조금 더 들어서 fork()의 경우에는 걸 수 있는 함수가 security_task_create()이며 task_create에 hook을 걸어서 사용할 수 있다. 이 외에도 include/linux/lsm_hooks.h를 확인하면 여러 hook들을 찾을 수 있다. 여기서 찾은 hook을 통해 security/security.c에서 이 hook을 사용하는 함수를 찾는다. 이후 이 함수가 어디에서 사용되는지를 추론하여 여러가지 지원되는 hook들을 정리할 수 있다. 앞에서는 프로세스와 관련된 것만 예를 들었지만 파일 시스템, 네트워크 등 여러 종류의 hook을 위한 인터페이스가 제공된다.





3. 자가 보호

  안티바이러스 프로그램은 특성 상 자기 자신을 보호할 필요가 있다. 안티바이러스에 대한 공격으로는 프로세스 종료(리눅스에서는 kill) 외데도 메모리 변조(리눅스에서는 ptrace 등을 이용한) 같은 공격이 있을 수 있다. 윈도우에서는 ObRegisterCallbacks()를 제공하여 프로세스 및 스레드 객체에 대한 특정한 사전 / 사후 동작에 관한 통지를 받는 콜백 함수를 등록할 수 있다. 이를 통해 유저 모드에서 프로세스나 스레드에 접근하려고 하는 경우 이 콜백이 먼저 호출되어 이러한 접근을 차단할 수 있다.

  리눅스의 경우 이러한 함수가 제공되는지 찾아보았지만 역시 존재하지 않았다. 사실 방어 이전에 리눅스에서는 어떤 공격이 존재하는지에 대한 지식도 부족한 상태였기 때문에 자질구레한 방어 기법들을 일일이 정리할 능력이 되지 못한다. 그러던 차에 앞에서 정리했던 LSM을 이용한 방식을 알게되었다. LSM을 정리하면서 가장 유명한 보안 모듈로 SELinux를 이야기했는데 여기서는 grsecurity라는 보안 모듈(공식 커널에 포함되어 있다)을 설명하고자 한다. 사실 SELinux를 보면서 복잡하기도 하고 보안 정책과 관련된 내용이겠구나 하는 생각에 그다지 관심을 가지지 않았는데 grsecurity를 보면서 지금까지 다루어왔던 익숙한 보안 기법들을 제공해준다는 사실에 놀랐다. 여기서는 프로세스 보호에 관련된 내용만 정리하겠지만 grsecurity는 이 외에도 PaX(윈도우의 DEP 같은), ASLR 같은 수많은 보안 기능들을 제공한다.

  직접 어떤 방식으로 구동하는지에 대한 설명은 당연히 위에서 설명한 hook을 이용한 방식일 것이므로 생략하고 어떤 공격이 있을 수 있는지 위주로 정리하겠다. 물론 당연히 grsecurity에서도 제공되는 방어 기능들이다. 가장 먼저 안티바이러스를 종료시키는 공격이 있을텐데 grsecurity에서는 LSM을 이용하여 특정 프로세스에 대한 kill을 못하게 설정할 수 있다. 종료 외에도 리눅스의 경우 ptrace를 이용하여 프로세스를 가로챌 수 있는데 특정 프로세스에 관해 이것을 막아주는 기능도 존재하며 사용자 모드의 프로세스가 실행 중일 때 메모리를 할당하여 코드를 생성하는 기능 및 패치하는 공격에 대한 방어도 존재한다. 다음 링크들은 grsecurity에서 제공되는 보안 기능들을 정리한 사이트들이다. [ https://grsecurity.net/compare.php ] [ https://en.wikipedia.org/wiki/Grsecurity ]





4. 정리

  현대 상용 안티바이러스 제품들이 어떤 형식으로 구현되는지에 대한 지식이 많이 부족한 상태에서 이렇게 나름대로 정리했다는 문서를 만들어서 올리는 것이 검색하는 사람들에게 민폐가 될 수 있겠다는 걱정이 있지만 그래도 대충이라도 훑어보면서 검색할만한 키워드 하나 쯤은 제공할 수 있지 않을까 하는 마음에 정리한 문서를 공개한다. 

  사실 그나마 자료가 있는 윈도우 안티바이러스에 대해서도 찾기 힘들었지만 리눅스 플랫폼의 안티바이러스는 정말 자료를 찾기 힘들어 보인다. 물론 ClamAV라는 오픈 소스 안티바이러스가 존재하지만 여기서는 fanotify를 이용한 on-demand 파일 스캐닝과 관련된 자료밖에 없어서 나머지 내용들은 나름대로 추리해 가면서 찾아보았다.

  실제로 판매되는 적절한 기능을 갖춘 리눅스 플랫폼의 안티바이러스에 대한 정보가 많이 존재했으면 하는 바람이며 이것은 윈도우도 마찬가지이다. 물론 시장이 좁아서 더더욱 없을 것이지만 다른 프로그램들과 달리 안티바이러스 제품들은 특히나 제대로 된 정보를 찾기 힘든 편이다. 어쨌든 배움이 있을 때마다 추가하도록 하겠다.



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

윈도우 권한과 UAC (User Access Control) 우회  (5) 2017.08.19
악성코드가 감염되기까지  (2) 2017.08.13
Windbg, Gdb 명령어 정리  (0) 2017.06.27
VC++ 옵션 정리  (0) 2017.06.03
다형성 바이러스  (4) 2017.05.16
Posted by SanseoLab
이전버튼 1 2 3 4 5 6 7 이전버튼

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2025.2
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

최근에 올라온 글

최근에 달린 댓글

글 보관함