一些绕过AV进行UserAdd的方法总结及实现

 

一些绕过AV进行UserAdd的方法总结及实现

相关项目:https://github.com/crisprss/BypassUserAdd

 

0x01 前置了解

我们知道,一般我们想要进行添加用户等操作时,基本运行的是:

net user username password /add
net localgroup administrators username /add

其实一般杀软只会检测前面添加用户的命令,而后面的命令并不会触发杀软的报警行为,其实在这里net是在C:\Windows\System32下的一个可执行程序,并且该目录下还有net1.exe,这两个程序的功能是一模一样的:

不论我们是使用net或者是net1都会被杀软检测,至于是不是对底层API的Hook操作,我们还并不清楚

因此在这里,我们来监测系统使用user add时具体对应的API:
实际上当我们使用net user username password /add时net程序会去调用net1.exe程序,然后使用相同的命令:

当尝试跟进net1.exe来跟踪相关操作时发现应该是使用RPC,并且endpoint是\PIPE\lsarpc来进行其他的操作

到这里就没有进一步跟进下去,其实在这里了解到实际上是通过MS-SAMR协议通过RPC实现的,MS-SAMR的官方IDL文档贴出:https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/1cd138b9-cc1b-4706-b115-49e53189e32e
可以看到其中SamrSetInformationDomain等方法都是其接口方法,这也为后文埋下了一定伏笔,因此使用BypassAV进行AddUser的方法并没有结束

 

0x02 正文

1.Win API 进行UserAdd

在MSDN搜索添加用户相关资料时就会发现微软官方是提供了标准API进行添加用户的,可以参考https://docs.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netuseradd
NetUserAdd其函数原型如下:

NET_API_STATUS NET_API_FUNCTION NetUserAdd(
  [in]  LPCWSTR servername,
  [in]  DWORD   level,
  [in]  LPBYTE  buf,
  [out] LPDWORD parm_err
);

其中level=1时,指定有关用户帐户的信息。此时BUF参数指向一个 USER_INFO_1结构:

typedef struct _USER_INFO_1 {
  LPWSTR usri1_name;
  LPWSTR usri1_password;
  DWORD  usri1_password_age;
  DWORD  usri1_priv;
  LPWSTR usri1_home_dir;
  LPWSTR usri1_comment;
  DWORD  usri1_flags;
  LPWSTR usri1_script_path;
} USER_INFO_1, *PUSER_INFO_1, *LPUSER_INFO_1;

这意味着我们本地实现时需要注意设置user_info_1这样一个结构体,在这里使用Golang进行简单的实现,其中核心源码如下:

果然不出意外,杀软应该是对该API进行Hook了,即使通过调用API这一方式也会轻易被杀软检测到相关可疑操作

互联网上一顿搜索发现21年6月份时该杀软就已拦截netapi加用户的方式:

到这里只好硬着头皮去尝试对netapi32.dll进行逆向分析,看其中是否有更加底层的API实现,如果本身NetUserAdd也是封装的话,那我们完全可以实现自己封装从而绕过该API

2.对NetAddUser的底层封装调用

在互联网上搜索看看有没有前人已经进行过相关工作,结果发现确实有前人已经做过相关逆向的工作:
https://idiotc4t.com/redteam-research/netuseradd-ni-xiang

在Win10中的netapi32.dll已经找不到相关添加用户的函数,只有一个NetUserAdd的导出函数,我们尝试逆向XP中的netapi32.dll:

Security Account Manager (SAM) 是运行 Windows 操作系统的计算机上的数据库,该数据库存储本地计算机上用户的用户帐户和安全描述符。

这里对UserAdd的实现也是首先尝试连接SAM数据库,判断SAM中是否已经存在该用户,然后利用RtlInitUnicodeString对新建用户信息等做一个初始化操作,最后调用SamCreateUser2InDomain来创建用户账户,创建成功会继续调用UserpSetInfo设置用户密码,因此实际上NetUserAdd就是被这样几个关键函数进行封装,因此我们需要做的是哪些函数能够直接调用,而哪些函数是还需要自己进一步封装

其中UaspOpenSam没有导出,而实际上对应的是SamConnect:

UaspOpenDomain同样没有导出,实际上对应的也是Sam系的函数:

这里SamOpenDomain的函数原型大致如下:

SamOpenDomain(ServerHandle, DesiredAccess, DomainSid, &DomainHandle)

因此我们是需要DomainSid的,也就是说我们还需要获取账户所在域的SID信息,经过搜索发现可以使用Sam函数获取的,而在ReactOS和mimikatz中就是使用的 LSA 函数进行查询的:

MSDN中查询该函数(LsaQueryInformationPolicy)发现存在:
函数原型如下:

NTSTATUS LsaQueryInformationPolicy(
  [in]  LSA_HANDLE               PolicyHandle,
  [in]  POLICY_INFORMATION_CLASS InformationClass,
  [out] PVOID                    *Buffer
);
// in Advapi32.dll

继续跟进,发现UserpSetInfo同样没有导出函数,继续跟进这个函数:

而在React OS的SetUserInfo函数中同样找到该方法的调用:

这里的UserAllInfo对应的就是USER_INFO结构体,而通常情况下我们都是使用USER_INFO_1,并且将值设置为1

因此大致过程已经比较清楚:

  • 1.调用SamConnect连接SAM数据库
  • 2.通过LsaQueryInformationPolicy获取SID信息后调用SamOpenDomain
  • 3.验证完成后调用SamCreateUser2InDomain创建用户信息
  • 4.最后通过SamSetInformationUser来设置新建用户的密码

最后成功绕过杀软进行用户添加。

3.Cobalt Strike argue参数欺骗

在CS 3.1版本后引入了argue参数欺骗技术,使得进程在创建时记录的参数与实际运行时不同

windows系统从进程的进程控制块的commandline中读取参数,并对参数做相应的处理,在线程未初始化完成前,我们可以修改这些参数,达到伪装commandline的目的

操作其实就是读取进程中PEB内RTL_USER_PROCESS_PARAMETERS结构体,在该结构体中对CommandLine指针进行修改

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;

这里我们使用argue进行参数混淆污染net1程序,然后在通过execute net1 username password /add方式来进行添加用户,笔者反复试验了多次,均为检测到,因此判断通过参数欺骗这种方式还是可以逃过杀软的检测,毕竟杀软对于commandline的检测也是通过读取进程PEB表实现的

4.C#利用命名空间和目录服务 添加用户

这其实是微软文档中自己给出的一种方式,具体可以参考:
https://docs.microsoft.com/zh-cn/troubleshoot/dotnet/csharp/add-user-local-system

注意需要添加对程序集System.DirectoryServices.dll的引用

这种方式通过C#中调用DirectoryServices添加本地用户,同时支持删除用户、添加用户组等实现

这里直接参考官方文档的描述就行,核心代码如下:

DirectoryEntry AD = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer");
DirectoryEntry addUser = AD.Children.Add(username, "user");
addUser.Invoke("SetPassword", new object[] { password });
addUser.CommitChanges();
DirectoryEntry grp;

grp = AD.Children.Find("Administrators", "group");
if (grp != null) { 
    grp.Invoke("Add", new object[] { addUser.Path.ToString() }); 
    }
grp = AD.Children.Find("Remote Desktop Users", "group");
if (grp != null) { 
    grp.Invoke("Add", new object[] { addUser.Path.ToString() }); 
    }

经过检测这种方式同样不会被杀软拦截,并且利用C#还有代码简洁,可以结合CS内存加载运行的特点,我们知道CS 3.1后便支持使用execute-assembly方式来调用NET程序集文件

5.编写反射DLL以及CS插件化实现

在编写的过程中其实已经发现,数字杀软已经对反射注入的行为特征进行检测,这里或许可以对反射DLL的一些属性进行修改,看到一些思路说是先不使用RWE属性的内存页,先使用RW属性执行DLL加载,加载完成后再将代码段改为RE可以绕过,在这里没有进行相关修改

在反射DLL编写中实现了2种方式,通过Win API创建用户和重构NetUserAdd,实现底层创建用户,当使用反射注入方式时2种方法都会被检测到

而前文已经提到,重构NetUserAdd实现底层创建,即通过SamSetInformationUser创建是不会被杀软拦截的,因此插件的实现只是包含反射注入的功能作为参考(或许对其他EDR比较好使),结合其他两种方式实现绕过杀软进行用户添加的功能

基于上述原理和方法,实现了一个简单的Bypass添加用户的插件,主要实现的有利用反射注入实现API添加用户和底层实现API添加用户,以及内存加载NET程序集实现C#活动目录添加用户和底层实现API的可执行程序上传再执行

实现效果如图:

但令我不解的是前一天利用C#编写的添加用户还能够成功绕过该杀软,第二天将上述这些能够绕过的封装到反射DLL或者封装到插件里面去之后,杀软都检测到了…这更新速度未必也太快了?

而且连前一天的argue参数欺骗都被检测了,中间间隔不到24h时间,在这里不得惊叹速度之快(可以看截图信息里的时间)

因此我也比较无奈,刚写好插件就发现没办法Bypass杀软了,只好作为实现原理和姿势分析,还有一个思路就是直接调用RPC接口方法,不过这种方式其实本质和重新封装NetUserAdd是一样的,具体能不能绕过我没有实现了…

发现@loong716师傅之前分享过利用ms-samr RPC实现过更改用户密码,可以参考:
https://github.com/loong716/CPPPractice/tree/master/ChangeNTLM_SAMR
这里应该在此基础上进行修改就能够实现创建用户,稍微留个坑等之后再来重写下


相关参考文章:
https://idiotc4t.com/redteam-research/netuseradd-ni-xiang
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/1cd138b9-cc1b-4706-b115-49e53189e32e
https://loong716.top/posts/Set_ChangeNTLM_SAMR/#2-%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8ms-samr

(完)