Use from typing import dataclass_transform
其实动机很简单,众所周知对于 SQLAlchemy 1.4 想要 typing 可以安装 sqlalchemy2-stub
,对于 < 1.4 也可以有 sqlalchemy-stub
。然而对于最新的 SQLAlchemy >= 2.0,因为它自己有类型注释,但是又很少,所以还没有一个很好的解决方案。本文就是为了介绍一种我摸索出的解决方案,基本上可以完美解决 SQLAlchemy Model / ORM 的 typing 问题。
当然,如果你在手写 raw sql,那肯定是没办法自动弄好类型的,不要做梦了🚫。本文只针对使用了 SQLAlchemy ORM Model 的用户。
基本思路是,Python 在 PEP 681 (>= Python 3.11) 当中为 typing
模块提供了一个 dataclass_transform
decorator,可以将第三方的 class 标注为和原生的 dataclass 提供类似的功能:
Most type checkers, linters and language servers have full support for dataclasses. This proposal aims to generalize this functionality and provide a way for third-party libraries to indicate that certain decorator functions, classes, and metaclasses provide behaviors similar to dataclasses.
These behaviors include:
- Synthesizing an
__init__
method based on declared data fields. <-- good for us- Optionally synthesizing
__eq__
,__ne__
,__lt__
,__le__
,__gt__
and__ge__
methods.- Supporting “frozen” classes, a way to enforce immutability during static type checking.
- Supporting “field specifiers”, which describe attributes of individual fields that a static type checker must be aware of, such as whether a default value is provided for the field. <-- mostly good for us
总体来讲这个 decorator 提供了所有我们想要的功能,唯一的问题是 field: type = mapped_column(...)
会导致 type checker 认为 field
是 Optional
的。但总归基本的类型检查是能用的,甚至能兼容外键和 relationship
,只是在初始化类的时候会有提供参数没写全的风险。如果感觉这样不够好,可以通过给每个 Model 编写一个 dummy __init__
的方式来把 optional 干掉:
1 | class A(Base): |
实践上,我们为了方便,可以直接在 SQLAlchemy 的 Base
class 上面使用这个 decorator,这样就不用每次定义一个 Model class 都要写一遍了。
1 | from sqlalchemy import Integer, String, Text, text, ForeignKey |
这里 class Base(DeclarativeBase):
写法得到的 Base
class 和 Base = declarative_base()
得到的 Base
在功能上是一样的,但是给了我们使用 dataclass_transform
的空间。
]]>
We (distro packagers) decided to modify GCC spec to get rid of just way too many atomic symbol missing error. AFAIK debian does the same thing in the same way.
So, you may notice that some program compiles without -latomic
flag, but that's not the case if you switch to some other distros. I wrote another blog post explaining this in detail.
In short: For GCC you should add -latomic
if you use std::atomic<T>
where sizeof(T)
is lower than 4. Also apply to those __atomic_compare_exchange_2()
stuff. Clang is not affected.
This approach requires you to be on a non-RISC-V Arch Linux (x86_64 or aarch64, etc.) machine, because we use pacstrap
and pacman
.
1 | # pacman -Syu then reboot is recommended before this |
The default root password is archriscv
. You will be asked to change the root password the first time you boot the machine. DO NOT LEAVE IT BLANK, or you won't be able to login as root user later.
If, in the last step, you find yourself stucked at [ OK ] Reached target Graphical Interface
for too long, just press Ctrl-C and re-run startqemu.sh
.
After booting the machine, you may need to install necessary packages:
1 | sudo pacman -S git vim gcc |
mkrootfs
首先执行 /usr/share/makepkg/util.sh
来初始化当前的 bash 环境。这个脚本由 Arch Linux 的 makepkg
提供,里面有许多工具函数,例如 msg
函数可以往屏幕上打印出好看的 log。
1 | . /usr/share/makepkg/util.sh |
解析参数、显示帮助的部分这里略过。
创建 rootfs 的第一步是确保将会被视作 rootfs 的 /
目录的新创建的文件夹权限正确:
1 | mkdir -p ./rootfs |
随后调用 pacstrap 来生成 rootfs。
1 | sudo pacstrap \ |
这里的 /usr/share/devtools/pacman-extra-riscv64.conf
是类似于 /etc/pacman.conf
的一个配置文件,其中声明了包括软件源在内的一些配置:
1 | #[testing] |
这里的软件源配置比较重要,是 pacstrap
魔法的一部分。实际上,在调用 pacstrap
时,传入的 base
参数就告诉 pacstrap
需要从上述源里面下载、安装 base
组里面所有的软件包,其中就包括了 linux
、glibc
和 pacman
。而上述源就是 riscv64gc
架构的源,其中的软件包均面向 riscv64gc
架构编译,因此可以在 RISC-V 64 位 CPU 上的 Arch Linux 操作系统中运行。在 pacstrap
执行完毕后,base
meta package 中的所有软件包都已安装完成,这时的 rootfs 已经安装了 Arch Linux 的最小发行版。
再往下,则是配置镜像源。
1 | sudo sed -E -i 's|#(Server = https://riscv\.mirror\.pkgbuild\.com/repo/\$repo)|\1|' ./rootfs/etc/pacman.d/mirrorlist |
https://riscv.mirror.pkgbuild.com 为 Arch Linux 维护者在 pkgbuild.com
域名上为 Arch Linux RISC-V 创建的全球镜像。如果不添加这个镜像,初始的默认源从国内访问可能较慢或干脆无法访问。
1 | sudo pacman \ |
清空 pacman 的软件包缓存。通过 --sysroot
参数指定了 rootfs。
1 | sudo usermod --root $(realpath ./rootfs) --password $(openssl passwd -6 "$password") root |
设置系统密码。默认为 archriscv
。usermod
的 --password
接受的是密码的哈希,因此需要使用 openssl passwd -6 "$password"
算出给定的密码的哈希,再传给 usermod
。
1 | sudo bsdtar --create \ |
将配置完毕的 rootfs 文件夹压缩为 .tar.gz
格式,使用 --xattrs
以确保文件的 extended attribute 得到保留。压缩成功后,可以删除刚才临时创建的 rootfs 文件夹。
mkimg
TBD
]]>
多目标对象追踪(Multi-Object Tracking, MOT)一直是计算机视觉(Computer Vision,CV)领域中非常重要的研究对象,其核心是通过分析输入的图像序列,构建出不同帧的物体间的对应关系。多目标对象追踪常被用于自动驾驶、人流量统计、水果分拣、嫌犯追踪等领域,在工业中存在着广泛的应用。
目前,研究者已经提出了许多的多目标对象追踪算法。按照工作流程分类,MOT 算法分为基于检测的追踪(Detection-Based Tracking, DBT)和无检测追踪(Detection-Free Tracking, DFT)两类[1]。DBT 依赖于对象检测,通常建立在一些已有的多目标对象检测及分类算法,如 YOLO[2] 等算法的基础上;而 DFT 则不需要对象检测的参与。有关 DBT 和 DFT 的特点可以参考表 1。
DBT | DFT | |
---|---|---|
初始化 | 自动;不完美 | 人工介入;完美 |
画面中对象数量 | 可变 | 固定不变 |
优点 | 无需人工介入,画面中物体数量可变 | 不需要识别器和分类器 |
缺点 | 性能受到识别器和分类器的限制 | 需要人工介入 |
常见应用场景 | 物体种类固定,画面中物体数量改变 | 物体种类不固定,但运动范围较小 |
而如果按照实现方法分类,多目标对象追踪算法可以被分成传统算法和基于机器学习的算法两类,其中传统算法通常工作在单摄像头场景下,基于机器学习的 MOT 算法在多摄像头场景下也工作良好,也更能适应工业上的多种应用需要[3]。因此,本文主要聚焦于近五年来提出的基于机器学习的 MOT 算法研究进展。
近期研究综述
近年来,随着机器学习理论和模型的不断发展,MOT 领域相关的研究热度也在持续上升。自 2017 年 DeepSORT[4] 被提出起,在短短的几年内涌现出一大批基于机器学习的高性能、高准确率的 MOT 算法,如 CenterTrack[5]、Tracktor++[6] 等。有些研究成果很好地解决了在某些特定领域内特定需求下的准确率问题,有些则提出了新的网络结构和模型架构,福泽所有在这一领域开展研究的研究人员。本文试图以时间顺序为主轴,按照这些研究所解决的问题的种类来分类不同的研究,并综述各研究的思路和主要成果。
虽然将机器学习引入 MOT 算法的研究很早以前就已经开展,但是真正在准确率上做出突破性提升的是 2017 年的 DeepSORT。DeepSORT 在原本的基于卡尔曼滤波(高斯滤波)预测和匈牙利匹配计算最优解的 SORT[7] 算法的基础上引入了深度学习的概念,在一个大规模的行人重识别数据集上训练,增加了对图像部分缺失和短时间遮挡的鲁棒性,同时保持了算法的高效性[4]。并且 DeepSORT 是一个在线(Online)算法,其在识别物体关联和轨迹时只需要参考过往的信息,无需参考未来的图像,因此它能够工作在实时输入的视频流上,使用场景更广。
不过,由于 DeepSORT 仍然工作在 SORT 基础上,因此 SORT 存在的缺陷仍然会在 DeepSORT 中存在。例如,SORT 采用卡尔曼滤波预测物体在下一帧中的位置。卡尔曼滤波为贝叶斯滤波在置信度用多元正态分布的特殊情况下推导得出,可得其函数表示如下: \[p(x)=\det(2\pi S)^{-\frac{1}{2}}\exp\left( -\frac{1}{2}(x-\mu)^{T}S^{-1}(x-\mu)\right)\\\] 其中,\(\mu\) 为样本均值,\(S\) 为样本方差。然而实际上,由相机录制的视频往往存在不满足正态分布的位移扰动如手持相机导致的不规律抖动,不难想到此时卡尔曼滤波模型产生的结果的准确度会降低。这也是 DeepSORT 算法的主要缺点。
在随后的一段时间内,虽然又有一些基于机器学习的 MOT 算法被提出,但它们对准确率的提升微乎其微,甚至在两年中仅仅使得最佳准确率(State-of-the-Art,SOTA)提升了 2%[1]。这时,Bergmann 团队在 ICCV2019 上发布了 Tracktor 算法的论文[6]。这篇论文不仅部分否定了过去两年中全世界研究人员普遍采用的检测器和追踪器配合的思路、简化了此前的网络模型,还对 SOTA 的提升做出了贡献。具体来讲,Bergmann 团队尝试了仅仅使用检测器和物体检测算法,通过引入回归层来调优物体检测的外接矩形(Bounding Box)位置的做法,实现了性能优秀且准确率高的 MOT 算法。这种实现方式的优势主要有两点:首先,无需额外训练追踪器,不仅节约了算力,也降低了性能和功耗要求;再者,检测器全部为在线算法,回归层也只需要参考此前的输入和输出,因此 Tracktor 算法同样是在线算法。不过,Tracktor 算法也同样存在一定的缺点。例如,Tracktor 无法解决由于物体相互遮挡导致身份交换(Identify Switch,IDSW)的问题。为了解决这一问题,在同一篇论文中作者还提出了 Tracktor++ 算法,通过引入短期(Short-Term)身份重识别(Re-Identification, ReID),基于 Siamese Network 提取出物体的表面特征来帮助匹配[6]。然而,加入 ReID 导致了计算耗时的增加和性能的下降,为此作者还额外提出了通过等速假设和增强型相关系数最大化这两种运动模型(Motion Model)来改善预测的外接矩形在下一帧中的位置以减少 ReID 匹配耗时。
这一阶段同样有一些别的改进 DeepSORT 的研究,例如有一些 MOT 爱好者在私下尝试将 DeepSORT 中的 SORT 部分替换为其它的目标检测算法如 YOLOv4 甚至 YOLOv5,同样也取得了接近 SOTA 的效果,并且摆脱了 SORT 算法的固有缺陷。在正式的期刊中同样出现了类似的研究,例如有团队提出通过遮挡组管理(Occlusion Group Management)来改进 DeepSORT 算法的匹配部分[8]。然而 2020 年提出的 CenterTrack 算法[5] 指出了他们所采用的检测器和追踪器同时训练(Joint Learning the Detector and Embedding Model,JDE)方法中存在的固有缺陷,2021 年提出的 FairMOT 算法[9] 同样也注意到了这些缺陷。二者均在传统的 JDE 基础上做出了有针对性的改进,通过将 Anchor-Based 检测替换为 Anchor-Free 检测,缓解了这些问题,在多个数据集上取得了 SOTA 的成绩。在 FairMOT 的论文中,作者提出,目前 JDE 训练的检测方式为同时提取检测框和检测框内物体的 ReID 信息,然而由于同一个物体可能出现在多个检测框中,会导致较高的网络模糊性。同时,物体的实际中心可能并不是其外接矩形的几何中心,这就可能导通过几何中心计算出的位移距离和实际位移距离存在偏差。CenterTrack 算法采用了基于其灵感来源 CenterNet 的基于物体实际中心点的检测[10],采用这种方式能够更加准确地提取到物体的特征用于 ReID 层,可以更好地避免身份互换的问题。FairMOT 算法同样采用了基于物体实际中心点的特征检测,并且还引入了类似编码器和解码器模型(Encoder-Decoder Model)的网络,通过逐层降采样、分层提取特征信息的方式产生经过融合的多层信息,恰好满足了 ReID 算法需要多层融合信息的需求。提取出的高分辨率特征信息将根据其维度分别送入检测器和 ReID 层,最终取得了很好的效果。
除了改进基于 ReID 的匹配算法外,也有一些研究聚焦于如何更好地计算不同实例的相似度以提升准确率。Pang 团队在 2021 年提出了 QDTrack 算法[11] 便是如此。Pang 团队认为,此前工作仅仅利用像素级先验知识进行追踪,这种方法大多只适合一些简单的场景,当目标较多、存在大量遮挡或拥挤现象时,单纯基于位置信息的匹配很容易产生错误的结果。因此,QDTrack 通过拟密集(Quasi-Dense)匹配,支持在一张图片中创建上百个兴趣区域,通过对比损失以学习网络参数,尽可能多地利用图片中的已有信息。下图为展示 QDTrack 创建上百个兴趣区域的示意图。
同时,由于目标较多的场合下一定会频繁出现新目标进入画面和已有目标在画面边缘消失的情况,因此作者将背景单独作为一类参与训练和匹配,从而能够通过双向 Softmax 函数增强一致性。由于学习到的实例相似度特征太好,在最终的关联步骤即使是仅仅采用最简单的最近邻搜索也能得到非常好的匹配准确率。QDTrack 在采用了 Softmax 函数后的目标函数如下所示[11]: \[\mathcal{L}_e=\log \left[1+\sum_{\mathbf{k}^{+}} \sum_{\mathbf{k}^{-}} \exp \left(\mathbf{v} \cdot \mathbf{k}^{-}-\mathbf{v} \cdot \mathbf{k}^{+}\right)\right]\] 其中,\(\mathbf v, \mathbf{k}^{+}, \mathbf{k}^{-}\) 分别为训练样本、正目标样本和负目标样本的特征嵌入(Embedding)。这里的 Embedding 指的是是一种把原始输入数据分布地表示成一系列特征的线性组合的表示方法。
随后的研究表明,Quasi-Dense 特征匹配方案的泛化性很强。Hu et al. 在 2022 年将 Quasi-Dense 泛化到三维空间中的多目标对象匹配问题上,提出了 QD-3DT 算法[12]。该算法能够基于单目摄像头的二维的图像序列输入,给出估计的物体在三维空间中的外接矩形,并且在相邻帧中以高准确率和良好的性能匹配识别到的物体。论文作者在自动驾驶的常见场景下测试了该算法,取得了非常优秀的结果。需要注意的是,自动驾驶的应用场景比较特殊,涉及到人类的生命安全,故通常要求算法的鲁棒性极高。QD-3DT 在测试中很好地适应了雨天和夜晚等行驶条件,证明了拟密集匹配算法的优越性[12]。
未来展望
目前看来,2021 年前后提出的一些算法和模型(如 QDTrack)已经能够满足绝大多数场景下的使用需求,二维场景下的多目标匹配问题可以认为已经得到了较好的解决。因此,未来的发展或许主要会聚焦在两个方向上:第一个方向是使得学术界的模型能够尽快在工业届投入使用,在实践中检验模型存在的不足,并尝试逐步替换掉目前广泛使用的 DeepSORT 模型;第二个方向是尝试将上述算法泛化、迁移至 3D 领域,解决三维空间中的多对象匹配问题,从而更好地服务于自动驾驶和工业控制等领域。不过,在解决三维空间中多对象匹配问题时,可能需要模型有能力接受来自不同位置的多个摄像头的输入以增强匹配的准确度,此方向的研究暂时还比较空白。考虑到工业届对这类算法存在较大的需求,可以预计在未来一定会有一些优秀的相关成果出现。
参考文献
[1] Luo, W., Xing, J., Milan, A., Zhang, X., Liu, W., & Kim, T.-K. (2021). Multiple object tracking: A literature review. Artificial Intelligence, 293, 103448. https://doi.org/10.1016/j.artint.2020.103448
[2] Bochkovskiy, A., Wang, C.-Y., & Liao, H.-Y. M. (2020). YOLOv4: Optimal Speed and Accuracy of Object Detection. doi:10.48550/ARXIV.2004.10934
[3] Kalake, L., Wan, W., & Hou, L. (2021). Analysis based on recent deep learning approaches applied in real-time multi-object tracking: A Review. IEEE Access, 9, 32650–32671. https://doi.org/10.1109/access.2021.3060821
[4] Wojke, N., Bewley, A., & Paulus, D. (2017). Simple online and realtime tracking with a Deep Association metric. 2017 IEEE International Conference on Image Processing (ICIP). https://doi.org/10.1109/icip.2017.8296962
[5] Zhou, X., Koltun, V., & Krähenbühl, P. (2020). Tracking objects as points. Computer Vision – ECCV 2020, 474–490. https://doi.org/10.1007/978-3-030-58548-8_28
[6] Bergmann, P., Meinhardt, T., & Leal-Taixe, L. (2019). Tracking without bells and whistles. 2019 IEEE/CVF International Conference on Computer Vision (ICCV). https://doi.org/10.1109/iccv.2019.00103
[7] Bewley, A., Ge, Z., Ott, L., Ramos, F., & Upcroft, B. (2016). Simple online and realtime tracking. 2016 IEEE International Conference on Image Processing (ICIP). https://doi.org/10.1109/icip.2016.7533003
[8] Song, Y.-M., Yoon, K., Yoon, Y.-C., Yow, K. C., & Jeon, M. (2019). Online multi-object tracking with GMPHD Filter and Occlusion Group management. IEEE Access, 7, 165103–165121. https://doi.org/10.1109/access.2019.2953276
[9] Zhang, Y., Wang, C., Wang, X., Zeng, W., & Liu, W. (2021). FairMOT: On the fairness of detection and re-identification in multiple object tracking. International Journal of Computer Vision, 129(11), 3069–3087. https://doi.org/10.1007/s11263-021-01513-4
[10] Duan, K., Bai, S., Xie, L., Qi, H., Huang, Q., & Tian, Q. (2019). CenterNet: Keypoint Triplets for object detection. 2019 IEEE/CVF International Conference on Computer Vision (ICCV). https://doi.org/10.1109/iccv.2019.00667
[11] Pang, J., Qiu, L., Li, X., Chen, H., Li, Q., Darrell, T., & Yu, F. (2021). Quasi-dense similarity learning for multiple object tracking. 2021 IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). https://doi.org/10.1109/cvpr46437.2021.00023
[12] Hu, H.-N., Yang, Y.-H., Fischer, T., Darrell, T., Yu, F., & Sun, M. (2022). Monocular quasi-dense 3D object tracking. IEEE Transactions on Pattern Analysis and Machine Intelligence, 1–1. https://doi.org/10.1109/tpami.2022.3168781
]]>
XieJiSS, revision 3
本文可能不会及时更新,请以RV64 板子更换 rootfs 指南 - archriscv-packages Wiki为准
请确保你或你的同伴可以在物理上(通过串口)访问到板子。否则,你的板子将在 0x02 的第 6 步重新启动时失联。
首先,以 root 身份回到根目录:
1 | sudo su |
下载 rootfs tarball(确保安装了 wget
):
1 | mkdir new && cd new |
在根目录创建 old
文件夹:
1 | mkdir old |
看一下 fstab
:
1 | cat /etc/fstab |
最好拍照或截图备查。
获取当前的 Linux 内核版本:
首先,初始化用于从 vmlinuz
提取内核版本的脚本。vmlinuz
一般位于 /boot
目录下,需要 root 权限才能访问。
1 | kver_generic() { |
执行 kver_generic /boot/vmlinuz
,记住 Linux 内核版本。可以将其赋值给一个临时的 shell 变量,也可以继续拍照记录:
1 | linux_kver=$(kver_generic /boot/vmlinuz) |
注意,如果你当前的系统没有开启内核压缩,那么可能需要将 vmlinuz
修改为 vmlinux
。
看一下 ip addr
和 router 的地址:
1 | 这里 eth0 可能需要按实际情况修改,ip addr show 可以查看全部 device |
记录 inet
行的 IP,这是你本机当前的 IP 地址。之后会用到,建议拍照或截图留存。(包括 IP 后面的 /24
,如果显示的不是 /24
那么之后步骤中也要对应修改成此时显示的后缀)
其实理论上这步不需要的,用 dhcp 即可。但似乎 PLCT 南京内网的端口转发本质上基于静态 IP,依赖了 dhcp 客户端重启后服务端优先发放此前释放的 IP 的特征,并不稳定。建议还是在 0x02 的第七步里配成静态 IP。
1 | ip route show dev eth0 |
记录 default via
后面的 IP 地址,这个就是你的板子所在局域网的 router 的地址。建议拍照或截图留存。
如果目标板子不在 PLCT 南京内网,并且你并不理解上述命令的用途:建议在此停下,先和 mentor 讨论清楚网络拓扑再继续。
记录几个关键路径(不用真的记,反正这里有):
1 | /new/lib/ # 后面步骤中会设置为 LD_LIBRARY_PATH |
在新的 rootfs 并非 Arch Linux 时,可能需要将 /lib
替换为 /usr/lib
。原因详见:The /lib directory becomes a symlink - Arch Linux News
移动文件夹
1 | mv etc home media mnt opt root srv var old/ # 这里保留 /boot |
继续移动文件夹
1 | LD_LIBRARY_PATH=/new/lib/ /new/lib/ld-linux-riscv64-lp64d.so.1 /new/bin/mv bin sbin usr lib old/ |
/lib/firmware
是 Ubuntu on Unmatched 的 firmware 路径,可能需要按实际情况修改。
把 kernel modules 移动回来
1 | mv old/usr/lib/modules/$linux_kver ./lib/modules/ |
这里的 $linux_kver
是前面「获取当前的 Linux 内核版本」步骤中设置的。
dtbs、vmlinuz 等均位于
/boot
下,此前并未覆盖/boot
因此在这一步不用操心它们。备注:一般来讲,新 rootfs 的 tar 文件里面并不会包含 dtbs 和 vmlinuz 等文件,它们一般会出现在.img
或.iso
内,这超出了 rootfs 的范畴,故本文不做详细解释。
修改 fstab
1 | echo "LABEL=cloudimg-rootfs / ext4 discard,errors=remount-ro 0 1" >> /etc/fstab |
注意这里可能需要根据之前看的 fstab
内容来酌情修改第一行,例如原本是 vfat
的盘你肯定不会希望它被设置成按照 ext4
格式读取。
第二行的 tmpfs 是因为我们稍后要启用 systemd-timesyncd
,但是不希望它太过影响硬盘寿命。如果你不在乎,可以丢掉这行。
重启机器。
重启后已经是 Arch Linux 了。开始配网络和 fstab
:
1 | echo test > test.txt # 测试 / 是否被 mount 为 rw |
1 | # /etc/dhcpcd.conf |
更新 pacman 软件源,随后再次重启。
1 | pacman -Syu --noconfirm |
跑完 Syu 如果不重启,可能会遇到很多「升级升了一半」导致的问题,例如找不到 kernel module、找不到各种符号等等。
]]>
flag1
in the source code1 | const user1 = createUser(~~(1 + Math.random() * 1000000), "test", fs.readFileSync(__dirname + "/flag1.txt", "utf8")); |
user1.flag
1 | bot.on("callback_query", async (query) => { |
This object represents an incoming callback query from a callback button in an inline keyboard.
Field Type Description id String Unique identifier for this query from User Sender data String Optional. Data associated with the callback button. Be aware that the message originated the query can contain no callback buttons with this data.
Conclusion: We need to make sure that in the provided callback_data
, the substring before the first _
equals to user1
's uid
.
We can tell that the callback data is set in the handler of /login
, and there are three types of them:
"0_login_callback:" + msg.chat.id + ":" + msg.message_id
authorizedUids[0].uid + "_login_callback:" + msg.chat.id + ":" + msg.message_id
"-1_login_callback:" + msg.chat.id + ":" + msg.message_id
Hence, we only need to click the button exactly when the second kind of callback data appears. Under the competition environment, the time frame available for this is about 400ms. Since the first type of callback data will last for 2 seconds to 16 seconds, trying to click the button with human hands and expecting the flag to appear is probably not feasible.
After a quick search in Google, we can find two major automated Telegram MTProto API Framework: Telethon
and Pyrogram
. Here, a solution based on pyrogram is provided:
1 | import asyncio |
The above code will be triggered twice per a
/login
's response message, but that's OK.
There are 3 expected methods to solve this challenge.
root
user's userid is set to 777000, which is the same as Telegram official account's userid. In other words, we need to let the official account send /iamroot
to the bot. This is not quite possible; however, if we search for "Telegram 777000" on Google, we can find a GitHub issue: [BUG] PTB detect anonymous send channel as 777000. By observing the screenshot, we can see that when a channel is linked to a group (see also: Discussion Groups), messages sent in the channel will be automatically forwarded to the discussion group. This forward operation is actually done by user 777000, which means that bot will think this message comes from Telegram's official account.
But the exploit is not so easy. If we invite the bot to a group, it will quit automatically:
1 | bot.on("my_chat_member", async (update) => { |
Thus, the problem becomes "how to stop the bot from quitting groups".
We may recognize that in the callback function bind to the my_chat_member
event, an if statement is used to check whether update.chat.id
starts with -100
. Telegram's groups and channels use merely the same underlying codes, and their chatId
s both start with -100
. However, people familiar with Telegram will know that not all groups starts with -100
. This is caused by one of the history burdens of Telegram. Specifically, Telegram has two types of chats: group and supergroup. Supergroup supports more functions in comparison with group, e.g. setting admins with different admin rights, linking to a channel to act as it's discussion group, obtaining a group username so that it becomes a public group, preserving all history messages, etc. The Telegram dev team is devoting much efforts to hide the UX difference between groups and supergroup. Newly created chats are all groups by default, which has negative chatId but not starting with -100 (Aha!), and will escalate to supergroup automatically when users try to perform actions that are not supported by groups on it. Note that during the escalation process, the group (which is becoming a supergroup) will discard its old chatId and obtain a new one, which starts with -100.
Knowing this, it is not hard to come up with a viable solution:
Update
s) again, which will probably trigger the my_chat_member
callback again, resulting in the bot leaving the group (because the now supergroup has a chatId starting with -100
). To avoid this consequence, you can send 100 garbage messages prior to linking the chat to your channel./iamroot
in your channel, and receive flag2.This path is added for those not familiar with Telegram.
Diving into the handler of /addkw key reply
command, we can discover that the program tries to write the reply specified by the user into the corresponding entry of user1
's keywordMap
:
1 | onText(/^\/addkw (\S+) (\S+)/, async (msg, match) => { |
Noticing keywordMap?.
looks suspicious, let's have a quick glance at its definition:
1 | get(target, prop) { |
Inside the getter function, the key is split at ?.
, before accessing corresponding values layer-by-layer. By doing so, it implements something similar to the ?.
optional chaining operator. However, here it does not filter the key to be accessed, hence we can construct a prototype pollution. For instance, we set the key to be __proto__
, and now we can overwrite Object.prototype
.
Send /addkw __proto__?.test 1
to bot, and we can pollute Object.prototype.test
:
1 | const a = {}; |
Read the source code of node-telegram-bot-api
, and we can know that the framework tries to determine Update type by a series of if
s:
1 | // ... |
Obviously, we can pollute any attribute access operation before update.my_chat_member
, e.g. chat_member
, so that the handler of my_chat_member
will never be invoked:
1 | /addkw __proto__?.chat_member 1 |
If the method of racing condition is to be carried out, some special techniques might be needed. The very first Update
the bot will receive after it enters the group is always the Update representing the bot's join chat event, hence making it impossible for other callbacks to be triggered before my_chat_member
. What's more, the auto-forwarding of channel messages to linked discussion groups in Telegram has a noticeable lag, so if the attacker invite the bot prior to sending the message in channel, the exploitation will never success.
So, we need to send /iamroot
in the channel first, and after sleeping for a proper duration, we'll invite the bot to join the discussion group, so that this message is forwarded to the group between the asynchronous my_chat_member
handler's await sendMessage
and await bot.leaveChat
call.
]]>
So we won't be bothered by this issue anymore.
The original blog post:
TL;DR: see "Wrap Up" and "Solution".
Recently, when we're working on a series of package rebuild pipelines triggered by a glibc
update (from 2.33 to 2.34), we discovered that some previously compiling packages, mostly configured to use CMake, refuse to compile now. Their error logs look like this:
1 | /usr/bin/ld: libbson-1.0.so.0.0.0: undefined reference to `__atomic_exchange_1' |
This is quite strange to us, because we used to think we have got rid of this for all by manually appending set(THREADS_PREFER_PTHREAD_FLAG ON)
to these packages' CMakeLists.txt
, despite replacing all -lpthread
with -pthread
. This was done in a per-package manner, modifying the patch to meet the need of the tech stack used by the specific package, and we are sure that they used to work before the glibc
upgrade.
Before proceeding, you need to know the difference between
-pthread
and-lpthread
, and that-pthread
is the most preferable way if you want to link to thepthread
library, at least forglibc<=2.33
.
So, what's happening? Is there anything vital got changed in this glibc
upgrade?
After reading the release note of glibc
2.34, we did notice something related:
...all functionality formerly implemented in the libraries
libpthread
,libdl
,libutil
,libanl
has been integrated intolibc
. New applications do not need to link with-lpthread
,-ldl
,-lutil
,-lanl
anymore. For backwards compatibility, empty static archiveslibpthread.a
,libdl.a
,libutil.a
,libanl.a
are provided, so that the linker options keep working.
Hmm, good, sounds like we don't need -pthread
anymore. Actually, it appeared to us that CMake thinks the same. After running cmake
for the failing packages, we can confirm that nothing looks like -pthread
is appended to either CXXFLAGS
or LDFLAGS
. However, if forced to compile with -pthread
, the previous-mentioned link error disappears. In other words, applying this patch fixes the error.
1 | - make |
At this point, every clue we have gathered so far seems to indicate a bug inside cmake
:
set(THREADS_PREFER_PTHREAD_FLAG ON)
appears to be "broken"-pthread
fixes the issue, so cmake
failed to recognize that -pthread
is necessarySo we dig into CMake's source code to see what happened. To our surprise, we didn't notice any change to the detection code it used for determining whether -pthread
is necessary. CMake's detection code, at the time of writing, looks like this:
1 |
|
It turned out that according to the glibc
upgrade, this detection code now compiles without additional arguments:
1 | gcc test_pthread.c |
As far as I can tell, this does not look like a CMake bug. The expected behavior, which is exactly what we have seen, is that if the test code can be compiled without -pthread
, then the argument should not be appended to the command like (or {C,CXX}FLAGS
, correspondingly). This lead to another question: why those packages are failing now, provided that the detection code is unchanged, and it used to work properly?
Let's take the package mongo-c-driver
as an example. As of version 1.21.1, we can see the code listed below in its src/libbson/src/bson/bson-atomic.h
(Feel familiar with this path? Remember the libbson-1.0.so.0.0.0: undefined reference
error log mentioned before?):
1 |
|
As we can tell from libbson
's source code, it attempts to use the __atomic_*
builtins of GCC, like __atomic_fetch_add
and __atomic_compare_exchange
. We can construct some test cases to check whether we're right:
1 | // test1.c |
1 | // test2.c |
1 | // test4.c |
1 | // test8.c |
Here, we use the gcc built-in
__atomic_*
becauselibbson
used it. Replacing this withstd::atomic<T>
(and compile it withg++
) yields identical results correspondingly. You may also useuint*_t
provided by thelinux/types.h
header, as their sizes are promised to be the same across all architectures.
Compile results:
1 | gcc test1.c |
If you are kind of familiar with C++, you may have heard that codes using atomic operations might need to link libatomic
(I remembered once reading this, but I can't find it now). It turned out that if we provide -latomic
to gcc, the code compiles:
1 | gcc test1.c -latomic |
And this also works for -pthread
:
1 | gcc test1.c -pthread |
Smart readers might have felt something unusual:
First, as we can see, gcc compiles test4.c
and test8.c
successfully without -latomic
or -pthread
, but it couldn't handle the atomic operations in test1.c
and test2.c
without making a call to libatomic
:
1 | gcc test1.c -latomic -S |
This is partially explained in a GitHub issue (riscv-collab/riscv-gcc#12). Till April 2022, i.e. when this blog is written, gcc does not support inlining subword atomics, and this is the reason why -latomic
must be presented when invoking gcc
.
But what about -pthread
? Why -pthread
works either? I mean, libpthread
itself does not provide those atomic symbols, right? Actually, if you use ldd
to check a.out
generated by gcc test1.c -pthread
, you will notice that libatomic
is linked, instead of libpthread
as we are expecting from -pthread
. Changing -pthread
with -lpthread
won't work, so gcc must have done something internally for -pthread
despite linking libpthread
. In order to figure out the difference, we have to check the gcc spec:
1 | gcc -dumpspecs | grep pthread |
Oh, the answer finally reveals: gcc silently append --as-needed -latomic
if -pthread
is provided. This also explains why changing -lpthread
to -pthread
works.
The reason why gcc decided to link libatomic
when -pthread
is provided had been lost in a squashed commit. My guess is that they tried to cover up the subword atomic pitfall using this strategy. (UPDATE: aswaterman
's initial fix did not limit the scope to -pthread
. The limitation was added one week later.) Generally, atomics are used with multi-threading, and you use pthread in such cases. However, one may use atomics without pthread, like when writing SIGNAL handlers. So IMO this workaround is not good enough, and should be replaced by subword atomic inline support.
UPDATE: We discussed this with some gcc RISC-V devs, and it turned out that when compiling gcc, at stage 1 it may not recognize libatomic (i.e.
-latomic
may not work) if you are compiling it with a gcc that uses the old spec So you'll have to set--with-spec
at stage 1 to workaround this. Hopefully there would be an--always-link-libatomic
configuration flag in the future.
Fun facts: gcc once had added --as-needed -latomic
to its LIB_SPEC
unconditionally, in this commit (2017-02-01):
1 | + #undef LIB_SPEC |
But later in the final port commit submitted to gcc (2017-02-07; svn r245224), they limited the change to -pthread
:
1 | - " " LD_AS_NEEDED_OPTION " -latomic " LD_NO_AS_NEEDED_OPTION \ |
The reason of making such limitation remains unknown.
glibc
moved libpthread
into libc
, and CMake's pthread sample code compiles directly. Hence, CMake thinks there's no need to provide -pthread
when invoking gcc
/ g++
. Since we used to patch such packages with -pthread
instead of -latomic
, and are actually relying on -pthread
's side effect (--as-needed -latomic
), this led to the consequence that neither -pthread
nor -latomic
is provided, so subword atomics refuse to compile as libatomic
is not linked. For ways of fixing this problem, please refer to the Solution chapter.
Also note that the dependence on libatomic
causes another problem. Some packages / frameworks, e.g. JUCE
insists that its atomic wrap only accepts lock-free values. However, when a program is compiled, the compiler cannot know whether a call to libatomic
will be lock-free or not (actually, the emitted sequence will be lock-free, but the compiler doesn't know). Hence, the compiler will return false for std::atomic<bool>::always_lock_free
, and this also happens for the macro ATOMIC_BOOL_LOCK_FREE
defined in atomic.h
. As a result, if you use juce::Atomic<bool>
, some static asserts will fail, and the package refuses to compile.
Solutions vary according to the scale of projects.
export CFLAGS="$CFLAGS --as-needed -latomic"
and likewise for CXXFLAGS
solve the problem. As an alternative, you can also patch the Makefile
file, and edit the flags inside it.CheckAtomic.cmake
, which (if you are using a newer version) has taken subword atomics into consideration.clang
and compiler-rt
. Clang should be able to handle -latomic
, but better perform a double-check. You can temporarily rely on the side-effect of gcc -pthread
(which is not recommended), or edit your configure script manually to disable -latomic
when clang
and compiler-rt
are detected.clang
and compiler-rt
, compatibility with older compilers / linkers that do not support --as-needed
and even -pthread
might need to be considered. CMake reverted their -pthread
fix on this because a compiler named XL does not support -pthread
, and interprets it as -p -t hread
, returning zero (oops! false negative) as its exit code.__atomic_fetch_add
, so the problem would not be mitigated (but this patch indicates a good start!).Complicated solutions may increase the burden on project developers / maintainers, and their willingness to port their projects to RISC-V might be reduced. Aiming to tackle the problem, we have proposed to change LIB_SPEC
back to its originally patched form to mitigate this issue partially (still not lock-free), and hope that the subword atomic pitfall can be solved completely in the future.
UPDATE: After discussing this with gcc RISC-V devs, we reached the consensus that maybe a gcc configuration argument
--always-link-libatomic
can be added.
UPDATE2: This breaks the bootstrap process of GCC. GCC compiles itself twice to get rid of e.g. dirty compiling environments stuffs. Modifying the spec string too early causes glitches, like libatomic calls itself infinitely. So we have to use
sed
to replace the spec string inside the first-stage GCC binary carefully, filling blanks (U+0020
) to make sure old and new spec strings have the same length.
]]>
This is a frequently updating doc, and you can find its latest version here.
This documentation aims to help you set up an Arch Linux RISC-V (riscv64gc
) development environment powered by usermode QEMU and systemd-nspawn
.
Currently, the documentation contains instructions for Arch Linux, Debian and Ubuntu.
Caution: If you are using a native RISC-V board, you should skip these steps, and scroll down to the bottom of this page. The following instruction is for x86_64, etc. users.
If you are using Ubuntu/Debian as the host OS, skip to "For Debian and Ubuntu".
Add [archlinuxcn]
Repo Source
We need to install some packages from [archlinuxcn] later.
Append these two lines to /etc/pacman.conf
:
1 | [archlinuxcn] |
There is also a list of public mirrors available.
After doing so, you need to trust the archlinuxcn
maintainers' PGP keys to install packages from it:
1 | sudo pacman -Sy && sudo pacman -S archlinuxcn-keyring |
Install Packages
1 | sudo pacman -S qemu-user-static qemu-user-static-binfmt |
where qemu-user-static-binfmt
is for registering QEMU interpreter to execute RISC-V ELF files. Other necessary packages like zstd
and systemd-nspawn
are listed in the dependency tree of base
meta package, so they will also be installed by the provided command, hence there's no need to install them explicitly.
1 | sudo apt install zstd qemu-user-static systemd-container |
where zstd
is for decompressing the Arch Linux RISC-V rootfs compressed tarball, and systemd-container
is for the systemd-nspawn
command, which we'll use later to spawn a container from the rootfs.
1 | curl -O https://archriscv.felixc.at/images/archriscv-20220727.tar.zst |
If you have poor connectivity upon reaching archriscv.felixc.at, you can also download the rootfs from mirror sites. They are listed at https://archriscv.felixc.at/.
root
user's password is sifive
, but probably you don't need it if you are creating a container with this rootfs to chroot
into it later.
Arch Linux
1 | $ mkdir archriscv |
Debian and Ubuntu
1 | $ mkdir archriscv |
tar
may spit out some warnings, which turn out to be harmless and we can safely ignore them.
1 | sudo systemd-nspawn -D ./archriscv -a -U |
where -D
provides the root directory for the container, -a
for preventing processes with PID 1 from not reaping zombie children, -U
for preventing processes in container from using the same UID range as those used outside the container.
1 | uname -m |
1 | pacman -Syu |
For example, if you want to install vim:
1 | pacman -S vim |
1 | echo 'export EDITOR=vim' >> ~/.bashrc && source ~/.bashrc |
1 | useradd -m <username> |
where -m
means to create a home directory for the user to be added.
Use visudo
if you'd like to grant sudo privilege for this user, and append this line under ## User privilege specification
:
1 | <username> ALL=(ALL) NOPASSWD: ALL |
1 | exec su username |
This guide is about compile & run a program manually. If you want to start your development from a git repo or so, jump to 3.2 Compile for instructions on how to install
git
and other packages needed by your toolchain. If you are trying to create or modify an Arch Linux package (i.e. dealing with aPKGBUILD
), you may refer to related ArchWiki pages.
Run vim hello.c
and type:
1 |
|
Exit with esc and :wqEnter, as this S/O answer suggested :-P
First of all, update your local packages repo data from remote package sources (like sudo apt-get update
):
1 | sudo pacman -Sy |
You should run this prior to any attempt to install a package, unless you are sure that your local copy of package info is up-to-date.
Then, install your development toolchain:
1 | sudo pacman -S gcc cmake foo bar bah |
Compile your code:
1 | gcc -o hello hello.c |
1 | file hello |
1 | ./hello |
For native RISC-V board users:
Edit your /etc/pacman.conf
with the following settings:
1 | [core] |
Finally, add the packager's public key to your keyring:
1 | pacman-key --recv-keys "B5971F2C5C10A9A08C60030F786C63F330D7CB92" # Always grab the latest key ID from https://archlinux.org/people/developers/#felixonmars |
]]>
gh: XieJiSS/plct-archrv-pkg-bot
、gh: cubercsl/archrv-pkg-notification-bot
和 gh: Ast-x64/plct-archrv-status-worker
实现。近日我们在维护标记和追踪状态时,发现 plct-archrv-pkg-bot
(以下简称 bot)在重启时会丢失几条记录。最开始,怀疑是重启的时候还有数据没写入到磁盘,导致数据丢失。但是经过排查,发现并不太可能,理论上所有的数据都是在修改时当场写入磁盘的。保险起见,增加了使用同步 IO 的 storePackageMarksSync
,通过 npm: async-exit-hook
在进程退出时强制保存。然而还是丢数据。
看 log 可以确认,在退出时把内存里的数据全部写入到磁盘了,那丢数据是怎么回事呢?
又开始考虑是不是 race condition,于是通过 npm: lockfile
实现了进程的保护锁,确保同时只有一个 bot 进程在运行。除了没效果以外,效果非常好。看来也不是多进程并行写入的问题。
最后经过排查,发现问题的根源在 export
和 import
上。
最小可复现样例如下:
1 | // a.js |
1 | // b.js |
可以看到,在执行 cleanUp
之后的数据全部丢失了。这是因为在 a.js
里对 A
重新赋值过后,b.js
解构赋值得到的 A
和 a.js
里的 A
不再指向同一个对象。解构赋值出来的 identifier A
指向的是 module.exports
的 A
属性在解构赋值那一时刻所指向的内存,随后执行的 A = A.filter
只是在 a.js
内部修改了 A
的指向,外部解构赋值出的 A
并不会随之更新。我们可以用 cpp 来翻译一下第一种写法:
1 | const auto *A = a.A; |
概括一下:如果把 JS 里面解构赋值拿到的东西当做 &ref
来理解(这是常有的事)就会导致这种 inconsistency 的出现。
如果想修复这个问题,需要在 a.js
里面把 A
声明为 const
,随后将 Array#filter
替换为自己实现的 in-place filter
,详见这个 commit。当然你也可以在 b.js
里始终使用 a.A
的方式来访问 A
。
]]>
Felix: they say that EU readers are blocked from your blog
me: wtf really? how?
also me (3 yrs ago): Haha GDPR goes brrrrrrr
This is funny, I used to have google analytics integrated in my blog (maybe it still presents now? I'm not sure), and in order to make ga work, I added an adblock detector (but I've just realized that I'm not using it from the very beginning!). Also, I'm not willing to prompt users every time they visit the blog, so I set a cookie named blker
(for blocker, I guess).
Then I realized that this might violate the GDPR.
Not good, I thought, so I searched on google for ways to create a valid, legal popup that informs users about those GDPR user agreement stuff. Unfortunately, the search results showed that I need to pay an amount of money to generate the legal text I need, and the fee was not what I could afford as I was still a high school boie at that time.
So the approach was simple: I need to use ga to count readers (I know they provides far more information despite the readers count, but I'm not interested), so I want readers to disable their adblocker; In order to tell them to disable adblocker, I need to prompt them; As I don't want them to be prompted every time they visit the site, I have to set a cookie to mark readers that have been prompted. At that time, I felt like setting cookies without asking for agreement is violating the GDPR, and I couldn't cope with it because I have no money to generate the legal text, so it looks like I have no chance to be compatible with the GDPR law.
Hmm, maybe I can stop EU readers from accessing the site, I thought. That's simple, just make a GET to /cgi-bin/trace
api provided by cloudflare, and you can get the country code determined by source IP. At the time I wrote these code, the UK was still part of the EU (legacy code, lol), so I think I should keep UK in the list, and remove it when they finally complete the transaction (of course, I totally forgot this lol)
Felix told me that if cookies are not correlated to a single user, then setting it may not violate the GDPR. But now I don't need cookies anymore: since 2020, I've been using busuanzi
which only counts the number of readers.
And I've just realized that they have shutdown the service (502 Bad Gateway for busuanzi).
UPDATE: seems like the busuanzi
service is working again, so I've just encountered a short outage?
]]>
riscv64gc
(will be referred to as rv64
in this article). We are packaging for the PLCT's Arch Linux RISC-V project, and thanks to the Arch wisdom, tools in the toolchain we use are always the latest. Still, we met a strange compile error:1 | error: unknown directive |
After performing some simple queries (cs.github.com is awesome!), we managed to locate the following code (ref):
1 | //! Shared RISC-V intrinsics |
TL;DR:
pause
isfence w,0
, which is.insn i 0x0F, 0, x0, x0, 0x010
.pause
is provided by theZihintpause
extension. Though the widely adoptedriscv64gc
does not containsZihintpause
, thefence w,0
won't trigger aSIGILL
, because it is treated similar to anop
instruction, so we can use it safely without bothering about compatibility.
Firstly, by reading the comments, we can infer that this .insn
assembly acts as the pause
instruction. This stunned me a bit because AFAIK, the HINT feature in RISC-V ISA has always been in the reserved state (at least until December 2021):
No standard hints are presently defined. We anticipate standard hints to eventually include memory-system spatial and temporal locality hints, branch prediction hints, thread-scheduling hints, security tags, and instrumentation flags for simulation/emulation.
It's not hard to realize, though, that the pause
instruction is introduced by Zihintpause
extension, as an alias for fence w, 0
. Previously, we have to use nop
to polyfill the pause
function implemented for other architectures:
1 | From 4e559dabe28e57ee27cb45c8297e1e387beed1d3 Mon Sep 17 00:00:00 2001 |
However, in RISC-V, the nop
instruction simply stands for "no operation", and does not provide any further clues to relax the CPU, hence not saving any energy (but instead wasting it). So, it's definitely better to replace the fake pause
(i.e. nop
) with the real pause
, as provided in the Zihintpause
extension.
One may say that, hey, this extension is not part of the riscv64gc
extension set! This argue is valid, as riscv64gc
stands for riscv64imafdc_Zicsr_Zifencei
(used to be riscv64imafdc
, when the I
baseline has not been split to I
+ Zicsr
+ Zifencei
). Let's look at the pause
instruction in detail:
Before doing this, you need to grab a RISC-V run-time, via QEMU or via buying a board from SiFive.
1 | cat pause.asm |
Uh-oh, seems like the assembler does not know this instruction! That's because 1) the pause
instruction is too new; 2) I'm using riscv64gc
, which does not include Zihintpause
. Never mind, we can still use fence w,0
as noted in the spec:
1 | cat pause.asm |
Not good. But at least the rust version should work, as they have RISC-V as tier-2 target, and managed to make the release pass the CI test, right?
1 | cat pause.asm |
Hmm, seems like the .insn i
is working (compiling, at least), but what does this fence w,unknown
stands for? Let's have a loot at the spec:
Shouldn't it be fence w,0
? Actually, it is fence w,0
, as denoted by the hex 0100000f
:
IMO this is a subtle bug (used to be a feature) of the disassembler, and as we can see, this is already fixed in the llvm toolchain:
1 | diff --git a/llvm/lib/Target/RISCV/MCTargetDesc/RISCVInstPrinter.cpp b/llvm/lib/Target/RISCV/MCTargetDesc/RISCVInstPrinter.cpp |
After digging into the underlying mechanism of the pause
instruction, we can easily conclude that it will not trigger a SIGILL
, as the fence
instruction is part of the RV32I
baseline instruction set, hence available in all valid RISC-V instruction sets.
1 | cat pause.c |
In conclusion, it's 100% safe to replace pause
with .insn i 0x0F, 0, x0, x0, 0x010
to make the code compile, regardless of what RISC-V extensions you are using.
Ah, I know someone must be already complaining: you've write so much analysis, but how are they related to the error occurred when compiling Rust? Actually, the direct answer to this question is simple and naive. Let's take the pause.c
, and use clang 13 to compile it, and see what will happen:
1 | clang -v |
Obviously, clang 13.0.1 still doesn't support compiling the .insn
directive. The support is to be added into clang since 14.0, as we can see from the target branch of llvm commit 28387979
: [RISCV] Initial support .insn directive for the assembler. This is imported into rust in this pull request (#91528).
Still, there's a tiny issue haunting: The rust PR (#91528
) is merged months before the initial release of llvm 14.0.0-rc1
. Sure, Rust guys are always keen on trying those nightly, bleeding-edge stuffs, but how can they grab the 14.x llvm toolchain before the upstream has ever released it?
After investigating PR #91528
, things become clear. The Rust team is maintaining a fork of llvm at rust-lang/llvm-project
, and they cherry-picked commit 28387979
to make the .insn
stuff compiles when using llvm 13.
By maintaining a fork and constantly modifying / cherry-picking on demand, the Rust team is able to benefit from unreleased changes, or add support for older OS/platforms that are not supported by the upstream. However, this is definitely not a good news for downstream packagers:
rust-lang/llvm-project
first, and use the compiled llvm toolchain (let's call it rust-llvm
) to compile Rust itself, then the rust
package would have conflicts with the llvm
package, as rustc
may need to link to .so
files provided by llvm
(or rust-llvm
, depends on which one you are using to compile rustc
), and the .so
files may have different ABI (Application Binary Interface), causing incompatibility. Also,rust
package to rust-llvm
+ rust
won't help. That's because the linked .so
files need to be presented at run-time, so rust-llvm
will be put into rust
's depends
array, hence failing to resolve the conflict.Currently, we can still hide the problem by letting the build fail for some time, and wait for newer llvm releases that contain those features required by building rustc
. Sure, this solution is not elegant, and things might get worse when the difference between rust-llvm
and llvm
becomes so huge that it's impossible to compile rustc
with the upstream llvm
. But we can't take the burden to make rust
incompatible with llvm
-- there are way too much packages that depend on both rust
and llvm
now. Fortunately, consider the Rust team's claim on rust-llvm
, that they will always attempt to submit their new features to upstream, maybe we don't need to worry too much.
]]>
python-mtrpacket
是使用 Python 实现的异步网络探测工具。其当前版本(1.0.0-3
)在 Arch Linux 主线的 x86_64
架构能正常编译通过,在 riscv64
下报错:1 | .E/bin/sh: line 1: mtr-packet-missing: command not found |
这里面 /bin/sh: line 1: mtr-packet-missing: command not found
是正常输出,属于第三个测试点,不用管它。研究下代码就能发现这个错误是预期行为,会被 self.assertRaises
抓到。E
(错误)的是第二个测试点,本不应该出现 mtrpacket.ProcessError
,但是出现了。
看一下 test/nc_mock.sh
,发现很简单,就这么一点东西:
1 |
|
那这怎么就在 riscv64
上锅了呢?
首先怀疑 qemu-user
的 argv[0]
导致找不到 nc_mock.sh
,但这很容易排除。只需要在 nc_mock.sh
中加入:
1 | sleep 30 |
然后发现测试在第二个点上卡住三十秒,就知道其实 nc_mock.sh
是被运行了的。由此可以确定:原本的报错提示具有误导性。
那么,既然运行了,怎么就报错了呢?修改一下 PKGBUILD
(Arch Linux 编译包时使用的脚本),把 check()
中的 ./test.sh || bash -i
,这样我们就在 test.sh
(它会间接调用 nc_mock.sh
)非零返回的时候得到进入到打包所用的 rootfs
环境的快捷通道。
之后通过修改 test/test.py
和 mtrpacket/__init__.py
,最终定位到出问题的地方:
首先是测试入口,在 test.py
里面创建了一个 mtr
的服务端,之后去启动对应的客户端。第二个测试点使用 nc_mock.sh
作为客户端:
1 | mtr_packet_executable = os.environ.get('MTR_PACKET') # ./nc_mock.sh |
这里可以看到,创建了一个 ./nc_mock.sh
的子进程。这个子进程会通过 nc
连接到 localhost
的 8901
端口,在这个端口上监听的是 test.py
预先创建好的服务端,于是就可以走正常测试流程了。
之后是测试流程,可以看到立马执行的是 check_support('send-probe')
,追进去:
1 | async def check_support(self, feature: str) -> bool: |
到这里,就出问题了。由于 python-mtrpacket
的 checkdepends
依赖是 gnu-netcat
,这个 netcat 在 riscv64
上的表现和 x86_64
上不一致,会提早退出。如果将 LOCALE
设置为 it
或 sk
,还能看到对应语言(意大利语/斯洛伐克语)的错误提示。为什么只有这两种语言呢?我也不知道,反正 Package Contents 里只有这两个:
1 | usr/share/locale/it/ |
So……反正我也不关心具体报错信息了,意大利语我也看不懂。直接把 PKGBUILD 里的 checkdepends
从 gnu-netcat
换成 openbsd-netcat
完事,反正 ./nc_mock.sh
里的简单用法还不至于触及到两个 variant 存在不兼容的黑色高级部分。
]]>
QEMU
的开发环境,不是真的 riscv64
机器,买真机请找 SiFive 下单 HiFive Unmatched 板子。首先,你要有一台 Arch Linux 的机器。
需要在一台 Arch Linux 机器上;
假如 ssh 连接不稳定,跑
extra-*
命令时推荐使用nohup
或类似物;确保 Linux 内核版本 >= 5.8,否则 glibc 会出问题,还会报怨找不到
/lib/ld-linux-riscv64-lp64d.so.1
。
将全部软件包的 riscv64 patch 下载到本地:
1 | git clone https://github.com/felixonmars/archriscv-packages |
修改 /etc/pacman.conf
,在文件末尾添加两行(SiFive 板子上不需要,archlinuxcn 没有 riscv64 的源):
1 | [archlinuxcn] |
安装依赖:
1 | sudo pacman -Sy |
注意,这里曾经需要手动给 devtools 打 patch,现在不需要了,肥猫已经将 devtools-riscv64 放到了 archlinuxcn 源里。如果是 SiFive 板子,可能得手动从 archlinuxcn 拉取 devtools-riscv64 的源代码(PKGBUILD 等)并手动 makepkg -si
得到 .tar.zst
再用 pacman -U foobar.tar.zst
来安装。
更新 asp
记录:
1 | asp update |
创建 alias
:
1 | cd ~ |
配置 ~/.makepkg.conf
:
1
echo 'MAKEFLAGS="-j15"' >> ~/.makepkg.conf # 假如你有 16 个核
(假设你的工作目录为 $HOME
,以下用 ~
代替)
更新 riscv64 patch 仓库:
1 | cd ~/archriscv-packages # 就是最开始 git clone 的目录 |
获取 x86_64 版本的包源码:
1 | asp checkout package-name |
将对应的 riscv64 patch(如果存在的话)移动到 x86_64 版本的包源码所在目录:
1 | cp ./archriscv-packages/package-name/*.patch ./package-name/trunk/ |
切换目录,应用 riscv64.patch
:
1 | cd ./package-name/trunk/ |
修改 PKGBUILD
中的 arch
:
修改前:
1 | arch=(any) # 这种不用修改 |
分别对应到修改后:
1 | arch=(any) |
总之就是把 x86_64 替换成 riscv64,别的不用动。这一步需要在应用
riscv64.patch
之后完成,顺序颠倒可能导致 patch 打不上。
编译:
1 | # 需要位于 PKGBUILD 所在的目录中 |
编译的过程其实就是使用 chroot 切换到一个干净的环境中,然后开始解析 PKGBUILD
文件。riscv64.patch
也是针对 PKGBUILD
文件的。
PKGBUILD
里会有一些东西:
source
数组,标明代码来源,在编译时会自动下载到本地。updpkgsums
,它在下载源码的同时会更新 PKGBUILD
中的 checksum。prepare()
负责准备工作,通常是打 patch 和跑一些 sed 命令。build()
负责编译工作,通常是 configure
make
cmake
之类的。check()
负责测试工作,常见内容:make test
或者 npm run test
或者 ctest
之类的。package()
负责最后的打包工作,例如指定依赖的 .so
文件,不用管。首先,下载镜像并解压:
1 | cd ~ |
其中,sha512sum
结果应为
1 | 6f012a169fe6f1ea15aeb3283091466e7992f78d823951ee2170940fa030e7fa2394aee11bf67c29943d21579ab42d2262a3d5ca973b5de8be779f338ba1dd44 archriscv-20210601.tar.zst |
随后,启动这个 archriscv
容器:
1 | sudo systemd-nspawn -D ~/archriscv/ --machine archriscv -a -U |
启动容器后,更新软件包:
1 | pacman -Syu |
检查自己是否在 riscv64
环境下:
1 | $ uname -m |
安装一些软件包:
1 | pacman -S vim nodejs-lts-gallium |
不是所有 x86_64
的包都能装上,我们还在做艰苦的迁移工作。
比如:electron 当前还不支持 riscv64
架构,自然也就无法安装。
如果遇到报错如下:
1 | FAILED (unknown public key FC1B547C8D8172C8) |
那么说明需要导入 GPG Key。
导入命令:
1 | gpg --recv-keys FC1B547C8D8172C8 # 这里的 hash 换成你在报错里看到的 key 就可以了 |
你也可以 cat PKGBUILD
并一次性导入其中提到的所有 key。
如果你参考了 Arch Linux Wiki - Building in a clean chroot,在 riscv64
环境下(nspawn
,或者你真的有 SiFive 的板子)使用如下命令:
1 | mkarchroot $CHROOT/root base-devel |
那么你可能会在 pacman -Syu
时遇到这个报错:
1 | error: failed retrieving file 'core.db' from archriscv.felixc.at : The requested URL returned error: 404 |
这是因为默认的 /etc/pacman.d/mirrorlist
配置有误,可以按如下方式修改:
1 | echo "Server = https://archriscv.felixc.at/repo/$arch" > /etc/pacman.d/mirrorlist |
之后重新 pacman -Syu
就可以了。
该镜像的 pacman
软件源由中科院软件所 PLCT 实验室 Arch Linux 小队维护。
参考链接:
]]>
riscv64gc
环境下编译运行 njs 16 并大获成功,记录一下流程步骤。当前 njs 16(gallium LTS)主线版本是 v16.13.1
,把源代码拉下来:
1 | wget https://nodejs.org/dist/v16.13.1/node-v16.13.1.tar.xz |
把同事(luyahan)的 patch 拉下来:
1 | wget https://github.com/nodejs/node/pull/41566.patch |
这个 patch 是上游 v8 的 4 个 commit 的 cherry-pick,修复了编译时
node_mksnapshot
卡住、测试时inspector
相关某个测试点CRASH signal 11
(也就是 SIGSEGV / Seg Fault)的问题。
把 patch 打上去:
1 | cd node-v16.13.1 |
之后用 riscv-gnu-toolchain
正常编译就行了,不会用 make
那套的话可以照抄 Arch Linux 官方源的 PKGBUILD
。如果是 gcc>=10
,注意在 make
的时候指定 CFLAGS="-fno-strict-aliasing $CFLAGS"
和 CXXFLAGS="-fno-strict-aliasing $CXXFLAGS"
,否则会挂一个测试点(cctest/test_node_postmortem_metadata.cc
)。
这个 toolchain 可以直接下载到针对 x86_64 平台 cross-compile 的版本。编译的时候会开 qemu-user 来模拟 RISC-V 环境,由于 QEMU 的 bug(和 feature),有几个测试点会挂,这是正常现象。
嫌麻烦可以开一台 Arch Linux,然后:
1 | wget -c http://ns2.felixcat.org/images/archriscv-20210601.tar.zst |
这样你就得到了一个经过了 chroot 的 Arch Linux RISC-V 环境,它的 repo 源指向 PLCT archrv 小队(其实主要是 Felix Yan)维护的 repo。
这样甚至不用编译 njs,我们已经编译好了,形成
.pkg.tar.zst
放到 repo 里,直接pacman -S nodejs-lts-gallium
就可以装上。
]]>
控制面板>硬件和声音>设备和打印机
在「未指定」栏找到 Redmi AirDots 3 Pro,如下图所示
双击打开属性页面,切换到「服务」选项卡,如下图所示
将列出的服务全部勾选(也可以部分勾选,关键是「音频接收器」和「可远程控制的设备」这两项):
点击「确定」,等待 Windows 在约 10 秒内重设设备类型并自动重新配对。
另外,需要注意输出设备要选择为「Redmi AirDots 3 Pro Hands-Free AG Audio」而非「Redmi AirDots 3 Pro Stereo」:
]]>
因为 Lexical this
导致没有 [[Construct]]
,所以不能 new
。
如果去查一下 ECMA spec,其实可以看到如下的解释:
A function object is an object that supports the [[Call]]
internal method. A constructor is an object that supports the [[Construct]]
internal method. Every object that supports [[Construct]]
must support [[Call]]
; that is, every constructor must be a function object. Therefore, a constructor may also be referred to as a constructor function or constructor function object.
所以想要对某个对象使用 new
,就得确保该对象具有 [[Construct]]
这个内部方法。而 ArrowFunction
没有 [[Construct]]
。
再多查一点的话,可以在 14.2.17 这一节看到如下的注解:
An ArrowFunction
does not define local bindings for arguments
, super
, this
, or new.target
. Any reference to arguments
, super
, this
, or new.target
within an ArrowFunction
must resolve to a binding in a lexically enclosing environment.
再多查一点的话,可以找到最终通过的 proposal:https://tc39wiki.calculist.org/es6/arrow-functions/
The goal of Arrow Functions is to address and resolve several common pain points of traditional Function Expression:
- Lexical
this
binding;- Shorter syntactical form (
() => {}
vs.function () {}
)Lexical
this
matches the dominant cohort measured by Kevin Smith (see the BTF Measurements thread that either does not usethis
or wants it lexically bound.
- Best results after modifying subject code to use method definition shorthand and only then scanning for function.
- From lexical
this
it follows that arrow functions are not constructors (no.prototype
or[[Construct]]
).
再多查一点的话,你可以在 2011 年的一次讨论里找到这个设计的来源:https://mail.mozilla.org/pipermail/es-discuss/2012-March/021953.html
We do not want to delegate to a target function that can
[[Construct]]
, because there is no such target function -- we're not trying to sugar.bind
on a full function. That is too expensive and general (no pre-args, no full function under the hood).Lexical-only
this
binding means no[[Construct]]
and no.prototype
for arrow functions. I'm ok with this.
考虑典型的代码如下:
1 | class A { |
在这两种构造实例的方法中,this
指向的都是正在被操作的变量 a
。 但是 arrow function 在还是个草案的时候就只计划支持 Lexical this
,因此它的函数体中的 this
不会指向正在被操作的变量 a
,无法修改赋值表达式的左值,因此就算 ArrowFunction
有 [[Construct]]
内部方法也无法用于构造新对象。
]]>
重拾半年多没更新的博客。毕竟此前写博客很大程度上是为了排解学业上每天机械性重复学习那点少到可怜的知识带来的烦闷,近来一则生活充实,于是便无甚更新博客之动力;二则生活充实,入了许多新坑,甚少有闲暇时间;三则因找到了稳定的内容输出渠道(私下的、公开的),且常能收到反馈,感觉确实已经无欲无求。
但是,但是,但是,但是这个逼站的 CTF 这两天属实是把我恶心到了,令人不吐不快,直欲将出题人的脑袋剖开了研究下其脑回路的卷绕数 [1] 究竟是多少。话不多说,直接开喷。
提示 需要使用bilibili Security Browser浏览器访问
,遂直接用 Chrome 插件 ModHeader
修改请求头中的 User-Agent
为 bilibili Security Browser
,拿到了 flag2
。又读了下网页源码,在一个弱智的 display: none
的元素里拿到了 flag1
。
略(让人无语。。。)
用户名 admin,密码 bilibili,没有理由。反正就一个弱密码组合扔你脸上,几次以内试出来是你欧气足,试不出来是你非酋。离谱。
页面提示为 有些秘密只有超级管理员才能看见
,挖了一下源码发现请求的 api 是 /api/ctf/4
,得到了 "code": 403
。那显然只能是 Cookie 鉴权,看眼 Cookie 发现除了通用的 session 之外还有个 role=ee11cbb19052e40b07aac0ca060c23e
,也就是 role=md5("user")
。解法是将 role
改成 md5("Administrator")
。可见 Bilibili 程序员的英语是有多么的差,「超级」= super 都不会!super 应该是小学英语词汇吧?「超级管理员」如果翻译成 Administrator,那「管理员」应该翻译成啥?还是说贵站的程序员认为超级管理员和管理员是一个意思?那可能不仅仅是小学英语没学好,看上去小学语文也没学好。不禁为十几年前的义务教育普及程度哀叹一声。当浮一大白。
什么你问我为啥 Administrator 的 A 要大写,但是上一题的用户名 admin 就不用大写?我怎么知道,可能这两道题不是一个人出的吧。
什么你问我为啥 md5("user")
的 u 就是小写?因为,因为出题人可能正好在那个地方键盘的 Shift 坏了。别问我,问他去
页面提示为 这里没有你想要的答案
,挖了一下源码发现请求了 api /api/ctf/5?uid=100336889
,得到 "code": 403
。于是立刻开始暴力枚举 uid
,开 30 线程从 uid=0
开始跑,跑到两三万的时候把服务器跑崩了。crash ++
之后坐等服务器恢复,在此期间看了看这个 uid 的人的 bilibili 空间,发现就是个可怜的无辜路人。后来想到从这个 uid 附近开始筛查,最终在这个 uid 加上一个不大于 100 的偏移量处拿到了 flag5
(这个偏移量每个人还不一样,就很弱智)
我最开始以为权限不够就得找高权限的人的 uid,但是手动试了 bishi 等人(还有陈睿)的 uid 发现同样 403 之后感觉这波是贵站程序员揭竿而起,立了一个普通路人(uid=100336889
)当皇帝,拒绝承认 bishi 等的历史地位和现任 CEO 陈睿的领导地位。看不懂了,不知道陈睿得知自己被一个 uid 在一亿多的人给 ntr 了会有什么感想。
首先看看这个弱智页面的初次加载的资源:
status | type | size | method | url |
---|---|---|---|---|
200 | text/html | 1487 | GET | http://45.113.201.36/blog/single.php?id=1 |
200 | text/css | 18754 | GET | http://45.113.201.36/blog/css/bootstrap.min.css |
200 | text/css | 8313 | GET | http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css |
404 | text/css | 0 | GET | http://45.113.201.36/blog/css/pace.css |
200 | text/css | 3134 | GET | http://45.113.201.36/blog/css/custom.css |
404 | application/javascript | 0 | GET | http://45.113.201.36/blog/js/jquery-2.1.3.min.js |
200 | application/javascript | 9223 | GET | http://45.113.201.36/blog/js/bootstrap.min.js |
404 | application/javascript | 0 | GET | http://45.113.201.36/blog/js/pace.min.js |
404 | application/javascript | 0 | GET | http://45.113.201.36/blog/js/modernizr.custom.js |
404 | application/javascript | 0 | GET | http://45.113.201.36/blog/js/script.js |
200 | application/javascript | 922 | GET | http://45.113.201.36/blog/js/canvas-nest.min.js |
404 | application/javascript | 0 | GET | http://45.113.201.36/blog/js/pace.min.js |
404 | application/javascript | 0 | GET | http://45.113.201.36/blog/js/modernizr.custom.js |
是的,你没看错,这个丑得一比没几个字还是抄的模板(gh: TheFifthMan/simple-blog
)的网页,有一堆 404。先纵观整个 ctf 活动,出现的框架和库就已经有 Vue.js、jQuery、Boostrap 和 Element UI 四种。我们再看看后端的架构:
1 | Service Unavailable |
1 | HTTP/1.0 500 Internal Server Error |
(笑)所以后端是跑在 Ubuntu 上的 Apache 2.4.7 后面反代的一个 PHP 5.5.9。都是古早版本,于是想看看有没有 CVE,搜了一圈发现不太行。惯例跑了一个 gh: maurosoria/dirsearch
,跑出来两个 PHP 文件:/blog/test.php
和 /blog/end.php
。打开 test.php
得到了一大长串 JSFuck,放进 Console 执行,得到了如下代码:
1 | var str1 = "程序员最多的地方"; |
不知道 console.log()
有啥用,反正先去 github 上搜了 bilibili1024havefun
,果然搜到了一个 repo:gh: interesting-1024/end
。白盒审计呗。放出来的 PHP 源码如下:
1 | $str = intval($_GET['id']); |
不难看出一个可行的攻击方法是 end.php?id[]=1
。至于文件路径猜解,经过一些尝试之后发现只要是包含 flag.txt
(但又不只有 flag.txt
)的 url 都是能拿到一张图片的,把图片扔进 StegSolve 或者 StegOnline,或者直接 tail 一下都能拿到位于 jpg 文件末尾的 flag10
(你没看错,甚至没有用到 LSB 隐写,而且拿到的是 flag10
)。
世界第八大未解之谜:flag6
在哪?
我们尝试了 SQL 注入 single.php?id=1
中的 id
参数(毕竟不加这个参数就显示不出页面),但似乎后端只是判断了 isset($_GET["id"])
,而没有真正用到这个 id
的值。从 -4000 到 120000 的暴力枚举结果也佐证了这个猜测。sqlmap 跑完之后,众人发现似乎有个 Referer 头的基于时间的盲注,但是此时因为同时有几十个人在跑 sqlmap,另外几十个人在跑 dirsearch,贵站这台配置比学生机高不了多少的服务器很快就卡到让人无从判断 timeout 是因为注入的 SLEEP
起效了还是因为服务器没空闲资源来处理这个请求了。于是就卡住了。
我们还尝试了 nmap 扫服务器的开放端口,扫出来了这样的结果:
1 | 25/tcp open smtp |
接下来就是喜闻乐见的 redis 未授权登录,直接用 redis-cli 连到 :6379 上,搞事情:
1 | > keys * |
就得到了 flag8
。之后当然要想办法删库跑路了,结果 set flag8 ""
虽然返回了 OK 但是 flag8
的值丝毫没变。百思不得其解,直到我发现这个 redis 是假的:
1 | > set flag8 "" |
所以这本质上就是个带有 tab 自动补全的、只支持少量特定 redis 命令的 OK 复读机。离谱。用 ncat 连到 6379 端口,甚至可以得到一个 output=input[1:]
的奇怪东西。比如你输入 ping,就会收到 ing。
于是乎,少年在打败恶龙解救公主的路途中,经商入仕不小心成了百万富翁和达官贵人,可是连半片龙鳞都还没看到。此情此景让我们不得不发问:nmd,你们究竟把恶龙藏到哪里去了?
83b92f73637ab8056346bb6b8a3af6d9840e8bb0.(jpg|png)
),但只有一张图的 sha1 哈希值能对上。single.php
都已经删了那么多东西了,为什么不把引入了 404 掉的资源的那些代码给删了?留着又没有用。flag10
(如 url=cnmflag.txt
),但是 url=flag.txt
却不会有 flag。且构造了特殊的输入 url=https://jiejiss.com/test
表明,end.php 真的通过 file_get_contents
去读了文件。猜测是如果文件不存在且路径包含 flag.txt 才会返回 flag10
,那么这就意味着当前目录下真的有一个 flag.txt
,只不过根据 end.php,它回显不出来。大胆猜测榜首 90 分的队伍差的就是这 10 分。。。可惜没法构造任意文件上传的洞,否则可以传一个 phar
包上去,直接 getshell。_2c
几个字符。接下来是找不到恶龙而无所事事的挑战者们玩的一些游戏。
end.php
真的会去读 url=
后面的文件路径,于是用 url=/dev/urandom
把 PHP 搞崩了,PHP 带走了 Apachesingle.php
里面似乎有一些和邮件相关的代码,于是对着服务器上开放的 25 和 110 端口(分别对应 SMTP 和 POP3)使劲搞事情。不过无论怎么搞都收不到邮件。flag6
了。flag8
删不掉,那我们总可以往里塞假 flag 吧?于是来得晚的人发现 redis-fake 里面有 flag1
到 flag10
,看上去跟真的一样。现在就等一个官方 WriteUp。毕竟不知道 6 7 9 的标答思路,没法喷。
UPDATE
TL;DR: 一个基于 HTTP Referer 头的简单注入题,有一些非常基础的过滤
这个题本身就是普通 ctf 题,没什么好说的。用 sqlmap 都能跑出来在 HTTP Referer 这里有注入。但让选手非常非常困惑的是,在盲注过程中由于服务器本身性能太差,无从判断 timeout 的成因。(详见分割线前的 0x06 大点)
Anyway,在从 MySQL 中拿到 flag6
之后还可以顺便构造一个文件路径爆破的攻击方法,通过二分实现目录猜解和文件内容读取。经过读取的 end.php 实际内容如下:
1 | <head> |
首先在第三题里,背景图的路径从 banner_bg.gif
切换成了 api/images?file=banner.png
。在前文的「疑点」中我已经提到过这个问题,因为通常来讲 ctf 中不会有无用功。虽然 b 站这次的 ctf 有一堆弱智一样的操作,凭空创造出 n 多个疑点让人非常无力吐槽,但是这个 api
开头的路径确实还是非常可疑,首先要考虑的就是任意文件读。可是 api/images?file=./banner.png
却返回了 404,dirsearch 的扫描也认为该入口是硬编码了白名单。
后来大家在 QQ 群里讨论了一下,有师傅一语道破天机:这个地方确实是硬编码了白名单,flag7
就藏在 ?file=../../../flag7.txt
里!于是一口老血郁结于心,闷闷不乐,直欲取贵站出题人之项上人头,蒸煮烹炸而啖之。但后来又仔细思虑,放弃了这个想法:一则实在无法进入 b 站工作区,二则也担心自身智力遭到同化,陷入只会出弱智猜谜题的可怜境地!
所以最终的解法是访问 /api/images?file=../../../flag7.txt
。
未授权访问,在前面讲过了。
首先你要猜到 /api/images?file=../../../secret.txt
这个路径。大多数人肯定想不到这一个 images?file 的 api,他们用了两次!(其实后面又用了一次)所以很容易错过。打开文件之后发现是一串 base64:
1 | SkRGWDZRZnJxelJQU21YME42MU04OWlwV1l0SlYvcEJRVEJPWWFYUXVHOGZBcnJ1bjNXS3hXRlpHd05uMjFjRw== |
两次解码之后发现出现大量特殊字符,长度为 48 bytes,猜测是经过了 AES 加密。但迫于找不到 key,最后这 10 分也没拿到。复盘发现原来又要用到 /api/images?file=
这个 api,这次需要传入 file=md5sum(secret.txt).jpg
,也就是 file=ae10c97f6de1129abb00b5c961394336.jpg
。然后在文件中寻找字符串,可得:
this is the key of secret.txt:aes_key
遂以 "\x00".repeat(16)
为初始化向量,以 "aes_key" + "\x00".repeat(9)
为 key,以 CBC Mode 解码之,得到 flag9
。
本题全场无一人做出,复盘思路来自第一天就到了 90 分的大佬和官方有关弱智人士交流时得到的提示。
用 dirsearch 跑出来一个 test.php 和一个 end.php,test.php 里的东西在 GitHub 上搜能搜到 end.php 的修改过的源码,然后随便构造下参数就过了。
wcnm
]]>
论文:这里
本来 SS/SSR 的非 AEAD 加密就已经死的差不多了,不缺这么一种攻击方式。不过攻击的思路确实很新颖。
单就这种攻击方法而言,不安全的 SS 加密方法:主要是 aes-*-cfb
,aes-*-ofb
, aes-*-ctr
;安全的 SS 加密方法主要是 chacha20
,aes-*-gcm
,*-poly1305
。
如何补救:将加密方法换成 aes-*-gcm
,或是转战其它工具。
写在开始之前:
我写这篇博文只是想仔细剖析一种新的重放攻击的具体过程,其中内容仅作学术讨论用。
对于 SS 和 SSR 的开发、维护团队的爱恨情仇,我无意参与;大家如果心中已有定论,也不要询问我相关的内容及我本人的意见,且本篇博文中也不会涉及相关内容。
才怪嘞
写了一半,回来补充:尊敬的论文作者,您能不能把细节写清楚啊……给您跪了啊……
流式加密的数据是被分装在不同的数据包里的,将这些数据包拼接在一块就能得到所有的密文。
密文格式如下:
1 | [IV][encrypted payload] |
其中的 IV
在解密 encrypted payload
时会用到,不需要操心它具体是干什么用的。
代理内容以明文 HTTP 请求为例,根据 SS 所使用的协议,将要向服务器发送的原始数据(加密前)的格式如下:
1 | [target data][payload] |
payload
部分储存着所有的 HTTP 请求的数据,target data
中储存着关于最终目的服务器(想要访问的网站所在的服务器)的重要信息。target data
的结构如下:
1 | [type][destination address][port] |
type
可能为 0x01
、0x02
或 0x04
(参见 RFC 1928)type == 0x01
表示随后的 destination address
部分是个 4 字节的 IPv4
地址,也就是目标服务器的 IP 地址。port
是 2 字节的端口号。
需要知道的知识:由于 AES 流式加密的特性,明文的每个
block
在加密后出现在密文的相同位置,类似于“先入先出”概念。因此,target data
对应的密文一定会出现在第一个数据包(“初始化数据包”)中。每个
block
的长度为 16 字节,一般与IV
(初始化向量)的长度相同,详见这个链接。
如果,我们能够修改密文,使得解密后的明文的 destination address
部分储存的 IPv4
地址指向我们自己控制的服务器的 IPv4
地址,不就能够使得服务器端的 SS 将解密后的 HTTP 请求发给我的服务器了吗?
备注:图 2 中对于 GFW 节点的工作方式有一定的简化,与实际情况不完全一致,仅作示意用途。
从上图中可以大概了解到这种攻击的流程。攻击者在收到境外服务器发送给境内用户的可疑数据包时,将数据包复制进内存,并在一定的延时(几十毫秒到几十秒)后,将修改过密文的数据包反向发送给 ss-server
,用一定的手段使 ss-server
把这个数据包解密后的明文发送给受攻击方控制的服务器。当然,由于不知道秘钥,攻击者不可能通过加密明文的方式得到他想要的密文。但是,由于 aes-*-cfb
和 aes-*-ofb
的特性,可以通过异或密文的最开始的一个 block
来对应地修改明文的第一个 block
:
以 aes-*-cfb
为例,上图的解密流程可以转化为:
密文、明文下标均从 1 开始,解密函数与加密函数都是 \(\mathrm{Encrypt}\) 函数。
\(K\) 是解密所需的秘钥(\(Key\))。
\(P_i = \mathrm{Encrypt}(C_{i-1}, K)\,\,\,\mathtt{xor}\,\,C_i\),其中 \(C_0=\mathrm{IV}\)。
我们不妨假设所有 ss-server
发送给 ss-local
的数据包都是 Encrypted HTTP Response
(未经 TLS 加密),也即解密后的明文遵循如下格式:
1 | HTTP 1.1 404 Not Found # HTTP 版本 状态码 状态文字 |
然而这些数据在网络上是加密传输的,我们如何在不知道 \(K\) 的情况下获得明文呢?
答案是:ss-server
有 \(K\),让 ss-server
帮我们解密,把明文发给我们的服务器就可以了~
换言之,我们要想办法修改 Encrypted HTTP Response
,使得修改后的密文解密所得的明文遵循如下格式:
为什么是修改 Encrypted Response 而不是修改 Encrypted Request 我会在后文的适当位置解释。
1 | [type (1 byte)][fake address (4 bytes)][port (2 bytes)]1 404 Not Found |
为什么是替换而不是在最前面添加内容我会在后文的适当位置解释。
Ta-da!ss-server
收到这个数据包,解密之后发现前七个字节符合 [type][address][port]
规则,认为这个数据包是 ss-local
发送给它的请求数据包,于是把解密后的明文发送到了 fake_address:port
。就这样,我们在 fake_address:port
监听的程序收到了如下解密后的明文:
1 | 1 404 Not⛬졷.ꖵ� # 版本(残缺) 状态码 状态文字(乱码) |
为什么解密修改后的密文得到的明文会有乱码我会在后文的适当位置解释。
顺便说一句,这三个“为什么”,原论文里一个字都没提……我佛了……鲨了我吧……
不难发现的是,aes-*-cfb
的加密方法会导致密文与明文逐字节一一对应,修改密文的第 \(k\) 个字节就会导致明文的第 \(k\) 个字节相应地被修改。
由于我们想要修改的是解密出的明文的第一个 block
,也就是 \(P_1\),因此我们只需要考虑 \(i=1\) 的情况:
\(P_1 = \mathrm{Encrypt}(C_0, K)\,\,\,\mathtt{xor}\,\,C_1\),等价于:
\(P_1 = \mathrm{Encrypt}(\mathrm{IV}, K)\,\,\,\mathtt{xor}\,\,C_1\)。
其中,\(\mathrm{IV}\) 可以从数据包的最开头(\(C_1\) 之前的部分)直接得到,\(K\) 是未知的且无法得到,但是 \(K\) 是个固定的值,因此 \(\mathrm{Encrypt}(\mathrm{IV},K)\) 也是个固定的值,设这个值为 \(E_0\)。因此,我们可以将式子改写为:
\(P_1=E_0\,\,\mathtt{xor}\,\,C_1\)。
接下来的操作就很有意思了。根据异或运算的运算规律,我们可以得知:如果 \(x\,\,\mathtt{xor}\,\,y=z\),那么一定有 \(x\,\,\mathtt{xor}\,\,(y\,\,\mathtt{xor}\,\,m)=z\,\,\mathtt{xor}\,\,m\),记为性质①。因此,有:
\(P_1\,\,\mathtt{xor}\,\,m = \mathrm{Encrypt}(\mathrm{IV}, K)\,\,\,\mathtt{xor}\,\,(C_1\,\,\mathtt{xor}\,\,m)\),其中的 \(m\) 就是我们接下来要想办法构造的东西。构造目标是让 \(P_1\,\,\mathtt{xor}\,\,m\) 的值等于我们控制的服务器的 IPv4
地址。同样是根据异或运算的性质,我们可以计算出 \(m\):
\(m = P_1\,\,\mathtt{xor}\,\,\mathtt{IPv4}\)。
有鉴于 \(P_1\) 的格式如下所示:
1 | [type][fake address][port][other data] |
备注:前文已经计算过,
[type][fake address][port]
总长度为 7 字节,一定会出现在第一个block
。
因此,我们其实只需要关心 \(P_1\) 的前 7 个字节。初始的 \(P_1\) 的前七个字节——根据 HTTP 协议——一定是 HTTP 1.
这七个字符,所以:
1 | m = "HTTP 1.\x00\x00\x00\x00\x00\x00\x00\x00\x00" ^ [0x01][addr][port][9 bytes data] |
HTTP Request
的前 7 个字符是不确定的,无法计算出 \(m\)。这就是为什么一定要操作Encrypted HTTP Response
。
计算出 \(m\) 之后,就可以计算出修改后的密文 \(C_1'\):\(C_1'=C_1\,\,\mathtt{xor}\,\,m\)。
没想明白?再讲一遍:
\(C_1\) 解密后为 \(P_1\),由于
aes-*-cfb
的性质,\(C_1\,\,\mathtt{xor}\,\,m\) 解密后也为 \(P_1\,\,\mathtt{xor}\,\,m\)。(上文提到的性质①)经过精心构造,\(P_1\,\,\mathtt{xor}\,\,m\) 的值是
[0x01][fake addr][port][other data]
,正好是我们想要的 \(P_1'\)。因此 \(P_1'\) 对应的密文 \(C_1'\) 就是 \(P_1\,\,\mathtt{xor}\,\,m\) 对应的密文 \(C_1\,\,\mathtt{xor}\,\,m\)。
最后,将 \(C_1'\) 拼接回密文中:
\(C=\mathrm{IV}+C_1'+C_2+C_3+\cdots\)
得到的最终的密文直接发送给 ss-server
,ss-server
就会解密出如下内容了:
1 | [0x01][fake address (4 bytes)][port (2 bytes)]1 404 Not⛬졷.ꖵ� |
接下来,ss-server
会根据 SOCKS5 协议,把从第八个字节(1
)开始的 [payload]
原封不动地发给 fake_address:port
,无秘钥获取明文至此成功。
Q:为什么是修改 Encrypted HTTP Response 而不是修改 Encrypted HTTP Request?
A:不是不想,是不能。Encrypted HTTP Response 的明文前七个字节固定且已知,可以依据此计算 \(m\);Encrypted HTTP Request 的明文前七个字节可能性太多,
GET url...
、POST url...
等,穷举成本太大。Q:在修改密文以修改对应的明文时,为什么是替换而不是在最前面添加内容?
A:不是不想,是不能。AES 加密以块为单位,在最前面添加字符会导致整体的错位,解密出来完全变成乱码。
Q:解密修改后的密文得到的明文为什么会有乱码?
A:其实也不一定,如果是用
aes-*-ofb
或aes-*-ctr
加密的,就不会有乱码。再看一遍aes-*-cfb
解密的模式图,可以发现,\(C_{i-1}\) 参与了 \(C_i\) 的解密。我们修改了 \(C_1\),因此在解密 \(C_2\) 时,会解密出乱码;但是我们没有修改 \(C_2\) 以及更往后的其它block
,因此在解密 \(C_3\) 以及更往后的其它block
时,不会出现乱码。(补充阅读:错误扩散)而
aes-*-ofb
在解密 \(C_i\) 时,不需要 \(C_{i-1}\) 或 \(P_{i-1}\) 参与(请看这个链接),因此就不会出现乱码。Q:如果加密前本身就不是明文(比如HTTPS),会受影响吗?
A:攻击者仍能知道你在使用 SS,但是你的数据安全和隐私不会受到威胁。攻击者仅能实现最外层的解密。
还是要感慨一句,SS、SSR 占领全世界的时代已经过去了。不论是 SS 的 aes-*-*fb
还是已经弃用的 OTA
,抑或者是停止维护的 SSR,显然已经不适合作为安全的选项。即使是理论上安全的 aes-*-gcm
类的 AEAD 算法,仍旧躲不过非常时期的大范围阻断(除非配合 *-obfs
)。以后的世界必将是伪装类软件的天下,无论是伪装成 HTTP/HTTPS(TLS),还是伪装成 WebSocket,都已经有了较为成熟的软件和较为活跃的社区。此次新提出的攻击方法,不过是旧时代的又一声残响,仅此而已。
]]>
中国大陆的互联网服务提供商经常劫持部分域名,转到自己指定的 IP,以强行插入自己的广告。dnscrypt-proxy
支持 DNSCrypt 协议和 DoH 方案等,可以避免受到 DNS 污染。
然而,很多人在按照官方教程安装完 dnscrypt-proxy
后,发现国内网站访问缓慢,甚至加载失败。这是由于 dnscrypt-proxy
在解析域名时,很可能会返回域名的国外主机的 IP。这就导致数据包传输距离大大变长,甚至可能需要经过海底光缆,耗时及久、速度下降明显。本教程采用的配置在很大程度上解决了这个问题,并提出了更多的可行思路。
SOCKS5 Proxy
。后文中,我们均假定它工作在本机的 8886
端口。HTTP/HTTPS Proxy
,当然更好。后文中,我们均假定它工作在本机的 8887
端口。HTTP/HTTPS Proxy
,需要另外再准备一个 SOCKS5 Proxy
。53
端口没有被占用。curl
或 wget
。本教程采用 curl
。nslookup
命令。dnscrypt-proxy
打开 Terminal.app
(或者任何你熟悉的 shell),在其中输入并执行:
1 | sudo -s |
你可能需要输入你的电脑密码。输入密码时,屏幕上不会有显示,这是为了防止你的密码被身后的陌生人偷看。
执行完成后,在命令行中输入并执行:
1 | whoami |
如果回显为 root
,说明你成功获得了 root 权限。
点击这个链接:gh: DNSCrypt/dnscrypt-proxy/releases
,下载最新版本的 dnscrypt-proxy
的二进制发布版本。文件大小大约在 1-10 MB,文件名格式如下:
1 | dnscrypt-proxy-macos-版本.tar.gz |
下载完成后,解压缩至任何便于记忆的目录。后文中均假定解压到 ~/dnscrypt-proxy
目录下。
解压后的文件应当包含 dnscrypt-proxy
和 example-dnscrypt-proxy.toml
文件。
dnscrypt-proxy
首先,在命令行中切换到 dnscrypt-proxy
所在目录:
1 | cd ~/dnscrypt-proxy |
随后,执行如下命令:
1 | mv ./example-dnscrypt-proxy.toml ./dnscrypt-proxy.toml |
因为是初次运行,dnscrypt-proxy
会自动初始化,需要较长时间。如果等待一段时间后命令行中不再显示新的文字,并且也没有出现 error
字样,表明初始化成功。
permission denied
字样,可以尝试执行:sudo ./dnscrypt-proxy
address already in use
字样,请检查本机的 53
端口是否被占用:sudo lsof -i:53
,并退出此命令显示的全部进程(不论 NODE
是 TCP
还是 UDP
,除非你确定完全知道你在做什么)。Syntax error
字样,说明你下载了错误的文件,需要重新下载。dnscrypt-proxy
是否正常工作新开启一个命令行窗口。
首先,在此窗口中执行:
1 | cd ~/dnscrypt-proxy |
你应当看到 IP addresses: xxx.xxx.xxx.xxx
的字样。如果出现 IP addresses: -
,说明此前的配置有问题,或你的电脑的网络连接已中断。
随后,在命令行中执行:
1 | nslookup www.baidu.com 127.0.0.1 |
你应当看到十行左右的文字出现。
接着,打开系统设置,在右上角搜索框中输入 DNS
,点击 DNS 服务器
(DNS servers
),并在左侧的白框左下角找到 +
号。点击 +
号,在新出现的输入框中输入 127.0.0.1
并回车;用鼠标将此条记录拖拽到所有其他记录的上方,以确保它是第一条记录。点击右下角的 确定
(OK
)按钮,待此窗口关闭后,点击其下方的设置窗口右下角的 应用
(Apply
)按钮。
打开你常用的浏览器,开启无痕模式(或无扩展模式),尝试访问 ip.sb
。你可能需要事先关闭所有全局代理。此时,网页应当正常加载。
最后,回到最开始打开的命令行窗口,按下 Control-C
组合键以停止 ./dnscrypt-proxy
。随后执行:
1 | ./dnscrypt-proxy -service install |
这两条命令需要 root 权限。此后,均可以通过 sudo ./dnscrypt-proxy -service start 或 stop
来开始或停止 dnscrypt-proxy
服务。
dnscrypt-proxy
正确解析国内域名也许此时你已经发现,dnscrypt-proxy
往往解析出海外 IP,如下所示:
1 | nslookup music.163.com 127.0.0.1 |
在这个例子中,网易云音乐的域名被解析到香港,显然不是最优解。
最简单的解决办法是,使用代码编辑软件编辑 ~/dnscrypt-proxy/dnscrypt-proxy.toml
文件:
96
行附近,找到:1 | # proxy = 'socks5://127.0.0.1:9050' |
并修改为(以 8886
端口为例):
1 | proxy = 'socks5://127.0.0.1:8886' |
103
行附近,找到:1 | # http_proxy = 'http://127.0.0.1:8888' |
并修改为(以 8887
端口为例):
1 | http_proxy = 'http://127.0.0.1:8887' |
最重要的一步:
访问 dnscrypt.info/map
,找到地理位置在中国大陆的圆点,单击圆点以查看其名称。在创作此教程时(2020年02月04日),中国大陆上共有三个圆点(三台可用的服务器),分别是:geekdns-south
,geekdns-doh-east
和 geekdns-hk
。
在 ~/dnscrypt-proxy/dnscrypt-proxy.toml
文件的第 30
行附近,找到:
1 | # server_names = ['scaleway-fr', 'google', 'yandex', 'cloudflare'] |
并修改为(以 geekdns-south
,geekdns-doh-east
和 geekdns-hk
为例):
1 | server_names = ['geekdns-south', 'geekdns-doh-east', 'geekdns-hk'] |
请注意,单引号为英文半角单引号。
保存文件并关闭代码编辑器。
在命令行中执行如下命令以重启 dnscrypt-proxy
服务:
1 | ./dnscrypt-proxy -service stop |
再次解析域名,就可以发现,此时的解析结果已经正常,如下所示:
1 | nslookup music.163.com 127.0.0.1 |
此结果与阿里巴巴公共 DNS 服务(223.5.5.5
)解析结果相同。
参见:酥酥乳.tools 第 511 篇博文
在转换出 dnsmasq_贵富网list.conf
之后,使用正则替换:将 /^server=\/([\S\s]+)\/127.0.0.1#5353
替换为 $1 127.0.0.1:53
,并将结果粘贴到 dnscrypt-proxy
配置文件的 301
行左右的 Example map entries
下方,配合自建的 DNSCrypt
协议或 DoH
的本机 DNS 服务器食用。
dnsmasq
+ lede
之类的,有成熟的教程了,不再赘述。
传统模式,需要经常维护,但是效果不错。
gh: CNMan/dnscrypt-proxy-config
]]>
参考:
ECharts.js 范例 https://www.echartsjs.com/examples/en/editor.html?c=effectScatter-bmap
百度地图 城市中心点坐标 - 胡dot https://www.cnblogs.com/hhhz/p/7591852.html
拾取坐标系统 - 百度地图 https://api.map.baidu.com/lbsapi/getpoint/index.html
1 | { |
]]>
写在最前:\(\mathrm{C}_n^k=\binom{n}{k}=\frac{n!}{(n-k)! \cdot k!}\)
前情提要:一种特殊的输出杨辉三角的方法
1 | 1 (a+b)**0 = 1 |
不难发现,杨辉三角每一行的数字正好就是 \((a+b)^n\) 展开之后的对应多项式,而每一项的系数又和组合数 \(\binom{n}{k}\) 是相等的。
一种简单的理解是,杨辉三角中的每一个数字,表示的是从顶点开始,只向左下或右下前进,最终到达该数字所在位置的所有可能路径的总数。经过基本的推理,可以看出对于第 \(n\) 行的左起第 \(k\) 个数字,想要从顶点到达该数字所在位置都需要且只需要向左 \(n-k\) 次、向右 \(k-1\) 次。假想小明现在按照此规则从顶点出发,显然,小明每向左或向右一次,都会向下移动一行;因此,小明向下移动 \(n-1\) 行后,便会到达目标数所在的行。换言之,小明在路途上将且仅将移动 \(n-1\) 次,而这 \(n-1\) 次中有且仅有 \(n-k\) 次是向左下方移动的。于是,得出结论:这本质上是个超几何分布问题,可能性总数可以表示为 \({\rm C}_{n-1}^{n-k}={\rm C}_{n-1}^{k-1}=\binom{n-1}{k-1}\)。
而与之对应的,多项式展开也是一个超几何分布问题。这是因为展开式中的 \(p\cdot a^qb^{n-q}\) 项,\(p\) 表示的是有多少个 \(a^qb^{n-q}\),而每个 \(a^qb^{n-q}\) 都是在 \((a+b)(a+b)\cdots(a+b)\) 中挑选出 \(q\) 组,令这 \(q\) 组的 \(a\) 和余下的 \(n-q\) 组中的 \(b\) 相乘所得到的,从 \(n\) 组中选出 \(q\) 组的可能性总数为 \(\binom{n}{q}\)。
这样就可以推出杨辉三角的第 \(n+1\) 行全部数字是 \((a+b)^n\) 展开式的二项式系数一一对应的。
稍作拓展:
令 \(f(x,y)=(x+y)^n\),求 \(f(x,y)\) 的展开式二项式系数之和。
实际上,它的展开式的第 \(i+1\) 项可以直接表示为 \(\binom{n}{i}\cdot x^{n-i}y^i\),其中 \(0 \le i \le n\)。换言之,我们如果想要求其二项式系数之和 \(\sum_{i=0}^{n}\binom{n}{i}\),就可以通过将 \(x=1,\,y=1\) 代入到 \(\sum_{i=0}^{n}\binom{n}{i}\cdot x^{n-i}y^i\) 中来计算。换言之,所求即为:\(f(1,1)=2^n\)。
注意到 \(2^n\) 还可以视作 \(n\) 位二进制数的总数,这仅仅是一个巧合吗?
在那耸立的高岗黑岩之上,恶魔的秽语悄然响起:\(\sum_{i=0}^n m^i\cdot\binom{n}{i}=(m+1)^n\)
引证:\(\binom{n}{n-k}=\binom{n}{k}\),这其实很好理解。\(\binom{n}{n-k}\) 表示从 \(n\) 个东西里挑出 \(n-k\) 个,剩下 \(k\) 个;\(\binom{n}{k}\) 表示从 \(n\) 个东西里挑出 \(k\) 个,剩下 \(n-k\) 个。但是实际上,每一种挑出来的组合,必然对应且仅对应一组被剩下了的组合;所以,剩下 \(k\) 个也可以理解为挑出来了 \(k\) 个,只不过挑出来它们是为了把它们剩下。当然最直观的证明方法是用组合数定义,展开阶乘,把相同的项消掉,再重新写成阶乘、转换为组合数的表达形式。
是的,这里有一个二项式定理的推广公式,就是 \(\sum_{i=0}^n m^i\cdot\binom{n}{i}=(m+1)^n\),其中 \(m,n \in \mathbb{Z^+}\)。现在我们要谈论的是该如何去简单理解这个公式。
我们先看看 \(m=1\) 的情况,这也就是上一个标题最后所给出的情况:\(\sum_{i=0}^{n}\binom{n}{i}=2^n\)。为了简单地解释它,我们暂时还是需要用到杨辉三角:小明移动 \(n\) 次后,一定会来到第 \(n+1\) 行的某一个数所在的位置。而他想要移动 \(n\) 次,就需要做出 \(n\) 个选择,每次都必须在向左下和向右下中二选其一。所以,总的可能数为:\(2^n\),这也就是推广公式的右手侧。而左手侧所表示的正是小明走到第 \(n+1\) 行的每一个数的可能之和,所以自然也就等于 \(2^n\)。
那么,对这种思考方式稍加抽象,我们可以用二进制数的思想来考虑:有一个 \(n\) 位二进制数,它的第 \(k\) 位为 \(1\) 则表示小明第 \(k\) 次选择了向左下,为 \(0\) 则表示选择了向右下。再进一步抽象,把小明完全剥离出去,就会变成:列举出所有的 \(n\) 位二进制数,这些二进制数当中,有且仅有 \(k\) 个 \(1\) 的数的数量就是 \(\binom{n}{k}\)(这等价于从 \(n\) 位里挑出 \(k\) 位,令它们为 \(1\),其余位为 \(0\))。这种方式,在数学上更容易说明 \(m=1\) 时该式子成立。
如果 \(m > 1\) 呢?我们该怎样推广这个式子?其实也很简单,只需要对应地用 \(k\) 进制数的思想就好了。
例如,令 \(m=2\):列举出所有的 \(n\) 位三进制数,共 \(3^n\) 个。这些三进制数当中,有且仅有 \(k\) 位不是 \(0\) 的数的数量就是 \(\binom{n}{k}\)(这等价于从 \(n\) 位里挑出 \(k\) 位,令它们为 \(1\) 或 \(2\),其余位为 \(0\))。注意到,每一个非 \(0\) 位都有两种选择:\(1\) 或 \(2\)。所以一个 \(n\) 位三进制数中如果有 \(n-k\) 个 \(0\),剩下的 \(k\) 位就会有 \(2^k\) 种可能性;而“ \(n\) 位三进制数中有 \(n-k\) 个 \(0\)”本身已经有 \(\binom{n}{n-k}=\binom{n}{k}\) 种可能,二者相乘正好就是 \(\binom{n}{k}\cdot 2^k\)。于是,所有的 \(\binom{n}{k}\cdot 2^k\) 相加,应该等于\(n\) 位三进制数的总数 \(3^n\)。
再往下其实也不用推了,思路是一样的。
]]>