2023癸卯年初一,笔者发布了今年的新年红包

在这个文件当中一共包含了3个拼手气红包。你都领取到了吗?现在笔者来揭晓这些红包的正确领取方式。

红包1

红包1是本次的签到红包,领取方式非常简单:在 64位 Windows 操作系统上执行从新年红包链接上下载的可执行文件,然后点击“获取”按钮,即可看到红包1的值,顺利领取红包1。

redpack1

红包2

红包2的领取其实非常简单:在DOS环境下执行上述链接下载下来的可执行文件,等待数小时程序自动穷举计算正确红包口令(以DOSBOX模拟器所需要的时间为例),即可获得红包2。

redpack2

大家总说微软的向前兼容做得很好,Windows 11可以执行从Windows 1.0以来的各种GUI可执行文件。

all-in-one

(网图,侵删)

但其实某种程度上,微软的向后兼容也做得很好,XD。每个Windows的PE可执行文件都包括MZ头部,即一个合法的DOS程序。不过大多数时候这个DOS程序只会输出一句话:“This program cannot be run in DOS mode.”

dos-mode

笔者给红包2留了下面的这些提示:

  1. 使用标准的8文件名+3拓展名的命名方式,企图唤起大家一些古老的回忆

  2. 故意在 64位 Windows 模式的窗口标题下增加了 (Win 64) 的后缀作为呼应

  3. 在DOS代码区段未对任何字符串进行混淆加密,可以用Hex编辑器看到在程序的开头有若干以$结尾的字符串,并写有(DOS Mode)等字样。

  4. 进一步地,笔者在MZ头部被链接器以0填充的部分用Hex编辑器改了一个This program CAN be run in DOS mode!!!作为提示。

这些提示大家发现了几个呢?

除了干等程序自动解算红包,还可以通过逆向分析的方式快速获取这个红包。

通过DOS时代的逆向分析工具,可以分析出,这个程序是在求算同余方程 2^k % 1000000007 == 743327059。 由于DOS的INT 21h调用的常数太大,所以导致这个O(n)的程序需要数小时才能完成。直接写一个小程序就能快速解算出这个同余方程的解。

这个红包领取起来并不困难,不过出这个红包可是费了番功夫。后续我会再写篇文章来详细介绍下这个红包在DOS下运行的故事。

红包3

在 64位 Windows 下执行可执行文件,点击“获取”按钮,在获取红包1的同时,也能同时发起对红包3的获取请求。但是红包3没有提示正确。

通过抓包分析红包3(当然逆向分析也行),可以发现红包3以Modbus TCP协议和服务端交互。

客户端没有什么特别的逻辑,就是向服务端发送了两个0x3读请求,并根据读请求渲染到界面上。(可惜没有找到又快又容易实现的Windows GUI软件栈,这里给大家逆向分析带来了很多干扰,抱歉)

客户端0x3命令可以读取0-2三个寄存器的值,通过抓包(或者逆向分析客户端),可以猜测0-1寄存器表示红包的值,2寄存器表示红包的正确与否。

随后按照Modbus TCP协议请求Server端,分析Server端的行为。

可以发现,Server端能响应unit id为1、3的请求,应该对应红包1、3。Server端能接受0x3(读)/0x6(写单个寄存器)/0x10(写多个寄存器)三个Modbus命令。

通过一些开源的modbus client进行适当的测试,可以发现,0x6/0x10可以写入0-1寄存器更新红包的值,同时,通过对红包1所对应的unit 1进行操作,可以验证,2寄存器的值会随着0-1寄存器的变化而变化。

基于红包2的提示,可以分析出,红包3应当是需要写一个Modbus Client,穷举所有可能的红包口令。

可是这个时候又会发现,如果按照最基础的实现方式来枚举红包口令,穷举速度会非常慢:网络延迟这个常数远大于红包2的INT 21h,下发0x10设置红包的值,然后再0x3读取寄存器2的值,需要2*RTT,这个时间会有几十到几百毫秒,这个超大的常数会让红包口令的枚举完全无法实现。

有了这个超大的常数,还能枚举吗?答案是肯定的,因为有这样两个办法解决它:

  1. TCP连接保序,可以在一个TCP连接上连续发一串Modbus报文,使得网络延迟被隐藏;

  2. 注意到Server端会为每个TCP Connection分配上下文,可以通过多连接的方式进一步增大并行度。

经过这两个优化,现在的瓶颈就到了Server端CPU消耗和带宽了。本次红包的Modbus Server是我专门写的高性能Modbus TCP Server(后续也会另写文章介绍下这个TCP Server的实现),CPU不是瓶颈(实际监控看CPU消耗不到10%),实际的瓶颈是带宽(国内云服务带宽太贵了,我只开了2Mbps),最终大概需要2h多可以枚举到红包口令(来自唯一领到红包3的 @NickCao同学实测)。

这里分享下@NickCao同学的代码实现,他通过rust函数式计算的方式,优雅地完成了红包口令的多线程枚举:

use byteorder::BigEndian as E;
use byteorder::WriteBytesExt;
use rayon::prelude::*;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Instant;

const UNIT_ID: u8 = 3;

fn main() {
    (0u32..99999999)
        .into_par_iter()
        .chunks(((u16::MAX - 1) / 2).into())
        .for_each(|chunk| {
            let now = Instant::now();

            let mut buf = vec![];
            for (i, x) in chunk.iter().enumerate() {
                // Transaction Identifier
                buf.write_u16::<E>(i as u16).unwrap();
                // Protocol Identifier
                buf.write_u16::<E>(0).unwrap();
                // Length
                buf.write_u16::<E>(11).unwrap();
                // Unit Identifier
                buf.write_u8(UNIT_ID).unwrap();
                // Function Code
                // 0x10 = Write Multiple registers
                buf.write_u8(0x10).unwrap();
                // Starting Address
                buf.write_u16::<E>(0).unwrap();
                // Quantity of Registers
                buf.write_u16::<E>(2).unwrap();
                // Byte Count
                buf.write_u8(4).unwrap();
                // Registers Value
                buf.write_u32::<E>(*x).unwrap();

                // Transaction Identifier
                buf.write_u16::<E>(u16::MAX - i as u16).unwrap();
                // Protocol Identifier
                buf.write_u16::<E>(0).unwrap();
                // Length
                buf.write_u16::<E>(6).unwrap();
                // Unit Identifier
                buf.write_u8(UNIT_ID).unwrap();
                // Function Code
                // 0x03 = Read Holding Registers
                buf.write_u8(0x03).unwrap();
                // Starting Address
                buf.write_u16::<E>(2).unwrap();
                // Quantity of Registers
                buf.write_u16::<E>(1).unwrap();
            }

            let mut conn = TcpStream::connect("8.130.19.255:502").unwrap();
            conn.write_all(&buf).unwrap();

            let mut res = vec![0; 23 * chunk.len()];
            conn.read_exact(&mut res).unwrap();
            res.chunks_exact(23).for_each(|inner| {
                match inner {
                    [tid0, tid1, 0, 0, 0, 6, UNIT_ID, 0x10, 0, 0, 0, 2, _, _, 0, 0, 0, 5, UNIT_ID, 0x03, 2, 0, chk] => {
                        match chk {
                            0 => (),
                            1 => {
                                let tid = u16::from_be_bytes([*tid0, *tid1]);
                                println!("answer is {:?}", tid as u32 + *chunk.first().unwrap());
                                std::process::exit(0);
                            },
                            _ => unreachable!(),
                        }
                    },
                    _ => unreachable!(),
                }
            });

            println!(
                "chunk: {} processed, took {} seconds",
                chunk.first().unwrap(),
                now.elapsed().as_secs()
            );
        });
}

Server端的binary可在这里下载。代码请期待后续的文章。

结语

今年这个红包不知道有没有给大家带来收获,反正给我倒是带来了许多收获2333。在文章的最后,笔者给大家拜个晚年,祝大家兔年吉祥~