我们知道,一般我们想要进行添加用户等操作时,基本运行的是:
net user username password /add
net localgroup administrators username /add
其实一般杀软只会检测前面添加用户的命令,而后面的命令并不会触发杀软的报警行为,其实在这里 net
是在 C:WindowsSystem32
下的一个可执行程序,并且该目录下还有 net1.exe
,这两个程序的功能是一模一样的:
不论我们是使用 net
或者是 net1
都会被杀软检测,至于是不是对底层 API
的 Hook
操作,我们还并不清楚
因此在这里,我们来监测系统使用 user add
时具体对应的 API
:
实际上当我们使用 net user username password /add
时 net
程序会去调用 net1.exe
程序,然后使用相同的命令:
当尝试跟进 net1.exe
来跟踪相关操作时发现应该是使用 RPC
,并且 endpoint
是 PIPElsarpc
来进行其他的操作
到这里就没有进一步跟进下去,其实在这里了解到实际上是通过 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
的方法并没有结束
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
对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 来设置新建用户的密码
最后成功绕过杀软进行用户添加:
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
表实现的
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
程序集文件
编写反射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
这里应该在此基础上进行修改就能够实现创建用户,稍微留个坑等之后再来重写下
本文作者:Crispr
原文地址:https://www.crisprx.top/archives/531
参考文章:
https://idiotc4t.com/redteam-research/netuseradd-ni-xiang
https://loong716.top/posts/Set_ChangeNTLM_SAMR/#2-%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8ms-samr
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/1cd138b9-cc1b-4706-b115-49e53189e32e
关注公众号后台回复 0001
领取域渗透思维导图,0002
领取VMware 17永久激活码,0003
获取SGK地址,0004
获取在线ChatGPT地址,0005
获取 Windows10渗透集成环境,0006
获取 CobaltStrike 4.9.1破解版
加我微信好友,邀请你进交流群
往期推荐
备用号,欢迎关注
原文始发于微信公众号(刨洞安全团队):绕过AV进行UserAdd的方法总结及实现