|
| 1 | +--- |
| 2 | +title: 逆向操作系统实验课的rust评测程序 |
| 3 | +date: 2025/04/14 14:35:00 |
| 4 | +updated: 2025/04/22 14:21:00 |
| 5 | +excerpt: |- |
| 6 | + 我们操作系统实验课的配套任务有一个专门的评测程序来判断是否通过, |
| 7 | + 就像oj一样。然而,他是在本地评测的,测完需要我们复制结果到平台上, |
| 8 | + 整个程序是用rust写的,不仅需要权限,而且报错也很抽象,让我用得很恼火。 |
| 9 | + `file`一看,竟然没剥符号!直接逆向启动,还原处理逻辑,手写注册机实现任意题目 |
| 10 | + token输出... |
| 11 | +tags: |
| 12 | + - non-ctf |
| 13 | + - rust |
| 14 | +thumbnail: /assets/trueblog/ehtoken.png |
| 15 | +--- |
| 16 | + |
| 17 | +我们操作系统实验课基于[tatakOS](https://github.com/yztz/tatakOS), |
| 18 | +在它的基础上做一些修改。具体的任务基于头歌平台分发,要拉取头歌平台的git仓库, |
| 19 | +做完任务后使用老师分发的`eh`程序运行评测,通过就会输出一串Token ~~flag~~, |
| 20 | +然后上传到头歌平台上通过评测。 |
| 21 | + |
| 22 | +{% note blue fa-square-check %} |
| 23 | +原先以为是学院里写的粗劣操作系统,让我们补全代码,没想到这个 *tatakOS* |
| 24 | +竟然是操作系统赛获奖的系统。我粗略地看了一眼代码,没有明显瑕疵, |
| 25 | +感觉可以做做。 |
| 26 | +{% endnote %} |
| 27 | + |
| 28 | +## 逆向 |
| 29 | + |
| 30 | +`eh`有个“安装程序”,会把评测用镜像文件释放到`/var/local/eh/testcases/`文件夹下, |
| 31 | +然而,这个目录只有root能创建,因此还要求root权限。你一个评测程序还需要root?? |
| 32 | +这对我这个日用Linux的人来说完全不能理解。然而,只是这样,还不能体现出`eh`的抽象。 |
| 33 | +它的报错写得根本不可读,只是抛出了一个rust异常,编译tatakOS出错了也不把信息打印全, |
| 34 | +异常的名字也奇奇怪怪,找不到git仓库的用户竟然是`UnrecognizedUser`, |
| 35 | +不看文档根本不知道怎么解决。 |
| 36 | + |
| 37 | +忍不了了,一看这个程序,竟然没剥符号!这下不得不逆向了。 |
| 38 | + |
| 39 | +有了符号,就能分清哪些函数来自库,哪些是程序自己的。在成功通过第一个评测后, |
| 40 | +下断点在输出token的位置,向上追踪,发现一串json字符串: |
| 41 | + |
| 42 | +```json |
| 43 | +{ |
| 44 | + "meta": { |
| 45 | + "edu_coder_uid": "phmuan9zc", |
| 46 | + "timestamp": "2025-04-08 16:40:35.689451041 +08:00", |
| 47 | + "lab_id": 1 |
| 48 | + }, |
| 49 | + "score": "22/22", |
| 50 | + "token": "8yP3tR6wQ9zX4vL2kM", |
| 51 | + "pass": true |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +但是输出的并不是json,而是一串base64。继续向下追踪,发现另一个json, |
| 56 | +随后被`serde`反序列化后,被 *chacha20* 解密,解密结果是一个rsa公钥pem字符串。 |
| 57 | +随后使用`RustCrypto/RSA`模块 *PKCS#1 v1.5* 加密后转换成base64 |
| 58 | +字符串打印出来。知道流程之后,只需要找到所有题目的token,就可以写一个注册机, |
| 59 | +直接上交平台,通过评测。 |
| 60 | + |
| 61 | +{% note green fa-lightbulb %} |
| 62 | +为了验证`eh`的流程和我说的是否一致,一开始我使用CyberChef来验证, |
| 63 | +结果它的RSA实现有问题,即使我截获了rng做重放,使用raw加密, |
| 64 | +结果也和程序的对不上,换用了python的模块才对上。为了追踪题目 |
| 65 | +token,还动用了`rr`,碰巧遇到了[pwndbg的bug](https://github.com/pwndbg/pwndbg/pull/2850), |
| 66 | +顺手修了一下,花费了我不少时间。rust的字符串是没有`'\0'`截断的, |
| 67 | +和go类似,在函数调用时将长度一起传入,再加上静态编译, |
| 68 | +`strings`直接输出一大坨,这也给还原代码带来了一些困扰。 |
| 69 | +{% endnote %} |
| 70 | + |
| 71 | +通过头歌平台,还可以泄露评测脚本和私钥,具体怎么做就不说了,留给读者自己探索。 |
| 72 | +最后写出的注册机大概是这样: |
| 73 | + |
| 74 | +```python get_flag.py |
| 75 | +#!/usr/bin/python3 |
| 76 | +import argparse |
| 77 | +import sys |
| 78 | +import json |
| 79 | +from datetime import datetime |
| 80 | +from random import randint |
| 81 | +from base64 import b64encode |
| 82 | + |
| 83 | +from Crypto.PublicKey import RSA |
| 84 | +from Crypto.Cipher import PKCS1_v1_5 |
| 85 | + |
| 86 | +PUBLIC_PEM = '[REDACTED]' |
| 87 | +TOKENS = '[REDACTED]' |
| 88 | + |
| 89 | +parser = argparse.ArgumentParser( |
| 90 | + usage='python %(prog)s -u <uid> -i <lab_id>', |
| 91 | + description='摆脱 eh ,直接生成Token!' |
| 92 | +) |
| 93 | + |
| 94 | +parser.add_argument('-u', '--userid', type=str, required=True, help='用户UID(头歌平台个人主页)') |
| 95 | +parser.add_argument('-l', '--lab-id', type=int, required=True, help='实验ID(1-17)') |
| 96 | + |
| 97 | +if len(sys.argv) == 1: |
| 98 | + parser.print_help(sys.stderr) |
| 99 | + sys.exit(1) |
| 100 | + |
| 101 | +args = parser.parse_args() |
| 102 | +if args.lab_id < 1 or args.lab_id > 17: |
| 103 | + print('实验ID超出范围', file=sys.stderr) |
| 104 | + sys.exit(1) |
| 105 | + |
| 106 | +timefmt = f'%Y-%m-%d %H:%M:%S.%f{randint(100, 1000)} %:z' |
| 107 | +nowtime = datetime.now().astimezone().strftime(timefmt) |
| 108 | +payload = { |
| 109 | + "meta": { |
| 110 | + "edu_coder_uid": args.userid, |
| 111 | + "timestamp": nowtime, |
| 112 | + "lab_id": args.userid |
| 113 | + }, |
| 114 | + "score": "22/22" if args.lab_id != 4 else "1/1", |
| 115 | + "token": TOKENS[(args.lab_id - 1) * 0x12: args.lab_id * 0x12], |
| 116 | + "pass": True |
| 117 | +} |
| 118 | + |
| 119 | +cipher = PKCS1_v1_5.new(RSA.import_key(PUBLIC_PEM)) |
| 120 | +ciphertext = cipher.encrypt(json.dumps(payload, separators=(',', ':')).encode()) |
| 121 | +print('[+] 已生成该实验ID的Token:', file=sys.stderr) |
| 122 | +print(b64encode(ciphertext).decode()) |
| 123 | +``` |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | +## 后续 |
| 128 | + |
| 129 | +在我写出注册机后,潘博帮我联系到了出题人,让我帮忙提点建议改进。他说, |
| 130 | +我们用的程序已经是老版了,新版改了不少。我问他要源码,他爽快地给了, |
| 131 | +新版在编译时会剥符号,而且还加了反调试(~~虽然patch一下就没了~~), |
| 132 | +除此之外新版会将运行结果打包成“token”,交给云端来判断是否通过, |
| 133 | +不再在本地判断是否通过了,这也意味着注册机不再能正常工作了, |
| 134 | +相对安全了不少。 |
| 135 | + |
| 136 | +## 杂谈 |
| 137 | + |
| 138 | +在各种内外部因素影响下,不少人学习rust,并到处宣扬rust,这无疑是令人反感的。 |
| 139 | +尽管如此,rust出色的包管理以及严格的编译器拉高了编程者的下限,这也使它成为了C++ |
| 140 | +的直接竞争者,注定会在未来影响世界编程格局。 |
| 141 | + |
| 142 | +不少rust软件,例如`uv`、`ruff`、`neovide`等具有卓越的性能,能够完美替代它们的竞争品。 |
| 143 | +~~点名`pip`,安装个包太折磨了,`uv`一分钟的事`pip`半小时还解决不了依赖问题,~~ |
| 144 | +使用rust写软件固然舒适,但是在不同的场合要使用合适的语言,有功夫锈化所有工具, |
| 145 | +不如先精进自己的代码技术,而不是用rust在用户面前班门弄斧。 |
| 146 | + |
| 147 | +## 参考 |
| 148 | + |
| 149 | +1. [yztz/tatakOS](https://github.com/yztz/tatakOS) |
| 150 | +2. [fix: adjust to `rr`'s vFile reply](https://github.com/pwndbg/pwndbg/pull/2850) |
0 commit comments