kancolle-worker
玩舰娘后,曾用 Clojure 写过一个自动远征的程序 kancolle-worker。
这个程序在 Clojure 的 REPL 中提供主要的舰娘 api 及基本的游戏逻辑模型,懂 Clojure 的人很容易写出一个自动远征程序。
api_port 参数
后来舰娘的主要 api 添加了一个校验参数: api_port ,用于屏蔽第三方挂机程序。该参数的生成算法是混淆过的,不易阅读。如果要继续使用外挂程序,就必须疏理一下诸如下面这种代码,然后用其他语言实现。
正则混淆
1
| new RegExp(".")(new RegExp("...$")(~(~[][{}] << ~[][{}])))
|
这是一段表示字符串 "6"
的代码。通过观察上面的表达式,发现其中所有的项都是常量,那么表达式的结果也应该是个常量。进一步分析各个表达式。
[][{}]
为 null
,~null
为 -1
,-1 << -1
为 -2147483648
,~(-2147483648)
为 2147483647
,(new RegExp("...$")(2147483647)
为 "647"
(转换成字符串后取末尾3个字符),new RegExp(".")("647")
为 "6"
(取第一个字符)。
这种常量混淆形式比较新颖。
常量折叠
自从 Air SDK 从 Flex 编译工具中独立出来后,添加了许多功能。其中就有一个基本的常量折叠功能。常量折叠就是把代码中的常量表达式替换为表达式的最终结果,这样就省去了运行时的计算,对程序的运行效率有所提升。比如 var a:int = 1 + 1;
编译后与 var a:int = 2;
一样。我试过用 Air SDK 重新编译上面的表达式,发现没有被折叠。原因可能是上面的常量混淆利用了 as3语言的一些隐式的类型转换,以及空数组与空对象、正则表达式的的特性,超出了编译器的常量判定能力。
有个简便的方法可以计算表达式 ~(~[][{}] << ~[][{}])
的结果。就是打开 Chrome 的 JS console, 运行上面的代码。正则表达式直接匹配则是 as3 语言的特性,JS 无法运行。/./"234"
在 as3中 是合理的表达式,结果是 "2"
,在 JS 中是错误的式子。
解析程序
为了快速得到混淆前的代码,我编写了这个 Flash 程序,它可以将舰娘的混淆代码简化。使用效果如图:
图中的常量是部分上周舰娘的代码。
除此以外,舰娘的另一类代码混淆是将简单的运算符替换成函数调用。比如将 x + y
替换成 k.a(x, y)
。这种方法与上面的混淆方法共同使用,产生的代码极难阅读。所以我写的 Flash 代码分析工具加入了表达式重新打印功能。效果如图:
图中的代码是部分上周舰娘的代码。它将常量折叠,并且将函数调用替换成 S-表达式,与 Clojure 的代码相差无几。
Flash 程序是用编译器生成工具所写,主要源码在附录。
它的主要目的在于分析舰娘中混淆的代码并转换为可读形式,所以并不是一个完整的语言。所涉及的运算符仅限于混淆代码使用的运行符。为了方便,正则表达式的调用写入了语法中。常量表达式与变量表达式在生成式中就分开了( Expr
与 VExpr
)。最后,纠正了某 Flash 反编译工具导出的代码的一个运行符优先级问题。
有了这个工具,加密后的算法会更快分析出来,自动远征程序又可以愉快地运行了。同时说一句:外挂有风险,使用需要谨慎。
附1:工具使用方法
- 从舰娘主程中分离出 Core.swf
- 用工具反编译出 api_port 类
- 使用我的工具分析其算法
附2:词法、语法源码
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
| /**
js tokens
**/
%lex
%class JsLexer
%package org.lala.gen
%%
[ \r\n\t] /* skip whitespace */
0x[0-9a-fA-F]+ $$ = parseInt($$); return "hex";
\d+ $$ = parseInt($$); return 'int';
new return 'new'
RegExp return 'RegExp'
[a-zA-Z_$@][\w_$@]* return 'id'
'[^']*'|"[^"]*" $$ = $$.substr(1, yyleng - 2); return 'string';
/** flowing with div **/
) return ')'
] return ']'
} return '}'
~ return '~'
false $$ = false; return 'boolean'
true $$ = true; return 'boolean'
null $$ = null; return 'null'
/** flowing with regexp **/
& return '&'
\( return '('
* return '*'
+ return '+'
, return ','
- return '-'
\. return '.'
/ return '/'
;
\<< return '<<'
=
>> return '>>'
>>> return '>>>'
\[ return '['
{ return '{'
| return '|'
/** div **/
[_$a-zA-Z][_$a-zA-Z0-9]* return 'id'
. return 'other';
%%
/lex
%class JsParser
%package org.lala.gen
/* %import org.lala.gen.JsLexer */
%lexer_name org.lala.gen.JsLexer
%start VExpr
%left ','
%left '|'
%left '&'
%left '<<' '>>'
%left '>>>'
%left '+' '-'
%left '*' '/'
%left '~'
%left 'new'
%left '(' '[' ']' '{' '}'
%right ')'
%%
VExpr /* 变量表式 */
: Expr %prec new
| id '.' id '(' ArgList ')' { $$ = ['mcall', $1, $3, $5]; }
| id '(' ArgList ')' { $$ = ['call', $1, $3]; }
| id
| '(' VExpr ')' { $$ = $2; }
| VExpr '[' VExpr ']' { $$ = ['index', $1, $3]; }
;
ArgList
: { $$ = []; }
| VExpr { $$ = [$1]; }
| ArgList ',' VExpr { $1.push($3); $$ = $1; }
;
Expr /* 常量表达式 */
: Number { $$ = $1; }
| '(' Expr ')' { $$ = $2; }
| Expr '+' Expr { $$ = $1 + $3; }
| Expr '|' Expr { $$ = $1 | $3; }
| Expr '&' Expr { $$ = $1 & $3; }
| Expr '-' Expr { $$ = $1 - $3; }
| Expr '*' Expr { $$ = $1 * $3; }
| Expr '/' Expr { $$ = $1 / $3; }
| Expr '<<' Expr { $$ = $1 << $3; }
| Expr '>>' Expr { $$ = $1 >> $3; }
| Expr '>>>' Expr { $$ = $1 >>> $3; }
| '~' Expr { $$ = ~$2; }
| '-' Expr %prec '~' { $$ = -$2 }
| Array
| Expr '[' Expr ']' { $$ = $1[$3]; }
| '{' '}' { $$ = {}; }
| new 'RegExp' '(' string ')' Expr { $$ = Number(String($6).match(new RegExp($4))[0]); }
;
Array
: '[' ArrayList ']' { $$ = $2; }
;
ArrayList
: { $$ = [] }
| Expr ',' ArrayList { $3.unshift($1); $$ = $3; }
| Expr { $$ = [$1]; }
;
Number
: hex
| int
;
|