《诗盗·大禹》:治水为命顺逆同,浩然莫测鬼神工。三过涂山生悟空,定海神针串鱼龙。
注解
改自霹雳角色“渊渟无迹静涛君”的诗号:
知水为命顺逆同,
浩然莫测深浅中。
无波沧海掩汹涌,
渊渟不动现鱼龙。
《诗盗·大禹》:治水为命顺逆同,浩然莫测鬼神工。三过涂山生悟空,定海神针串鱼龙。
改自霹雳角色“渊渟无迹静涛君”的诗号:
知水为命顺逆同,
浩然莫测深浅中。
无波沧海掩汹涌,
渊渟不动现鱼龙。
C 和 C++ 都有一些很基本的语句出现不同派系的写法,比如 * 靠左还是靠右,抑或居中?这种还是排版问题,并不影响有效字符数,但下面这个就直接影响有效字符数了!
《对于选择恐惧症患者来说,指针判空究竟要怎么写才不纠结?》
指针判空有两大类写法:
if (p) {}
if (nullptr != p) {}
后者被挺多人推荐的,比如林锐的《高质量C++/C编程指南》。
搜一下,能发现不少网友都挺纠结。这么基础的问题,如果不交代清楚,就是给 C++ 黑很好的攻击理由。
稣刚大学毕业时,是使用 C,并不屑 C++ 的,当时看了不少代码,就是直接 if (p)
和 if (!p)
,所以也坚持这种写法,并认为这样写比较短,对手指好,比较养生。
后来 C++17 出现,稣就坚定地改用 C++,于是也更多遵守 C++ 类型安全的原则,开始认为 if (p)
和 if (!p)
这种写法隐含类型转换,不太好。记得 2017 年以前,其实就看过 Google C++ Style Guide,里面也曾经建议写成 if (nullptr != p)
和 if (nullptr == p)
。
现在,再去看 Google C++ Style Guide,已经没有这样的建议。
C++ Core Guidelines 中有一条:ES.87: Don’t add redundant == or != to conditions,它的理由是:
Reason Doing so avoids verbosity and eliminates some opportunities for mistakes. Helps make style consistent and conventional.
if、while、for 的条件语句本身就是在选择 true 或 false,对于指针来说,会自动与 nullptr 比较。通常来说,if (p)
可以读作 “if p is valid”。
接着,还有一个例子:
1 | if (auto pc = dynamic_cast<Circle>(ps)) { ... } // execute if ps points to a kind of Circle, good |
同样是推荐更简短的写法。但这个例子却有人提出疑问:推荐的写法不会被怀疑是 == 少写了一个 = 吗?
还真不会!这个语句其实不是经典 C++ 的语法,而是 C++17 的 init-statement 语法,这里的 auto 是类型,说明 = 不可能是 == 错误地少写成 =。
对于部分 C 程序员和“经典 C++”程序员会把赋值语句写在 if 条件里的做法,建议是改为分开写。
1 | // p is defined before |
稣会在新工程里坚持使用 C++ Core Guidelines 的建议,但也不反对另一种写法,只要不混合使用。允许有不同派系,但最好别精神分裂。
Windows 自带 OpenSSH 用的是 LibreSSL。要不要也用一下?甚至,直接动态链接到系统自带的 LibreSSL 的 dll,减少 exe 体积!
Chrome 用的是 BoringSSL,要不要……折腾一下?
稣使用 vcpkg,安装几个库,还不是手到擒来?
结果……连装都不让装!这是 LibreSSL 的:
1 | Building libressl:x64-windows... |
这是 BoringSSL 的:
1 | Building boringssl:x64-windows... |
在忍痛 vcpkg remove openssl
后,陷入一阵思考——稣主要通过 Boost.Asio 使用 SSL,所以问题转换为:Boost.Asio 对这几个 SSL 库支持得如何?其中,OpenSSL 是使用多年没有任何问题的,只需要调研其它两个!
先试试 LibreSSL,毕竟是 OpenBSD 的,而且微软也用它。拿 ClipboardSync 代码编译,发现顺利通过!但是运行时抛了异常,说不支持 TLS 1.3……遂查阅官网,说是从 3.2.0 就支持!那就是 Boost.Asio 不对了,果断给它提 issue。
接着尝试 BoringSSL,毕竟是 Google 的,号称重视安全,而且有 Chrome 这个大型流行软件做背书!然而很打脸的是:它居然不支持 SM3!稣当年特地选择用国密标准里的 SM3 做 Hash 算法,就是因为爱国!不支持国密这点岂能忍?立刻 vcpkg remove boringssl
。
Boost.Asio 和稣联合推荐 OpenSSL 为唯一好用又爱国的 SSL 库。
Boost.Beast 是一个 HTTP/WebSocket 库。本文只讨论 WebSocket。
UMU 先是用过 WebSocket++(websocketpp),又用过 libwebsockets,再用的 Boost.Beast,之后就一直使用 Boost.Beast。
2018 年,参与 EOS 开发时,它是用 WebSocket++ 的,跟着学习了一阵子。
2020 年,在金山云时,内部版云游戏用 libwebsockets,跟着学习了一阵子。
做开源版云游戏——鎏光云游戏引擎时,特地学习并使用 Boost.Beast。因为公司其实是要求和内部版有一些差异的,正好之前一直用 Boost.Asio,对它的熟悉可以快速套用在 Boost.Beast,于是果断切到 Boost.Beast。
如果您原本使用裸 TCP,您应该知道 TCP 的流式传输,导致您需要自己分包,即界定一个“消息”的边界。鎏光本来设计为同时支持裸 TCP 和 WebSocket 的,所以还保留着处理分包的代码:
这对 WebSocket 其实并不需要,它的发送和接受已经是都是“消息”,带着长度的。所以如果您原来用 Asio 写 C/S 程序,把它们改为 Beast,是很容易的,而且代码量会缩减不少。
对于工具性的 C/S 程序,建议下次直接用 Beast 写更省事。
另一种适用场景是需要支持浏览器,即同时支持 C/S 和 B/S 模型。
text 和 binary 模式需要区分清楚,如果用于发送音视频,显然应该使用 binary 模式。
stream 的默认接收长度是 16MiB,如果不够可以改长点,0 表示最大的 std::uint64_t。
参考:boost/beast/websocket/stream.hpp
1 | /** Set the maximum incoming message size option. |
前文《防爆破远程桌面密码》提到可以用 Powershell 实现自动加 IP 黑名单到防火墙,这个坑还是得填,毕竟爆破依然在持续……
核心点:
IP 黑名单会持续新增,过去已经加入防火墙的名单也需要保存。所以应该把新增 IP 和防火墙已有 IP 求并集。
如果名单没变,不应该覆盖防火墙规则。
见 Github 仓库:
https://github.com/UMU618/scripts/blob/master/pwsh/add-ip-blacklist-to-firewall.ps1
面向公网开了个远程桌面端口,无论用多少号端口,都会被爆破!
虽然稣的密码很安全,几乎爆破不了,但会在系统日志里留下大量记录,实在很不雅观!
改端口是没用的,因为只有 65535 个,只要机器 IP 被发现,扫描端口很快就能完成。
封 IP 是有用的,虽然爆破者(攻击者)有很多 IP,但一定是有限的,有多少封多少!
打算使用 PowerShell 7 来编写脚本,首先学习 Get-WinEvent
命令:
1 | Get-Help Get-WinEvent -Online |
日志的过滤条件可以用“事件查看器”来协助生成:
当然,以上全部 XML 是 -FilterXml 的参数,比较长,可以用 -FilterXPath 来简化,只需要中间一部分。
条件还可以再加上 LogonType,以缩小范围,其中 3 表示“网络登录”:
1 | Get-WinEvent -LogName 'Security' -FilterXPath '*[System[EventID=4625] and EventData[Data[@Name="LogonType"]=3]]' -MaxEvents 1 | Format-List Message |
以下是完整代码,它会打印出 IP,和这个 IP 的登录失败次数:
1 | $ips = @{} |
目前收集到这些:
1 | 112.184.96.197 |
打开 wf.msc
,新建一个阻止型的防火墙策略,然后加入到“作用域”的“远程 IP 地址”里。
加防火墙也可以用 PowerShell 搞定,这次先偷个懒,下次再说吧!
不同编码规范对函数名的命名格式有不同要求,但主流有以下几类:
PascalCase:比如 Google C++ Style Guide
camelCase:比如 LLVM Coding Standards
snake_case:比如 PPP Styple Guide、K&R Style
哪种适合 Windows 开发呢?
PascalCase 偶尔会遇到和 Win32 API 宏冲突的情况:
1 |
|
以上代码无法编译,因为 SDK 头文件里有这样的定义:
1 |
导致以下编译错误:
1 | 1>function_name.cpp(7,5): warning C4003: not enough arguments for function-like macro invocation 'CreateWindowW' |
改成下面这样,才能编译:
1 |
|
以上代码虽然编译通过,但实际上 CreateWindowEx 还是个宏。用 IDA 逆向编译后的 exe,并加载 pdb 后,可以看到:
1 | ; int __cdecl main(int argc, const char **argv, const char **envp) |
虽然没啥危害,但 C++ 20 程序员不喜欢宏!
如果函数名使用 camelCase,则没有机会与 Win32 API 的宏定义冲突。
另一个好处是在做 API Hooking 时,命名可以更短。比如 Hook ShowWindow,那么替代函数可以就叫 showWindow,而用 PascalCase,则可能需要叫 MyShowWindow。
那么是不是把 Google C++ Style 的函数名由 PascalCase 改为 camelCase 就完美了?
更好,并不是完美……camelCase 也有个小问题——只有一个单词时,无法区分是 camelCase,还是 snake_case。比如 size,是函数(camelCase),还是临时变量(snake_case)?
UMU 建议,如果已经在使用 Google C++ Style,应该避免函数名与 Win32 API 一样。如果正在从头制定一套 Coding Style,则可以考虑函数用 camelCase。
因为种种原因……需要调试本机内核,用 bcdedit 开启调试模式:
1 | bcdedit -debug ON |
结果重启后,BitLocker 提示输入恢复密钥!
首先,要强调——调试本机内核,本身就是一个很危险的操作!建议还是用虚拟机调试。
其次,您一定已经备份了 BitLocker 的恢复密钥。只是它不一定在身边,比如说放在家里,人在公司,一来一回需要很长时间,所以得想办法节省时间。
最后,不要被 BitLocker 吓倒!即使,您没有备份恢复密钥,也还有救!在这个界面按 ESC,再点“跳过此驱动器”,后面是可以进入“控制台”的,只需要在“控制台”里撤销操作即可!
尝试关闭调试模式:
1 | bcdedit -debug OFF |
先别急着重启,因为这么敲——无效!不信您可以重启后再运行 bcdedit,会发现这个 debug 选项还是 Yes。
看来在 WindowsRE 环境下,直接运行 bcdedit,并不能修改 C 盘里的启动选项。您需要加上个 ID,一般为 {default}。
1 | bcdedit -debug {default} OFF |
先别急着重启,因为这么敲——还是无效!原本没有 debug 这个值,现在多了一个,数据是 No 而已,因为默认值就是 No,看似没有改变“调试模式”,但其实 BCD 数据库是变了的。正确的做法是删除这个 debug 值:
1 | bcdedit -deletevalue {default} debug |
重启后不再要求输入恢复密钥。这时可以在 BitLocker 的控制面板里先暂停保护,然后再操作 BCD,即可。
其中,鎏光一开始是用 C++ 17 的,在 MSVC 的 std::format 可用时,第一时间切换到 C++ 20。而 LiveNet 主要运行于 Linux,开发时 gcc 还不支持 std::format,使用 fmt::format 代替,但一直用 cxxstd=20 编译。
ClipboardSync 和 PowerEconomizer 使用了 C++ 20 modules。
现在还在开发的云桌面产品也是使用 C++ 20,但没有用 modules。
根据 jetbrains 的统计,2022 年时,C++ 20 的使用率是 23%,已经超过经典 C++ 的 8%。在游戏开发领域,C++ 20 的使用率为 25%,甚至已经超过 C++ 14 的 24%。
以下列举能够很容易想到的一些好处:
好用、安全的新类:std::format、std::span、std::jthread、原子(Atomic)智能指针
designated-initializers 安全初始化,防止因为调整结构体而顺序不对
modules 加速编译
更多标签 [[likely]], [[unlikely]], [[nodiscard(reason)]]
可以 using enum
对模板形式的 Lambda 有更好支持
范围 for 循环支持初始化
三路比较运算符 <=>
Boost 的 awaitables 协程(BOOST_ASIO_HAS_CO_AWAIT)需要 C++ 20
ranges 库
曾经遇到 clang-format 对 modules 支持不好的问题,后来升级 clang-format 解决。但目前还不建议在大型项目里使用 modules。
有些隐式转换无法编译,尤其在编译驱动代码时,容易遇到连 WDK 里的头文件都无法编译。这是因为 C++ 20 比 C++ 17 都严格。建议内核态驱动使用 C++ 17;用户态驱动可以 C++ 20。
想修改 API 的返回结果。举个例子,想把前文提到的 Windows Release 信息改为 “0618”。
从技术角度看,需求就是在 RegQueryValueExW 返回时,改 lpData 指向的内存。
1 | LSTATUS RegQueryValueExW( |
lpData 为第 5 个参数,根据《x64 软件约定》,它位于栈。具体来说,断点时,rip 处于 call 指令执行完毕的时间点,这时调用方的返回地址已经被压入栈里,所以此时的栈顶为返回地址。按照内存地址递增方向,返回地址后面就是 6 个参数。
1 | DisplayVersion |
dq rsp L7
显示返回地址和 6 个参数:1 | 0:000> dq rsp L7 |
其中,00000268`e4a96d10
对应 lpData,000000d7`e975e820
对于 lpcbData。
1 | 0:000> dd 000000d7`e975e820 L1 |
1 | 0:000> gu |
1 | eu 00000268`e4a96d10 "0618" |
bm KERNELBASE!RegQueryValueExW
断不下来?您可能是在 Windows 10 下实践才遇到这个问题。可以用 x KERNELBASE!RegQueryValueExW*
看看是不是有多个。一般来说,即使有多个,无银第八哥也能都断,用 bl 可以看到多个都被加入。万一多个函数的类型不一样,可能就只加了一个 void 类型的,可以直接用地址指定另外类型的(没有被添加的)。
Windows 11 是这样:
1 | 0:000> x KERNELBASE!RegQueryValueExW* |
Windows 10 可能是这样:
1 | 0:000> x kernelbase!RegQueryValueExW* |
KERNELBASE!RegQueryValueExW
返回后的指令怪怪的?1 | 0:000> gu |
前一条的 call qword ptr [shcore!_imp_RegQueryValueExW (00007ffc
2e5e5c90)]` 指令是 7 字节的,如果为了对齐也不应该补 5 字节呀。
1 | 0:000> u 00007ffc`2e53e6b6 |
看!后面的指令是从 00007ffc`2e53e6c2
开始的,也没有对齐。所以,如果不是为了对齐,那就可能是为了方便调试时在函数返回时加 int 3
了。如果有新的答案,将在本系列后续文章分享。