Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
MemFiles — Beacon이 생성한 파일을 디스크 대신 메모리에 기록하는 CobaltStrike 툴킷 | Kitploit
도구/GitHubGitHub/octoberfest7/memfiles
Data ExfiltrationPost-ExploitationCommand and ControlRed Teaming
GitHuboctoberfest7/memfiles

MemFiles

Beacon이 생성한 파일을 디스크 대신 메모리에 기록하는 CobaltStrike 툴킷

저장소 보기
477622년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

MemFiles

면책 고지:

이 프로젝트는 복잡하며, 작동 방식을 이해하고 충분히 테스트하지 못하면 Beacon이 크래시되어 액세스를 잃을 수 있습니다!

"기술적 세부 사항, 설계 고려 사항 및 해설" 섹션까지의 모든 문서를 읽을 것을 강력히 권장합니다!

소개

MemFiles는 CobaltStrike용 툴킷으로, 운영자가 Beacon 프로세스에서 생성된 파일을 대상 시스템의 디스크에 쓰는 대신 메모리에 쓸 수 있게 해줍니다. Windows 7, 10, 11에서 성공적으로 테스트되었으며, 해당 서버 버전도 문제없이 작동할 것입니다. MemFiles는 x64 Beacon에서만 사용할 수 있습니다.

이는 NTDLL.dll 내의 여러 NtAPI를 후킹하고 해당 API에 대한 호출을 Beacon 프로세스 메모리 공간에 주입된 함수로 리디렉션하여 수행합니다.

MemFiles는 Beacon 프로세스에 깨끗하고 후킹되지 않은 NTDLL 사본이 있다고 가정합니다. EDR 후크가 여전히 적용된 Beacon 프로세스에서는 MemFiles의 작동 가능성에 대해 어떠한 보장도 하지 않습니다. MemFiles를 사용하기 전에 NTDLL을 복구/새로 고치십시오!

MemFiles 툴킷 내에 "특수한" 존재하지 않는 디렉터리가 정의됩니다. 이 특수 디렉터리에 기록되는 모든 파일은 MemFiles에 의해 캡처되어 메모리에 기록되며, 이후 Teamserver로 다운로드할 수 있습니다.

MemFiles는 Beacon 프로세스 내에서 실행되고 출력을 특정 디렉터리로 보내도록 지시할 수 있는 대부분(전부는 아님)의 도구와 호환됩니다. 작동에 관리자 권한이 필요하지 않습니다.

여기에는 다음이 포함됩니다:
-BOF
-inline-executeAssembly와 같은 도구를 사용하여 인라인으로 실행되는 .NET 어셈블리
-Inline-Execute-PE와 같은 도구를 사용하여 인라인으로 실행되는 PE

이 모든 것은 관련 NtAPI가 후킹된 Beacon 프로세스 내부에서 실행되므로 호환됩니다.

MemFiles는 다음 항목과는 작동하지 않습니다:
-execute-assembly
-shell
-run

이들은 모두 NtAPI가 후킹되지 않은 별도의 프로세스를 생성하기 때문에 호환되지 않습니다.

MemFiles는 Beacon 프로세스 내에서 실행될 때 Rubeus, SharpHound, Procdump 및 Powershell과 같은 도구로 성공적으로 테스트되었습니다.

설정

저장소를 클론하고 선택적으로 /PIC/Source/NtCreateFile.c 및 /PIC/Source/NtOpenFile.c의 56행에 정의된 hookdir 변수를 변경합니다. 이 변수는 MemFiles가 생성되는 파일을 가로채야 함을 알리는 "특수" 디렉터리입니다. hookdir 변수는 기본적으로 "redteam"으로 설정되어 있습니다. 이 변수가 대상 시스템의 실제 디렉터리가 아닌지, 그리고 두 파일에서 동일한지 확인하십시오!

image

'make all'을 실행하여 필요한 BOF와 PIC 함수를 모두 컴파일하십시오.

MemFiles.cna를 CobaltStrike Client에 로드하십시오. CobaltStrike가 실행되는 디렉터리가 사용자에 의해 쓰기 가능한지 확인하십시오. MemFiles는 작동에 필요한 데이터의 가용성을 보장하기 위해 해당 위치에 텍스트 파일(memfiles.txt)을 생성합니다.

MemFiles는 Teamserver에 콜백하는 각각의 새 Beacon에 설치되도록 구성할 수 있습니다. 이는 MemFiles->Config 메뉴 항목을 사용하여 수행됩니다. 기본적으로 MemFiles는 새 Beacon에 자동 설치되지 않습니다. 이는 전역 설정입니다. 두 Client가 Teamserver에 연결되어 있고 둘 다 MemFiles.cna를 로드한 경우, Client A가 "비콘 초기 설치" 설정을 전환하면 Client B에도 변경 사항이 적용됩니다!

image

명령

MemFiles는 BOF를 실행하는 4개의 대상 지향 명령과 프로젝트 데이터 구조를 조작하는 1개의 내부 명령으로 구성됩니다.

대상 지향:

  1. meminit
  2. memlist
  3. memfetch
  4. memclean

내부 데이터 구조:

  1. memtable

meminit

meminit은 Beacon 프로세스에 MemFiles를 설치하는 역할을 담당합니다.

MemFiles가 후킹하는 NtAPI 목록은 다음과 같습니다:

  1. NtCreateFile
  2. NtWriteFile
  3. NtClose
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. NtOpenFile
  8. NtReadFile
  9. NtFlushBuffersFile

meminit은 다음 주요 작업을 수행합니다:

  1. 후킹된 각 NtAPI에 대한 위치 독립적 대체 함수를 Beacon으로 전송합니다.
  2. Beacon 메모리에 MemFiles의 수명 주기 동안 필요한 다양한 값을 보관하는 구조체를 생성합니다.
  3. 이 구조체의 주소를 각각의 PIC 대체 함수에 패치합니다.
  4. 메모리를 할당하고 각 PIC 대체 함수를 Beacon 프로세스 메모리에 주입합니다.
  5. 후킹된 각 NtAPI에 대한 트램펄린을 생성합니다.
  6. 나열된 각 NtAPI를 일부 또는 전체 바이트를 덮어써서 후킹하고 실행을 PIC 대체 함수로 리디렉션합니다.

memlist

memlist는 특정 Beacon에 대해 MemFiles가 현재 메모리에 저장한 모든 파일을 표시하는 데 사용됩니다.
image
여러 필드가 표시되며, 사용자에게 가장 관련성이 높고 관심 있는 필드는 파일 이름과 저장된 데이터의 길이입니다.

memfetch

memfetch는 특정 Beacon에 대해 MemFiles가 메모리에 저장한 파일을 실제로 검색하는 데 사용됩니다.

기본적으로 memfetch는 "핸들"이 닫힌 MemFiles에 의해 저장된 모든 파일을 검색합니다. 이 설계 선택은 프로그램/애플리케이션이 아직 쓰기를 완료하지 않은 파일을 다운로드하려고 할 때 발생할 수 있는 문제를 피하기 위한 것입니다.

즉, 프로그램/애플리케이션이 파일에 대해 연 핸들을 닫지 못하면 해당 파일은 memfetch로 다운로드되지 않습니다.
이는 memfetch와 함께 "force" 인수를 사용하여 완화할 수 있습니다. 즉, 'memfetch force'는 핸들의 상태와 관계없이 메모리에서 모든 파일을 검색합니다.

memfetch가 메모리에서 검색한 파일은 다운로드로 Teamserver에 다시 전송되며, CobaltStrike의 Downloads 탭을 통해 Teamserver에서 Client로 동기화할 수 있습니다.

파일이 Teamserver에 의해 다운로드되면 Beacon 프로세스의 메모리에서 제거되고 memlist로 표시되는 해당 항목도 제거됩니다.

memclean

memclean은 Beacon 프로세스에서 MemFiles를 정리하고 제거하는 역할을 담당합니다.

MemFiles의 일반적인 사용 사례는 이를 설치하고 Beacon의 수명 기간 동안 설치된 상태로 두는 것입니다. 그러나 MemFiles를 도구와 함께 사용하여 파일 출력을 캡처하고 검색한 다음, 해당 아티팩트가 메모리에 남지 않도록 MemFiles를 제거하려는 경우 memclean을 사용하여 meminit이 실행되기 전의 원래 상태로 Beacon 프로세스를 되돌릴 수 있습니다.

여기에는 다음이 포함됩니다:

  1. 후킹된 각 NtAPI의 후킹 해제
  2. 생성된 각 트램펄린을 0으로 채우고 해제
  3. 주입된 각 PIC 대체 함수를 0으로 채우고 해제
  4. MemFiles 구조체를 0으로 채우고 해제

memclean이 이러한 작업을 수행하기 전에 메모리에 저장된 모든 파일을 강제로 다운로드한다는 점에 유의하십시오. 단일 도구와 함께 MemFiles를 사용한 다음 제거하려는 경우 memfetch를 건너뛰고 memclean만 사용하여 파일을 검색하고 동시에 Beacon 프로세스에서 MemFiles를 제거할 수 있습니다.

memtable

memtable은 MemFiles가 현재 설치된 Beacon에 대한 정보를 표시하고 추적하는 데 사용됩니다. 또한 전역 구성 정보를 표시합니다.

각 CobaltStrike Client는 고유한 memtable을 가지고 있습니다. MemFiles는 모든 연결된 CobaltStrike Client 간에 데이터 동기화를 보장하기 위해 많은 노력을 기울이며, 모든 운영자가 모든 Beacon에서 MemFiles를 사용할 수 있도록 합니다. 자세한 내용은 "설계 고려 사항 및 해설"을 참조하십시오.

image

사용법

meminit 명령을 사용하여 Beacon에서 MemFiles를 초기화합니다. MemFiles->Config 메뉴에서 옵션을 전환하여 자동으로 수행되도록 구성할 수 있습니다.

image

MemFiles가 초기화되면 이제 선호하는 도구를 사용하여 파일을 메모리에 쓸 수 있습니다! 방법은 특정 도구에 따라 다릅니다. 일부는 여러 파일을 출력할 디렉터리를 지정할 수 있게 하고, 다른 일부는 도구가 생성하는 단일 파일의 절대 경로를 지정할 수 있게 합니다. 몇 가지 예는 아래에서 볼 수 있습니다:

SharpHound:

여기서는 SharpHound가 생성된 모든 파일을 c:\redteam\ 디렉터리(우리의 특수 MemFiles 디렉터리)로 출력하고 파일을 압축하지 않도록 지정합니다. MemFiles는 프로그램이 메모리에서 파일을 읽는 것을 지원하지 않고 쓰기만 지원하므로 SharpHound의 압축 기능은 작동하지 않습니다.

image

Rubeus:

"dump" 명령이 Rubeus와 함께 사용되며, 모든 콘솔 출력을 (특수 디렉터리에 있는) 파일로 보내도록 지시합니다.

image

Powershell:

이 예에서는 Inline-Execute-PE를 사용하여 powershell.exe를 Beacon 프로세스에 로드하고 'Get-ADUser'를 실행하여 도메인 사용자 목록을 검색합니다. 파이프와 'out-file'을 사용하면 데이터를 메모리에 쓴 다음 검색할 수 있습니다.

image

파일을 검색하려면 memfetch를 실행하십시오:

image

MemFiles 사용이 끝났거나 Beacon 프로세스에 설치된 상태로 두고 싶지 않으면 memclean을 실행하십시오:

image

위 예에서는 아직 다운로드되지 않은 파일이 하나 있었습니다. memclean은 MemFiles를 제거하기 전에 이 파일을 다운로드하고 메모리에서 제거합니다.

memtable을 사용하여 MemFiles의 상태와 구성을 확인하십시오. 장기간 작업하는 동안 죽었거나 오래된 Beacons의 항목을 memtable에서 지워 복잡함을 피하십시오.

image

기능 및 제한 사항

소개에서 강조했듯이 MemFiles는 작동하려면 Beacon 프로세스에 깨끗한 NTDLL 사본이 필요합니다. 이는 NtFunction의 원래 바이트를 읽고 특정 바이트를 트램펄린으로 복사하기 때문에 필요한데, 트램펄린은 나중에 MemFiles가 간섭해서는 안 되는 NtFunction에 대한 정상적인 호출을 완료하는 데 사용됩니다. 이 주제는 "기술적 세부 사항, 설계 고려 사항 및 해설" 섹션에서 더 자세히 다룹니다.

MemFiles는 각 파일에 대해 초기 1048576바이트를 할당합니다. 데이터가 메모리에 기록되면 더 큰 파일을 보관하기 위해 필요에 따라 이 할당을 확장할 수 있고 확장합니다.

MemFiles 구조체에 저장된 파일 이름은 대체 NtCreateFile 함수에 전달되는 인수에서 구문 분석됩니다. MemFiles는 파일 경로 인수에서 "특수" 디렉터리를 찾고, 그 끝으로 이동한 다음, "특수" 디렉터리와 파일 이름을 구분하는 '\' 문자를 처리하기 위해 포인터를 1만큼 증가시키는 다소 단순한 방식으로 이를 수행합니다. 예를 들어 'C:\users\tom\redteam\myfile.txt' 경로에서 MemFiles는 'redteam'을 찾고 백슬래시 문자를 처리한 다음 'myfile.txt'를 파일 이름으로 선택합니다.

MemFiles는 파일 경로의 선행 디렉터리에 대해 신경 쓰지 않습니다. MemFiles의 관점에서 'C:\redteam\myfile.txt'와 'c:\users\tom\appdata\local\redteam\myfile.txt'는 동일하게 유효한 경로입니다.

MemFiles가 파일 경로에서 파일 이름을 구문 분석하는 방식을 고려할 때 MemFiles는 하위 디렉터리 생성을 지원하지 않는다는 점에 유의해야 합니다. 즉, MemFiles는 예를 들어 c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt 등을 생성하려는 도구와 제대로 작동하지 않습니다.

MemFiles가 후킹하는 NtAPI는 다양한 프로그램이 I/O 작업에 사용하는 것으로 확인되었습니다. 앞서 언급했듯이 MemFiles/이 후킹된 NtAPI 세트로 성공적으로 테스트된 도구/기능은 SharpHound, Rubeus, Powershell, Procdump, BOF, 파일 쓰기 작업을 수행하는 일반 C 프로그램, 그리고 운영자가 원격 파일 위치를 지정할 수 있게 해주는 CobaltStrike의 bupload_raw 명령입니다. MemFiles와 즉시 호환되는 다른 도구도 분명히 존재합니다. 반면 호환되지 않는 도구도 있을 것입니다.

언뜻 보기에 Windows 파일 생성 프로세스는 NtCreateFile->NtWriteFile->NtClose로 간단해 보입니다. 이 프로젝트를 파고들자 곧 다른 여러 API가 관련되어 있다는 것을 발견했고, 더 흥미롭게도 관련된 다른 API는 프로그램마다 다릅니다. 일부 프로그램은 I/O 프로세스의 일부로 Win32 API SetFilePointerEx를 호출하며, 이는 다시 NtAPI NtSetInformationFile을 호출합니다. SharpHound를 포함한 .NET 프로그램과 같은 다른 프로그램은 결국 NtFlushBuffersFile을 호출합니다.

파일을 생성하고 쓰기 위한 단일 공통 API 호출 체인이 없다는 점은 MemFiles가 사용되는 도구에 따라 비호환성이 발생할 여지를 만듭니다. 프로그램이 MemFiles가 후킹하지 않은 다른 NtAPI를 호출하고, 대체 NtCreateFile 함수에서 MemFiles가 생성한 가짜 핸들을 전달하여 실행을 중단시키는 "Invalid Handle" 오류가 발생할 수 있습니다. 다른 프로그램은 MemFiles가 적절하게 스푸핑/대체하지 못하는 더 복잡한 작업을 수행합니다. 이미 확인된 비호환 사례 중 하나는 ADExplorer.exe입니다.

ADExplorer.exe는 액티브 디렉터리 열거에 사용되는 서명된 Microsoft 바이너리입니다. 런타임 동안 ADExplorer는 지정된 출력 파일에 데이터를 쓴 다음, 실행이 끝날 때 최종 출력을 파일에 쓰기 전에 나중에 다시 참조합니다. 출력 파일을 일종의 캐시로 사용하기 때문에 이미 파일에 쓴 데이터를 읽을 수 있어야 하며, 이를 단순하고 예측 가능한 방식으로 수행할 가능성은 매우 낮습니다.

MemFiles는 현재 프로그램이나 애플리케이션이 메모리 내 파일을 읽는 것을 지원하지 않지만, 사용자 정의 NtReadFile 함수를 더 보완하고 MemFiles 구조체에 몇 가지 추가 변수/데이터 추적을 추가하면 가능할 것입니다.

ADExplorer는 생성하는 파일 크기에서 또 다른 문제를 제기합니다. 대규모 엔터프라이즈 환경에서는 출력 파일이 1GB를 초과할 수 있습니다. MemFiles는 프로그래밍 방식으로 이를 처리할 수 있어야 하지만, 확실히 그러한 사용 사례를 위한 것은 아닙니다.

호환되지 않는 도구

커뮤니티에서는 MemFiles가 제대로 작동하지 않는 도구를 분명히 발견할 것입니다. 호환되지 않는 프로그램/도구와 이를 실행한 상황(예: BOF인지, inline-executeAssembly를 통한 것인지, Inline-Execute-PE를 통한 것인지 등)을 자세히 설명하는 이슈를 열어 주시기 바랍니다. MemFiles를 확장하여 작동시킬 수 있는지 확인해 보겠습니다.

IOC 및 AV/EDR

MemFiles와 관련된 IOC는 다음과 같지만 이에 국한되지는 않습니다:

VirtualAlloc을 사용하여 메모리 할당
WriteProcessMemory를 사용하여 데이터 쓰기
할당된 메모리의 보호를 RW와 RX 사이에서 변경
NTDLL.dll 내의 메모리 덮어쓰기

AV/EDR

MemFiles는 제대로 된 EDR을 대상으로 개발되거나 테스트되지 않았습니다. Microsoft Defender만 사용할 수 있었습니다. 그렇긴 하지만, Beacon이 파일을 생성하기 위해 실행하는 어떤 도구/프로그램이 MemFiles가 해당 파일을 메모리에 캡처하거나 저장하는 것보다 더 탐지될 가능성이 높다고 말하고 싶습니다. NTDLL에서 메모리를 덮어쓰고 NtAPI를 후킹하는 것은 일부 제품이 문제 삼을 수 있는 부분이라고 생각하지만, 이를 뒷받침할 증거는 없습니다. MemFiles가 캡처해야 하거나 캡처해야 하는 파일과 관련되지 않은 후킹된 NtFunction에 대한 호출의 경우에도 시스템 콜은 여전히 NTDLL.dll 주소 공간 내에서 이루어지는데, 보안 제품은 이 영역 밖에서 이루어지는 시스템 콜을 탐지하고 경고하기 때문입니다.

MemFiles가 메모리에 저장하는 파일은 인코딩되거나 암호화되지 않는다는 점에 유의해야 합니다. AV/EDR이 메모리 내 생성 파일에 대해 경고하는 실제 사용 사례/사례가 확인되면 이 기능이 추가될 수 있습니다.

기술적 세부 사항, 설계 고려 사항 및 해설

몇 달 전 KFiveFour의 Tradecraftcon에서 열린 컨퍼런스 발표에서 인메모리 파일 시스템 개념을 처음 접했습니다. 발표자(@DexterGerig)가 클라이언트-서버 모델을 사용하여 인메모리 파일 시스템을 만드는 POC를 시연했습니다. 제가 구상한 그러한 프로젝트의 기능 중 절반은 지난 주요 릴리스인 Inline-Execute-PE로 충족되었습니다. 나머지 절반, 즉 도구가 생성한 파일을 디스크가 아닌 메모리에 캡처하여 저장할 수 있는 아이디어는 해당 프로젝트에서 충족되지 않았으며 분명한 이유로 매우 바람직한 기능으로 남아 있었습니다.

MemFiles는 저에게 엄청나게 어려운 작업이었습니다. 이 프로젝트 이전에는 디버거를 거의 사용해 본 적이 없었고, 어셈블리에 대한 이해도 없었으며, API 후킹에 대해서도 이해하지 못했기 때문입니다. 이 프로젝트를 진행하면서 10~20시간 동안 지속된 여러 장애물을 만났지만, 인내심 덕분에 다행히 극복할 수 있었습니다. 아마 가장 효율적인 방법은 아니었겠지만, 디버거에 대한 많은 친숙함을 얻었고 어셈블리, 레지스터, 스택, 호출 규약에 관한 컴퓨터의 내부 작동 방식에 대한 더 깊은 이해를 얻었습니다.

다음은 MemFiles에 적용된 몇 가지 중요한 기술적 세부 사항과 설계 고려 사항에 대한 심층 분석입니다.

Windows에서의 파일 및 MemFiles 작동 방식

Windows에서 파일 생성은 NtCreateFile로 시작됩니다. 원하는 파일의 경로가 주어지고, 그 결과 Windows는 해당 위치에 파일을 생성하고 이에 대한 핸들을 제공합니다. 반환된 핸들은 파일과 관련된 모든 후속 호출(예: NtWriteFile 및 NtClose)에 사용됩니다.

이 모든 API에 대한 호출을 가로채고 조작하려는 호출과 그대로 두려는 호출로 분리하는 방법을 고민하면서 NtCreateFile 호출에서 키워드를 찾는 방법을 떠올렸습니다. 이는 NtCreateFile 호출의 파일 경로 일부로 고유하고 존재하지 않는 디렉터리를 지정하여 달성되었습니다. 후크가 실행을 대체 NtCreateFile 함수로 리디렉션하면 NtCreateFile에 인수로 전달된 파일 경로에서 해당 고유 "키워드"의 존재 여부를 검사합니다. 키워드를 찾으면 MemFiles는 이 NtCreateFile 호출이 디스크가 아닌 메모리에 배치되어야 하는 파일과 관련되어 있음을 알게 됩니다. 이 경우 MemFiles는 여러 변수를 초기화하고 파일 사용을 위해 초기 1MB 메모리를 할당하지만, 가장 중요한 것은 MemFiles 구조체에 지정된 파일 이름과 가짜 핸들을 연결하고 이 가짜 핸들을 호출자에게 반환한다는 것입니다.

MemFiles가 후킹하는 다른 모든 NtAPI의 경우, 해당 대체 NtFunction은 인수로 전달된 핸들을 확인하고 MemFiles 구조체에 존재하는지 확인합니다. 핸들이 MemFiles 구조체에 존재하면(MemFiles가 생성한 가짜 핸들은 실제 핸들과 교차할 일이 없을 만큼 충분히 가짜입니다), MemFiles는 이 호출을 인메모리 파일에 관한 호출로 식별하고 그에 따라 작동합니다.### 후킹 이론 및 대체 함수 디스크에 저장될 파일을 메모리에 쓰기 위해, MemFiles는 프로그램이 파일을 생성하려 할 때 호출하는 특정 API 호출을 가로채야 합니다. API 후킹은 아주 오래전부터 존재해 왔으며, 많은 EDR 제품에서 핵심 기능으로 적극적으로 사용되고 있습니다. 특정 API에 대한 호출은 EDR 주소 공간으로 리디렉션되어, API 호출과 전달된 변수에 대한 분석이 이루어집니다. EDR이 해당 호출을 악성이라고 판단하면(예: 공격 도구나 킬 체인의 일부), 호출이 완료되지 않도록 차단하고 경고를 발생시킵니다. EDR이 해당 호출이 양성이라고 판단하면, 실행을 리디렉션된 지점으로 다시 패치하여 API 호출이 원래 의도대로 완료되도록 허용합니다. 간단한 비유를 들자면, 친구에게 편지를 보냈는데 친구가 받기 전에 제3자가 편지를 열어 읽고, 편지에 불법적인 내용이 있다고 판단하면 친구는 편지를 받지 못하고 경찰에 신고되는 것과 같습니다.

MemFiles는 경고(또는 이론상의 경찰 개입) 없이 동일한 이론을 따릅니다. API 후킹은 일반적으로 유저랜드에서 가능한 가장 낮은 수준, 즉 NTDLL.dll 내의 NtFunctions에서 구현됩니다. 후킹이 이루어지기 전의 NtCreateFile을 살펴보겠습니다.

image

모든 NtFunctions는 syscall 번호를 제외하면 동일합니다. 이 예에서 syscall 번호는 55입니다. syscall 번호는 NtFunctions마다 다르며, 이 번호는 Windows 버전 간에도 변경될 수 있다는 점에 유의해야 합니다. 이 OS(Windows 11)에서 NtCreateFile의 syscall 번호는 55이지만, Windows 10에서는 다를 수 있습니다(Windows 7에서는 확실히 다릅니다).

TEST 및 JNE 명령어에 주목할 필요가 있습니다. 이들은 NtFunction이 일반 syscall 명령어를 사용해야 하는지, 아니면 레거시 INT 2E 명령어를 사용해야 하는지를 결정하기 위해 존재합니다. klezvirus의 게시물 SysWhispers is dead, long live SysWhispers!에서 인용하겠습니다:

이제 흥미로운 부분입니다. 함수는 SharedUserData[0x308] (BYTE PTR DS:[7FFE0308])가 1로 설정되어 있는지 확인합니다. SharedUserData는 커널 모드 구조체 KUSER_SHARED_DATA를 가리키는 심볼입니다.

KUSER_SHARED_DATA 구조체는 사용자 모드 소프트웨어와 정보를 공유하기 위한 고정(또는 사전 정의된) 메모리 공간을 정의합니다. 물론 이는 사용자 영역 코드가 매번 사용자 모드와 커널 모드 실행 사이를 전환하는 오버헤드 없이 특정 전역 시스템 정보를 사용할 수 있도록 하기 위한 것입니다.

인덱스 0x308의 값은 syscall 명령어를 나타내며, 이는 Windows 1511부터 모든 Windows 버전에서 지원됩니다. 예상할 수 있듯이, Windows 1511 이전의 모든 버전에서는 syscall을 실행하는 표준 방법이 int 2Eh 인터럽트를 호출하는 것이었습니다.

...

Windows가 이제 1511보다 훨씬 높은 버전임에도 불구하고 이 int 2Eh가 여전히 존재하는 이유가 궁금하다면, 이 명령어가 여전히 사용되기 때문입니다. 실제로 HVCI(하이퍼바이저 보호 코드 무결성)가 활성화되면 SharedUserData[0x308]이 0으로 설정되고 syscall 명령어 대신 int 2Eh가 사용됩니다. 이는 주로 성능상의 이유로, Ring3에서 Ring0으로의 전환이 두 명령어 중 어느 것을 사용하느냐에 따라 다르게 작동하기 때문입니다.

이 주제에 대해 트위터에서 추가 설명을 요청했고, @yarden_shafir가 다음과 같이 답변했습니다:

image

요컨대, NtFunction의 모든 명령어는 어떤 시점에서는 필요할 수 있습니다(끝부분에 있는 다중 바이트 NOP는 도달할 수 없는 것으로 보이므로 예외일 수 있음). NtFunction의 명령어를 덮어쓸 경우, 최종 syscall(또는 상황에 따라 INT 2E)을 실행하기 전에 해당 명령어들을 저장하고 어느 시점에 실행하도록 해야 합니다.

syscall을 실행하기 전에 syscall 번호가 RAX로 이동된다는 점에 주목할 필요가 있습니다(스크린샷에서는 EAX로 표시됨). syscall 번호가 이동되기 전에 RAX가 스택에 푸시되는 것을 볼 수 없기 때문에, syscall 번호가 이동되기 전에 RAX에 있던 값은 syscall이 실행된 이후에는 중요하지 않거나 필요하지 않다고 (어쩌면 순진하게) 가정했습니다. 이는 syscall이 실행되기 전에 RAX에 syscall 번호가 들어 있는지만 확인하면 RAX 레지스터를 자유롭게 사용할 수 있다는 뜻이므로 좋은 소식입니다.

실행을 우리의 사용자 정의 코드/대체 NtFunction으로 리디렉션하기 위해 원본 NtAPI의 일부를 덮어쓰고, 대체 NtFunction의 주소를 RAX로 이동한 다음 JMP 명령어를 사용하여 해당 코드로 이동합니다.

image

MOV 및 JMP 명령어에는 12바이트가 필요합니다. NtAPI의 첫 12바이트를 덮어써서 다른 명령어를 손상시키기 때문에, NtAPI의 적절한 간격과 정렬을 유지하기 위해 해당 명령어들은 NOP로 대체되었습니다.

이제 프로그램이 NtCreateFile을 호출하면 실행이 우리의 대체 NtFunction으로 점프합니다.

사용자 정의 코드 및 대체 NtFunctions

MemFiles는 후킹된 API가 리디렉션되는 대체 함수가 위치하는 방식에 있어서 EDR의 후킹 방식과 다릅니다. 많은 EDR은 자체 DLL을 프로세스에 로드합니다. 후킹된 API는 이 로드된 DLL의 주소 공간으로 리디렉션되어 분석이 이루어집니다. 이 프로젝트의 전적인 목적이 디스크에 파일을 떨어뜨리는 것을 피하는 것이었기 때문에, DLL을 디스크에 배치하고 우리의 Beacon 프로세스가 이를 로드하여 대체 NtFunctions에 접근하는 것은 좋지 않은 방법처럼 보였습니다. DLL을 메모리에서 로드할 수 있는 몇 년 된 POC가 있으며, 이는 우리의 요구에 실현 가능한 전략이지만 프로젝트가 유지 관리되지 않고 몇 가지 문제가 있는 것으로 보입니다. 게다가 1200줄의 코드로 되어 있어 BOF 형식으로 변환하기에는 벅찬 작업입니다.

참고로, 우리의 대체 NtFunctions는 BOF에 상주할 수 없습니다. CobaltStrike는 BOF를 로드하고 실행한 다음 완료되면 프로세스 메모리에서 제거합니다. 프로세스가 후킹된 NtAPI 중 하나를 호출할 때마다 호출될 수 있는 지속적인 함수가 메모리에 필요하기 때문에 BOF로는 작동하지 않습니다.

제가 내린 해결책은 위치 독립 코드(PIC)였습니다. 이름에서 알 수 있듯이, 특정 위치에 로드되어야 하거나 특정 부분이 다른 부분과 특정 관계를 유지해야 하는 일반 실행 파일과 달리, PIC는 메모리 어디에나 배치되고 실행될 수 있습니다. 이를 통해 대체 NtFunctions를 PIC 실행 파일로 작성하고, 이를 Beacon 프로세스에 주입하고, 후킹된 NTAPI가 호출될 때 우리의 후크가 실행을 해당 함수들로 리디렉션하도록 할 수 있습니다.

이 PIC NtFunctions의 템플릿은 Cracked5pider의 ShellcodeTemplate 프로젝트에서 가져왔습니다.

기본 프로젝트와의 주목할 만한 차이점은 기본 프로젝트가 전체 PIC exe를 만드는 데 중점을 두고 있다는 것입니다. 즉, 스택 포인터를 저장하고, 스택에 공간을 만들고, exe에 포함된 지정된 대체 NtFunction을 호출하고, 함수가 반환된 후 스택 포인터를 복원하는 ASM이 포함되어 있습니다. PIC exe가 만든 call 명령어는 문제를 야기합니다. call이 수행되면 (PIC exe ASM에서) 호출이 이루어진 위치의 반환 주소가 스택에 푸시되고, 이로 인해 나중에 만나는 ret 명령어가 원래 NtAPI의 호출자로 반환되는 대신 PIC exe로 실행을 되돌리게 됩니다.

이 문제를 완화하기 위해 ShellcodeTemplate 프로젝트의 ASM 파일을 편집하여 스택 설정, 함수 호출, 함수 실행 완료 후 스택 포인터 복원과 관련된 ASM을 제거했습니다. 그 결과 NtAPI에 설치된 후크는 이제 원래 NtAPI가 프로그램에 의해 호출되었을 때의 스택과 레지스터 상태(우리의 JMP에 사용되는 RAX 제외)를 유지한 채 실행을 대체 NtFunction으로 직접 점프시킵니다.

원본 ShellcodeTemplate ASM:
image

MemFiles ASM:
image

각 후킹된 NtAPI에는 다음 중 하나를 수행하는 데 필요한 로직을 포함하는 자체 PIC NtFunction이 있습니다:

A. 가짜 핸들 생성, 메모리에 데이터 쓰기, MemFiles 구조체의 변수 수정 등 MemFiles 특정 작업 수행
또는
B. API 호출을 다시 정상 궤도로 되돌리고 syscall이 이루어질 수 있도록 NTDLL로 패치하는 트램펄린으로 실행을 유도

일부 대체 NtFunctions는 다른 것보다 더 복잡합니다. 후킹된 NtAPI에 대한 호출이 MemFiles와 관련된 경우, NtCreateFile 및 NtQueryVolumeInformationFile과 같은 일부는 MSDN 문서, 테스트 결과, 그리고 약간의 추측/상식에 따라 NtAPI에 인수로 전달된 변수를 수정합니다. NtClose 및 NtReadFile과 같은 다른 함수는 MemFiles가 생성한 가짜 핸들을 전달할 때 발생할 수밖에 없는 "Invalid Handle" 오류를 피하기 위해 원래 호출자에게 단순히 STATUS_SUCCESS를 반환합니다.

후킹된 NtAPI에 대한 호출이 MemFiles와 관련이 없는 경우, 상황을 다시 정상 궤도로 되돌리기 위해 실행을 트램펄린으로 유도해야 합니다:

image

트램펄린

트램펄린은 API가 후킹되어 원래 NtAPI에서 실행되지 않은 모든 명령어를 실행하는 역할을 합니다. 여기에는 원래 후크에 의해 부분적으로 또는 완전히 덮어쓴 명령어도 포함됩니다. API를 후킹하다 보면 필요한 모든 명령어를 수행할 공간이 부족한 문제가 빠르게 발생할 수 있습니다. 트램펄린은 원래 NtAPI로 점프하기 전에 레지스터 및/또는 스택을 설정하기 위해 원하는 만큼의 작업을 수행할 수 있으므로 이 문제를 완화하는 데도 도움이 됩니다. NtCreateFile 트램펄린은 아래에서 볼 수 있습니다:

image

가장 확실하게, 초기 후크에 의해 덮어쓴 세 명령어가 트램펄린의 처음 세 명령어로 표시됩니다:

MOV R10, RCX
MOV EAX, 55
TEST BYTE PTR DS:[7FFE0308], 1

앞서 언급했듯이, 이 모든 조작의 큰 요구 사항은 syscall 번호(위 예에서는 55)가 syscall을 실행하기 전에 RAX(EAX)에 있어야 한다는 것입니다. 그러나 실행을 원래 NtAPI로 되돌리기 위해 여전히 JMP 명령어를 사용해야 한다는 문제가 있습니다. 중요한 정보를 보유하지 않아 이를 활용할 수 있는 다른 레지스터가 있을 수도 있지만, 우리가 후킹하는 NtAPI의 수와 각각이 레지스터를 다르게 사용할 수 있다는 점을 고려할 때 일관되게 안전한 옵션을 찾지 못했습니다. 안전한 옵션은 계속 RAX를 사용하는 것입니다. RAX에 저장된 syscall 번호를 스택에 푸시하면 됩니다. 그런 다음 원래 NtAPI에서 점프하려는 주소를 RAX로 이동하고 JMP 명령어를 사용하여 NTDLL로 돌아갈 수 있습니다:

image

NtAPI에 설치된 후크에는 POP RAX 명령어가 포함되어 있으며, 트램펄린을 사용하여 점프하는 곳이 바로 이 지점입니다. 이 명령어를 실행하면 스택의 맨 위에서 syscall 번호가 RAX로 복원되고 syscall을 실행할 준비가 됩니다. 원래 후킹되지 않은 NtAPI의 JNE 명령어가 여전히 남아 있다는 점에 유의하십시오. ZF 플래그를 설정하고 JNE를 수행할지 여부를 결정하는(수행되면 syscall을 건너뛰고 INT 2E로 점프하게 되는) 해당 TEST 명령어는 트램펄린에서 수행되었습니다. 이렇게 구성하면 NtAPI를 성공적으로 후킹하고 실행을 대체 PIC NtFunction으로 리디렉션하는 동시에 후킹 결과로 인해 어떤 것도 건너뛰거나 기능을 잃지 않도록 보장할 수 있습니다.

앞뒤가 바뀐 문제

지금까지 다룬 내용을 간단히 요약하면, MemFiles가 초기화될 때 Beacon 프로세스 메모리에 Memfiles의 기능에 중요한 정보를 포함하는 구조체가 생성됩니다. 이 구조체의 정보는 MemFiles의 수명 주기 동안 지속적으로 참조되며, 각 대체 PIC NtFunction뿐만 아니라 MemFiles가 메모리에 저장한 파일을 쿼리하고 가져오는 데 사용되는 BOF에서도 참조됩니다. 이를 위해 구조체가 생성될 때 구조체가 위치한 메모리 주소가 Teamserver로 다시 전달됩니다:

image

이 주소를 memtable에 저장한 후, 이후 MemFiles 명령(memlist, memfetch, memclean)은 이 주소를 BOF에 인수로 전달하여 구조체를 찾고 참조할 수 있게 합니다. 그런데 PIC NtFunction은 구조체를 어떻게 찾을까요?

명백한 문제는 PIC NtFunction이 MemFiles 구조체의 메모리 주소를 필요로 하지만, 구조체가 생성될 때 이미 컴파일되어 있다는 것입니다. MemFiles의 초기 구현은 MemFiles 초기화를 두 개의 별도 BOF로 분할하여 이 문제를 해결했습니다. 첫 번째 BOF는 구조체를 생성하고 주소를 Teamserver로 다시 보냈고, Teamserver는 Aggressor 스크립트 매직을 통해 주소를 파싱하여 각 NtFunction의 소스 코드 파일에 삽입한 다음 최종 PIC NtFunction으로 다시 컴파일했습니다. 두 번째 BOF는 완성된 PIC NtFunctions를 전송하고 실제 주입 및 NtAPI 후킹을 수행했습니다.

지저분하고 추가 시간이 걸리는 것 외에도, 여러 비콘이 동시에 MemFiles를 초기화하려고 하는 상황에서는 실제 문제가 발생할 수 있습니다. Beacon 2가 자체 MemFiles 구조체 주소를 콜백하는 동안 Beacon 1이 NtFunction 소스 코드 파일을 패치하고 다시 컴파일하는 중이라면 상황이 지저분해질 수 있습니다.

이 문제에 대한 우아한 해결책은 PIC NtFunction에 바이너리 패치를 수행하는 것입니다. 즉, MemFiles 구조체 주소를 컴파일된 NtFunction에 패치하여 런타임 중에 접근할 수 있게 하는 것입니다. 이를 위해 각 NtFunction에 자리 표시자 문자열이 작성됩니다:

image

이 변수는 xxd와 같은 도구를 사용하여 컴파일된 코드에서 볼 수 있습니다:

image

meminit 명령이 실행되면 각 PIC NtFunction이 InstallHooks BOF와 함께 Beacon으로 전송됩니다. 이 BOF는 MemFiles 구조체를 생성하는 역할을 합니다. 구조체를 생성한 후 각 PIC NtFunction에 대해 patchAddr 함수를 호출합니다. patchAddr은 A 문자열을 찾아 구조체 주소의 문자열 표현으로 바꾸는 역할을 합니다. 실제로 이전 스크린샷의 메모리 주소를 사용하면 pFileInfoStr 변수는 이제 다음과 같이 보입니다:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

그런 다음 이 문자열 표현은 실제 16진수 값, 즉 우리의 메모리 주소로 변환될 수 있습니다. BOF는 _strtoi64 API를 사용하여 이 작업을 아주 간단하게 수행합니다:

image

물론 PIC NtFunctions에게 일이 이렇게 쉬울 리가 없습니다.

이 짧은 어셈블리 스니펫은 PIC NtFunctions 중 하나의 끝부분을 보여줍니다. JMP RAX 명령어에 주목하십시오. 이는 PIC NtFunction이 트램펄린을 호출하는 것입니다(즉, MemFiles가 스푸핑/간섭하지 않은 호출이라는 뜻입니다):

image

알 수 없는 이유로, _strtoi64(또는 stroull이나 atoll 같은 유사 API)를 사용하려고 하면 이 JMP RAX 명령어가 CALL RAX 명령어로 바뀌었습니다. 분명 여기에는 "컴퓨터가 작동하는 방식"에 대한 깊은 수준과 관련된 타당한 이유가 있을 것이라고 확신하지만, 이 무시할 수 있을 것 같은 변경은 꽤 많은 것들을 깨뜨립니다. MemFiles 구조체 주소의 문자열 표현을 실제 16진수 값으로 변환하여 내부에 저장된 값을 활용할 방법을 찾기 위해 15시간 이상을 보낸 끝에 이 StackOverflow 게시물을 발견했습니다. 해당 게시물의 댓글 작성자는 마이크로컨트롤러용으로 설계된 문자열을 uint32로 변환하는 사용자 정의 루틴을 제공했습니다. 고맙게도 수정 없이 uint64에서도 작동했고, 가장 중요한 것은 나중에 PIC NtFunction에서 JMP RAX 명령어를 CALL로 변경하지 않고 보존했다는 점입니다. 최종 스니펫:

image

구형 NtAPI VS 신형 NtAPI

단순화를 위해 앞서 언급하지 않았지만, NtAPI의 형식은 수년에 걸쳐 변경되었습니다. Windows 7에서 사용되는 것과 같은 초기 버전은 최신 버전보다 훨씬 짧아서 32바이트 대신 16바이트에 불과합니다:

image

이로 인해 MemFiles가 NtAPI를 후킹하는 방식과 트램펄린이 구성되는 방식이 변경되어야 합니다. MemFiles가 처리 중인 NtAPI의 버전을 식별하기 위해 InstallHooks BOF는 먼저 해당 NtAPI의 주소를 확인한 다음 해당 위치에서 32바이트를 읽습니다. 형식이 다르기 때문에 syscall 명령어는 NtAPI 버전 간에 서로 다른 위치에 있습니다. 특정 바이트 오프셋에서 syscall의 존재 여부를 확인함으로써 MemFiles는 현대식 NtAPI 구현을 다루는지 레거시 NtAPI 구현을 다루는지 판단하고 그에 따라 대응할 수 있습니다:

image

후킹 후 레거시 NtCreateFile API는 다음과 같이 보입니다:

image

그리고 레지스터를 설정한 후 NtCreateFile로 다시 점프하는 데 사용되는 트램펄린:

image

전반적으로 기술은 거의 동일하지만, 상황이 훨씬 더 빠듯하고 여유 공간이 별로 없습니다. 모든 것을 제대로 맞추고 작동시키기 위해 syscall 명령어를 NtCreateFile 내에서 이동해야 했다는 점에 주목할 필요가 있습니다. syscall은 여전히 NTDLL 내의 NtCreateFile API 메모리 공간에 있지만, API의 9번째 및 10번째 바이트에서 14번째 및 15번째 바이트로 이동했으며, 모든 것을 제대로 채워 넣기 위해 다중 바이트 NOP를 희생했습니다.

I/O 관련 NtAPI 찾기

파일 I/O 작업과 관련된 API 중 일부는 직관적이어서 식별하고 후킹하기 쉬웠지만, 다른 API는 훨씬 더 찾기 어려웠으며 WinDbg와 x64dbg에서 수많은 시간을 들여 어셈블리를 단계별로 실행하면서 어떤 API가 호출되는지 식별해야 했습니다. 이 작업을 더 효율적으로 수행할 방법이 분명 있었을 것이라고 믿지만, 저는 배워가며 진행하고 있었습니다.추가로, 돌이켜 보면 매우 간단했어야 할, SharpHound가 20시간 동안 작동하지 못하게 만든 NtAPI를 식별하는 과정을 자세히 설명하고자 합니다. .NET 프로그램인 SharpHound는 "핸들이 유효하지 않습니다"라고 판단한 것과 관련된 매우 지저분한 콜 스택을 내뱉었습니다. .NET은 프로그래머에게 친숙한 언어이지만, 대부분(전부?)의 기능은 번역되어 궁극적으로 Win32 API(그 결과 NtAPI)를 통과하며, 우리는 다른 것과 마찬가지로 이를 관찰하고 후킹할 수 있습니다.

sharphounderror

"Invalid Handle" 오류는 SharpHound가 사용하는 NtAPI 중 제가 후킹하지 않은 것이 있다는 확실한 신호였습니다(기존 PIC NtFunctions 중 하나가 제대로 작동하지 않는 것이 아니라). 그래서 그것이 무엇인지 알아내기 위해 시도하기 시작했습니다. 지금까지 다른 도구에 사용했던 기법은 먼저 NtCreateFile에 중단점을 설정하고 제 "특수" 디렉터리와 관련된 호출을 찾는 것이었습니다. 거기서부터 프로그램을 단계별로 실행하면서(길을 잃기 때문에 보통 여러 번) 프로그램이 다음에 호출하는 함수를 확인했습니다. 이 방법을 따라가다 보니 NtQueryVolumeInformationFile, NtQueryInformationFile, NtSetInformationFile이 호출되며 후킹이 필요하다는 것을 발견했습니다.

SharpHound는 많은 작업을 비동기적으로 수행한다는 점에서 추가적인 난관을 던집니다. 이로 인해 단일 파일이 API 호출을 거치는 선형적 단계를 추적하기가 훨씬 어려워집니다. 여러 파일이 동시에 프로세스를 진행하기 때문입니다. 또한 NtCreateFile 호출 후 프로그램을 단계별로 실행해 보면, 결국 NtCreateFile 호출이 이루어진 스레드가 종료된다는 것을 발견했습니다. 이후의 NtWriteFile 호출(및 이 탐색의 대상이 되는 다른 알 수 없는 API 호출)은 다른 스레드에서 발생하므로 검색 과정이 더욱 혼란스러워집니다.

.NET을 잠깐만 다뤄본 나는 오류로 생성된 콜 스택을 해석하는 데 능숙하지 않았고, async 사용으로 두 배로 지저분해진 콜 스택은 더더욱 그랬습니다. 20시간에 걸친 문제 해결 과정에서, 이전 전략으로는 진전이 없었기 때문에 나는 계속 그 콜 스택으로 돌아갔고, 천천히 그러나 확실하게 그것을 이해하게 되었습니다. "--- End of stack trace..." 줄로 구분된 위에서 세 번째 "덩어리"쯤에 "at Sharphound.Writers.JsonDataWriter..."라는 줄이 보였습니다. 이는 실제 SharpHound 코드(오픈소스이며 Github에서 볼 수 있음)에서 시작할 상대적 위치를 제공했습니다. 이름이 암시하듯이 SharpHound 함수는 JSON 출력을 파일로 작성하는 작업을 처리했습니다. 데이터가 파일로 성공적으로 기록되지 않고 있다는 것을 이미 알고 있었으므로 새로운 소식은 아니었습니다. 콜 스택을 한 단계 위로 추적하면 다음 관련 줄은 "at System.IO.Streamwriter."였습니다. System.IO 접두사는 이것이 SharpHound 특정 함수가 아닌 .NET 고유의 것임을 알려주었습니다. 콜 스택 맨 위 섹션을 보면 "at System.IO.FileStream.FlushOSBuffer()"라는 줄이 눈에 띄었습니다. 나는 FlushOSBuffer를 검색해서 무엇을 찾을 수 있는지 알아보기로 결정했습니다.

그렇게 해서 Microsoft의 .NET 문서 filestream.cs를 찾게 되었습니다. 거기서 FlushOSBuffer에 대한 정의를 발견했습니다:

image

그것은 Win32 API인 FlushFileBuffers를 호출하는 것으로 보입니다. Win32Native.FlushFileBuffers의 정의는 Win32Native 문서에서 찾을 수 있습니다:

image

.NET에서 P/Invoke로 작업해 본 사람이라면 그 형식을 알아볼 것입니다. 이제 문제의 .NET 함수인 System.IO.Filestream.FlushOSBuffer()가 호출하는 Win32 API를 알게 되었습니다. KERNEL32!FlushFileBuffers에 중단점을 설정하고 SharpHound를 실행하면 이를 확인할 수 있었고, 단계별 실행을 통해 FlushFileBuffers가 내부적으로 NtFlushBuffersFile을 호출한다는 것을 빠르게 확인했습니다. 이 API를 후킹함으로써 SharpHound가 겪던 문제가 해결되었고, 출력 파일을 메모리에 기록하면서 성공적으로 실행될 수 있었습니다.

메모리에서 파일 다운로드

이 프로젝트의 중요한 부분은 파일이 메모리에 저장된 후 실제로 CobaltStrike Teamserver로 다운로드할 수 있는 능력입니다. 예상대로 일반적인 CobaltStrike download 명령은 실제로 존재하지 않는 파일 경로에서는 작동하지 않습니다. 지금 알게 된 지식으로 보면, 솔루션은 메모리 내 파일이 단지 기록되는 대신 읽힐 수 있도록 대체 PIC NtReadFile 코드를 보완하는 데 있을 것입니다. 사전에 그 지식을 갖지 못했기 때문에 실제로 파일을 검색할 수 있는 것이 주요 장애물이었습니다.

우연히 EspressoCake가 작성한 BOF를 발견했는데, 그 안에 눈에 띄는 함수가 있었습니다:

image

코드를 살펴보면 문서화되지 않은 Beacon CALLBACK 옵션을 사용하는 것으로 보입니다:

image

이 함수는 BOF가 클라이언트가 아닌 대상으로부터 Teamserver로 파일 다운로드를 시작할 수 있게 합니다. 이 기능(나중에 @Cr0Eax, @EthicalChaos, @anthemtotheego를 포함한 여러 사람들의 공동 작업이었다는 것을 알게 되었습니다)은 이전에 존재했던 주요 장애물을 제거했습니다. 이제 메모리 내 파일을 대상 시스템에서 파일 전송을 시작할 수 있는 방법이 생겼기 때문입니다. 이 코드 조각을 만드는 데 참여한 모든 분께 큰 감사를 드립니다. 앞으로도 유용할 것이라고 생각합니다.

MemFiles 데이터 구조

이 프로젝트의 어려운 부분 중 하나는 Team Server에 연결된 모든 CobaltStrike Client에서 MemFiles 기능을 사용할 수 있도록 보장하는 것이었습니다. MemFiles의 데이터는 MemFiles.cna에 의해 생성된 구조에 저장되며, 이 도구를 사용하려는 각 Client에 로드되어야 합니다. 그 결과 이러한 데이터 구조는 Team Server가 아닌 각 Client 안에 존재합니다. 이 데이터가 단일 중앙 위치(TS)에 존재한다면 각 Client에서 이를 검색하는 것은 사소한 일이 될 것이고 이 모든 문제는 문제가 되지 않을 것입니다. CobaltStrike 팀이 MemFiles와 같은 기능을 CobaltStrike에 공식적으로 통합한다면 저는 그들이 이 방향으로 갈 것이라고 확신합니다. 그러나 이것은 커뮤니티 애드온이기 때문에 우리가 가진 것으로 해결합니다.

각 CobaltStrike Client가 Beacon 내 MemFiles 상태 및 구성에 관한 최신의 정확한 데이터를 가지고 있는지 확인하는 것과 관련하여 걱정해야 할 몇 가지 시나리오가 있습니다:

TS에 연결하고 현재 memtable이 필요한 새 Client
단일 Client만 TS에 연결되어 있고 CobaltStrike를 다시 시작하는 경우(따라서 Client 메모리에 저장된 memtable을 잃는 경우)
Client A가 MemFiles 데이터를 변경하여 Client B에 전달되어야 하는 경우

이러한 시나리오를 해결하기 위해 다각적인 접근 방식이 사용되었습니다. 단일 CobaltStrike Client만 TS에 연결된 경우(따라서 memtable 데이터를 가진 유일한 주체인 경우)를 처리하기 위해, Client가 memtable(meminit, memclean)을 변경할 때마다 memtable의 내용을 CobaltStrike 디렉터리의 로컬 텍스트 파일에 기록합니다. Client가 종료/재시작하거나 MemFiles.cna가 다시 로드되면, 먼저 로컬 memtable.txt 파일을 읽어 메모리 내 memtable을 채우려고 시도합니다.

여러 Client가 TS에 연결되어 있고 새 Client가(이벤트 로그에 따라) 연결되면 각 Client는 TS에 연결된 모든 사용자 목록을 가져와 알파벳순으로 정렬합니다. 목록에서 첫 번째인 Client가 "Broadcast" Client로 선택되고, 5초를 기다린 후(새 Client가 초기화하고 로컬 memtable.txt를 읽을 수 있도록) memtable의 각 항목에 대해 이벤트 로그에 메시지(Actions)를 보냅니다. 다른 모든 Client(브로드캐스트하는 Client 제외)는 이러한 메시지를 읽고 브로드캐스트 정보로 자신의 memtable을 업데이트합니다. 여기에는 기존 항목 업데이트와 각 memtable에 없는 추가 항목을 추가하는 것이 포함됩니다.

MemFiles와 관련된 일반적인 작업도 이벤트 로그에 메시지를 보내는 방식에 의존합니다. Client A가 meminit을 실행하면 관련된 모든 memtable 정보를 포함한 메시지가 브로드캐스트됩니다. 모든 Client는 "on Event_Action" 훅을 사용하여 브로드캐스트된 이벤트 로그 메시지를 파싱하여 각자의 memtable을 업데이트합니다. meminit이 BOF 실행을 완료할 때 MemFiles 데이터에도 변경이 이루어집니다. 이러한 변경은 Beacon에 의해 다시 전달되며(예: meminit 실행 후 Beacon이 pMemAddrs 구조체의 메모리 위치로 콜백), 따라서 연결된 모든 Client가 이를 볼 수 있고, 각 Client는 "on Beacon_Output" 훅을 사용하여 각자의 memtable을 업데이트합니다.

이러한 개별적인 노력이 결합되어 MemFiles는 여러 Client 간에 중요한 데이터를 효율적이고 안정적으로 동기화할 수 있게 되었습니다.

크레딧 및 감사의 말

이 프로젝트는 다음 개인과 프로젝트의 기여 없이는 불가능했을 것입니다:

  1. x64-NTAPI-inline-hook globalpolicy 작성
  2. x64 Function Hooking by Example Kyle Halladay 작성
  3. ShellcodeTemplate Cracked5pider(일명 @C5pider) 작성
  4. @ilove2pwn_, Cracked5pider 경유
  5. DLL-Exports-Extraction-BOF EspressoCake(일명 @the_bit_diddler) 작성
  6. @anthemtotheego, EspressoCake 경유
  7. @Cr0Eax, @anthemtotheego 경유
  8. @EthicalChaos, @anthemtotheego 경유
  9. SysWhispers is dead, long live SysWhispers! KlezVirus 작성
  10. @yarden_shafir
  11. @DexterGerig
도구 다운로드