Alexandre Borges의 블로그 Exploit Reversing의 악성코드 분석 시리즈 'Malware Analysis Series(MAS)'를 리뷰하며 공부해보겠습니다.
네 번째 아티클에서는 MAS 시리즈 처음으로 .NET에 대해 다룹니다.
Malware Analysis Series (MAS) – Article 4
[Instruction]
이번 아티클에서는 처음으로 .NET 악성코드 분석에 대해 다뤄볼 것입니다. 여러 가지 기술과 트릭들로 어려울 수 있지만, MSIL(Microsoft Intermediate Language)로 코드를 디컴파일하고 원본에 근접한 고급 .NET언어로 코드를 제공해주는 dnSpy와 ILSpy 같은 훌륭한 도구들이 도움이 될 것입니다. 하지만 일부 커스텀된 인코딩과 암호화된 데이터 경우에는 다른 기술을 사용하여 바이너리를 처리할 수 있습니다.
저는 우선 이번 아티클에서 .NET 코드를 리버싱하는데 필요한 기본 정보를 이해할 수 있도록 최소한의 이론을 제공하려고 합니다. 그렇지만 .NET 악성코드 분석은 매우 방대한 주제이기 때문에 다른 아티클에서도 다룰 것입니다.
.NET으로 작성된 악성코드를 분석할 때 추가적인 단계들이 나타날 텐데, 그중 일부는 패커, 난독화기 또는 현대 프로텍터로 보호되어 있을 수 있습니다. 그렇기 때문에 결국 우리의 임무는 이러한 각 단계를 처리하고, 복호화한 후, 다음 단계로 나아가 최종 페이로드를 찾는 것입니다. 이 과정은 쉽게 진행되지 않을 수도 있습니다.
바이너리 악성코드와 마찬가지로, .NET 악성코드 분석에서 유사한 위협을 식별하는데 도움이 될 수 있는 지속성 기술, C2 통신, 회피 기술, 데이터 탈취, 평문 URL, 크리덴셜 같은 모든 IOC를 찾아보겠습니다. 분명히 우리는 .NET(managed code)을 분석할 때 다양한 챌린지와 장애물에 마주칠 것이며 이 작업은 native code보다 어려울 수 있습니다. 왜냐하면 그동안의 아티클에서 많이 배운 native 바이너리와는 다른 .NET 아키텍처에 대한 구체적인 개념을 알아야 되고, 여러 문제(일반적으로 난독화 및 암호화)를 관리해야 되기 때문입니다. 오늘날 .NET 악성코드 위협은 어디에나 있고 많은 위협 캠페인에서 적극적으로 사용되고 있습니다.
이제 실험 환경을 설정하고 핵심 개념을 정리해보겠습니다.
[Lab Setup]
.NET 리버싱에 초점을 맞추어 .NET 관련 도구들을 집중적으로 사용해보겠습니다. 이번 글에서 쓰이지 않더라도 다른 아티클에서 사용할 도구들입니다.
- DnSpy: .NET 어셈블리 편집기이자 디버거입니다. 해당 프로젝트는 더이상 아카이브되지 않습니다.
- DnSpyEx: 원래의 dnSpy 프로젝트가 부활한 것으로, 지속적으로 업데이트되고 있습니다.
- De4dot: .NET 난독화 해제 도구 및 언패커입니다. 이 도구는 dnlib을 사용하여 어셈블리를 읽고 씁니다. 또한 de4dot은 많은 리눅스 배포판에서 사용할 수 있습니다.
- dnlib: .NET 어셈블리를 조작(읽기/쓰기)하는데 사용되는 모듈입니다.
- ILSpy: 오픈소스 .NET 어셈블리 브라우저이자 디컴파일러입니다.
[.NET Concepts]
C, C++, C# 같은 여러 언어로 프로그래밍을 배우는 것이 리버스 엔지니어링을 수행하는데 꼭 필요한 것은 아니지만, 확실히 이러한 지식은 코드 분석 중 더 잘 이해하여 더 좋은 결정을 내리게 되며 다음 단계로 나아가는데 도움이 됩니다.
따라서 이 전제를 바탕으로 이번 섹션에서는 .NET 프로그래밍과 관련된 주요 개념을 리뷰하려고 합니다. 물론 코드 프로그래밍 방법을 설명하지는 않겠지만 .NET 악성코드 샘플을 분석하는데 독자들이 더 편안하게 접근할 수 있도록 .NET 관련 개념들을 소개해보겠습니다.
이미 아시겠지만 .NET 코드는 managed code로 실행되기 위해서는 .NET 런타임이 필요합니다. 이런 .NET 바이너리는 기본적으로 MSIL(Microsoft Intermediate Language) 명령어와 메타데이터로 구성됩니다. 물론 IL(Intermediate Language) 명령어는 거의 다루지 않겠지만(일부 난독화된 샘플에서는 필요할 수도 있지만요), 원하시면 다음 명령어로 모든 .NET 런타임과 SDK를 나열할 수 있습니다.
- dotnet --list-runtimes
- dotnet --list-sdks
.NET 악성코드 분석을 위해서는 아마 .NET Framework와 .NET Core로 작성된 샘플을 찾아야할 것입니다. .NET Framework 어셈블리로 컴파일된 악성코드 샘플은 메타데이터(manifest)를 포함하고 .dll 또는 .exe 파일일 수 있습니다. 반면 .NET Core 샘플은 항상 .dll 파일로 컴파일됩니다(보통 dotnet <assmbly>.dll 명령어로 컴파일됩니다). 또 다른 미묘한 차이점은 .NET Core는 .NET Framework가 사용하는 GAC(Global Assembly Cache)를 사용하지 않는다는 것입니다. GAC는 프레임워크 라이브러리의 공통 설치 디렉토리입니다.
.NET 리소스에 내장된 암호화된 페이로드는 dnSpy 같은 일반적인 도구나 특정 프로그램을 사용하여 메모리에서 추출, 언팩할 수 있습니다. 그리고 native 바이너리처럼 .NET 악성코드도 다른 .NET 악성코드(.dll 모듈 또는 .exe 파일)나 native 코드를 실행 중인 프로세스에 주입(inject)할 수 있습니다. 이 주입된 악성 바이너리는 다음 단계의 다운로드(downloader)가 되어 native 코드나 managed 코드를 다운로드하고 실제 감염 행위를 시작할 수 있습니다. 더 심각한 것은 일부 .NET 악성 페이로드가 자체 .NET 런타임을 공격하여 전체 환경을 손상시킬 수 있다는 것입니다.
일상적인 악성코드 분석 작업에서 ConfuserEx, .NET Reactor, Dotfuscator, babelfor.NET, Agile 등과 같은 잘 알려진 난독화 도구나 커스텀 프로텍터까지도 난독화된 .NET 악성코드 샘플 분석에 사용할 수 있을 것입니다. 이 경우 다양한 접근 방식이 존재하기 때문에 샘플을 언팩하고 난독화를 해제하는데 시간일 걸리 수 있습니다. 사용되는 난독화 기법에 따라 다음과 같은 다양한 트릭을 발견할 수 있습니다.
- 메서드 시그니처, 필드, 메타데이터 이름 변경
- 암호화된 문자열
- 정크(junk) 코드
- 제어 흐름 난독화
- 교차 참조 난독화
- 난독화된 구현 메서드
- 난독화된/숨겨진 교차 참조
모든 .NET 코드(물론 악성코드 바이너리도 포함!)는 Process, ProcessModule, ProcessThread, ProcessThreadCollection, ProcessStartInfo 등과 같은 System.Diagnostics 네임스페이스의 클래스들을 사용하여 시스템과 상호 작용할 수 있습니다. 또한 Start(), Kill(), GetProcesses(), GetCurrentProcess(), GetProcessById() 등 다양한 메서드가 있으며, 이는 방금 언급된 Process 타입에 적용되어 실행 중인 시스템과 직접 상호 작용합니다. 프로그래밍 개념으로서 System.Diagnostics 네임스페이스로 어셈블리를 컴파일하려면 System.Linq 네임스페이스가 필요한데 이 점이 나중에 필요한 힌트가 될 것입니다.
(하나 이상의 어셈블리로 구성된).NET 애플리케이션은 애플리케이션 도메인 내에서 호스팅되며, 이는 AppDomain.CurrentDomain 정적 속성을 사용하여 접근할 수 있습니다. 구성되는 어셈블리는 System.Reflection 네임스페이스를 사용하여 접근할 수 있으며 이는 악성코드 분석가들이 배워야하는 중요한 부분입니다. 왜냐하면 대부분의 .NET 악성코드 샘플에서 .NET Reflection 메서드가 사용되기 때문입니다.
다음은 .NET 악성코드 위협에서 사용할 수 있는 System, System.Reflection 및 다른 네임스페이스에서 잘 알려진 메서드들의 간략한 목록입니다. 이 메서드들은 동적 분석 중 중단점(breakpoint)을 설정하기 좋은 대상이 됩니다.
- Activator.CreateInstance
- 늦은 바인딩(late binding)이라는 기법을 사용하여 지정된 인스턴스를 생성합니다. 이 기법은 주어진 타입의 인스턴스를 생성하고 코드에서 외부 어셈블리의 멤버에 대해 미리 결정된 참조 없이, 런타임에서 해당 멤버를 호출할 수 있도록 합니다.
- 늦은 바인딩(late binding)이라는 기법을 사용하여 지정된 인스턴스를 생성합니다. 이 기법은 주어진 타입의 인스턴스를 생성하고 코드에서 외부 어셈블리의 멤버에 대해 미리 결정된 참조 없이, 런타임에서 해당 멤버를 호출할 수 있도록 합니다.
- Assembly.CreateInstance
- 어셈블리에서 타입을 찾아 인스턴스를 생성합니다.
- 어셈블리에서 타입을 찾아 인스턴스를 생성합니다.
- Assembly.GetExecutingAssembly
- 현재 실행 중인 코드가 포함된 어셈블리를 가져옵니다.
- 현재 실행 중인 코드가 포함된 어셈블리를 가져옵니다.
- Assembly.GetEntryAssembly
- 기본 애플리케이션 도메인에서 실행 가능한 프로세스를 가져옵니다.
- 기본 애플리케이션 도메인에서 실행 가능한 프로세스를 가져옵니다.
- Assembly.GetFile
- 어셈블리의 매니페스트(manifest) 파일 테이블에 지정된 파일에 대한 FileStream을 반환합니다.
- 어셈블리의 매니페스트(manifest) 파일 테이블에 지정된 파일에 대한 FileStream을 반환합니다.
- Assembly.GetModule
- 어셈블리에서 지정된 모듈을 가져옵니다.
- 어셈블리에서 지정된 모듈을 가져옵니다.
- Assembly.GetType
- 문자열 등을 사용하여 타입을 가져옵니다.
- 문자열 등을 사용하여 타입을 가져옵니다.
- Assembly.Load
- 어셈블리를 로드합니다.
- 어셈블리를 로드합니다.
- Assembly.LoadFile
- 어셈블리 파일의 내용을 로드합니다.
- 어셈블리 파일의 내용을 로드합니다.
- Assembly.LoadFrom
- 해당 메서드 또한, 어셈블리 파일의 내용을 로드합니다.
- 해당 메서드 또한, 어셈블리 파일의 내용을 로드합니다.
- Assembly.LoadModule
- 어셈블리 내부의 모듈을 로드합니다.
- 어셈블리 내부의 모듈을 로드합니다.
- Assembly.GetLoadedModules
- 어셈블리의 모든 로드된 모듈을 가져옵니다.
- 어셈블리의 모든 로드된 모듈을 가져옵니다.
- AssemblyDependencyResolver.ResolveAssemblyToPath
- 어셈블리 이름을 주면 해당 어셈블리 경로를 반환해줍니다.
- 어셈블리 이름을 주면 해당 어셈블리 경로를 반환해줍니다.
- AppDomain.GetAssemblies
- 애플리케이션 도메인 컨텍스트에 로드된 어셈블리를 가져옵니다.
- 애플리케이션 도메인 컨텍스트에 로드된 어셈블리를 가져옵니다.
- ConstructorInfo.Invoke
- 인스턴스에서 제공한 생성자를 호출합니다.
- 인스턴스에서 제공한 생성자를 호출합니다.
- System.Reflection.AssemblyName GetAssemblyName
- 주어진 파일에 대한 AssemblyName을 가져옵니다.
- 주어진 파일에 대한 AssemblyName을 가져옵니다.
- Module.GetField
- 지정된 필드를 반환합니다.
- 지정된 필드를 반환합니다.
- Module.GetFields
- 주어진 모듈에서 전역 필드를 반환합니다.
- 주어진 모듈에서 전역 필드를 반환합니다.
- Module.GetMethod
- 문자열 이름으로 메서드를 반환합니다.
- 문자열 이름으로 메서드를 반환합니다.
- Module.IsResource
- 주어진 객체가 리소스인지 여부를 판단합니다.
- 주어진 객체가 리소스인지 여부를 판단합니다.
- MethodBase.Invoke
- 메서드나 생성자를 호출합니다.
- 메서드나 생성자를 호출합니다.
- ResourceManager Class
- 문화별 자원(culture resource)에 접근할 수 있는 리소스 관리자를 나타냅니다.
- 문화별 자원(culture resource)에 접근할 수 있는 리소스 관리자를 나타냅니다.
- Module.GetMethodImpl
- 메서드의 구현을 반환합니다.
.NET 악성코드 바이너리의 구조는 다음과 같습니다.
- 파일 헤더
- CLR(Common Language Runtime) 파일 헤더
- Manifest
- IL 코드 (managed 코드)
- 인베디드된 리소스
- 타입 메타데이터
여기서는 Reflection과 관련된 Assembly와 Module 같은 몇 가지 클래스(타입)만 언급했지만 AssemblyName, EventInfo, FieldInfo, MemberInfo, MethodInfo, PropertyInfo 등과 같은 다른 클래스도 많습니다. 마찬가지로 System.Type과 같은 다른 타입 클래스는 속성(IsClass, IsArray, IsCOMObject, IsEnum, …) 그리고 메서드(GetMembers( ), GetType( ), GetMethods( ), GetProperties( ), GetFields( ), InvokeMember( ), …)를 제공하고, 이는 System.Reflection을 사용하여 반환되는 타입에 대한 정보를 얻는데 사용할 수 있습니다.
메타데이터는 그저 클래스, 델리게이트(delegate), 인터페이스, 열거형, 구조체 등의 애플리케이션 구성 요소에 대한 디스크립터(descriptor)라는 설명이 적절합니다. 각 타입은 TypeDef 토큰에 의해 참조되며, 이는 참조된 타입의 전체 메타데이터 정의(TypeRef)에 대한 포인터입니다. 또한 CLR에 대해 이야기할 때는 로더와 JIT 컴파일러를 고려하고 있다는 점을 기억해야합니다.
메타데이터는 관계형 데이터베이스처럼 교차 참조를 사용하여 조작되며, 각 클래스가 어디에서 왔는지 찾을 수 있게 합니다. 메타데이터는 이름이 붙여진 스트림으로 표현되며, 이는 메타데이터 힙(heap)과 메타데이터 테이블로 분류됩니다.

관리되는 리소스(managed resource)는 .rsrc가 아니라 .text 섹션에 포함된다는 점을 기억하세요.
.NET 내부를 간단히 살펴보면 메타데이터 힙은 다음과 같이 구성될 수 있습니다.
- GUID heap: 크기가 16바이트인 객체를 포함합니다.
- String heap: 문자열을 포함합니다.
- Blob heap: 4바이트씩 정렬된 임의 바이너리 객체를 포함합니다.
다음은 명명된 스트림 6가지입니다.
- #GUID: 전역 고유 식별자(Global Unique Identifiers)를 포함합니다.
- #Strings: 클래스, 메서드 등의 이름을 포함합니다.
- #US: 사용자 정의 문자열을 포함합니다.
- #~: 압축된 메타데이터 스트림을 포함합니다.
- #-: 압축되지 않은 메타데이터 스트림을 포함합니다.
- Blob: 바이너리 객체의 메타데이터를 포함합니다.
참고로 압축된 스트림과 압축되지 않은 스트림은 상호 배타적(mutually exclusive)입니다.
메타데이터 테이블에 대해서는 40개 이상의 테이블이 존재하며 모두 다 다루기에는 시간이 많이 걸릴 것 같습니다. 그중 흥미로운 테이블은 ImplMap, MethodImpl, MethodDef, ModuleRef, ManifestResource, TypeRef, TypeDef, Field, Property, Member, MemberRef, Method, File table 정도가 있습니다.
native 파일 헤더와 CLR 헤더는 다음 명령어를 사용하여 확인할 수 있고, 시각화하면 아래 그림(Figure 3)과 같습니다.
- 파일 헤더: dumpbin /headers filename.dll
- CLR 헤더: dumpbin /clrheader filename.dll
참고: 제 시스템에서 dumpbin.exe는 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\dumpbin.exe 위치에 있습니다.


대부분 .NET 악성코드는 하나 이상의 클래스 생성자(.cctor())와 인터스턴스 생성자(.ctor())를 포함합니다. .cctor() 클래스 생성자는 main 메서드 실행, 클래스 초기화 실행 또는 진입점에 도달하기 전에 호출됩니다. dnSpy와 같은 도구를 사용할 때는 항상 이 생성자들을 검사해야 합니다. 왜냐하면 .cctor()와 .ctor()는 난독화된/난독화 해제된 .NET 코드를 삽입할 때 선호되는 곳 중 하나이기 때문입니다.
과거에는 ICorJitCompiler::getJit() + ICorJitCompiler::compileMethod()를 하이재킹(hijacking)하여 JIT를 제어하고 최종 결과 코드에 영향을 미칠 수 있었지만, 해당 이슈는 수정되어 Windows Defender에 포함되었습니다. 다른 진화된 악성코드 위협은 런타임 라이브러리를 IL 코드 수준에서 변경하거나 심지어 후킹(hooking)을 시도합니다. 만약 이게 성공한다면 많은 애플리케이션에 치명적이며 당연히 전체 시스템을 타격할 수 있습니다.
저는 .NET 내부와 관련된 MSIL 코드에 대한 자세한 내용을 다루지 않겠습니다. 그 지식은 이 아티클을 이해하는데 필요하지 않기 때문입니다. (혹시 궁금하시면 공유드리는 제 자료를 확인해보세요:
https://exploitreversing.files.wordpress.com/2021/12/alexandreborges_defcon_2019-3.pdf)
[General Procedure]
".NET 샘플을 분석할 때 어떤 세부 사항과 단서를 주의 깊게 살펴야할까요?"
.NET 악성코드 위협을 분석할 때 전문가들이 가장 자주 묻는 질문 중 하나입니다. 고정된 규칙은 없지만 몇가지 고려사항이 있습니다.
- 악성코드가 실제로 .NET 코드인지 확인하세요.
- 악성코드가 패킹되어 있는지 확인하세요. 내장된 리소스가 존재하는 것만으로도 악성코드가 숨겨져 있을 가능성(그리고 난독화되었을 가능성)을 충분히 시사하고 있습니다.
- 실제 진입점(EP)을 찾으세요. (.cctor와 .ctor 생성자에 주의하세요.)
- 코드를 검사하고 잠재적인 난독화 도구의 존재를 파악하세요.
- de4dot와 같은 도구(파워쉘에서 실행하는 게 편집 기능이 더 좋습니다) 및 기타 커스텀 도구들이 코드를 난독화 해제하는데 도움될 것입니다.
- 어떻게 언팩할 건가요? 정적 분석과 동적 분석을 혼합한 접근 방식을 고려해야 합니다.
- 대부분의 .NET 악성코드는 매우 크기 때문에 모든 코드를 한 줄씩 분석하려고 하지 마세요. 대부분 그럴 필요 없지만, 몇몇 경우에는 다른 대안이 없을 수도 있습니다. (C#에 대해 아는 것이 도움 될 수 있습니다.)
- 동적 분석을 사용할 경우(아마도 dnSpy를 사용하여), 위에서 나열한 주요 메서드에 중단점(breakpoint)을 설정하세요.
- 메서드를 분석할 때 사용되지 않는 매개변수에 주의하세요.
- dnSpy를 사용할 때, Local, Call Stack, Modules와 같은 디버거 탭이 매우 유용합니다.
- 악성 모듈은 언제든지 로드될 수 있다는 것과 이를 항상 메모리에서 덤프할 수 있다는 것을 기억하세요.
- .NET 악성코드 샘플 중에는 최종적으로 .NET 악성코드로 이어지는 경우도 있고, native 악성 바이너리로 이어지는 경우도 있습니다. 그러니까 미리 결론 짓지는 마세요.
[Collecting .NET information]
확실히 .NET 샘플에 대한 유용한 정보를 수집하는 가장 뛰어난 방법 중 하나는 Powershell에서 System.Reflection 네임스페이스를 사용하는 것입니다. 독자들이 이미 알고 있듯이 이 주제에 관한 훌륭한 참고 자료가 인터넷에 수십 개 있으니 세부사항에 대해 깊이 다룰 계획은 없지만, 간단하게만 설명해보겠습니다.
PowerShell은 .NET의 정적 및 인스턴스 메서드를 사용하여 정보를 액세스하고 수집할 수 있는 무수한 옵션을 제공하며 실행된 각 명령은 메서드를 호출하는 문법과 속성을 읽고 쓰는 문법을 이해해야 합니다.
잘 알려져 있는 몇 가지 문법은 다음과 같습니다.
- [Class Name]::PropertyName
- $ObjectReference.PropertyName
- [Class Name]::MethodName(arguments list)
- $ObjectReference.MethodName(arguments list)
다음 명령어들은 .NET 바이너리의 기본 정보를 수집하는데 사용할 수 있으며 물론 각 경우에 맞게 조정해야 합니다.
● 모든 로드된 어셈블리 목록 나열
# List all loaded assemblies.
PS C:\ > [appdomain]::currentdomain.GetAssemblies() | ft Location | Select-Object -First 10
Location
------------
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.ConsoleHost\v4.0_3.0.0.0__31bf
3856ad364e35\Microsoft.PowerShell.ConsoleHost.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\Syste
m.Core.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3
856ad364e35\System.Management.Automation.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.Management.Infrastructure\v4.0_1.0.0.0__3
1bf3856ad364e35\Microsoft.Management.Infrastructure.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Management\v4.0_4.0.0.0__b03f5f7f11d50a3a
\System.Management.dll
C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.DirectoryServices\v4.0_4.0.0.0__b03f5f7f11d50
a3a\System.DirectoryServices.dll
● 특정 어셈블리 로드 (.NET 악성코드)
# Load an specific assembly (.NET malware).
PS C:\> $Malware_Assembly =
[System.Reflection.Assembly]::LoadFile("C:\Users\Administrator\Desktop\MAS\MAS_4\malware_dotne
t.bin")
● 특정 어셈블리에 로드된 모든 모듈 가져오기
# Get all loaded modules from a specific assembly.
PS C:\ > $LoadedModules = $Malware_Assembly.GetLoadedModules( )
PS C:\ > $LoadedModules
MDStreamVersion : 131072
FullyQualifiedName : C:\Users\Administrator\Desktop\MAS\MAS_4\malware_dotnet.bin
ModuleVersionId : 53d49999-e4ad-4b0b-be7a-8497530feeda
MetadataToken : 1
ScopeName : WaitCallb.exe
Name : malware_dotnet.bin
Assembly : WaitCallb, Version=1.7.3.0, Culture=neutral, PublicKeyToken=null
CustomAttributes : {}
ModuleHandle : System.ModuleHandle
● 특정 어셈블리의 모든 모듈 가져오기
# Get all modules from a specific assembly.
PS C:\ > $Malware_Assembly.GetModules()
MDStreamVersion : 131072
FullyQualifiedName : C:\Users\Administrator\Desktop\MAS\MAS_4\malware_dotnet.bin
ModuleVersionId : 53d49999-e4ad-4b0b-be7a-8497530feeda
MetadataToken : 1
ScopeName : WaitCallb.exe
Name : malware_dotnet.bin
Assembly : WaitCallb, Version=1.7.3.0, Culture=neutral, PublicKeyToken=null
CustomAttributes : {}
ModuleHandle : System.ModuleHandle
● 어셈블리의 "FullName" 속성 가져오기
# Get the “FullName” property of the assembly.
PS C:\ > $Malware_Assembly.FullName
WaitCallb, Version=1.7.3.0, Culture=neutral, PublicKeyToken=null
● 어셈블리의 런타임 버전 가져오기
# Get the Runtime Version of the assembly.
PS C:\ > $Malware_Assembly.ImageRuntimeVersion
v4.0.30319
● 어셈블리의 진입점(entry-point) 메서드 가져오기
# Get the entry-point method of the assembly.
PS C:\ > $Malware_Assembly.EntryPoint
Name : MapVisitor
DeclaringType : WaitCallb.Filter.GlobalValueFilter
ReflectedType : WaitCallb.Filter.GlobalValueFilter
MemberType : Method
MetadataToken : 100663315
Module : WaitCallb.exe
IsSecurityCritical : True
IsSecuritySafeCritical : False
IsSecurityTransparent : False
MethodHandle : System.RuntimeMethodHandle
Attributes : PrivateScope, Private, Static, HideBySig
CallingConvention : Standard
ReturnType : System.Void
ReturnTypeCustomAttributes : Void
ReturnParameter : Void
IsGenericMethod : False
…
● 어셈블리의 모든 클래스 목록 나열하기
PS C:\ > $Malware_Assembly.GetModules().gettypes()|?{$_.isPublic -AND $_.isClass}
IsPublic IsSerial Name BaseType
-------- -------- ---------- ---------------
True False ReponseListState System.Windows.Forms.Form
True False MappingValueFilter System.Windows.Forms.Form
True False InterceptorExpressionMessage System.Windows.Window
True False Singleton System.Windows.Window
True False ObjectAttributePool System.Windows.Window
True False DicMethodAnnotation System.Windows.Application
True False OrderValueFilter System.Object
True False ParamsHelperRole System.Object
True False Definition System.Object
True False Tag System.Object
True False Getter System.Object
True False Pool System.Object
True False StubTokenizerImporter System.Object
True False MerchantExpressionMessage System.Object
True False MessageAttributePool Tourield.Messages.MerchantExpressionMessage
True False Interceptor Tourield.Messages.MerchantExpressionMessage
True False Bridge System.Object
…
● 어셈블리의 모든 리소스 이름 나열하기
# List all resources’ names of the assembly.
PS C:\ > $Malware_Assembly.GetManifestResourceNames()
WaitCallb.g.resources
WaitCallb.States.ReponseListState.resources
WaitCallb.Filter.MappingValueFilter.resources
aR3nbf8dQp2feLmk31.lSfgApatkdxsVcGcrktoFd.resources
Tourield.Properties.Resources.resources
● 주어진 리소스의 정보 가져오기
# Get Information of a given resource
PS C:\ >
$Malware_Assembly.GetManifestResourceStream("aR3nbf8dQp2feLmk31.lSfgApatkdxsVcGcrktoFd.reso
urces")
CanRead : True
CanSeek : True
CanWrite : False
Length : 5650
Capacity : 5650
Position : 0
PositionPointer :
CanTimeout : False
ReadTimeout :
● 로드된 어셈블리가 참조하는 모든 어셈블리 목록 나열하기
PS C:\ > $Malware_Assembly.GetReferencedAssemblies()
Version Name
------- --------------------------------
4.0.0.0 mscorlib
4.0.0.0 PresentationFramework
4.0.0.0 System.Windows.Forms
4.0.0.0 System
4.0.0.0 System.Drawing
4.0.0.0 PresentationCore
4.0.0.0 System.Xaml
4.0.0.0 WindowsBase
4.0.0.0 System.Core
● 주어진 클래스에 선언된 메서드 목록 나열하기
PS C:\ > $MyClass = $Malware_Assembly.GetModules().gettypes()|?{$_.Name.equals("Interceptor")}
# List declared methods for a given class.
PS C:\ > $MyClass.DeclaredMethods | Out-String -stream | Select-String "^Name”
Name : InsertProcess
Name : RunProcess
● 주어진 클래스에 대한 public 메서드 목록 나열하기
# List public methods for a given class
PS C:\ > $MyClass.GetMethods() | Select-Object Name
Name
--------
Equals
GetHashCode
GetType
ToString
● 비공개 인스턴스 목록 나열하기
# List return non-public, instance methods.
PS C:\ > $MyClass.GetMethods([Reflection.BindingFlags]::NonPublic -bor
[Reflection.BindingFlags]::Instance) | Select-Object Name
Name
----
Finalize
MemberwiseClone
● 주어진 클래스에 대한 선언된 생성자 목록 나열하기
# List declared constructors for a given class.
PS C:\ > $MyClass.DeclaredConstructors | Out-String -stream | Select-String "^Name"
Name : .ctor
● 주어진 클래스에 대한 모든 멤버 타입 목록 나열하기
# List all member types for a given class.
PS C:\ > $MyClass.GetMembers() | ft memberType, Name -auto
Member Type Name
------------- ---------------
Method Equals
Method GetHashCode
Method GetType
Method ToString
Constructor .ctor
Field m_Merchant
Field _Server
Field _Listener
Field producer
Field database
● Public 인스턴스 메서드 목록 가져오기
# Get a list of public instance methods.
PS C:\ > $MyClass.GetMethods([Reflection.BindingFlags]::Public -bor [Reflection.BindingFlags]::Instance)
| Select-Object Name | ft -HideTableHeaders
Equals
GetHashCode
GetType
ToString
● 비공개 인스턴스 메서드 목록 가져오기
# Get a list of non-public instance methods.
PS C:\ > $MyClass.GetMethods([Reflection.BindingFlags]::NonPublic -bor
[Reflection.BindingFlags]::Instance) | Select-Object Name | ft -HideTableHeaders
Finalize
MemberwiseClone
● 비공개 정적 메서드 목록 가져오기
# Get a list of non-public static methods.
PS C:\ > $MyClass.GetMethods([Reflection.BindingFlags]::NonPublic -bor
[Reflection.BindingFlags]::Static) | Select-Object Name | ft -HideTableHeaders
InsertProcess
RunProcess
● Public 정적 메서드 목록 가져오기
# Get a list of public static methods.
PS C:\ > $MyClass.GetMethods([Reflection.BindingFlags]::Public -bor [Reflection.BindingFlags]::Static) |
Select-Object Name | ft -HideTableHeaders
● 비공개 인스턴스 필드 목록 가져오기
# Get a list of non-public instance fields.
PS C:\ > $MyClass.GetFields([Reflection.BindingFlags]::NonPublic -bor
[Reflection.BindingFlags]::Instance) | Select-Object Name | ft -HideTableHeaders
● 비공개 정적 필드 목록 가져오기
# Get a list of non-public static fields
PS C:\ > $MyClass.GetFields([Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Static) |
Select-Object Name | ft -HideTableHeaders
분석 중에 .NET 악성코드의 어떤 메서드를 호출해볼 수 있지만 이 주제는 다음 아티클에서 다시 다룰 예정입니다.
.NET 악성코드 분석 중에 동적 어셈블리를 마주칠 수 있는데 이는 정적 어셈블리와 개념이 매우 다릅니다. 정적 어셈블리는 디스크에서 파일로 로드되는 반면, 동적 어셈블리는 System.Reflection.Emit이라는 특별한 네임스페이스를 사용하여 메모리에서(런타임에) 생성됩니다. 이 네임스페이스는 런타임에 어셈블리, 모듈을 생성하고 CIL 구현을 수행하는 등의 기능을 제공합니다.
System.Reflection.Emit 네임스페이스에는 다음과 같은 클래스 멤버들이 있습니다.
- AssemblyBuilder: 런타임에 어셈블리를 생성하는데 사용됩니다.
- TypeBuilder: 모듈 내에서 인터페이스, 델리게이트, 구조체 그리고 물론 클래스를 생성하는 것을 제어합니다.
- ModuleBuilder: 주어진 어셈블리 내에서 모듈을 정의하는데 사용합니다.
- MethodBuilder: 메서드/생성자를 정의하고 나타냅니다.
- EnumBuilder: .NET 열거형 타입을 생성하는데 사용됩니다.
ILGenerator 클래스를 사용하고, Emit, EmitCall, BeginScope, DeclaredLocal과 같은 관련 메서드를 사용하여 원시 CIL opcode를 내보내고 동적으로 전체 어셈블리를 생성해야 합니다.
이 아티클은 프로그래밍에 관한 내용은 아니지만, 주제에 대해 조금 더 배우고자 하는 독자들에게 도움이 될 추가 정보는 다음과 같습니다.
- System.Reflection.Emit NuGet 패키지가 설치되어야 합니다.
- System.Reflection과 System.Reflection.Emit 네임스페이스가 임포트되어야 합니다.
- 어셈블리의 고유 아이디를 설명하기 위해 AssemblyName 클래스의 AssemblyName() 생성자를 사용해야 합니다. (ex: MASassembly)
- 어셈블리 생성: var mybuilder = AssemblyBuilder.DefineDynamicAssembly(varMASassembly, AssemblyBuilderAccess.Run)
주의) varMASassembly는 "MASassembly"라는 어셈블리 정의를 포함하는 AssemblyName 변수입니다. - 모듈의 이름 정의: ModuleBuilder mymodule = mybuilder.DefineDynamicModule(“MASassembly”)
- "MASclass"라는 public 클래스 설정: TypeBuilder masClassExample = mymodule.DefineType(“MASassembly.MASClass”, TypeAttributes.Public)
이 시점부터 .cctor()를 정의하고, 새로운 변수들을 설정하며, GetILGenerator()와 Emit() 메서드를 사용하여 코드를 내보낼 수 있습니다.
위 정보는 .NET 악성코드 분석에도 도움될 수 있고, 동적 어셈블리와 관련된 명령어를 탐지하는데 더 쉽게 만들 수 있습니다. 이는 많은 전문가들에게 잘 알려지지 않은 주제입니다.
프레임워크를 통한 접근 방식을 선호한다면, 훌륭한 Mono를 사용하여 .NET 바이너리에서 유용한 정보를 얻을 수 있습니다.
Linux (REMnux / Ubuntu 20.04)에 설치하려면 다음 명령어를 따르세요:
- sudo apt install gnupg ca-certificates
- sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys
3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF - echo "deb https://download.mono-project.com/repo/ubuntu stable-focal main" | sudo tee
/etc/apt/sources.list.d/mono-official-stable.list - sudo apt update
- sudo apt install mono-devel
- sudo apt install mono-complete
Windows 설치 방법은 다음과 같습니다:
- Download it from: https://download.mono-project.com/archive/6.12.0/windows-installer/mono6.12.0.107-x64-0.msi
- Add “C:\Program Files\Mono\bin” to the PATH environment variable.
설치가 완료되면 아래와 같은 메타데이터 테이블과 추가 정보를 나열할 수 있습니다.

물론 메타데이터 테이블에서 훨씬 더 많은 정보를 얻을 수 있으며 아래에서 보시다시피 쉽게 임베디드 리소스를 다운로드할 수 있습니다.


간단한 관찰 결과는 다음과 같습니다.
- ImplMap 테이블은 비어 있는 것 같습니다. 이 현상은 패커, 난독화, 동적 어셈블리 또는 여러 다른 가능한 이유들로 인한 결과일 수 있습니다.
- 모든 메서드, 인터페이스, 타입 정의 및 매니페스트의 내용을 나열할 수 있습니다.
- 모든 관리되는 리소스(managed resource)를 덤프할 수 있습니다.
- 모듈 이름과 exported 타입과 같은 유용한 정보들이 있으며 이는 어셈블리의 모듈 내에 정의된 여러 타입의 항목들을 포함하고 외부 어셈블리로 내보냅니다.
언급된 절차는 주어진 .NET 악성코드에 대한 분석을 시작하기 전에 첫 번째 정보를 수집하는데 유용하며, 우리가 무엇을 예상해야 할지에 대한 아이디어를 제공합니다. 물론 코드 분석을 위한 정적 분석과 주로 동적 분석이 중요하며 dnSpy (또는 dnSpyEx)와 같은 도구는 훌륭한 작업을 수행할 수 있습니다.
[Threat information]
이번 아티클에서 분석할 샘플의 SHA256은 다음과 같습니다: 7cb92356a0170028fabc20f0cb9736b149efab01824ab1173b3277340a6a2ec4
Malware Bazaar에서 다운로드할 수 있습니다.

Malware Bazaar에서 샘플에 대한 세부정보를 확인한 결과입니다.


Virus Total 평가는 다음과 같습니다.

해당 샘플에 대해 Figure 7, 8, 9, 10에서 확인한 좋은 정보는 다음과 같습니다.
- 원래 이름은 tup.exe인 것 같습니다.
- 코드 인젝션을 수행할 것으로 보입니다. (WriteProcessMemory + SetThreadContext)
- 실행 중에 권한 상승을 수행하는 것 같습니다. (AdjustPrivilegeToken)
- 후킹 기법을 사용하는 것으로 보입니다. (SetWindowsHookEx)
- 코드를 주입할 대상을 찾기 위해 프로세스를 열거하는 것 같습니다. (EnumeratesProcess)
- WMI가 악성코드에서 사용됩니다. 이는 무한한 가능성이 있습니다: 안티-VM, 안티-디버깅 등
- 새 프로세스가 실행되며 이는 네이티브 프로세스일 가능성이 있습니다.
- 파일이 생성됩니다.
- 이 샘플은 AgentTesla(또는 그 변종 중 하나)이며, .NET(mscoree.dll)으로 작성되었습니다.
- .text 섹션의 엔트로피가 너무 높습니다(7.91). 그래서 뭔가 숨겼거나 패킹된 것일 수도 있습니다. 그러나 기억해야될 것은 .NET에서는 임베디드 리소스가 .text 섹션에 포함되므로 높은 엔트로피가 임베디드 리소스를 반영한 것일 수 있습니다.
위의 목록은 단지 가능성에 불과한 첫 번째 정보입니다. 악성코드는 아마도 패킹되거나 난독화되었을 가능성이 높으므로, 발견해야할 많은 아티팩트가 존재합니다. .NET 악성코드에서 패커/난독화 존재를 확인하려면 Exeinfo PE 또는 DiE를 사용할 수 있습니다. 이들은 패커와 난독화 도구의 존재를 확인하는데 유용한 훌륭한 도구입니다.

두 도구 모두 .NET Reactor의 존재 가능성을 알려주지만 코드 분석을 통해 패커의 존재 여부를 확실히 확인해야 합니다. .NET 샘플을 분석할 때 항상 추천되는 마지막 도구는 pestudio입니다.

pestudio에서 사용된 블랙리스트 함수, 라이브러리, 리소스의 첫 번째 바이트를 시각화 및 덤프하고, 매니페스트를 나열하는 등의 유용한 정보를 수집할 수 있습니다. 상당히 가치가 있는 정보들입니다.
[Analysis]
우리가 위협을 분석하고 종합적으로 이해하기 위한 시작점입니다. .NET 분석에 대해 기억하시겠지만 대부분의 샘플에는 임베디드 리소스(managed resource)가 포함되어 있으며, 이는 실시간으로 언팩되는 바이너리(managed moudle 또는 바이너리)일 수 있습니다. 그중 몇몇은 외부 리소스를 다운로드하는 단순 다운로더로 작동하며 이는 실행되는 실제 악성 페이로드입니다.
그렇더라도 이 부분이 중요한 지점입니다. .NET 악성코드를 언팩하는 세 가지 잘 알려진 접근 방법이 있습니다.
- dnSpy/dnSpyEx와 같은 .NET 전용 디버거 및 어셈블리 편집기를 사용하고 수동으로 분석을 진행하는 방법
- 네이티브 디버거를 사용하고 몇 가지 관련 트릭을 사용하여 반자동으로 수행하는 방법
- 전문도구를 사용하여 이 작업을 자동으로 수행하는 방법
실제로 "언패킹"이라는 용어는 리소스가 단순히 인코딩되었거나(혹은 평문으로 있을 수 있기 때문에) 정확하지 않을 수 있지만 그래도 의미의 손실 없이 이 용어를 계속 사용할 수 있습니다. 이전에 제시된 몇 가지 개념을 강조하기 위한 동기로 첫 번째 접근 방법을 사용할 것이며 다음 아티클에서는 다른 두 가지 가능성을 시도할 것입니다.
모든 디버깅 세션 중에 시스템이 감염될 수 있고, 아마도 감염될 것이므로 네트워크 통신을 비활성화하고, 공유 폴더도 비활성화하고, 스냅샷 찍는 것을 잊지 마세요.
이제 dnSpy에서 악성코드(mas_4.bing)를 열고 샘플에 대해 몇 가지 메모를 해봅시다.

우리는 다음과 같은 사항을 알 수 있습니다.
- 5개의 임베디드 리소스가 있습니다.
- Entry point는 WaitCallb.Filter.GlobalValueFilter.MapVisitor입니다.
- Type References를 열면 다음 내용을 볼 수 있습니다.
- Classes
- Enumerations
- Structures
- Delegations
- 어셈블리 이름은 WaitCallb입니다.
- 모듈 이름은 WaitCallb.exe입니다.
- 2개의 <Module> 클래스(<Module> @02000001, <Module>{FFAD4D1F-94F7-4211-ACBAFABE281ED9F5})가 있습니다. 이 클래스들은 CLR의 기능인 모듈 initializer를 포함할 수 있습니다. 결국 이는 모듈의 생성자(constructor)로 작동합니다. 일반적으로 <Module>의 정적 생성자는 어셈블리 로딩 중에 한 번만 실행되지만 클래스는 자체 클래스 생성자(.cctor)를 가질 수 있습니다.
entry point를 검사한 결과 우리는 다음과 같은 정보를 얻었습니다.

위 코드에 따르면 분석할 만한 몇 가지 흥미로운 메서드가 있습니다.
- Application.EnableVisualStyles()
- Application.SetCompatibleTextRenderingDefault(false)
- RecordParam.SelectConfig()
- Application.Run(new ReponseListState())
이 메서드들 각각은 수백 줄의 코드로 이어질 수 있으며 분석하는데 상당한 시간이 걸릴 수 있습니다. 실행 흐름을 제어하는 변수(num)가 처음에는 4로 설정되어 있기 때문에 가장 먼저 실행되는 함수는 EnableVisualStyles()이며, 이 메서드는 자체적으로 로드된 어셈블리의 전체 경로를 가져옵니다. 또 이 메서드는 Application.EnableVisualStylesInternal를 호출합니다.

코드에서 확인할 수 있듯이 이 메서드는 두 개의 인수를 사용합니다. 하나는 Assembly Location을 정확히 받는 text(line 994)이고, 다른 하나는 101(line 1002)입니다. 이 메서드로 들어가면 다음과 같습니다.

위 코드를 통해 다음 사실을 알게 되었습니다.
- 첫 번째 인수는 Assembly 파일의 이름입니다.
- 두 번째 인수는 네이티브 리소스 ID입니다(이 경우: 101).
- 특이한 클래스인 UnsafeNativeMethods를 사용하고 그 클래스의 CreateActivationContext() 메서드를 호출하고 있습니다.
UnsafeNativeMethods 클래스는 네이티브 메서드를 호출하고 액세스하는데 사용되며 코드에서는 CreateActivationContext()를 호출하여 메모리 내에 데이터를 설정하고, 구체적인 DLL 모듈이나 COM 객체 인스턴스 로드를 위한 정보를 저장할 데이터 구조체를 설정하고 있습니다. 물론 활성화 컨텍스트와 관련된 함수들은 많은데 예를 들어 ActivateActCtx(), QueryActCtxW(), ReleaseActCtx() 등이 있습니다.
Application.EnableVisualStyles()가 호출되고 나면 num 변수는 3으로 설정되고 그 후 Application.SetCompatibleTextRenderingDefault()와 RecordParam.SelectConfig() 같은 두 개의 메서드가 호출됩니다. 이들에 대해서는 특별히 언급할 만한 사항은 없습니다.
브레이크 명령어가 실행되면 다음으로 호출되는 메서드는 Application.Run(new ReponseListState())(Figure 14 - line 47)이며 이 메서드가 우리 분석 경로를 명확하게 제시해줍니다. 이 진입점 클래스(GlobalValueFilter)에 대한 마지막 메모는 QueryProcess()와 SearchProcess() 메서드가 단지 "true"를 반환하는 것 외에는 아무것도 하지 않는다는 점입니다. ReponseListState 클래스는 다음과 같은 인스턴스 생성자를 가집니다.

다시 한 번 코드의 어느 부분이 실행될지 결정하는 상태 변수(num)가 있습니다. 처음에는 num이 6으로 설정되어 있어 다음으로 호출될 메서드는 RecordParam.SelectConfig()와 ReponseListState.PostProcess()입니다.
for 루프 내에서도 여러 메서드들이 호출되는 것을 볼 수 있습니다.
- RecordParam.SelectConfig()
- ReponseListState.PostProcess( )
- Invoke(obj, parameters)
- GetMethod("InvalidCast")
- CompareVisitor( )
어쨌든 num 변수는 6으로 설정되어 있으므로 다음에 실행될 메서드는 다음과 같습니다.
- RecordParam.SelectConfig (line 34)
- ReponseListState.PostProcess (line 36)
- 그리고 num은 2로 설정되고(line 35), 실행은 IL_0C 레이블로 점프합니다.
SelectConfig() 메서드는 아무 작업도 하지 않으며 PostProcess()는 "false"만 반환합니다. 그래서 "continue" 명령어(line 40)가 실행되고 코드 흐름은 어쨌든 IL_0C 레이블로 넘어갑니다. 따라서 다음으로 실행될 메서드는 CompareVisitor()이지만 그 전에 인스턴스 생성자(.ctor)가 실행됩니다.
만약 CompareVisitor() 내부로 들어가면 많은 그래픽 관련 메서드들이 실행되는 긴 switch-case (17개의 case)가 있으며 겉보기에는 아무 이상한 점이 없습니다. 그러나 두 번째 단계(다른 .NET 모듈)로의 트리거가 이 메서드 안에 숨겨져 있습니다. 왜냐하면 그 직후에 다음 명령어가 있기 때문입니다: this.Text = “Form 1” (line 258). "Text" 속성은 접근자/변경자와 연결되어 있으며 이는 292번째 줄에서 다른 접근자/변경자로 오버라이드됩니다.

dnSpy를 사용하는데 익숙하지 않는 사람은 어떤 메서드를 우클릭한 후 Analyze (CTRL+SHIFT+R)를 선택하면 해당 메서드가 오버라이드한 메서드, 오버라이드된 메서드, 의존성(Uses), 의존하는 메서드(Used by) 목록을 확인할 수 있습니다. 위 그림에서는 저는 오버라이딩하는 뮤테이터(mutator)의 관점에서 화면을 보여줬지만 아래와 같이 오버라이트된 뮤테이터의 관점에서 동일한 분석을 할 수도 있습니다.

실제로 WaitCallb.States.ReponseListState.set_Text(string) : void @06000005 메서드는 System.Windows.Forms.Form.set_Text(string) : void @0600234C (line 2260)을 오버라이드하며 이 메서드는 public virtual string Text 속성의 기본 뮤테이터를 3784번 줄에서 호출합니다.
ResetVisitor()가 호출되면 ResourceManager 클래스가 인스턴스화되고 관리되는 리소스(managed resource) "Vargo"가 array 변수에 로드됩니다. 이 array 변수는 이제 로드되어 실행될 인코딩된 .NET 모듈(두번째 단계)을 포함합니다.
Vargo 리소스가 디코딩되기 전에 악성코드는340번 줄에서text 변수를 "P7C455RF8EBCYHA8URJ585" (XOR 키)로 설정하고, 349번 줄에서 num2를 92182 (리소스 크기)로 설정합니다. 마지막으로 디코더가 ResetVisitor()로부터 호출됩니다.

물론 우리는 Python 또는 Powershell 스크립트를 작성하여 이 리소스를 수동으로 디코딩할 수 있습니다. 하지만 암호화된 리소스가 많이 있을 수 있기 때문에 그럴 필요는 없습니다. 따라서 예를 들어 323번 줄에 breakpoint를 설정하고 디버거를 사용하여 동적 접근 방식을 따르는 것이 시간을 절약하는데 가장 좋은 방법입니다.
dnSpy 단축키에 대해 잘 모른다면 다음이 가장 중요한 단축키입니다.
- F11: stepping-in
- F10: stepping over
- SHIFT+F11: stepping out
- F9: set / clear a breakpoint
단축키를 사용하고 싶지 않다면 Debug 메뉴를 통해 동일한 명령을 사용할 수 있습니다.따라서 323번 줄에서 breakpoint를 설정하고 디버깅을 시작하세요. 디버거는 리소스가 디코딩되기 전에 정확히 323번 줄에서 실행을 멈추게 되므로 잠시 기다리면 됩니다. 이 동일한 메서드(ResetVisitor())에는 370번 줄에 Vargo 리소스를 실제로 로드하는 중요한 명령이 있습니다.
ReponseListState.DefineVisitor(Assembly.Load(array), 11);
앞에서 Assembly.Load() 메서드에 대해 본 것과 같이, 이 시점에서 Load() 메서드가 디코딩된 리소스(Vargo)를 로드하고 이 새로운 모듈의 메서드를 사용할 것이라고 예상할 수 있습니다.
만약 dnSpy 환경에서 Modules 창이 표시되지 않는 경우 아래와 같이 Debug → Windows → Modules로 이동하면 됩니다.


아래에 새로운 모듈(SharpStructures)이 로드되기 전과 후의 모듈 목록을 보여드리겠습니다.

마지막 줄에 SharpStructures 모듈이 메모리 내에서 로드된 것을 알 수 있습니다(해당 열에 "yes"라고 표시됨). 따라서 이 모듈을 우클릭한 후 "Save Module"을 선택하여 SharpStructures.dll로 쉽게 저장할 수 있습니다.

기억해야될 것은 디버거는 370번 줄에서 멈춰두어야 한다는 것입니다. 먼저 저장된 모듈을 확인할 예정이기 때문입니다. 앞서 언급했듯이 많은 .NET 악성코드 샘플은 메인 페이로드가 드러나기 전에 여러 단계를 거치며 보통 이런 중간 단계는 암호화되어 있습니다. 그래서 진행하기 전에 이를 확인해야하며 추천 도구 중 하나는 Exeinfo PE입니다.

추출된 모듈은 SmartAssembly Obfuscator로 난독화되어 있습니다. 코드 난독화가 있는지 확인하려면 아래와 같이 dnSpy에서 로드된 모듈을 확인하세요.

코드가 실제로 난독화되었음을 나타내는 여러 유니코드 표기를 확인할 수 있습니다. 또한 왼쪽에서 SmartAssembly와 관련된 속성들이 있음을 확인할 수 있습니다.
이 코드의 난독화를 해제할 수 있는 여러 가지 기술과 도구가 있지만 그중 de4dot이 가장 추천되는 도구 중 하나입니다. 물론 de4dot은 많은 다양한 유형의 .NET 악성코드 샘플을 난독화 해제/압축 해제할 수 있지만 모든 샘플을 처리할 수는 없고 일부 경우에는 특정 언패커가 필요할 수도 있습니다. 그렇지만 어려운 작업은 아닙니다. 어쨌든 추출된 모듈의 난독화를 해제해보겠습니다.

de4dot을 사용하여 추출된 모듈의 난독화를 해제하면 다음과 같습니다.

Figure 26의 코드보다 훨씬 낫습니다. 이 코드를 개선할 수 있는 다른 방법들이 있지만 아직 완벽하지는 않지만 지금은 분석하기에 충분합니다. 여기서 강조하고 싶은 점은 우리가 수동으로 정리하지 않고 (de4dot을 사용한 것처럼) 진행할 수 있다는 점입니다. 왜냐하면 샘플이 자체 디코딩 루틴을 가지고 있어 우리가 그 작업을 대신할 수 있기 때문입니다. 하지만 디버깅은 조금 더 복잡해질 수 있습니다.
악성코드로 돌아가서 메모리에서 두 번째 단계(.NET 모듈)를 추출한 후 디버깅을 계속하면, 곧 MapVisitor()가 호출되고 그 후 ReponseListState 클래스의 ReponseListState() 생성자가 호출됩니다. 스택 창을 확인하면 다음과 같습니다.

GetMethod() 함수는 InvalidCast 메서드를 가져오려고 시도하고 이 메서드는 새로 추출된 .Net 모듈의 일부입니다(이미 예상했듯이 난독화되어 있습니다).

이런 경우를 처리하는 잘 알려진 접근 방식은 메모리에서 난독화된 모듈의 내용이 로드되기 전에 우리만의 난독화 해제된 모듈로 교체하는 것입니다. 이 방법은 이상하게 보일 수 있지만 난독화 문제를 처리하는 것보다 디버깅을 훨씬 쉽게 해주므로 실용적인 결과를 제공합니다. 어떻게 할 수 있을까요?
여러 가지 옵션이 있어 아마 취향의 문제일 것입니다. 일부 전문가들은 16진수 편집기와 Notepad++를, 다른 전문가들은 CyberChef를 선호합니다. 개인적으로 저는 후자를 선호합니다. 그리고 두 번째 단계 모듈을 로드하는 명령어에 breakpoint를 설정해야 합니다. 이것은 Figure 22에서와 같은 명령어입니다. 또한 두 번째 단계에서 메서드를 호출하는 첫 번째 명령어에도 설정해야 합니다.
ReponseListState.DefineVisitor(Assembly.Load(array), 11); (from ResetVisitor() method)
methodInfo = ((Type)ReponseListState.param).GetMethod("InvalidCast"); (from ReponseListState() method)
CyberChef를 사용하여 두 번째 단계의 정리된 버전(de4dot.exe에서 나온 결과)을 로드하고, From Hex 레시피를 사용하면 다음과 같이 모든 공백을 제거할 수 있습니다.

16진수 내용을 클립보드에 복사하세요.(위 그림의 4번째 아이콘)
다시 dnSpy를 디버깅 모드에서 실행하세요(디버그 → 디버깅 시작 또는 플레이 버튼 클릭) 앞서 언급한 두 개의 breakpoint를 설정한 것을 기억해야 합니다.
디버거는 첫 번째 명령어인 ReponseListState.DefineVisitor(Assembly.Load(array), 11);에서 멈추고, 모듈 창을 보면 SharpStructures 모듈(두 번째 단계)이 아직 로드되지 않았음을 알 수 있습니다. Locals 창에서 PE 형식의 내용을 담고 있는 array 변수를 우클릭하고 Show In Memory menu → Memory 1을 선택하면 다음 두 화면을 볼 수 있습니다.


실행 파일의 시작 부분(4D 5A)에 커서를 놓고 우클릭 → Paste Special → Paste를 선택하세요. CyberChef에서 클립보드로 복사한 정리된 모듈의 모든 내용이 메모리 영역을 덮어씁니다.
디버깅 세션을 계속 진행하면 실행은 두 번째 breakpoint (methodInfo = ((Type)ReponseListState.param).GetMethod("InvalidCast"))에서 멈출 것입니다. 또한 정리된 모듈이 로드되어야 하며 InvalidCase() 메서드를 시각화하면 아래와 같은 그림을 보게될 것입니다.

지금까지 모든 것이 잘 진행되고 있습니다. 우리는 난독화된 모듈을 메모리에서 로드되기 바로 전에 정리된 모듈로 교체했습니다. 여기서 중요한 몇 가지 포인트가 있습니다.
- 분석을 진행하기 전에 항상 인스턴스 생성자(.ctor)와 클래스 생성자(.cctor)를 찾아야 합니다.
- InvalidCast 메서드의 시작 부분(SharpStructures.Sorting.SortHelper 클래스)을 breakpoint로 설정하여 실행 제어를 유지해야 합니다.
- 진행하기 전에 가상머신의 스냅샷을 찍는 것이 권장됩니다.
- 우리 이전에 정리된 모듈에서 Exeinfo PE에서 보여준 것처럼 새로운 난독화된 코드가 있을 수 있습니다.

같은 이름을 가진 두 개의 메서드가 있고 서로 다른 클래스에 속하므로 올바른 InvalidCast 메서드를 어떻게 찾을 수 있는지 궁금해할 수 있습니다.
- SharpStructures.Main.SortHelper 클래스
- SharpStructures.Sorting.SortHelper 클래스
디버깅을 계속 진행하면(F10 - step over) FullName 속성에서 답이 자동으로 나옵니다.

따라서 올바른 InvalidCast() 메서드에 breakpoint를 설정하고 디버깅을 계속 진행하세요(F10 - step over). 그러면 아래 표시된 라인에서 InvalidCast()로의 전환이 발생할 것입니다.

실제 변환은 methodBase 변수에 있는 Invoke() 메서드에 의해 수행되며 이 변수는 올바른 InvalidCast() 메서드를 포함하고 있습니다. (올바른 InvalidCast() 메서드: Invoke → Invoke → UnsafeInvokeInternal → InvalidCast)
목표로 하는 SharpStructures.Sorting.SortHelper.InvalidCast 메서드는 다음과 같은 명령어들을 포함하고 있습니다.

첫 번째 명령어들을 분석하면 흥미로운 세부사항들을 발견할 수 있는데 그것들을 고려해야 합니다.
- 처음 두 명령어에서 딜레이(delay)가 설정됩니다.
- Class0라는 클래스가 있으며 이 클래스에는 중요한 메서드들이 포함되어 있습니다.
- Class0의 메서드 중 분석해야 할 것들: DemandResources, smethod_[1,4,5,6], ConstructionResponse
- 마지막의 Exit() 메서드
- 언급된 모든 메서드 아래에 숨겨진 여러 하위 메서드들이 존재합니다.
이제 2단계 분석을 시작하겠습니다. 추천하는 첫 번째 단계는 Class0 클래스를 확인하여 사용 가능한 메서드들과 관련된 세부 정보를 얻는 것입니다.

Class0 내용에 대한 관찰된 결과는 다음과 같습니다(Figure 39).
- smethod_0과 smethod_1은 배열을 조작(manipulating)하며 동일합니다.
- smethod_2와 smethod_4는 메서드를 호출하는데 사용되며 동일합니다.
- smethod_5와 smethod_7은 문자열을 생성하며 동일합니다.
- smethod_3과 smethod_6은 어셈블리를 로드하며 동일합니다.
InvalidCast()에 대한 우리의 노트를 바탕으로 다음과 같이 가정할 수 있습니다.
- 문자열 생성 (smethod_5)
- 배열 조작 (smethod_1)
- 어셈블리 로딩 (smethod_6)
- 어셈블리에서 메서드 호출 (smethod_4)
InvalidCast() 메서드(Figure 38)로 돌아가면, 또 다른 흥미로운 메서드인 DemandResource()가 있으며 그 내용은 다음과 같습니다.

DemandResources()는 ResourceManager 클래스를 인스턴스화하여 리소스에 접근하고 smethod_5에서 생성된 주어진 리소스 이름을 읽습니다.
ConstructionResponse() 메서드(Figure 41)를 살펴보면 이는 InvalidCast 메서드(Figure 38)에서 26번 줄에서 호출되며 다음과 같은 세부사항들을 제공합니다.
- smethod_1()에서 바이트 배열을 받습니다.
- 해당 바이트들을 빅 엔디안 바이트 순서를 사용하는 UTF-16 형식으로 덮어씁니다.
- 마지막 바이트와 숫자 112를 사용하여 XOR 연산을 수행합니다.
- 새 바이트 배열(이름: array)을 할당합니다.
- 각 바이트를 읽고 두 번의 XOR 연산을 수행합니다.
- 결과 바이트 배열의 크기를 조정합니다.
- 최종 배열을 반환합니다.
ConstructionResponse() 메서드의 내용은 아래와 같습니다.

지금까지 일어나고 있는 일이 대강 파악되었습니다.
- 바이트 시퀀스가 리소스에서 읽혀지고(s_method_5), 그 리소스의 이름은 smethod_1 반환값으로 주어집니다.
- 모든 읽어들인 바이트는 ConstructionResponse()에 의해 디코딩됩니다. 결과 배열의 내용은 모듈(세 번째 단계)입니다.
- 결과 배열은 smethod_6()에 의해 로드됩니다.
- 로드된 어셈블리에서 모든 타입(클래스, 인터페이스, 배열, 값, 열거형 등)이 반환되며 그 중 하나(클래스)가 선택됩니다.
- 같은 방식으로 GetTypes()로 반환된 타입에 대해 모든 public 메서드가 GetMethods()를 사용해 반환되고 그 중 하나가 선택됩니다.
- 마지막으로 선택된 메서드는 smethod_4()의 Invoke() 메서드로 호출됩니다.
따라서 합리적인 접근 방식은 다음과 같습니다.
- Breakpoint 설정(F9)하기
- InvalidCast()의 26번째 줄에서(Figure 38) byte_ 배열의 내용을 분석할 수 있고 아마 새로운 모듈을 찾을 수 있을 것입니다.
- InvalidCast()의 27번째 줄에서 smethod_6()가 호출되는 부분
- smethod_6() 내에서 Assembly.Load()에 대한 호출
- 발견된 메서드가 호출되기전 30번째 줄
- byte_ 배열 변수에 로드된 모듈을 추출하기
- Exeinfo PE 또는 Die를 사용해 난독화기/패커가 있는지 확인하기
- 난독화/패커가 있다면 de4dot 또는 다른 난독화 해제 도구를 사용해 이를 제거하기
- 클래스 이름(29번째 줄)과 호출되는 메서드(30번째 줄)를 확인하기
- 저장된 모듈의 이름을 바꾸고 메모리에서 교체하기
- 이 설정을 완료한 후 가상머신의 스냅샷을 찍는 것이 좋습니다. 이 절차를 반복할 수도 있기 때문입니다.
물론 4개의 중단점을 설정할 필요는 없고 F10(step over)을 사용해 코드를 실행하는 것만으로 충분합니다. 어쨌든 자신에게 가장 좋은 접근 방식을 결정할 수 있습니다. 따라서 우리는 다음과 같이 breakpoint를 설정하였습니다.

코드를 실행한 결과, 다음과 같은 byte_ 변수에 로드된 새로운 단계에 대한 정보를 얻을 수 있습니다.

우선 _byte 배열을 오른쪽 클릭 → Save을 클릭합니다. 이름을 선택하세요(stage_3.bin으로 했는데 이름을 변경할 예정입니다). 그리고 저장합니다. 아래에 표시된 대로 Exeinfo PE 또는 Die를 사용하여 난독화/패커 존재를 확인합니다.

추가 정보입니다.
- 새로 로드된 모듈의 이름은 DotNetZipAdditionalPlatforms.dll이고, 버전은 v2.0.50257입니다.
- 타입 변수(클래스)는 "LajJueXX7RvrQwTLPl.XcuCxUwDbNNwbx89AI" 문자열을 담고 있으며 이는 난독화의 지표입니다.
- 호출되는 메서드의 이름은 RrRUhxJmfM()입니다.

알게된 것에 따르면 dnSpy를 사용해 메모리에 로드되고 추출된 모듈은 .NET Reactor를 사용해 난독화되었습니다. 대부분의 난독화 도구들이 클래스 생성자(.cctor)나 인스턴스 생성자(.ctor)를 사용해 정보를 조작하거나, 심지어 난독화를 해제하거나 언팩하는 경우도 많기 때문에 난독화된 버전을 확인할 가치가 있습니다.

위 그림에 나와있지 않은 많은 클래스들이 존재하지만 각 클래스에는 해당하는 .cctor() 메서드가 있습니다. 또한 XcuCxUwDbNNwbx89AI 클래스는 자체 클래스 생성자와 인스턴스 생성자가 있으며 이 인스턴스 생성자는 .ctor() 생성자를 호출합니다. 그리고 InvalidCast()에서 호출되는 함수(RrRUhxJmfM)도 난독화되어 있으며 여러 개의 switch-case가 있습니다(위에 나와있진 않습니다).
이제 유용한 정보를 얻었으므로 추출된 모듈을 난독화 해제(3번째 단계)하여 메모리에 로드된 난독화된 모듈을 이 모듈로 교체할 수 있습니다. 다시 한 번 de4dot을 사용하여 작업을 수행하겠습니다.

이번에는 난독화 해제 과정이 완벽하지는 않지만 우리 목적에는 충분합니다.
이전과 유사한 단계를 반복하여 메모리에서 난독화된 모듈을 난독화 해제된 모듈로 교체할 수 있으며, 이를 가장 잘 수행하는 방법은 smethod_6에 의해 모듈에 로드되기 전에 byte_ array 변수를 조작하는 것입니다.
따라서 절차를 다시 한 번 반복해봅시다.
- de4dot을 사용하여 저장된 모듈을 난독화 해제합니다.
- CyberChef에서 ToHex 레시피를 선택한 후 공백 없이(no spaces) 변환합니다.
- CyberChef에서 결과를 클립보드로 복사합니다.
- byte_ array 변수에 대해 오른쪽 클릭 → Show in Memory Windows → Memory 1를 선택합니다.
- 실행 파일의 시작 부분(MZ / 4D 5A)에서 우클릭 → Paste Special → Paste를 선택합니다.
- F10(step over)을 사용하여 28번째 줄(어셈블리가 로드된 후)까지 디버깅을 계속합니다. 그리고 실제로 로드되었는지 확인합니다.
- 30번째 줄까지 실행을 진행하고 세 번째 단계에서 호출되는 클래스 이름과 메서드 등의 정보를 수집합니다.
메모리에서 byte_ array 변수의 내용을 교체하고 실행을 30번째 줄까지 step over하면 다음 그림을 볼 수 있습니다.

위 그림에서 알 수 있는 사실은 다음과 같습니다.
- type 변수는 클래스 타입을 가지고 있습니다.
- 클래스 이름은 Class12이고 네임스페이스는 ns0에 속합니다.
- 29번과 30번 줄에서 목표로 하는 메서드는 smtethod_10입니다.
- Class12는 자체 .cctor (클래스 생성자)를 가지고 있습니다.
- GetProcessAddress()와 LoadLibrary()와 같은 몇몇 native API 참조가 등장했고 다른 API도 존재합니다.
따라서 두 개의 breakpoint를 설정해야 합니다.
- line 8) .cctor() 클래스 생성자
- line 6) smethod_10()
breakpoint를 설정한 후 F10(step over)을 사용하여 실행을 진행하세요.
순조롭게 진행되면 .cctor()에서 breakpoint가 히트됩니다. 3단계에 오신 것을 환영합니다. 이 단계에서 호출되는 첫 번째 메서드는 아래 그림의 내용을 가지게 됩니다.

위 그림에서 강조할 몇 가지 중요한 사항을 다음과 같습니다.
- 악성코드는 64비트 시스템에서 실행되므로 그 코드가 WOW64 스레드(32비트 스레드)의 컨텍스트를 Wow64GetThreadContext()를 사용하여 가져옵니다.
- smethod_8은 GetDelegateForFunctionPointer()를 사용하여 native(unmanaged) 함수 포인터를 델리게이트로 변환하며, 이 델리게이트는 어떤 델리게이트 타입으로도 캐스팅할 수 있습니다.
- .NET에서 델리게이트(delegate)에 대해 설명하자면 델리게이트 타입은 언제든지 호출할 수 있는 메서드 또는 메서드 목록에 대한 참조(포인터)를 제공하는 객체(데이터 구조체/클래스)입니다. 사실 델리게이트 타입은 메서드의 주소(함수 포인터와 유사), 해당 파라미터, 반환 타입을 보유하기 때문에 구조체로 해석할 수 있습니다. 그렇기 때문에 예를 들어 두 개의 문자열을 인수로 받고 다른 문자열을 반환하는 함수에 대한 델리게이트 타입을 만들 수 있습니다. 또한 델리게이트 키워드를 사용하여 델리게이트 타입을 정의하면 델리게이트에 필요한 모든 정보를 보유하는 클래스(데이터 구조체)가 생성됩니다.
- 델리게이트 타입은 특정 조건이 트리거될 때 호출하는 함수에 알림(callback)을 보내는데 사용할 수 있지만 이는 악성코드의 주요 목적은 아닙니다.
- 이 샘플의 경우, GetDelegateForFunctionPointer()가 네이티브 함수에 대한 포인터를 .NET 코드 내에서 호출할 수 있는 델리게이트 타입으로 변환하는데 사용됩니다.
- 악성코드는 VirtualAllocEx, WriteProcessMemory, SetThreadContext, ResumeThread와 같은 네이티브 함수를 델리게이션을 통해 사용하는 code injection을 수행합니다.
- (델리게이트를 통한) CreateProcessA는 프로세스를 생성하는데 사용되긴 하지만 이에 대한 추가 정보가 필요합니다.
- 사실 이 샘플에서 .cctor()는 네이티브 함수에 대한 델리게이트(참조)를 생성하는데만 사용되며 2단계는 실제로 smethod_10을 호출하고, 해당 메서드는 여러 메서드를 호출하며 그 중 많은 메서드들이 앞서 언급된 델리게이트를 사용합니다.
따라서 권장하는 접근 방법은 다음과 같습니다.
- .cctor() 내에서가 아니라 smethod_10 내부의 핵심 함수/메서드에 breakpoint를 설정합니다.
- Analyze 기능을 사용하여 관련 메서드를 필터링합니다. 이 기능은 분석 중인 메서드에서 사용되는 메서드와 분석된 메서드를 사용하는 메서드를 보여줍니다.
.cctor는 Class12를 사용하며 Class12는 9개의 델리게이트를 가지고 있습니다. 이 델리게이트들은 Analyze 기능을 사용하여 얻을 수 있습니다.

앞서 언급했듯이 smethod_10(2단계에서 호출되는 실제 메서드)에서는 많은 메서드들이 호출되며, 우리는 그 중에서 가장 관련있는 메서드를 필터링해야 합니다.

.cctor()에 대한 간단한 분석을 마무리하고, 우리의 유일한 목표는 그 안에서 생성되는 모든 델리게이트의 목록을 만드는 것입니다. 왜냐하면 이 모든 델리게이트가 smethod_10에 의해 사용될 것이기 때문입니다.
- Class12.delegate0_0 → "ResumeThread"
- Class12.delegate1_0 → "Wow64SetThreadContext"
- Class12.delegate2_0 → "SetThreadContext"
- Class12.delegate3_0 → "Wow64GetThreadContext"
- Class12.delegate4_0 → "GetThreadContext"
- Class12.delegate5_0 → "VirtualAllocEx"
- Class12.delegate6_0 → “WriteProcessMemory"
- Class12.delegate7_0 → "ReadProcessMemory"
- Class12.delegate8_0 → "ZwUnmapViewOfSection"
- Class12.delegate9_0 → "CreateProcessA"
그리고 좋은 소식은 모든 델리게이트가 smethod_9에서 사용되고 있다는 것입니다.

smtheod_9()는 네이티브 함수와 관련된 여러 델리게이트를 사용하는 역할을 하지만, smethod_9는 smethod_10(.cctor() 이후에 실행되는 첫 번째 메서드)에서 직접 호출하지 않습니다. 다 시 한 번 호출 순서를 파악하기 위해 Analyze 기능을 사용해야 합니다.

위 그림에서 순서가 smethod_10 → smethod_12 → smethod_9임을 알았습니다. 이 네이티브 함수들의 실행을 제어하려면 smethod_9()로 가서 사용되는 각 델리게이트에 breakpoint를 설정해야 합니다. 이 작업은 아래와 유사합니다.

이제 smethod_10의 첫 번쨰 명령어에 breakpoint를 설정했는지 확인하고, Play 버튼을 사용하여 smethod_10에 도달할 때까지 디버깅을 계속 진행하세요.
smethod_10()은 매우 길어서 코드의 각 부분을 이해하는데 시간을 걸릴 수 있습니다. 따라서 일반적인 아이디어는 다시 한번 Analyze 기능을 사용하여 어떤 메서드가 흥미로운 코드를 포함하고 있는지 파악하는 것입니다.
따라서 각 smethod_#을 하나씩 빠르게 분석해보겠습니다. smethod_0()부터 시작하면 다음과 같습니다.

많은 악성코드들이 분석 중에 사용되는 도구를 탐지하기 위해 사용하는 FindWindow()와 함께, 샌드박스 탐지 코드에 대한 좋은 지표도 있습니다.
▪ if ((int)Class1.FindWindow("Afx:400000:0", (IntPtr)0) != 0)
▪ bool flag2 = Operators.CompareString(string_0, "C:\\file.exe", false) != 0;
▪ num2 = ((((int)Class1.GetModuleHandle("SbieDll.dll") == 0)
▪ num2 = ((((int)Class1.GetModuleHandle("SbieDll.dll") == 0)
▪ bool flag6 = string_0.ToUpper().Contains("SAMPLE")
▪ bool flag7 = Operators.CompareString(stringBuilder.ToString().ToUpper(), "SANDBOX", false) ==
0;
▪ bool flag9 = Operators.CompareString(stringBuilder.ToString().ToUpper(), "MALWARE", false) ==
0;
▪ num2 = (string_0.ToUpper().Contains("SANDBOX")
▪ bool flag = string_0.ToUpper().Contains("\\VIRUS")
smethod_1()내에서 사용되는 메서드를 분석해보면 좋은 지표와 아티팩트가 있습니다.

HeGwfEiyF() 메서드는 여러 번 호출되며 그 인수로 사용되는 문자열을 통해 이 메서드의 목적이 가상 머신 탐지임을 알 수 있습니다.
VMWARE:
▪ bool flag3 = Class1.HeGwfEiyF("SOFTWARE\\VMware, Inc.\\VMware Tools",
"InstallPath").ToUpper().Contains("C:\\PROGRAM FILES\\VMWARE\\VMWARE TOOLS\\");
▪ bool flag5 = Operators.CompareString(Class1.HeGwfEiyF("SOFTWARE\\VMware, Inc.\\VMware
Tools", ""), "noValueButYesKey", false) == 0
▪ bool flag6 = Class1.HeGwfEiyF("HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 0\\Scsi Bus 0\\Target
Id 0\\Logical Unit Id 0", "Identifier").ToUpper().Contains("VMWARE");
▪ bool flag7 = Class1.HeGwfEiyF("SYSTEM\\ControlSet001\\Control\\Class\\{4D36E968-E325-11CEBFC1
-08002BE10318}\\0000\\Settings", "Device Description").ToUpper().Contains("VMWARE");
▪ bool flag8 = Class1.HeGwfEiyF("HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 1\\Scsi Bus 0\\Target
Id 0\\Logical Unit Id 0", "Identifier").ToUpper().Contains("VMWARE");
▪ bool flag12 = Class1.HeGwfEiyF("SYSTEM\\ControlSet001\\Control\\Class\\{4D36E968-E325-
11CE-BFC1-08002BE10318}\\0000", "DriverDesc").ToUpper().Contains("VMWARE");
▪ bool flag13 = Class1.HeGwfEiyF("HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 2\\Scsi Bus 0\\Target
Id 0\\Logical Unit Id 0", "Identifier").ToUpper().Contains("VMWARE");
▪ num = (Class1.HeGwfEiyF("SYSTEM\\ControlSet001\\Services\\Disk\\Enum",
"0").ToUpper().Contains("vmware".ToUpper()) ? 4010111179U : 2868294220U);
▪ num4 = ((Operators.CompareString(managementObject["Description"].ToString(), "VMware
SVGA II", false) == 0)
VIRTUALBOX:
▪ bool flag10 = Class1.HeGwfEiyF("HARDWARE\\Description\\System",
"VideoBiosVersion").ToUpper().Contains("VIRTUALBOX");
▪ bool flag4 = Operators.CompareString(Class1.HeGwfEiyF("SOFTWARE\\Oracle\\VirtualBox Guest
Additions", ""), "noValueButYesKey", false) == 0;
▪ bool flag = Class1.HeGwfEiyF("HARDWARE\\Description\\System",
"SystemBiosVersion").ToUpper().Contains("VBOX");
▪ bool flag11 = Class1.HeGwfEiyF("HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 0\\Scsi Bus 0\\Target
Id 0\\Logical Unit Id 0", "Identifier").ToUpper().Contains("VBOX");
▪ num4 = (((Operators.CompareString(managementObject["Description"].ToString(), "VM
Additions S3 Trio32/64",false) == 0)
▪ bool flag15 = Operators.CompareString(managementObject["Description"].ToString(),
"VirtualBox Graphics Adapter", false) == 0;
QEMU:
▪ num = ((!Class1.HeGwfEiyF("HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 0\\Scsi Bus 0\\Target Id
0\\Logical Unit Id 0", "Identifier").ToUpper().Contains("QEMU")) ? 3150288510U : 2854005095U);
▪ bool flag9 = !Class1.HeGwfEiyF("HARDWARE\\Description\\System",
"SystemBiosVersion").ToUpper().Contains("QEMU");
WMI 쿼리는 가상머신과 관련된 모든 세부 정보를 확인하기 위해 시스템 정보를 수집하는데 사용됩니다. 어쨌든 smethod_1()의 주요 목적은 가상 머신 환경을 탐지하는 것이므로 해당 명령어를 무효화할 수 있습니다. 그렇지만 이 샘플에서는 필요하진 않습니다.
다음 메서드인 smethod_2()는 매우 간단하며, smethod_3()에서 제공된 스레드를 시작하는 역할만 합니다. 이 작업은 다음 명령어를 통해 수행됩니다.
new Thread(new ThreadStart(Class12.smethod_3));
따라서 두 메서드는 서로 관련이 있고 추가 정보는 없습니다.
smethod_4()와 smethod_5()는 다음과 같은 API를 통해 ACL을 관리합니다.
- DirectorySecurity
- SetAccessControl
- SetAccessRuleProtection
- setAttributes
- FileSystemAccessRule
- AddAccessRule
smethod_6은 좀 더 흥미롭습니다. 왜냐하면 이 메서드는 schtasks.exe를 사용하여 작업을 처리하고 이를 지원하기 위해 임시 파일을 디스크에 생성하기 때문입니다. 아래의 base64 문자열은 단지 XML 템플릿을 나타내며 RunLevel, Triggers, Settings, StartWhenAvailable, AllowStartOnDemand 등의 태그를 사용하여 애플리케이션을 정의하고 제어합니다. 이 템플릿은 schtasks.exe가 작업 예약 중에 사용합니다.


우리는 Powershell을 사용하여 문자열을 쉽게 디코딩할 수 있지만, 다시 한 번 Cyberchef를 사용하여 ("From Base64" 레시피 선택) 디코딩하고 실제로 XML 파일이 있다는 것을 증명해 봅시다.

다음 메서드인 smethod_7()은 인터넷 다운로드가 있어 흥미롭습니다.

smethod_8()은 .cctor()에 의해 호출되며 smethod_10()에 의해 호출되지 않습니다. 이 메서드는 잘 알려진 LoadLibraryA()와 GetProcAddress()를 사용하여 델리게이트를 통해 사용될 네이티브 API 주소를 찾습니다.

smethod_9()는 이미 언급된 네이티브 API들을 델리게이트를 사용하여 호출합니다.

smethod_11()은 새로운 어셈블리를 로드한다는 점에서 매우 중요합니다. 그래서 Assembly.Load() 줄에 breakpoint를 설정할만 합니다. 이 새로운 모듈이 다음 단계이거나 지원 모듈(리소스)일 수도 있기 때문입니다.

smethod_12()는 smethod_13()의 프록시 메서드로 새로 로드된 어셈블리의 멤버를 호출하지만 분석을 위해 다음 코드 줄 (및 문자열들)을 제공합니다.
▪ string text = Path.Combine(path, "RegSvcs.exe");
▪ string text = Path.Combine(path, "MSBuild.exe");
▪ string text = Path.Combine(path, "vbc.exe");

메서드 특징을 요약하면 다음과 같습니다.
- smethod_0: 샌드박스 탐지
- smethod_1: 가상머신 탐지
- smethod_2: 스레드 시작
- smethod_3: 시작될 애플리케이션을 스레드로 제공
- smethod_4와 smethod_5: ACL 관리
- smethod_6: schtasks.exe로 새로운 작업 예약
- smethod_7: 인터넷에서 파일을 다운로드하는 것으로 추측
- smethod_8: native API 주소 리졸빙
- smethod_9: native API 호출 관련
- smethod_10: 메인 메서드 (dispatcher)
- smethod_11: 새 어셈블리 로드
- smethod_12와 smethod_13: 메서드 호출 관련 작업
breakpoint를 설정해야 하며, 아래에 가능한 몇몇 breakpoint 목록을 나열해보겠습니다.
- smethod_1
(line 488) Start of the loop
- smethod_3
(line 186) Process.Start(Class12.string_10)
- smethod_7
(line 583) webClient.DownloadFile(string_11, text)
- smethod_11
(line 1490) Assembly assembly = Assembly.Load(Class12.byte_0);
- smethod_13
o (line 1617) string path = (string)typeof(RuntimeEnvironment).InvokeMember("GetRuntimeDirectory",
BindingFlags.InvokeMethod, null, null, null);
o (line 1633) string text = Path.Combine(path, "RegSvcs.exe");
o (line 1654) string text = Path.Combine(path, "MSBuild.exe");
o (line 1664) string text = Path.Combine(path, "vbc.exe");
- smethod_9
o (line 776 / WriteProcessMemory) num6 = (((!Class12.delegate6_0(struct2.intptr_0, num10
+ num11, array, array.Length, ref num4)) ?
1777126585U : 974911055U) ^ num3 *3593627777U)
o (line 803 / WriteProcessMemory) bool flag5 = !Class12.delegate6_0(struct2.intptr_0,
num13 + 8, bytes, 4, ref num4)
o (line 859 / VirtualAllocEx) int num10 = Class12.delegate5_0(struct2.intptr_0, num14,
length, 12288, 64)
o (line 867 / CreateProcessA) bool flag10 = !Class12.delegate9_0(string_11, string.Empty,
IntPtr.Zero, IntPtr.Zero, false, 134217732U, IntPtr.Zero,
null, ref @struct, ref struct2)
o (line 880 / WriteProcessMemory) num6 = ((!Class12.delegate6_0(struct2.intptr_0, num10,
byte_1, bufferSize, ref num4)) ? 1884772482U : 172468949U);
o (line 895 / GetThreadContext) num6 = ((Class12.delegate4_0(struct2.intptr_1, array2) ?
1127022864U : 23477936U) ^ num3 * 3000738847U);
o (line 921 / ReadProcessMemory) bool flag6 = !Class12.delegate7_0(struct2.intptr_0, num13
+ 8, ref num16, 4, ref num4);
o (line 929 / Wow64GetThreadContext) bool flag11 = !Class12.delegate3_0(struct2.intptr_1,
array2)
o (line 941 / ZwUnmapViewOfSection) num6 = (((Class12.delegate8_0(struct2.intptr_0,
num16) != 0) ? 3120432759U : 2659671650U) ^ num3 * 1247483263U);
o (line 984 / SetThreadContext) bool flag13 = !Class12.delegate2_0(struct2.intptr_1, array2);
o (line 1035 / Wow64SetThreadContext) bool flag4 = !Class12.delegate1_0(struct2.intptr_1,
array2);
o (line 1045 / ResumeThread) bool flag12 = Class12.delegate0_0(struct2.intptr_1) == -1;
언급된 breakpoint를 설정한 후 필요할 경우 다시 시작할 수 있도록 가상머신의 스냅샷을 찍어두세요. 실행을 재개하면 몇몇 breakpoint에서 히트되지만 그렇지 않은 곳도 있습니다.
- smethod_13
- (line 1617) @"C:\Windows\Microsoft.NET\Framework\v4.0.30319\"
- (line 1633) @"C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegSvcs.exe"
- (line 1617) @"C:\Windows\Microsoft.NET\Framework\v4.0.30319\"
- smethod_9
- (line 867 / CreateProcess)
- lpApplicationName:
@"C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegSvcs.exe" - dwCreationFlags: 134217732U == 0x 0x08000004 == CREATE_SUSPENDED
- lpApplicationName:
- (line 895 / GetThreadContext)
- nothing important
- nothing important
- (line 921 / ReadProcessMemory)
- lpBuffer: 0x00C50000
- nSize: 4
- *lpNumberOfBytesRead: 4
- (line 859 / VirtualAllocEx)
- hProcess: 0x354 (handle to RegSvcs.exe)
- lpAddress: 0x00400000
- dwSize: 0x0003A000
- flProtect: 64 == 0x40 == PAGE_EXECUTE_READWRITE
- (line 880 / WriteProcessMemory)
- hProcess: 0x354 (handle to RegSvcs.exe)
- lpBaseAddress: 0x00400000
- lpBuffer: contains the executable to be injected
- nSize: 0x00000200
- (line 776 / WriteProcessMemory)
- hProcess: 0x354 (handle to RegSvcs.exe)
- lpBaseAddress: num10 + num11 = 0x00400000 + 0x00002000 = 0x00402000
- lpBuffer: contains the the second session of executable to be injected
- (line 776 / WriteProcessMemory)
- hProcess: 0x354 (handle to RegSvcs.exe)
- lpBaseAddress: num10 + num11 = 0x00400000 + 0x00036000 = 0x00436000
- lpBuffer: contains the the second session of executable to be injected
- (line 776 / WriteProcessMemory)
- hProcess: 0x354 (handle to RegSvcs.exe)
- lpBaseAddress: num10 + num11 = 0x00400000 + 0x00038000 = 0x00438000
- lpBuffer: contains the the second session of executable to be injected
- (line 984/ SetThreadContext)
- nothing important
- nothing important
- (line 1045/ ResumeThread)
- nothing important
- (line 867 / CreateProcess)
저는 상황을 좀 더 쉽게 이해할 수 있도록 위에 표시한 것처럼 디버깅 실행 중 일부 파라미터를 기록해두었습니다. 이는 stage_3.bin의 명령어에서 무슨 일이 일어나고 있는지를 이해하는데 도움이 될 것입니다.
일부 API 파라미터는 자료로 남겨두었으며 많은 breakpoint들이 중단되지 않았습니다. 이는 좋은 징후처럼 보입니다.


또한 “C:\MAS_4>handle -p 4452 -a” 명령어(SysInernals에서 제공)를 실행하여 주어진 핸들과 연결된 프로세스를 확인했습니다.

디버깅 세션에서 해당 breakpoint를 통해 수집한 정보 바탕의 몇 가지 고려 사항이 있습니다.
- 악성코드는 GetRuntimeDirectory()를 실행하여 현재 .NET 런타임 디렉터리를 찾습니다.
- .NET 런타임 버전과 관련된 GetRuntimeDirectory()의 결과를 바탕으로, 악성코드는 사용 가능한 합법적인 애플리케이션 중 하나를 로드합니다. 제 환경에서는 RegSvcs.exe가 로드되었으며 이는 .NET 서비스의 설치 도구입니다.
- 악성코드는 로드된 모듈(RegSvc.exe) 안에 악성코드를 인젝션합니다. 하지만 전체 악성코드를 한 번에 인젝션하는 것이 아니라 섹션별로 복사하여 인젝션합니다.
- 악성코드가 섹션별로 인젝션되기 때문에 dnSpy로 인젝션된 코드를 각각 저장하는 것은 비효율적입니다. 왜냐하면 나중에 모든 코드를 결합해야 하고 이를 위해 시간을 할애하는 것은 가치가 없기 때문입니다.
- 가장 추천되는 접근 방식은 프로세스(RegSvcs.exe)의 메모리 주소를 시각화하고 RWX 섹션을 검색하는 것입니다. 이 섹션은 0x400000에서 시작할 가능성이 높습니다. 이 두 가지 정보는 smethod_9 메서드(line 859 / VirtualAllocEx)에서 수집한 파라미터를 통해 확인할 수 있습니다.
Process Hacker 도구에서 메모리 영역을 저장하려면 해당 영역을 더블 클릭하여 PE 실행 파일임을 확인하고 Save 버튼을 클릭합니다.

저장된 바이너리 파일을 PE Bear에서 열어 Imports를 확인하면 엉망으로 표시되어 있고, 겉보기에 유용한 정보가 없을 것입니다. 그 이유는 메모리에서 바이너리 파일을 덤프하여 매핑된 형식이기 때문이고, 이를 언매핑 형식으로 변환해야 합니다.


이전 아티클에서 이미 매핑된 형식에서 언매핑 형식으로 변환하는 방법을 설명하여 알고 있겠지만, 다시 한 번 과정을 반복할 가치가 있습니다.
- Section Hdrs 탭에서 각 섹션에 대해 Virtaul Address를 Raw Address로 복사합니다.
- 각 섹션의 크기를 계산하려면 다음 섹션의 주소에서 현재 섹션의 주소를 빼고, 그 결과를 사용하여 Raw Szie를 변경합니다.
- Raw Szie를 변경한 후, Raw Size 값을 Virtual Size 필드에 복사합니다.
- 바이너리의 이름을 우클릭하고 새 이름을 지정하여 바이너리를 저장합니다.


저장된 바이너리를 수정한 후 DiE와 Exeinfo PE를 사용하여 패커/난독화를 검색합니다.


네 번째 단계는 Obfuscar를 통한 난독화된 바이너리고, 이는 .NET 용으로 사용할 수 있는 여러 패커 중 하나입니다. 저는 dnSpy를 사용하여 분석을 진행하고, de4dot이나 다른 사용 가능한 난독화 해제 도구를 사용하여 난독화를 해제해보겠습니다. 어쨌든 진행하기 전 이 새로운 단계에서 열린 핸들을 보여주는 것이 흥미롭습니다. 왜냐하면 핸들 이름 \Device\Afd에 따르면 이 단계에서 네트워크를 통해 통신을 시도하는 것 같기 때문입니다.

네 번째 단계를 분석해보겠습니다. 첫 번째로 추천하는 것은 가상머신 스냅샷을 찍어두는 것입니다. 만약 문제가 생기면 되돌릴 수 있게 하기 위함입니다.
.NET 바이너리기 때문에(mscoree.dll를 import하고 있습니다) dnSpy에서 열어보고 무슨 일이 일어나고 있는지 확인해야 합니다.

진입점(EP), 네임스페이스, 클래스 이름, 메서드에 따르면 이 단계도 난독화된 것으로 보이며, Die와 Exeinfo PE를 기반으로 패커는 Obfuscar인 것 같습니다.
EP로 이동할 수 있습니다. 바로 위의 Figure 76에서 4번째 줄의 마지막 "A"를 클릭하거나, 어셈블리 이름을 우클릭하고 "Go to Entry Point"를 선택하면 됩니다.


여기서 난독화의 첫 번째 영향을 볼 수 있습니다. 클래스 이름이 "b", "C"와 같이 단순하고 메서드 이름도 동일하게 "A()"와 같이 명명되어 있습니다.
어쨌든 C.A() 메서드가 호출되고 그 후에 Application.Run() 메서드가 호출됩니다. A() 메서드로 돌아가보면 다음과 같습니다.

몇 가지 메서드 이름은 보이지만 문자열은 보이지 않습니다. 예를 들어 50번째 줄에서 문자열이 나타나야할 곳에 "4AE7E02E-291A-4676-9641-A6E499CD2831.aw()"가 보이는데, 이는 문자열이 아니라 <class.method()> 형태로 보입니다. 첫 번째 것을 클릭하면 dnSpy는 다음 코드로 안내합니다.

위 그림에서 부분적으로 보여지는 루틴에는 메인 메서드(private static string <<EMPTY_NAME>>(int A_0, int A_1, int A_2))와 여러 메서드들이 포함되어 있으며 이 메서드들은 복호화 루틴을 호출합니다.
보시다시피 악성코드는 동적으로 문자열 테이블을 디코딩하여 767개의 문자열을 생성하고, 이 문자열들이 디코딩된 후에는 주어진 인덱스에 따라 거기서 문자열을 가져옵니다. 이 모든 과정은 .cctor() 메서드 안에서 발생하는데, 여기에는 긴 시퀀스의 요소들(11,566개의 요소)이 있고 마지막에는 각 요소를 읽고 자신만의 인덱스와 값(170)을 사용하여 디코딩하는 for-loop가 있습니다.
첫 번째 단계로 Obfuscar에 대한 지원을 제공하지 않지만 de4dot을 사용하여 가능한 모든 심볼들을 난독화 해제하려고 시도해보겠습니다.

de4dot을 사용한 후, dnSpy에서 열어보면 일부 클래스가 이름이 변경된 것을 볼 수 있지만 문자열은 아직 복호화되지 않은 상태입니다. 아래에서 확인할 수 있습니다.

문자열을 복호화하기 위해서는 두 가지 방법이 있습니다. 다른 난독화 해제 도구를 사용할 수 있거나 de4dot에서 제공하는 다른 옵션을 사용해 시도할 수 있습니다. Obfuscar 용으로 작동하는 난독화 해제 도구는 Static Obfuscar Deobfuscator (https://github.com/DarkObb/DeObfuscar-Static)입니다. 사용하려면 이 도구를 클론(clone)하고 Visual Studio 2019 또는 Visual Studio 2022를 사용하여 컴파일해야 합니다. 솔루션 빌드는 간단하고 직관적입니다.

사용법은 매우 간단하고 결과를 즉시 제공해줍니다.

stage_4_decrypted-Dec.exe를 dnSpy에서 열면 다음과 같습니다.

이제 이전에 <class>.<method>로 보였던 문자열을 볼 수 있게 되었으므로, 네 번째 단계를 추가적인 문제 없이 분석할 수 있습니다.
네임스페이스, 클래스 및 메서드가 de4dot이나 DeObfuscar-Static 도구를 사용해도 복귀되지 않은 이유에 대해 궁금할 수 있습니다. 하지만 이는 우리에게 문제가 되지 않습니다. 왜냐하면 샘플 내 다른 모든 정보는 여전히 존재하기 때문입니다. 또한 <PrivateImplementationDetails>{A78A1E33-EFB4-4B39-84DB-A2C18EC95E34} 네임스페이스는 더이상 필요없기 때문에 삭제해도 문제가 없으며 이는 개인적인 선택입니다.
다른 접근 방법은 de4dot 자체를 사용하여 이 샘플의 문자열을 난독화 해제하는 것인데, 여러 알려지지 않은 난독화 도구에 대해 잘 작동하는 비전통적인 옵션을 사용합니다. 이를 이해하려면 Figure 79로 돌아가서 다음에 주목해야 합니다.
4AE7E02E-291A-4676-9641-A6E499CD2831.aw()
이 명령어에서 우리는 다음과 같은 정보를 얻을 수 있습니다.
- 4AE7E02E-291A-4676-9641-A6E499CD2831 ➔ 클래스
- aw() ➔ 메서드
따라서 앞서 언급한 것처럼 이 클래스는 스크립트를 복호화하는데 사용되는 모든 메서드를 포함하고 있으며 이를 동적으로 풀 수 있다면 문자열 복호화 문제가 해결됩니다. 위의 메서드를 확인해보겠습니다.

복호화 메서드는 여러 가지가 있는데 중요한 부분은 각 방법에 해당하는 토큰들입니다. 전체적인 코드에서 확인해보면 첫 번째 토큰은 0x060001F8이고, 마지막 토큰은 0x060004F6입니다. 우리가 찾고 있는 de4dot 옵션은 도움말에 나와있습니다.
- --strtyp TYPE 문자열 복호화 유형
- --strtok METHOD 문자열 복호화 방법 토큰 또는 [type::][name][(args,...)]
간단히 말해서 de4dot은 각 토큰에 해당하는 복호화 메서드들을 동적으로 호출할 수 있는 옵션을 제공합니다. 따라서 모든 문자열을 복호화하는 구문은 다음과 같습니다.
- de4dot --strtyp delegate –strtok <method token> –strtok <method token> --strtok…
유일한 문제는 토큰이 너무 많고 (연관된 메서드도 너무 많고) 앞서 언급한 것처럼 767개의 문자열이 있기 때문에 커맨드 라인이 길어질 수 있다는 점입니다. 하지만 이정도는 감당할 수 있습니다. Python과 Jupyter Notebook를 사용하여 커맨드 라인을 생성하는 코드 몇 줄을 작성해봤습니다.

보이다시피 이 스크립트는 필요한 명령의 여러 부분을 합쳐서 모든 필수 토큰과 옵션을 포함하는 출력을 생성합니다.
Python의 range() 함수는 마지막 요소를 제외하므로 이를 포함시키기 위해 1을 더했습니다. 전체 명령을 PowerShell 터미널에 복사하여 아래와 같이 실행해야 합니다.

dnSpy로 열면 다음과 같습니다.

여기에 유용한 점이 몇 가지 있습니다.
- de4dot은 Windows와 Linux 환경 모두 실행 가능합니다. (apt install de4dot을 통해 설치 가능) 그리고 이 시스템에서 명령을 테스트할 수 있습니다.
- 보통 저는 예기치 않은 문제를 피하기 위해 최신 버전의 Windows (10 또는 11)에서 .NET 관련 명령과 도구를 실행하는 것을 선호합니다. 이는 개인적인 취향입니다.
- de4dot의 최신 버전을 컴파일하여 사용하는 것이 좋습니다. 보통 최신 버전에는 업데이트된 컴포넌트가 포함되어 있습니다.
- 명령 프롬프트(Command Prompt)보다 Powersehll 창을 사용하는 것이 좋습니다. Powershell 편집 기능이 훨씬 뛰어나기 때문입니다.
- 만약 de4dot을 사용해 생성된 긴 명령 실행에 문제가 있다면 최신 버전의 Windows 시스템을 사용하고 자신만의 컴파일된 버전을 사용해 보세요.
이 시점부터는 바이너리에 더이상 난독화가 없으며 코드 읽기, API 및 구조 분석만 하면 됩니다. 물론 아직 암호화된 데이터는 있을 수 있습니다. 이 아티클에는 코드 조각 세 개만 포스팅하지만 여러분은 AgentTesla 악성코드가 제공하는 모든 기능을 배우기 위해 여러 방법을 파싱해야 합니다. (방법은 많습니다)
난독화된 코드를 분석하는 주요 접근 방식 중 하나는 Table Streams를 검사하여 흥미로운 함수, 주로 네이티브 API를 찾는 것입니다. 이는 악성코드의 일부 특성에 대한 아이디어를 제공할 수 있습니다. 물론 다른 테이블도 있지만 ImplMap 테이블이 여기에 도움이 될 수 있습니다.

그림(Figure 90)에서 확인할 수 있듯이 SetWindowsHookEx, CallNextHookEx, UnhookWindowsHookEx, GetKeyboardState, GetKeyboardLayout, EnumProcessModules, SetClipboardViewer 등과 같은 잘 알려진 API들이 네이티브 악성코드에서 사용되고 있습니다.
이는 ImplMap table의 42개 API만 중요한 것이 아니라, 이들이 시작 지점을 제공할 수 있다는 의미일 수도 있습니다. 각 클래스에서 이들을 찾기 위해 CTRL+F를 사용하여 검색할 수 있습니다. dnSpy에서 항상 사용하는 표기법은 <namespace>.<class>.<subclass>.method( )인데 많은 경우에서 서브클래스는 존재하지 않습니다.



솔직히 이 트로이목마는 수십 개의 메서드에 걸쳐 광범위한 기능을 가지고 있어서 모든 걸 설명할 필요는 없다고 생각합니다.
숫자 세 개만 사용하여 여러 단서들이 나타났고 이로 인해 악성코드는 Tor를 사용하고, 외부 웹사이트에 정보를 게시하며, 결국 도구를 다운로드하고, 물론 고전적인 지속성 메커니즘도 사용한다는 사실이 드러났습니다.
여기서 궁금한 점은 IDA Pro를 사용할 수 있을까요? IDA Pro는 네이티브 바이너리, 쉘코드, raw 파일 및 UEFI 펌웨어 분석에도 사용되고, .NET (managed) 악성코드도 분석할 수 있습니다. IDA Pro는 바이너리의 고급 레벨까진 아니지만 IL(Intermediate Language) 해석을 제공해주므로 여러 상황에서 코드에 일어나는 일을 이해하는데 도움이 됩니다. 게다가 MSIL 코드 내비게이션을 쉽게 하기 위한 그래프로도 표현 가능합니다.

처음에는 MSIL 표현이 쉽지 않아 보이기 때문에 managed 코드(.NET 코드)를 분석할 때 IDA Pro를 사용하는 것이 적합하지 않을 수 있다고 생각할 수 있습니다. 하지만 저는 아래와 같은 많은 경우에 IDA Pro를 사용했습니다.
- 최종 난독화 기법을 이해하기 위해
- 호출된 모든 네이티브 API를 빠르게 찾기 위해
- 그래프 모드를 사용하여 호출된 함수들의 순서를 파악하기 위해
또한 저는 IDA Pro를 사용하여 최종 페이로드를 분석할 수 있었고, 함수 목록 관찰, 그래프 모드를 통해 코드 따라가기 및 dnSpy에서 사용한 것처럼 ALT+T와 CTRL+T를 통한 텍스트 검색 수행으로 실행된 동작에서 빠르게 방향을 찾을 수 있었습니다.
결국 .NET 악성코드 샘플을 분석하는데 사용하는 도구와 접근 방식은 개인의 선택입니다. 하지만 항상 작업을 더 명확하고 빠르게 만들 수 있는 도구를 사용하는 것이 좋습니다.
[ConClusion]
분석해야 할 방법과 함수가 수십 개, 아니 수백 개가 될 수도 있습니다. 모두 분석하면 더 완전한 악성코드 프로파일을 추적할 수 있습니다. 예를 들어 다음과 같습니다.
- 다른 지속성 메커니즘 찾기
- 시스템에서 정보 수집하기
- 훅(hooks)과 키로거(keyloggers) 분석하기
- 모든 네트워크 통신 분석하기
제 목표는 악성코드 분석에 대한 리뷰를 제공하고, 리버스 엔지니어들이 새로운 것을 배우도록 돕고, 가이드를 제공하는 것입니다.
'Study > malware analysis series' 카테고리의 다른 글
| Malware Analysis Series (MAS) – Article 6 (0) | 2025.04.25 |
|---|---|
| Malware Analysis Series (MAS) – Article 5 (0) | 2025.03.25 |
| Malware Analysis Series (MAS) – Article 3 (0) | 2025.02.17 |
| Malware Analysis Series (MAS) – Article 2 (2) | 2025.01.16 |
| Malware Analysis Series (MAS) – Article 1 (0) | 2025.01.06 |