1. 项目背景与核心挑战
作为一名长期从事Windows系统开发的工程师,我经常遇到这样的场景:在Xshell、远程桌面等应用中保存了密码后,由于星号掩码的遮挡,时间一长就忘记了原始密码内容。Windows系统对密码框的安全保护机制使得我们无法通过常规手段获取这些被掩码的密码。这就是我开发Windows星号密码查看器的初衷。
Windows系统对密码框的保护机制主要基于以下几个层面:
- 内存保护:密码框控件在内存中存储的是明文密码,但通过ES_PASSWORD样式属性设置了掩码显示
- 进程隔离:从Windows XP SP2开始,系统会检查WM_GETTEXT等消息的发送方进程,阻止跨进程的密码读取
- API限制:直接修改控件样式或属性的尝试也会被系统拦截
2. 技术方案选型与对比
2.1 传统方法的局限性
在开发初期,我尝试了多种传统方法,但都遇到了系统级的限制:
- 直接消息发送法:
go复制procSendMessageW.Call(
uintptr(hwnd),
uintptr(WM_GETTEXT),
uintptr(len(buffer)),
uintptr(unsafe.Pointer(&buffer)),
)
这种方法在跨进程场景下会被系统直接拦截,返回空字符串或星号。
- 样式修改法:
go复制style := getWindowLongPtrW(hwnd, GWL_STYLE)
setWindowLongPtrW(hwnd, GWL_STYLE, style & ^ES_PASSWORD)
同样会因为跨进程权限问题而失败。
- 内存直接读取法:
go复制handle := procSendMessageW.Call(hwnd, EM_GETHANDLE, 0, 0)
系统会返回无效句柄或空指针。
2.2 Shellcode注入方案的确定
经过多次尝试和验证,最终选择了Shellcode注入方案,主要基于以下考虑:
- 原理可行性:在目标进程内部执行代码可以绕过跨进程保护
- 性能考量:相比其他注入方式(如DLL注入),Shellcode体积小、执行快
- 兼容性:可以同时支持32位和64位进程
- 隐蔽性:不需要在目标进程加载额外模块
方案的核心流程如下:
- 获取目标进程句柄和窗口句柄
- 在目标进程分配内存空间
- 写入定制化的Shellcode
- 创建远程线程执行Shellcode
- 读取执行结果
3. 关键技术实现细节
3.1 Shellcode的生成与注入
3.1.1 64位Shellcode实现
64位架构使用Fastcall调用约定,参数通过寄存器传递:
go复制func buildShellcode64(hwnd uintptr, bufPtr uintptr, bufSize uintptr, sendMessageAddr uintptr) []byte {
buf := []byte{
// sub rsp, 0x28 (保留栈空间)
0x48, 0x83, 0xEC, 0x28,
// 参数设置
0x48, 0xB9, // mov rcx, hwnd
byte(hwnd), byte(hwnd>>8), byte(hwnd>>16), byte(hwnd>>24),
byte(hwnd>>32), byte(hwnd>>40), byte(hwnd>>48), byte(hwnd>>56),
// 函数调用
0x48, 0xB8, // mov rax, sendMessageAddr
byte(sendMessageAddr), byte(sendMessageAddr>>8), /*...*/,
0xFF, 0xD0, // call rax
// 恢复栈并返回
0x48, 0x83, 0xC4, 0x28,
0xC3,
}
return buf
}
3.1.2 32位Shellcode实现
32位架构使用Stdcall调用约定,参数通过栈传递:
go复制func buildShellcode32(hwnd uintptr, bufPtr uintptr, bufSize uintptr, sendMessageAddr uintptr) []byte {
buf := []byte{
// 参数压栈
0x68, // push bufPtr
byte(bufPtr), byte(bufPtr>>8), byte(bufPtr>>16), byte(bufPtr>>24),
// 函数调用
0xB8, // mov eax, sendMessageAddr
byte(sendMessageAddr), byte(sendMessageAddr>>8), /*...*/,
0xFF, 0xD0, // call eax
// 返回并清理栈
0xC2, 0x10, 0x00, // ret 16
}
return buf
}
3.2 进程架构检测与兼容处理
在64位Windows系统上,需要特别处理32位进程(WoW64)的情况:
go复制func isProcess32Bit(pid uint32) (bool, error) {
handle, _ := windows.OpenProcess(PROCESS_QUERY_INFORMATION, false, pid)
defer windows.CloseHandle(handle)
var wow64 bool
err := windows.IsWow64Process(handle, &wow64)
return wow64, err
}
对于32位进程,需要特殊处理函数地址解析:
go复制func resolveSendMessageW32(pid uint32) (uintptr, error) {
snapshot, _ := windows.CreateToolhelp32Snapshot(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32, pid)
defer windows.CloseToolhelp32Snapshot(snapshot)
var moduleEntry windows.ModuleEntry32
moduleEntry.Size = uint32(unsafe.Sizeof(moduleEntry))
for windows.Module32First(snapshot, &moduleEntry) {
modName := windows.UTF16ToString(moduleEntry.Module[:])
if strings.EqualFold(modName, "user32.dll") {
return findExportAddress(pid, uintptr(moduleEntry.ModuleBase), "SendMessageW")
}
windows.Module32Next(snapshot, &moduleEntry)
}
return 0, fmt.Errorf("user32.dll not found")
}
3.3 PE文件格式解析
为了在32位进程中定位SendMessageW函数地址,需要解析PE文件格式:
go复制func findExportAddress(hProcess windows.Handle, moduleBase uintptr, funcName string) (uintptr, error) {
// 读取DOS头
var dosHeader [64]byte
var bytesRead uintptr
windows.ReadProcessMemory(hProcess, moduleBase, unsafe.Pointer(&dosHeader[0]),
uintptr(len(dosHeader)), &bytesRead)
// 定位PE头
peOffset := *(*uint32)(unsafe.Pointer(&dosHeader[0x3C]))
// 读取PE头
var peHeader [24]byte
windows.ReadProcessMemory(hProcess, moduleBase+uintptr(peOffset),
unsafe.Pointer(&peHeader[0]), uintptr(len(peHeader)), &bytesRead)
// 获取导出表RVA
exportDirRVA := *(*uint32)(unsafe.Pointer(&peHeader[16]))
// 详细解析过程...
return findAddress, nil
}
4. 用户界面设计与交互实现
4.1 十字准星拖拽机制
为了实现类似Spy++的窗口选择体验,设计了以下交互逻辑:
go复制// 鼠标按下事件处理
case WM_LBUTTONDOWN:
if isMouseOnCrosshair(lParam) {
dragging = true
windows.SetCapture(hwndMain)
}
// 鼠标移动事件处理
case WM_MOUSEMOVE:
if dragging {
target := windows.WindowFromPoint(pt)
if target != lastTargetHwnd {
if lastTargetHwnd != 0 {
drawHighlight(lastTargetHwnd, false)
}
drawHighlight(target, true)
lastTargetHwnd = target
}
}
// 鼠标释放事件处理
case WM_LBUTTONUP:
if dragging && lastTargetHwnd != 0 {
extractAndDisplay(lastTargetHwnd)
}
dragging = false
windows.ReleaseCapture()
4.2 异步密码提取设计
为了避免UI卡顿,采用goroutine+消息通知机制:
go复制func extractAndDisplay(hwnd windows.HWND) {
if extracting {
return
}
extracting = true
setWindowText(hwndPasswordValue, "Reading...")
go func() {
password := readRemoteEditText(hwnd)
windows.PostMessage(hwndMain, WM_APP_EXTRACT_DONE, 0, 0)
extracting = false
}()
}
5. 项目架构与模块设计
项目采用模块化设计,主要分为以下几个核心模块:
code复制┌─────────────────┬──────────────────────┐
│ 文件 │ 说明 │
├─────────────────┼──────────────────────┤
│ main.go │ 程序入口、消息循环 │
├─────────────────┼──────────────────────┤
│ window.go │ 窗口创建与管理 │
├─────────────────┼──────────────────────┤
│ crosshair.go │ 拖拽选择逻辑 │
├─────────────────┼──────────────────────┤
│ extractor.go │ 密码提取协调器 │
├─────────────────┼──────────────────────┤
│ remotereader.go │ Shellcode注入引擎 │
├─────────────────┼──────────────────────┤
│ winapi.go │ Win32 API封装 │
└─────────────────┴──────────────────────┘
6. 实际应用与效果验证
经过测试,工具可以成功获取以下应用的密码:
- Xshell 5/6/7
- 远程桌面连接(mstsc)
- PuTTY 0.7+
- 各种基于标准Edit控件的应用
使用效果:
- 运行passwordviewer.exe
- 按住十字准星并拖动到目标密码框
- 释放鼠标按钮
- 密码将显示在工具窗口中
7. 开发经验与注意事项
在开发过程中积累了一些重要经验:
-
内存管理:
- 必须确保正确释放VirtualAllocEx分配的内存
- 跨进程读写后要及时关闭句柄
- 示例:
go复制defer windows.VirtualFreeEx(hProcess, remoteBuf, 0, MEM_RELEASE) defer windows.CloseHandle(hThread) -
权限问题:
- 需要PROCESS_VM_OPERATION权限分配内存
- 需要PROCESS_VM_WRITE权限写入内存
- 需要PROCESS_CREATE_THREAD权限创建远程线程
-
错误处理:
- 每个Win32 API调用都要检查返回值
- 提供详细的错误信息便于调试
- 示例:
go复制if err := windows.GetLastError(); err != nil { return fmt.Errorf("VirtualAllocEx failed: %v", err) } -
兼容性考虑:
- 不同Windows版本API行为可能有差异
- 32/64位进程需要不同处理
- 某些安全软件可能会拦截注入行为
8. 编译与部署建议
推荐使用以下编译参数:
bash复制go build -ldflags "-H windowsgui" -o passwordviewer.exe
参数说明:
-H windowsgui:隐藏控制台窗口- 建议静态编译以避免运行时依赖
部署注意事项:
- 在目标机器上可能需要以管理员权限运行
- 某些安全软件可能会误报为恶意工具
- 建议在可信环境中使用
9. 法律与道德考量
需要特别强调的是:
- 本工具仅限用于恢复自己遗忘的密码
- 未经授权获取他人密码是违法行为
- 不得用于任何非法用途
- 建议在内部系统中谨慎使用
在实际开发和使用过程中,我深刻体会到技术本身是中性的,关键在于使用者的目的和方式。作为开发者,我们有责任确保技术被用于正当用途。