一些绕过AV进行UserAdd的方法总结及实现
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