0x0 类型速查表

类型速查表(C → Go)

C 类型 说明 Go 中的对应类型
DWORD 32 位无符号整数 uint32
LPDWORD DWORD*,指向一个 32 位整数的指针 *uint32
LPBYTE* BYTE**,指向缓冲区指针的地址 *uintptr
LPCWSTR 常量宽字符字符串(UTF-16)指针 *uint16uintptr(0)
PDWORD LPDWORD 类似,指针类型 *uint32

0x1 netapi32.dll

netapi32.dll 是 Windows 操作系统中的一个系统动态链接库,提供了许多网络管理相关的 API,特别是管理网络用户、组、共享资源等。

通过这个 DLL,可以用来编程实现对 Windows 本地或远程计算机上的用户账户、组账户的管理,比如枚举用户、添加用户、删除用户、修改用户属性等。

0x11写些小玩意

使用Go的syscall包进行Windows的一些API的调用

syscall包中用syscall.LoadDLL() syscall.NewLazyDLL()来调用dll

写一个可以列出系统用户的小程序

NetUserEnum函数可以检索有关服务器上面的的所有的用户的账户的信息。

在微软给出的API文档中其是这么定义的

1
2
3
4
5
6
7
8
9
10
NET_API_STATUS NET_API_FUNCTION NetUserEnum(
[in] LPCWSTR servername, // 服务器名 null表示本机
[in] DWORD level, // 返回信息级别 0 只返回用户名
[in] DWORD filter, // 过滤器 0 无过滤 返回所有用户名
[out] LPBYTE *bufptr, // 缓冲区
[in] DWORD prefmaxlen, // 建议缓冲区大小,0xFFFFFFFF 表示不限制
[out] LPDWORD entriesread, // 实际读取了多少条记录
[out] LPDWORD totalentries, // 实际存在的总记录数
[in, out] PDWORD resume_handle // 用于继续现有的用户搜索
);

OK 知道了这个接口你们来写一个小程序来发现Windows 上面的系统用户

level的值为2的时候 返回一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _USER_INFO_2 {
LPWSTR usri2_name;
LPWSTR usri2_password;
DWORD usri2_password_age;
DWORD usri2_priv;
LPWSTR usri2_home_dir;
LPWSTR usri2_comment;
DWORD usri2_flags;
LPWSTR usri2_script_path;
DWORD usri2_auth_flags;
LPWSTR usri2_full_name;
LPWSTR usri2_usr_comment;
LPWSTR usri2_parms;
LPWSTR usri2_workstations;
DWORD usri2_last_logon;
DWORD usri2_last_logoff;
DWORD usri2_acct_expires;
DWORD usri2_max_storage;
DWORD usri2_units_per_week;
PBYTE usri2_logon_hours;
DWORD usri2_bad_pw_count;
DWORD usri2_num_logons;
LPWSTR usri2_logon_server;
DWORD usri2_country_code;
DWORD usri2_code_page;
} USER_INFO_2, *PUSER_INFO_2, *LPUSER_INFO_2;

类型的结构体包含有关用户帐户的信息,包括帐户名称、密码数据、特权级别、用户主目录的路径以及其他与用户相关的网络统计信息。

定义一个结构体用来接收这些返回的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type userInfo struct {
Usri2Name *uint16 // 用户名
Usri2Password *uint16 // 用户密码(仅用于设置,不会返回)
Usri2PasswordAge uint32 // 密码年龄(单位:秒)
Usri2Priv uint32 // 权限级别(0=访客,1=普通用户,2=管理员)
Usri2HomeDir *uint16 // 主目录(用户登录后的目录)
Usri2Comment *uint16 // 注释(通常为管理员备注)
Usri2Flags uint32 // 用户账户控制标志(如启用/禁用)
Usri2ScriptPath *uint16 // 登录脚本路径
Usri2AuthFlags uint32 // 认证标志(通常未使用)
Usri2FullName *uint16 // 用户全名
Usri2UsrComment *uint16 // 用户注释(用户可读的)
Usri2Parms *uint16 // 管理参数(自定义字段)
Usri2Workstations *uint16 // 允许登录的工作站(以逗号分隔)
Usri2LastLogon uint32 // 上次登录时间(UNIX时间戳)
Usri2LastLogoff uint32 // 上次注销时间(很少使用)
Usri2AcctExpires uint32 // 账户过期时间(0xffffffff 表示永不过期)
Usri2MaxStorage uint32 // 最大存储空间(以字节为单位,0xffffffff 表示无限制)
Usri2UnitsPerWeek uint32 // 一周的时间单位数(用于 logon_hours)
Usri2LogonHours *byte // 登录时间限制(bitmask,按小时排列)
Usri2BadPwCount uint32 // 错误密码尝试次数(连续错误次数)
Usri2NumLogons uint32 // 登录次数(成功的)
Usri2LogonServer *uint16 // 登录的服务器名称
Usri2CountryCode uint32 // 国家/地区代码(如 86 表示中国)
Usri2CodePage uint32 // 代码页(如 936 表示简体中文)
}

加载dll

1
2
3
4
//加载 相关dll 以及 函数
netapi32 := syscall.NewLazyDLL("netapi32.dll")
netUserEnum := netapi32.NewProc("NetUserEnum")
procNetApiBufferFree := netapi32.NewProc("NetApiBufferFree")

调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ret, _, _ := netUserEnum.Call(
0,
2, // level 级别为 2 查询更多信息
0,
uintptr(unsafe.Pointer(&bufptr)),
0xFFFFFFFF, // 自动选择缓冲区大小
uintptr(unsafe.Pointer(&entriesread)),
uintptr(unsafe.Pointer(&totalentries)),
uintptr(unsafe.Pointer(&resume_handle)),
)
if ret != 0 {
fmt.Printf("NetUserEnum failed with code: %d\n", ret)
return
}
// 释放buffer
defer procNetApiBufferFree.Call(bufptr)

打印相关数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
infoSize := unsafe.Sizeof(userInfo{})
fmt.Println("══════════════════════════════════════════════用户名列表══════════════════════════════════════════════")
for i := uint32(0); i < entriesread; i++ {
user := (*userInfo)(unsafe.Pointer(bufptr + uintptr(i)*infoSize))
fmt.Println("────────────────────────────────────────────────────────────────────────────────────")
fmt.Printf("用户名 : %s\n", utils.UTF16PtrToString(user.Usri2Name))
fmt.Printf("用户全名 : %s\n", utils.UTF16PtrToString(user.Usri2FullName))
fmt.Printf("权限级别 : %d (%s)\n", user.Usri2Priv, getUserPriv(user.Usri2Priv))
fmt.Printf("主目录 : %s\n", utils.UTF16PtrToString(user.Usri2HomeDir))
fmt.Printf("登录服务器 : %s\n", utils.UTF16PtrToString(user.Usri2LogonServer))
fmt.Printf("注释 : %s\n", utils.UTF16PtrToString(user.Usri2Comment))
fmt.Printf("账户状态 : %s\n", getUserFlags(user.Usri2Flags))
fmt.Printf("登录次数 : %d\n", user.Usri2NumLogons)
fmt.Printf("错误密码次数 : %d\n", user.Usri2BadPwCount)
fmt.Printf("上次登录时间 : %s\n", formatUnixTime(user.Usri2LastLogon))
fmt.Printf("账户过期时间 : %s\n", formatExpiry(user.Usri2AcctExpires))
fmt.Println("────────────────────────────────────────────────────────────────────────────────────")
}
fmt.Println("══════════════════════════════════════════════用户名列表══════════════════════════════════════════════")

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//infoType.go
package function

import "time"
/*
*leval 级别为 2的时候返回的结构体类型
typedef struct _USER_INFO_2 {
LPWSTR usri2_name;
LPWSTR usri2_password;
DWORD usri2_password_age;
DWORD usri2_priv;
LPWSTR usri2_home_dir;
LPWSTR usri2_comment;
DWORD usri2_flags;
LPWSTR usri2_script_path;
DWORD usri2_auth_flags;
LPWSTR usri2_full_name;
LPWSTR usri2_usr_comment;
LPWSTR usri2_parms;
LPWSTR usri2_workstations;
DWORD usri2_last_logon;
DWORD usri2_last_logoff;
DWORD usri2_acct_expires;
DWORD usri2_max_storage;
DWORD usri2_units_per_week;
PBYTE usri2_logon_hours;
DWORD usri2_bad_pw_count;
DWORD usri2_num_logons;
LPWSTR usri2_logon_server;
DWORD usri2_country_code;
DWORD usri2_code_page;
} USER_INFO_2, *PUSER_INFO_2, *LPUSER_INFO_2;
*/
type userInfo struct {
Usri2Name *uint16 // 用户名
Usri2Password *uint16 // 用户密码(仅用于设置,不会返回)
Usri2PasswordAge uint32 // 密码年龄(单位:秒)
Usri2Priv uint32 // 权限级别(0=访客,1=普通用户,2=管理员)
Usri2HomeDir *uint16 // 主目录(用户登录后的目录)
Usri2Comment *uint16 // 注释(通常为管理员备注)
Usri2Flags uint32 // 用户账户控制标志(如启用/禁用)
Usri2ScriptPath *uint16 // 登录脚本路径
Usri2AuthFlags uint32 // 认证标志(通常未使用)
Usri2FullName *uint16 // 用户全名
Usri2UsrComment *uint16 // 用户注释(用户可读的)
Usri2Parms *uint16 // 管理参数(自定义字段)
Usri2Workstations *uint16 // 允许登录的工作站(以逗号分隔)
Usri2LastLogon uint32 // 上次登录时间(UNIX时间戳)
Usri2LastLogoff uint32 // 上次注销时间(很少使用)
Usri2AcctExpires uint32 // 账户过期时间(0xffffffff 表示永不过期)
Usri2MaxStorage uint32 // 最大存储空间(以字节为单位,0xffffffff 表示无限制)
Usri2UnitsPerWeek uint32 // 一周的时间单位数(用于 logon_hours)
Usri2LogonHours *byte // 登录时间限制(bitmask,按小时排列)
Usri2BadPwCount uint32 // 错误密码尝试次数(连续错误次数)
Usri2NumLogons uint32 // 登录次数(成功的)
Usri2LogonServer *uint16 // 登录的服务器名称
Usri2CountryCode uint32 // 国家/地区代码(如 86 表示中国)
Usri2CodePage uint32 // 代码页(如 936 表示简体中文)
}
// userInfo 信息辅助函数
func getUserPriv(priv uint32) string {
switch priv {
case 0:
return "访客"
case 1:
return "普通用户"
case 2:
return "管理员"
default:
return "未知"
}
}

func getUserFlags(flags uint32) string {
if flags&0x0001 != 0 {
return "账户禁用"
}
return "正常"
}

func formatUnixTime(ts uint32) string {
if ts == 0 {
return "从未登录"
}
return time.Unix(int64(ts), 0).Format("2006-01-02 15:04:05")
}

func formatExpiry(expiry uint32) string {
if expiry == 0xFFFFFFFF {
return "永不过期"
}
return time.Unix(int64(expiry), 0).Format("2006-01-02 15:04:05")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// CollectInfo.go
package function

import (
"fmt"
"syscall"
"unsafe"
"windowsFinder/utils"
)

// 接受 NetUserEnumerate 返回的数据
var (
bufptr uintptr
entriesread uint32
totalentries uint32
resume_handle uint32
)

func collectSystemUserInfo() {
//加载 相关dll 以及 函数
netapi32 := syscall.NewLazyDLL("netapi32.dll")
netUserEnum := netapi32.NewProc("NetUserEnum")
procNetApiBufferFree := netapi32.NewProc("NetApiBufferFree")

ret, _, _ := netUserEnum.Call(
0,
2, // level 级别为 2 查询更多信息
0,
uintptr(unsafe.Pointer(&bufptr)),
0xFFFFFFFF, // 自动选择缓冲区大小
uintptr(unsafe.Pointer(&entriesread)),
uintptr(unsafe.Pointer(&totalentries)),
uintptr(unsafe.Pointer(&resume_handle)),
)
if ret != 0 {
fmt.Printf("NetUserEnum failed with code: %d\n", ret)
return
}
// 释放buffer
defer procNetApiBufferFree.Call(bufptr)

infoSize := unsafe.Sizeof(userInfo{})
fmt.Println("══════════════════════════════════════════════用户名列表══════════════════════════════════════════════")
for i := uint32(0); i < entriesread; i++ {
user := (*userInfo)(unsafe.Pointer(bufptr + uintptr(i)*infoSize))
fmt.Println("────────────────────────────────────────────────────────────────────────────────────")
fmt.Printf("用户名 : %s\n", utils.UTF16PtrToString(user.Usri2Name))
fmt.Printf("用户全名 : %s\n", utils.UTF16PtrToString(user.Usri2FullName))
fmt.Printf("权限级别 : %d (%s)\n", user.Usri2Priv, getUserPriv(user.Usri2Priv))
fmt.Printf("主目录 : %s\n", utils.UTF16PtrToString(user.Usri2HomeDir))
fmt.Printf("登录服务器 : %s\n", utils.UTF16PtrToString(user.Usri2LogonServer))
fmt.Printf("注释 : %s\n", utils.UTF16PtrToString(user.Usri2Comment))
fmt.Printf("账户状态 : %s\n", getUserFlags(user.Usri2Flags))
fmt.Printf("登录次数 : %d\n", user.Usri2NumLogons)
fmt.Printf("错误密码次数 : %d\n", user.Usri2BadPwCount)
fmt.Printf("上次登录时间 : %s\n", formatUnixTime(user.Usri2LastLogon))
fmt.Printf("账户过期时间 : %s\n", formatExpiry(user.Usri2AcctExpires))
fmt.Println("────────────────────────────────────────────────────────────────────────────────────")
}
fmt.Println("══════════════════════════════════════════════用户名列表══════════════════════════════════════════════")
}

image-20250715115728380

0x2 wevtapi.dll

wevtapi.dll 是 Windows 事件日志 API 的动态链接库,从 Windows Vista 开始引入,替代了旧版事件日志接口(如 eventlog.dll)。

提供了访问和管理 Windows 事件日志的功能,支持现代化的事件查询、读取、渲染和订阅等。

0x21 写些小玩意

写一个可以提取会话管理日志信息的小程序

日志通道名称 主要功能/用途 典型事件类型
Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational 远程连接管理,负责RDP连接的建立和断开 连接尝试(成功/失败)、认证事件、连接断开等
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational 本地会话管理,管理本地终端服务器的会话状态,包括登录、注销、断开等 用户登录、注销、会话状态变化、重连等事件

从以上两个通道中提取出必要的会话信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//EvtQuery函数

EVT_HANDLE EvtQuery(
[in] EVT_HANDLE Session, //远程会话句柄。 设置为 NULL 以查询本地计算机上的事件
[in] LPCWSTR Path, //通道的名称或日志文件的完整路径,其中包含要查询的事件。
[in] LPCWSTR Query, //一个查询,指定要检索的事件类型。 可以指定 XPath 1.0 查询或结构化 XML 查询。
[in] DWORD Flags //一个或多个标志,指定要接收事件的顺序,以及是针对通道还是日志文件进行查询。
);

//EvtNext函数
BOOL EvtNext(
[in] EVT_HANDLE ResultSet, // 事件结果集的句柄,来自 EvtQuery、EvtSeek 或 EvtSubscribe
[in] DWORD EventsSize, // Events 数组的最大容量(即可以接收多少个事件句柄)
[in] PEVT_HANDLE Events, // 用于接收事件句柄的数组,大小为 EventsSize
[in] DWORD Timeout, // 等待事件可用的超时时间 常用值:INFINITE:一直等待0:立即返回 其它值:最多等待该时间
[in] DWORD Flags, // 保留,目前必须为 0
[out] PDWORD Returned // 实际返回的事件句柄数量。你需要根据这个值来遍历 Events 数组
);

//EvtRender 函数
BOOL EvtRender(
[in] EVT_HANDLE Context, // 渲染上下文句柄 通常为 NULL
[in] EVT_HANDLE Fragment, // 单个事件句柄,来自 EvtNext 函数返回的 Events[] 数组。
[in] DWORD Flags, // 渲染方式的标志位,常见取值如下:
// - EvtRenderEventXml:以 XML 字符串格式输出(常用)
// - EvtRenderEventValues:以结构体属性形式输出
[in] DWORD BufferSize, // Buffer 缓冲区的大小(单位:字节) 初次调用时可设为 0 以获取所需大小
[in] PVOID Buffer, // 指向接收输出数据的缓冲区。类型和格式取决于 Flags:
[out] PDWORD BufferUsed, // 实际使用的缓冲区大小(字节数),常用于判断是否需要重新分配缓冲区。
[out] PDWORD PropertyCount // EvtRenderEventValues 模式下表示事件属性个数
//若为 XML 渲染(EvtRenderEventXml),则忽略。
);

那么思路边清晰了

我们可以先用 EvtQuery函数 获取事件句柄 - > 再用EvtNext从事件句柄中获取事件内容 - > 再用EvtRender将结果解析为xml等易读格式

1
2
3
4
5
6
//定义想要查询的 日志通道 与 事件ID
channels := []string{
"Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational",
"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational",
}
query := "*[System[(EventID=21 or EventID=25 or EventID=1149)]]"
1
2
3
4
5
6
7
8
9
10
11
12
//调用 EvtQuery 函数获取事件句柄
handle, _, err := procEvtQuery.Call(
0,
uintptr(unsafe.Pointer(utf16Ptr(channel))),
uintptr(unsafe.Pointer(utf16Ptr(query))),
uintptr(EvtQueryChannelPath),
)
if handle == 0 {
fmt.Printf("[!] EvtQuery 失败: %v\n", err)
continue
}
defer procEvtClose.Call(handle)
1
2
3
4
5
6
7
8
9
10
//调用EvtNext函数获取事件内容

ret, _, _ := procEvtNext.Call(
handle,
uintptr(batchSize),
uintptr(unsafe.Pointer(&events[0])),
0,
0,
uintptr(unsafe.Pointer(&returned)),
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//调用EvtRender格式化处理每一个事件

ret, _, err := procEvtRender.Call(
0,
uintptr(events[i]),
EvtRenderEventXml,
uintptr(len(buf)*2),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&used)),
uintptr(unsafe.Pointer(&propCount)),
)
if ret == 0 {
fmt.Printf("[!] 第 %d 条事件渲染失败: %v\n", total-(int(returned)-int(i)), err)
procEvtClose.Call(uintptr(events[i]))
continue
}

完整的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package main

import (
"fmt"
"syscall"
"unsafe"
)

var (
wevtapi = syscall.NewLazyDLL("wevtapi.dll")
procEvtQuery = wevtapi.NewProc("EvtQuery")
procEvtNext = wevtapi.NewProc("EvtNext")
procEvtRender = wevtapi.NewProc("EvtRender")
procEvtClose = wevtapi.NewProc("EvtClose")
)

const (
EvtQueryChannelPath = 0x00000001
EvtRenderEventXml = 1
BufferSize = 1 << 16 // 64KB
batchSize = 6 // 每批读取事件数
ERROR_NO_MORE_ITEMS = 259 // Windows 错误码:没有更多项
)

// 将字符串转为 *uint16(UTF-16 指针)
func utf16Ptr(s string) *uint16 {
ptr, _ := syscall.UTF16PtrFromString(s)
return ptr
}

func main() {
channels := []string{
"Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational",
"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational",
}
query := "*[System[(EventID=21 or EventID=25 or EventID=1149)]]"

for _, channel := range channels {
fmt.Printf("\n==== 查询通道: %s ====\n", channel)

// 1. 打开查询句柄
handle, _, err := procEvtQuery.Call(
0,
uintptr(unsafe.Pointer(utf16Ptr(channel))),
uintptr(unsafe.Pointer(utf16Ptr(query))),
uintptr(EvtQueryChannelPath),
)
if handle == 0 {
fmt.Printf("[!] EvtQuery 失败: %v\n", err)
continue
}
defer procEvtClose.Call(handle)

total := 0

// 2. 循环读取所有事件
for {
var events [batchSize]syscall.Handle
var returned uint32

ret, _, _ := procEvtNext.Call(
handle,
uintptr(batchSize),
uintptr(unsafe.Pointer(&events[0])),
0,
0,
uintptr(unsafe.Pointer(&returned)),
)

if ret == 0 {
lastErr := syscall.GetLastError()
if errno, ok := lastErr.(syscall.Errno); ok && errno == ERROR_NO_MORE_ITEMS {
fmt.Println("[*] 所有事件已读取完毕")
break
}
fmt.Printf("[!] EvtNext 失败: %v\n", lastErr)
break
}

fmt.Printf("[+] 本次读取 %d 条事件\n", returned)
total += int(returned)

// 3. 遍历处理每条事件
for i := uint32(0); i < returned; i++ {
// 渲染事件 XML
buf := make([]uint16, BufferSize)
var used, propCount uint32

ret, _, err := procEvtRender.Call(
0,
uintptr(events[i]),
EvtRenderEventXml,
uintptr(len(buf)*2),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&used)),
uintptr(unsafe.Pointer(&propCount)),
)
if ret == 0 {
fmt.Printf("[!] 第 %d 条事件渲染失败: %v\n", total-(int(returned)-int(i)), err)
procEvtClose.Call(uintptr(events[i]))
continue
}

xml := syscall.UTF16ToString(buf[:used/2])
fmt.Printf("\n--- 第 %d 条事件 ---\n%s\n", total-(int(returned)-int(i))+1, xml)

// 释放事件句柄
procEvtClose.Call(uintptr(events[i]))
}
}
}
}