马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 调用 Windows 安全 API 实战:UAC 提权检测、用户 SID 与 DPAPI 保护本地密钥
阅读说明
1. 适用版本:PB 12.5 及以上(本文示例基于 PB 12.5 Unicode 版本)
2. 支持数据库:本文不涉及数据库操作(纯 Win32 安全 API,与具体数据库无关)
3. 操作系统与环境要求:Windows 7+(DPAPI 需 Windows 2000 以后);需先 Import uo_winsec.sru 与 DATA_BLOB 结构对象(DPAPI 依赖系统自带的 crypt32.dll,无需额外安装)
4. 难度系数:★★★☆☆
5. 其它阅读说明:示例使用窗口按钮 clicked 事件直接运行;DPAPI 部分无需自己管理密钥,最适合"把口令/主密钥安全地存到本机配置文件"这类场景
在 PowerBuilder 里做企业应用时,经常会碰到几个和安全相关的现实需求:程序是不是以管理员身份运行(写 Program Files、改系统设置前得先自查);当前登录的是谁、属于哪个组;以及最棘手的——怎么把一个数据库连接串或主密钥安安稳稳地存到本机配置文件里,又不把它明文写死在代码里。
这些需求都不需要引入第三方组件。PowerBuilder 通过 FUNCTION ... LIBRARY "xxx.dll" 声明外部函数,就能直接调用任意 Win32 DLL。本文用一个非可视对象 uo_winsec 串起四条典型链路:取当前用户/SID、UAC 提权检测、AES 对称加密、以及最实用的 DPAPI 本地密钥保护。所有外部函数原型都来自《Windows 安全参考手册》,DPAPI 那部分手册没收录,按 Win32 标准签名补齐。
一、前置准备
前置:把下面两个文件 Import 进你的 PBL:
- uo_winsec.sru —— 封装好的非可视对象(含本文全部方法);
- DATA_BLOB.srs —— 独立结构对象,导出名 data_blob(DPAPI 用)。
步骤:1) 在 Library 画板右键 PBL → Import,选这两个 .sru/.srs;2) 新建窗口 w_demo,放一个按钮 cb_run;3) 在 cb_run 的 clicked 事件贴入后续代码,运行窗口并点击按钮即可看到 MessageBox 结果。所有 Win32 API 由系统 DLL 提供,无需安装任何额外运行库。
二、获取当前用户与 SID
拿到当前登录用户名和 SID,是很多鉴权逻辑的第一步(比如按用户名写专属配置目录)。
- // 示例输入:无(取当前进程身份)
- uo_winsec lu_sec
- lu_sec = create uo_winsec
- // 1) 当前登录用户名
- string ls_user
- ls_user = lu_sec.uf_get_current_user()
- MessageBox("当前用户", ls_user)
- // 2) 当前用户的 SID 字符串(形如 S-1-5-21-...)
- string ls_sid
- ls_sid = lu_sec.uf_get_current_user_sid()
- MessageBox("我的 SID", ls_sid)
- destroy lu_sec
复制代码
uf_get_current_user 内部走 GetUserNameW,uf_get_current_user_sid 则是 用户名 → LookupAccountNameW 取 SID 字节 → ConvertSidToStringSidW 转可读字符串。
三、判断管理员权限与 UAC 提权
写 Program Files、改 HKLM 注册表、启 Windows 服务前,必须先确认自己是不是真有管理员权限——在开了 UAC 的机器上,"属于管理员组"和"已提权"是两回事。
- // 示例输入:无(检测当前进程令牌)
- uo_winsec lu_sec
- lu_sec = create uo_winsec
- // 1) 是否已真正提权(UAC 弹过并且同意)
- boolean lb_elevated
- lb_elevated = lu_sec.uf_is_elevated_admin()
- if lb_elevated then
- MessageBox("", "已以管理员身份运行(UAC 已提权)")
- else
- MessageBox("", "当前是普通用户权限,写系统目录会被拒绝")
- end if
- // 2) 是否属于 Administrators 组(26 = WinBuiltinAdministratorsSid)
- boolean lb_admin
- lb_admin = lu_sec.uf_is_member_of(26)
- if lb_admin then
- MessageBox("", "当前用户属于 Administrators 组")
- else
- MessageBox("", "当前用户不属于 Administrators 组")
- end if
- destroy lu_sec
复制代码
uf_is_elevated_admin 用 OpenProcessToken + GetTokenInformation(TokenElevation=20) 回读 4 字节标记;uf_is_member_of 用 CreateWellKnownSid(26) + CheckTokenMembership 判定令牌里有没有这个组。注意:UAC 开着时普通用户"属于管理员组"但未必"已提权",两者要分开判断。
四、用密码做 AES 对称加密(及它的局限)
如果只是想在本地把一段文本加密存起来,用密码派生 AES 密钥是最直接的办法。uo_winsec 用 CryptoAPI(CryptAcquireContextW + SHA256 派生 CALG_AES_128)实现:
- // 示例输入:要加密的明文与口令(实战中口令不要写死,应由用户/配置提供)
- string ls_plain
- ls_plain = "Server=db;Uid=sa;Pwd=Secret123"
- string ls_pwd
- ls_pwd = "MyWeakPassword!"
- // 示例输入:落盘路径
- string ls_hex_path
- ls_hex_path = "C:\temp\pb_aes.txt"
- uo_winsec lu_sec
- lu_sec = create uo_winsec
- // 加密并转十六进制便于写文本文件
- blob lb_cipher
- lb_cipher = lu_sec.uf_aes_encrypt(ls_plain, ls_pwd)
- string ls_hex
- ls_hex = String(lb_cipher, EncodingHex!)
- int li_f
- li_f = FileOpen(ls_hex_path, StreamMode!, Write!, LockWrite!, Replace!)
- FileWriteEx(li_f, ls_hex)
- FileClose(li_f)
- // 读回并解密
- li_f = FileOpen(ls_hex_path, StreamMode!, Read!, LockRead!, Shared!)
- string ls_hex_back
- ls_hex_back = Space(4096)
- FileReadEx(li_f, ls_hex_back)
- FileClose(li_f)
- ls_hex_back = Trim(ls_hex_back)
- blob lb_back
- lb_back = Blob(ls_hex_back, EncodingHex!)
- string ls_dec
- ls_dec = lu_sec.uf_aes_decrypt(lb_back, ls_pwd)
- MessageBox("AES 解密结果", ls_dec)
- destroy lu_sec
复制代码
局限:这种方式本质是"用口令保护数据",但口令(或它派生出的密钥)必须存在于你的代码或配置里,等于你还是得自己管密钥。如果目标只是"安全保存本机配置",有更省心的方案——DPAPI。
五、DPAPI 保护本地密钥(★重点,最实用)
DPAPI(Data Protection API,crypt32.dll)由操作系统托管密钥。加密时系统用当前用户的登录凭据(或本机密钥)锁住数据,解密时只有同一用户/同一台机器能开。你的代码里完全不出现密钥本身,这是它相对"自己 AES + 写死口令"最大的优势。
uo_winsec 用两个方法封装(手册未收录,已按 Win32 标准签名补齐):
- // 加密:a_machine_scope=true 密钥绑定本机(适合无交互服务账户),false 仅当前用户可解
- public function blob uf_dpapi_protect (string a_text, boolean a_machine_scope)
- // 解密:还原 uf_dpapi_protect 的密文
- public function string uf_dpapi_unprotect (blob a_cipher, boolean a_machine_scope)
复制代码
配套结构 data_blob(独立 .srs,与 uo_winsec.sru 同 PBL):
- // DATA_BLOB.srs —— 导出名 data_blob
- global type data_blob from structure
- unsignedlong cbData // 数据字节数
- blob pbData // 数据缓冲区(PB 以 blob 成员传入其指针)
- end type
复制代码
完整可运行示例——把主密钥加密存盘,再读回解密:
- // 示例输入:要保护的主密钥明文(实战可由随机生成或配置提供)
- string ls_master
- ls_master = "MySuperSecretMasterKey-2026"
- // 示例输入:密文落盘路径
- string ls_path
- ls_path = "C:\temp\pb_master.key"
- // 示例输入:是否绑定本机(false=仅当前用户可解,更适合桌面程序)
- boolean lb_machine
- lb_machine = false
- uo_winsec lu_sec
- lu_sec = create uo_winsec
- // 1) DPAPI 加密并写文件
- blob lb_cipher
- lb_cipher = lu_sec.uf_dpapi_protect(ls_master, lb_machine)
- int li_f
- li_f = FileOpen(ls_path, StreamMode!, Write!, LockWrite!, Replace!)
- FileWriteEx(li_f, lb_cipher)
- FileClose(li_f)
- MessageBox("已保存", "密文长度=" + String(Len(lb_cipher)) + ",已写入 " + ls_path)
- // 2) 重新读回并解密
- blob lb_back
- li_f = FileOpen(ls_path, StreamMode!, Read!, LockRead!, Shared!)
- FileReadEx(li_f, lb_back)
- FileClose(li_f)
- string ls_plain
- ls_plain = lu_sec.uf_dpapi_unprotect(lb_back, lb_machine)
- MessageBox("DPAPI 解密还原", ls_plain)
- destroy lu_sec
复制代码
几个要点:uf_dpapi_protect 默认带 CRYPTPROTECT_UI_FORBIDDEN(后台/服务必须,禁止弹窗),传入 a_machine_scope=true 再叠加 CRYPTPROTECT_LOCAL_MACHINE(密钥绑本机)。默认用户范围的密文,换用户或拷到别的机器都解不开——这正好满足"本机静态存储"的安全诉求。
六、组合实战:安全保存并读取数据库连接串
把四、五两节拼起来就是一条真实可用的链路:DPAPI 保护一把"主密钥"落盘 → 需要时解密主密钥 → 用主密钥 AES 解密真正的连接串。连接串本身从不下明文,主密钥也不出现在代码里。
- // 示例输入:真正的连接串(实战来自配置/服务端,这里仅示例)
- string ls_conn
- ls_conn = "Server=db;Database=ERP;Uid=app;Pwd=TopSecret"
- // 示例输入:主密钥(实战应由随机生成并用 DPAPI 保护,不要写死)
- string ls_master
- ls_master = "MySuperSecretMasterKey-2026"
- // 示例输入:两个落盘路径
- string ls_master_path
- ls_master_path = "C:\temp\pb_master.key"
- string ls_conn_path
- ls_conn_path = "C:\temp\pb_conn.txt"
- uo_winsec lu_sec
- lu_sec = create uo_winsec
- // 第 1 步:DPAPI 保护主密钥(仅当前用户可解)
- blob lb_mk
- lb_mk = lu_sec.uf_dpapi_protect(ls_master, false)
- int li_f
- li_f = FileOpen(ls_master_path, StreamMode!, Write!, LockWrite!, Replace!)
- FileWriteEx(li_f, lb_mk)
- FileClose(li_f)
- // 第 2 步:用主密钥 AES 加密连接串,存文本(十六进制)
- blob lb_conn
- lb_conn = lu_sec.uf_aes_encrypt(ls_conn, ls_master)
- string ls_conn_hex
- ls_conn_hex = String(lb_conn, EncodingHex!)
- li_f = FileOpen(ls_conn_path, StreamMode!, Write!, LockWrite!, Replace!)
- FileWriteEx(li_f, ls_conn_hex)
- FileClose(li_f)
- // 第 3 步(模拟下次启动):读主密钥 → DPAPI 解密 → 读连接串密文 → AES 解密
- blob lb_mk_back
- li_f = FileOpen(ls_master_path, StreamMode!, Read!, LockRead!, Shared!)
- FileReadEx(li_f, lb_mk_back)
- FileClose(li_f)
- string ls_master_back
- ls_master_back = lu_sec.uf_dpapi_unprotect(lb_mk_back, false)
- string ls_conn_hex_back
- li_f = FileOpen(ls_conn_path, StreamMode!, Read!, LockRead!, Shared!)
- ls_conn_hex_back = Space(4096)
- FileReadEx(li_f, ls_conn_hex_back)
- FileClose(li_f)
- ls_conn_hex_back = Trim(ls_conn_hex_back)
- blob lb_conn_back
- lb_conn_back = Blob(ls_conn_hex_back, EncodingHex!)
- string ls_conn_back
- ls_conn_back = lu_sec.uf_aes_decrypt(lb_conn_back, ls_master_back)
- MessageBox("最终还原的连接串", ls_conn_back)
- destroy lu_sec
复制代码
七、坑与注意
- UAC 下"属于管理员组"≠"已提权":写系统目录前一定用 uf_is_elevated_admin 判真实提权,而不是只看组成员。
- DPAPI 作用域:默认用户范围加密的文件换用户/换机器解不开;a_machine_scope=true 才绑本机(同机任意用户都能解,安全性下降),按需选择。
- DPAPI 输出缓冲:CryptProtectData 内部用 LocalAlloc 分配输出缓冲,建议在方法返回后立即复制到自有 buffer 并落盘,避免长时间持有系统分配内存。
- 未在本机 PB 编译器验证:uo_winsec.sru 按 Win32 语义与 PB 12.5 外部函数规则编写,导入后请先对 uf_aes_encrypt/decrypt 与 uf_dpapi_protect/unprotect 做单测(用本文示例即可)。
- DPAPI 不是传输加密:它只解决本机静态存储安全;网络传输请改用 TLS 或 uo_crypto 的国密/RSA。
|