tmxklab

Reflective DLL Injection 본문

Security/07 Malware Technique

Reflective DLL Injection

tmxk4221 2021. 1. 7. 13:49

0. 목차


1. Reflective DLL Injection

1.1 배경

  • Fileless기법을 사용하는 악성코드들이 계속 등장하고 있으며 공격에는 주로 파워 쉘 스크립트를 난독화하여 이용하거나 Process Hollowing, Reflective DLL Injection등의 기법을 사용한다.

1.2 DLL Injection과 Reflective DLL Injection

1) DLL Injection

  • 디스크에 저장된 악성 DLL을 타겟 프로세스에 강제로 삽입하는 형식
  • CreateRemoteThread()를 통해 타겟 프로세스에서 LoadLibrary()로 악성 DLL을 로드
  • 하지만, CreateRemoteThread()의 인자로 Injection하고자 하는 DLL의 파일 경로가 포함되어 쉽게 탐지되는 단점을 가진다.

1-1) DLL Injection 과정

① OpenProcess()를 통해 타겟 프로세스 핸들을 구한다.

② VirtualAllocEx()를 통해 타겟 프로세스의 메모리 공간 확보한다.

③ WriteProcessMemory()로 타겟 프로세스의 메모리에 Injection할 DLL경로 입력한다.

④ GetProcAddress()와 GetModuleHandle()을 통해 LoadLibraryA()의 주소를 구한다.

⑤ CreateRemoteThread()를 호출하여 타겟 프로세스를 통해 LoadLibrary("dll 파일 경로")을 호출한다.

2) Reflective DLL Injection

  • 기존의 DLL Injection과 달리 실행중인 프로세스 메모리에 DLL에 대한 데이터 삽입 후 매핑하여 실행시키는 방법으로 동작한다.
  • 디스크가 아닌 메모리에서 DLL을 로드한다.
  • 악성 행위를 하는 DLL은 백그라운드에 존재하지 않아 탐지하기 어렵다.
  • 이러한 이유로 파일리스 악성코드에서 많이 사용된다.
    • SMB취약점 이터널 블루(Eternal Blue), 더블펄서(Double Pulsar)에서도 사용됨

2-1) Reflective DLL Injection 과정

① OpenProcess()를 통해 타겟 프로세스의 핸들을 구한다.

② VirtualAllocEx()를 통해 타겟 프로세스의 메모리 공간을 확보한다.

③ WriteProcessMemory()를 통해 타겟 프로세스에 할당된 메모리 공간에 Injection할 DLL 데이터를 입력한다.

④ CreateRemoteThread()을 호출하여 타겟 프로세스를 통해 ReflectiveLoader()를 호출한다.

  • 필요한 함수의 주소를 구한다.(LoadLibraryA, GetProcAddress, VirtualAlloc)
  • VirtualAlloc()을 통해 메모리 공간(rwx)을 확보한다.
  • DLL의 헤더와 섹션을 메모리에 로드시킨다.(메모리에 DLL 매핑)
  • Import Table 재작성(IAT 재작성)
  • 이미지가 충돌없이 메모리에 로드될 수 있도록 Relocation
  • DLL EntryPoint인 DllMain을 호출

1.3 차이점

위 두 가지 Injection 방법은 DLL을 어떠한 형태로 프로세스에 인젝션 하는가에 따라 달라진다.

DLL Injection

  • 인젝션 방법 : 인젝션하고자 하는 DLL의 경로를 직접 삽입
  • 탐지 방법 : 레지스트리(AppInit_DLLs)값 체크, 스레드 함수 내부 인자 값 체크

Reflective DLL Injection

  • 인젝션 방법 : 인젝션하고자 하는 데이터를 메모리에 삽입 후 매핑
  • 탐지 방법 : 정상 동작에서 벗어나는 메모리 영역 권한 체크(메모리에 로드해야 하므로 메모리 영역에 rwx권한을 부여함)


2. Reflective DLL Injection 유형 분석

2.1 분석 환경

IndexTags
OSWindows 10(x64)
Debugging ToolIDA Prox64dbg
etcCFF ExplorerProcessExplorer

2.2 소스코드 및 실행 파일

다음 깃허브 주소에서 소스코드와 실행파일을 가져와 분석하도록 진행하였다.


3. 분석

3.1 정적 분석

1) DLL 데이터 호출 프로세스에 저장

[ inject.x64.exe - main() ]

  • 호출 프로세스 아이디 값을 저장("inject.x64.exe"의 pid)
  • "reflective_dll.x64.dll"의 파일 핸들러 값을 구하여 파일 사이즈를 구함
  • GetProcessHeap() : 호출 프로세스(inject.x64.exe)의 힙 영역에 대한 핸들 값을 구함
  • HeapAlloc() : "reflective_dll.x64.dll"의 파일 사이즈만큼 힙 메모리 블록 할당하여 해당 메모리 블록에 대한 포인터 반환
  • ReadFile() : v9는 "reflective_dll.x64.dll" 파일 핸들러 값이고 v13은 방금 힙 메모리 할당받은 영역의 포인터이다. 즉, 힙 메모리 영역에 "reflective_dll.x64.dll"파일의 데이터를 쓰는 기능을 한다.

정리 : "inject.x64.exe"프로세스에 힙 메모리를 할당받고 할당받은 주소에 "reflective_dll.x64.dll"의 데이터를 쓴다.

2) 윈도우 토큰권한(보호된 프로세스 권한 재설정)

  • 윈도우는 Vista버전부터 프로세스가 활동한 것에 있어 보안 속성 추가됨
  • Vista이전에 활성화된 프로세스가 후킹 기법으로 프로세스를 캡쳐하여 악의적인 행위를 미연에 방지하기 위함
  • Vista이상에서 프로세스를 OpenProcess함수로 호출하면 실패한다.

    이유) 해당 프로세스가 '최소 권한'으로 설정되어 권한이 없기 때문

  • 따라서 보호된 프로세스의 권한을 상승시키기 위해 아래 3가지 함수 사용
    • OpenProcessToken()
    • LookupPrivilegeValueA()
    • AdjustTokenPrivileges()
  • 토큰(Token) : 프로세스마다 갖고 있는 일종의 자격증
  • GetCurrentProcess() : 현재 프로세스 즉, "inject.x64.exe"프로세스의 pseudo 핸들을 구하여 OpenProcessToken의 인자로 들어간다.
  • OpenProcessToken() : "inject.x64.exe"프로세스의 토큰 핸들을 획득하고 획득한 토큰으로 권한 설정 및 조회, 활성화, 비활성화
  • LookupPrivilegeValueA() : "inject.x64.exe"프로세스의 특정 권한을 찾아 LUID형식으로 가져옴, 특정 권한 제어 준비 작업
  • AdjustTokenPrivileges() : "inject.x64.exe"프로세스의 권한 수정(상승)

2-1) OpenProcessToken()

→ 프로세스의 토큰 핸들을 획득하고 획득한 토큰으로 권한 설정하는 함수

  • ProcessHandle : 권한 상승하고자 하는 프로세스 핸들
  • DesiredAccess : 토큰에 접근하기 위한 접근 권한
    • TOKEN_ADJUST_PRIVILEGES : 액세스 토큰에서 권한을 활성화 또는 비활성화하는데 필요합니다.
    • TOKEN_QUERY : 액세스 토큰을 조회하는데 필요합니다.
  • TokenHandle : 함수가 반환될 때 새로 열린 액세스 토큰을 식별하는 포인터

2-2) LookupPrivilegeValueA()

→ 로컬로 지정된 privilege name을 나타내기 위해 시스템에서 사용되는 'LUID'를 검색하는 함수

→ 프로그램의 특정 권한을 찾아 LUID형식으로 가져오는 함수로 해당 함수를 사용해서 특정 권한을 제어할 준비를 함

→ LUID(Locally Unique IDentifier) : 특정 권한을 표현해주는 일련의 구조체

  • lpSystemName : 명시된 특권을 찾기 위한 특정 시스템의 이름 지정, NULL이면 자동으로 로컬 시스템에서 특권 이름을 찾음
  • lpName : 찾고자 하는 권한 이름을 나타내는 문자열
    • SE_DEBUG_NAME : 다른 계정이 소유한 프로세스의 메모리를 디버그하고 조정하는데 필요, 사용자 권한에서 프로그램을 디버그함
  • lpLuid : lpSystemName의 매개변수로 지정된 특권의 LUID를 수신하는 변수에 대한 포인터

2-3) AdjustTokenPrivileges()

→ 실질적인 권한을 설정하는 함수

  • TokenHandle : 액세스 토큰에 대한 핸들
  • DisableAllPrivileges : 함수가 모든 토큰의 권한을 사용 불가능하게 할 것인지 여부를 지정, 만약 True일 경우 모든 권한 비활성화하고 False일 경우 권한을 수정한다. (권한 상승을 위해서 False로 설정)
  • NewState : 새로 설정할 권한을 나타내며, TOKEN_PRIVILEGES구조체의 포인터를 가리킴
  • BufferLength : PreviousState 매개 변수가 가리키는 버퍼의 크기 지정
  • PreviousState : 이전의 권한을 저장, TOKEN_PRIVILEGESPrivilegeCount 멤버가 0이면 이 함수에 의해 변경된 권한이없는 것, 해당 매개 변수는 NULL일 수 있음, 만약 이전 권한을 저장하려면 똑같은 TOKEN_PRIVILEGES구조체 포인터를 생성한 뒤 인자로 넣어주면 됨
  • ReturnLength : PreviousState 매개 변수가 가리키는 버퍼의 필요한 크기 (바이트)를 받는 변수에 대한 포인터, PreviousState가 NULL인 경우 이 매개 변수는 NULL로 삽입

+) TOKEN_PRIVILEGES(토큰 구조체)

  • PrivilegeCount : Privilege 배열의 항목 수로 설정
  • Privileges : 권한의 속성을 설정
    • SE_PRIVILEGE_ENABLED : 권한이 활성화됨
    • SE_PRIVILEGE_ENABLED_BY_DEFAULT : 해당 권한은 기본적으로 활성화
    • SE_PRIVILEGE_REMOVED : 권한을 제거하는데 사용
    • SE_PRIVILEGE_USED_FOR_ACCESS : Object, Service에 대한 액세스를 얻는데 사용

3) LoadRemoteLibraryR() 호출

  • OpenProcess() : "inject.x64.exe"프로세스를 Open하여 핸들을 구함
  • LoadRemoteLibraryR() : 밑에서 분석하면서 다시 확인할 것
  • WaitForSingleObject() : hThread가 시그널을 발생할 때까지 대기

3-1) WaitForSingleObject()

→ 지정된 Object가 비시그널 상태이면 시그널 상태가 될 때까지 설정한 타임아웃 시간만큼 WaitForSingleObject에서 대기한다. 시그널을 받으면 다음 작업 수행

  • hHandle : Event Object Handle
  • dwMilliseconds : time-out interval (단위 : millisec)

4) CreateRemoteThread()

[ inject.x64.exe - LoadRemoteLibraryR() ]

param)

  • hProcess : "inject.x64.exe"프로세스 핸들러
  • lpBuffer : 힙 영역 주소("reflective_dll.x64.dll" 데이터 저장되어 있음)
  • nSize : "reflective_dll.x64.dll"파일 사이즈

  • GetReflectiveLoaderOffset() : lpBuffer를 인자로 주고 offset에 반환 값을 받는다.
  • VirtualAllocEx() : "inject.x64.exe"프로세스 공간에 0x3000크기만큼 자동으로 할당받아 baseaddress에 저장(권한 : rwx활성화)
  • WriteProcessMemory() : baseaddress에 buffer에 있는 값(reflective_dll.x64.dll)을 bufferSize만큼 쓴다.
  • CreateRemoteThread() : "inject.x64.exe"프로세스를 통해 &v12[v9]에 있는 함수를 호출한다.(이 때, 파라미터는 없음)

4-1) GetReflectiveLoaderOffset()

  • "reflective_dll.x64.dll"의 Export Directory에서 3개의 특수한 테이블 사용
    • AddressOfFunctions, AddressOfNames, AddressOfNameOrdinals
  • 위 3개의 테이블을 통해 ReflectiveLoader함수의 offset을 반환한다.

LoadRemoteLibraryR()에서 CreateRemoteThread함수의 파라미터로 v12[v9]라는 함수를 실행시키는데 이 때, v12에는 새로 할당된 메모리 영역에 "reflective_dll.x64.dll"의 시작 주소가 들어있고 v9에는 ReflectiveLoader()의 offset이 존재한다.

따라서, CreateRemoteThread함수에서 호출하고자 하는 함수는 "reflective_dll.x64.dll"의 ReflectiveLoader()임을 추정해볼 수 있다.

5) ReflectiveLoader() - 사실 이 부분이 제일 핵심임

처음에 PEB구조체 포인터를 가져와서 InMemoryOrderModuleList를 통해서 kernel32.dll을 찾고 kernel32.dll의 AddressOfFunctions, AddressOfNames, AddressOfNameOrdinals테이블을 통해서 각 필요한 함수의 주소 값을 찾는 과정은 생략(자세한 과정은 다음 링크에서 확인)

Universal Shell code(x86)원리 및 실습
Example) Python 코드를 이용하여 WinExec함수의 주소 값 확인(환경 : Windows7) 상위 2bytes의 주소 값이 바뀌는 것을 확인할 수 있다. OS는 시스템 자원들(메모리, 디바이스, 프로세스, 파일 등)을 관리하는 역할을 한다. 애플리케이션이 이러한 자원들을 사용하기 위해서는 커널에게 자원 사용을 요청해야 하며 API란? API(Application Programming Interface)란 OS가 애플리케이션을 위해 제공하는 함수의 집합으로 애플리케이션과 디바이스를 연결해주는 역할을 한다.
https://rninche01.tistory.com/entry/Universal-Shell-codex86%EC%9B%90%EB%A6%AC-%EB%B0%8F-%EC%8B%A4%EC%8A%B5?category=838537

5-1) 필요한 함수들의 주소 값 찾기

  • kernel32.dll과 ntdll에서 LoadLibrary, GetProcAddress, VirtualAlloc, NtFlushInstructionCache함수의 주소 값을 구해옴

5-2) 메모리 할당 및 헤더 데이터 입력

  • VirtualAlloc으로 메모리 공간할당(rwx)하고 할당된 공간에 SizeOfHeader크기만큼 1byte씩 reflective.dll 헤더 데이터를 씀

5-3) 섹션 데이터 입력

  • 각 섹션별로 또 씀

5-4) IAT 재작성

  • LoadLibrary()를 사용하여 reflective.dll에 import된 user32.dl, kernel32.dll을 로드하고 GetProcAddress()를 호출하여 함수들의 주소 값을 얻는다.
  • 그리고 IAT를 재작성한다.(아까 할당된 공간에서)

5-5) Relocation

  • Relocation Table을 사용하여 주소를 재배치
  • 재배치하는 이유는 이미지가 충돌없이 메모리에 로드될 수 있도록 하는 것
  • 추가로 아래 참고 자료에서 찾아보니 재배치를 하는 이유는 기존에 PE파일은 프로세스 가상 메모리에 로딩될 때 PE헤더에 지정된 ImageBase에 맞추어서 로딩되는데 만약에 DLL이 원래 지정된 ImageBase에 로딩되어 있다면 동일한 주소에 로딩할 수 없다. 따라서 다른 곳에 로딩을 하게 되는데 이 행위가 PE 재배치이다.

5-6) DllEntryPoint 호출

  • injection된 dll의 entry point수정후 FlushInstructionCache()호출하여 플러시
    • 여기서 사용된 함수는 메모리에서 코드를 생성하거나 수정하는 경우 위 함수를 호출해야 함
  • 마지막으로 EntryPoint()를 호출한다. 즉, DllEntryPoint()로직으로 이동함

5-7) Dll EntryPoint 진입

  • DllEntryPoint()진입하고 DllMainCRTStartup호출

  • Dllmain호출

  • 여기서 사용된 reason값은 DLL_PROCESS_ATTACH(0x1)인데 이건 dll injection에서도 보았듯이 최초로 dll이 매핑되거나 loadlibrary될 경우 세팅되는 reason값
  • 근데 v3 = fdwReason(0x1) - 1, v3는 0이되어 참이 아니므로 else문 실행
  • 즉, 최종적으로 MessageBoxA()를 호출하여 "Reflective Dll Injection"문자열을 띄울 것임

3.2 동적 분석

참고로 타겟 프로세스를 "CFF Explorer"로 정해두고 진행하였다.

1) DLL 데이터 호출 프로세스에 저장

1-1) ReadFile() 호출 前

  • "reflective_dll.x64.dll" 파일 핸들러와 이전에 힙 영역에 할당받았던 공간을 파라미터로 줌

1-2) ReadFile() 호출 後

  • 할당받은 힙 영역에 "reflective_dll.x64.dll"파일의 데이터가 입력 됨

2) LoadRemoteLibraryR()

2-1) OpenProcess() 호출 前

  • 파라미터로 타겟 프로세스인 "CFF Explorer.exe"pid 값을 주었다.

2-2) OpenProcess() 호출 後

  • "CFF Explorer.exe"파일을 Open하고 Handler값 0xa0을 얻어온다.

2-3) VirtualAllocEx() 호출 後

  • "CFF Explorer.exe"에 메모리 공간이 할당되는 것을 확인
  • 참고로 밑에 사진은 Attach한 거임

2-4) WirteProcessMemory() 호출 後

  • 할당받은 공간에 "reflective_dll.x64.dll"의 데이터가 써진 것을 확인

CreateRemoteThread()호출 이후 "CFF explorer.exe"로 이동

Thread Debugging은 다음 링크 참고해서 진행

Thread Debugging(feat. x64dbg)
악코분을 하다보면 가끔 CreatRemoteThread()를 호출하여 타겟 프로세스에 스레드로 동작시키게 한다. 그래서 스레드가 동작하는 과정을 x64dbg를 통해 디버깅하는 방법에 대해서 간략하게 소개하려고 한다. 먼저, CreatRemoteThead()와 같이 타겟 프로세스에서 Thread를 동작하게 해주는 프로그램을 동작시킨다. 그리고 x64dbg를 하나 켜주고 [Alt + a]를 눌러서 Attach할 항목을 선택한다. 여기서 나는 타겟 프로세스가 CFF Explorer(pid : 37800)이므로 이거를 선택한다.
https://rninche01.tistory.com/entry/Thread-Debuggingfeat-x64dbg?category=838532

3) ReflectiveLoader()

3-1) 필요한 함수들 주소 값 찾기

  • LoadLibraryA(), VirtualAlloc(), GetProcAddress(), FlushInstructionCache()

3-2) 메모리 할당 및 헤더 데이터 입력

  • "reflective_dll.x64.dll"의 이미지 크기만큼 메모리 할당

  • 0x8b0000에 메모리 할당 완료

  • 할당받은 공간에 헤더 데이터 1byte씩 입력

3-3) 섹션 데이터 입력

  • Section수와 OptionalHeader의 크기를 구한다.

  • OptionalHeader에서 BaseOfCode를 구해 0x8b1000부터 각 섹션 데이터를 입력

3-4) IAT 재작성

  • Import Directory RVA값을 구한다.

  • Name RVA를 구한다. → DLL에 포함된 함수의 이름 문자열이 들어있는 주소를 구하기 위해서
  • 이후에 GetProcAddress()의 파라미터로 들어감

  • user32.dll을 파라미터로 LoadLibraryA()를 호출한다.

  • Import Directory의 OFTs와 FTs(IAT)값을 가져온다. → user32.dll, kernel32.dll 순서로

  • GetProcAddress()를 통해 user32.dll의 MessageBoxA()의 주소를 가져온다.

  • GetProcAddress()로 가져온 함수의 주소 값을 reflective 의 IAT에 위치시킨다.

3-5) Relocation

  • Relocation Directory RVA값을 구한다.

  • Relocation Directory의 Size Of Block값을 구한다.

  • Relocation Directory의 각 Item에 r9(AllocAddress - ImageBase(reflective.dll))값을 쓴다.

3-6) DllEntryPoint 호출

  • "reflective_dll.x64.dll"의 EntryPoint값을 가져온다. 이후에 할당받은 시작주소 값과 더해서 EntryPoint주소를 구한다.

  • NtFlushInstructionCache()를 호출한다.

  • "reflective_dll.x64.dll"의 EntryPoint주소로 이동한다.

3-7) Dll EntryPoint 진입

  • DllMain()로 진입하여 MessageBoxA()를 호출한다.


4. 정리

1) 호출 프로세스(inject.x64.exe)에 힙 영역을 할당하여 ReflectiveLoader()가 포함된 reflective_dll.x64.dll 데이터를 작성한다.

2) 호출 프로세스(inejct.x64.exe)의 윈도우 토큰 권한(보호된 프로세스 권한)을 재설정한다.

3) 타겟 프로세스의 핸들을 구한다.

4) 타겟 프로세스에 메모리 할당을 하고 reflective_dll.x64.dll 데이터를 입력한다.

5) CreateRemoteThread()를 통해 타겟 프로세스에서 reflective_dll.x64.dll ReflectiveLoader()를 호출한다.

(이전에 ReflectiveLoader()의 offset을 구하고 VirtualAllocEx()를 통해 BaseAddress를 구했으니 위치를 알 수 있음)

6) ReflectiveLoader() 실행

6-1) ReflectiveLoader()를 실행하기 위해 필요한 함수들인 LoadLibrary(), GetProcAddress(), VirtualAlloc(), NtFlushInstructionCache()의 주소를 구한다.

6-2) 메모리 할당을 받고 reflective_dll.x64.dll의 헤더 데이터와 각 섹션 데이터를 쓴다.

6-3) LoadLibrary()를 통해 reflective_dll.x64.dll에 import된 user32.dll, kernel32.dll을 로드하고 각 dll에서 사용된 함수들의 주소를 구하기 위해 GetProcAddress()를 호출한다. 그리고 주소 값을 구하여 IAT를 재작성한다.

6-4) Relocation Table을 구하여 재배치한다. 재배치하는 이유는 이미지가 충돌없이 메모리에 로드될 수 있도록 하는 작업이다.

6-5) 마지막으로 DllEntryPoint를 호출한다.

6-6) DllMain에서 reason값이 DLL_PROCESS_ATTACH(0x1)인 경우 MessageBoxA()를 호출한다.

5. 몰랐던 사실

가끔 CreateFile()를 보면 dwShareMode멤버 변수에 널 값을 넣는 경우를 볼 수 있다.

dwShareMode변수는 파일의 공유 모드를 지정하기 위한 것으로 NULL값인 경우 다른 프로세스가 읽기, 쓰기, 액세스 요청하는 경우 파일이나 장치를 열지 못하도록 한다. 이를 통해 악성 DLL파일 분석을 위해 임의의 로더에 Injection시키는 경우 해당 인자에 의해 생성되는 파일 핸들을 정상적으로 가지고 올 수 없고 이로 인해 더이상의 행위가 진행되지 않는다고 한다.

즉, dwShareMode변수에 0x0으로 설정되면 디버깅 상태에서 정상적으로 파일 생성을 할 수 없게 되어 안티 디버깅 효과를 볼 수 있다.

추가로 몰랐던 사실)

  • IAT, Relocation
  • Thread Debugging(x64dbg)


6. 참고 자료

6.1 Reflective DLL Injection

6.2 윈도우 토큰 권한 설정을 통한 권한 상승

6.3 RVA to RAW

6.4 IAT

6.5 Relocation

'Security > 07 Malware Technique' 카테고리의 다른 글

파일리스(Fileless)기법 설명  (0) 2021.01.16
UAC bypass(레지스트리 어뷰징) 분석  (0) 2021.01.09
Dropper 3-4(Thread Injection)  (0) 2020.12.30
Dropper 3-3(PE Injection)  (0) 2020.12.29
Dropper 3-2(Process Hollowing)  (3) 2020.12.29
Comments