How to Draw a KanColle Map

This is a post about how kcmap draw a SVG using map SWF only.

Basic Knowledge

In order to understand this post, you may need these knowledge.

Display Container of Flash (AS3)

Flash (AS3) renders its display from a DisplayObjectContainer tree.
A container is isolated system, having self-governed coordinate system, child containers and so on.

These are common container: MovieClip, Sprite, Shape, etc.

Coordinate System of Flash

Coordinate of flash allow interger only, while one point equals to 0.05 pixel.
It means (8000, 6000) is (400, 300) on screen.

Decompilation of SWF

Usage of JPEXS Free Flash Decompiler (ffdec).

Container

Structure

According to the decompilation of map swf, a Sprite named map contains entire data inside the swf.

When game client calls /api_req_map/start or /api_req_map/next, the api_no value in response is used to indicates the route.
For example, api_no: 11 indicates child Sprite line11 of Sprite map.
If Sprite line11 isn’t a start spot, it will contain a Shape which contain the route Bitmap.

The container structure is as follows.

1
2
3
4
5
6
7
8
Sprite@name=map
├─┬ child[@name=line11][@id=32] => Sprite@id=32
│ └─┬ child@id=31 => Shape@id=31
│ └─┬ shapeBounds
│ └ child@id=30 => Bitmap@id=30
├─┬ child[@name=line12][@id=34] => ...
│ └── ...
└ ...

Coordinate

As said, each container has its owned coordinate system.
The real coordinate of current container is got by summing up x and y from root to current container.

It’s easy.
Let’s mark line 11 of map 2-3 for an intuitive feeling.

  • The GREEN cross is the origin of Sprite line11 in its parent Sprite map.
  • The CYAN cross is the origin of Shape (line11) in its parent Sprite line11.
  • The RED rectangle is bounds of Shape (line11) which consists of Bitmap (line11).

Route

Take a look at all container marked map 2-3.

It shows routes’ end position, routes’ image position, routes’ image size, …
Wait, where do routes start?
In fact, start position of routes don’t appear inside SWF.
Guessing is required to find out routes’ start position.

Guess

Here’s my method to guess start coordinate, using line 11 of map 2-3 as exmaple again.

  1. Take all routes’ end coordinate as map spots.
  2. Calculate the coordinate of route image center(named C).
  3. Draw a line from line11 end spot (named E) to C stopped at S with twice length of EC.
  4. The spot nearest S is start spot.

Then, we can draw the map image.

Spot Name

Old Map

Spot is named as ABC letter by KanColle community to facilitate communication.
They’re not offical map information, so aren’t included in SWF.
Manual operation is required for assigning spot name.

New Map

Since a time I don’t know, KanColle production team begin to name spot as community did.
However, they embed the letters into the background, making it impossible to extract letters from SWF.
So, manual operation is also required.

Draw

After the steps above, information for drawing a map is complete.

Let’s take a look at the title image again.

「艦隊これくしょん」混淆与反混淆

「艦隊これくしょん」出于反 Bot 和未知目的,对游戏资源进行了不少混淆处理。

本文列举将会列举部分资源的混淆算法,并给出整理后的混淆算法或反混淆算法。

注意:此些算法都具有时效性,建议在使用前进行测试。

体例

  • Core.swf/common.util.SoundUtil/createFileName:表示 Core.swf 文件中的 common.util.SoundUtil 类中的 createFileName 函数。

Core.swf

Last Update: 2017-02-15

Core.swf 是游戏加载的第二个 swf 文件,由 mainD2.swf 读取、反混淆和加载运行。其反混淆函数为 mainD2.swf/mainD2/___,该函数本身亦经过复杂的代码混淆。

反混淆算法原理:

  1. 将 Core.swf 的 0~127 bytes 划为第一区域,128~EOF 划为第二区域。
  2. 将第二区域按相等大小划分为 8 块,按 0, 7, 2, 5, 4, 3, 6, 1 的顺序重新排列。

反混淆脚本 Core-decode.py Core.swf Core-dec.swf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3

import sys

ORDER = [0, 7, 2, 5, 4, 3, 6, 1]
SRC = sys.argv[1]
DEST = sys.argv[2]

with open(SRC, "rb") as f:
org = f.read()

with open(DEST, "wb") as dec:
size = (len(org) - 128) >> 3
dec.write(org[0:128])
for i in ORDER:
dec.write(org[ i*size+128 : (i+1)*size+128 ])

活动地图的 SP 部分

Last Update: 2017-02-15

角川于 2016 年秋活(2016-11)新增了达成一定条件后解锁新增海域路径的机制。

该机制原理为:

  1. 将新增的海域路径储存到额外的 SWF 文件中。
  2. 将该额外 SWF 文件使用 Core.swf 相同的算法混淆处理。
  3. 将混淆后的 SWF 文件作为二进制数据(Binary Data)嵌入到 /kcs/scenes/SallyMain.swf 中。

因此,只需要将 SallyMain.swf 嵌入的二进制文件保存为 SWF 文件,然后使用上述 Core-decode.py 即可反混淆。

活动地图的文件名 (swf)

Last Update: 2017-02-15

自 2016 年冬活(2016-02-10)起,角川对活动地图的 swf 文件名进行了混淆,但通常图的文件名仍然遵循 AA_BB.swf 的格式。其混淆函数位于 Core.swf/common.resources.MapResourceLoader/_convertName

该函数内 __loc10__:String 的值为一串正则表达式的结果,其值为 "push",这是该函数内唯一的代码混淆。

该混淆函数的本质是对 RND:Array 的解压缩算法,建议直接阅读下面的算法。

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
#!/usr/bin/env node

// Map ID
const mapArea = 33
const mapNo = 1

// RND is extract from Core.swf/common.resources.MapResourceLoader/RND
const RND = [3367, 28012, 6269, 26478, 24442, 27255, 28017, 3366,
6779, 7677, 7179, 28011, 24421, 27502, 3366, 7779,
24439, 27762, 6474, 7463, 28515, 3364, 6672, 28006,
27999, 27254, 7363, 6868]

// Procedure 1: Decode
let RND1 = []
;(function () {
for (let rnd of RND) {
rndstr = rnd > 10000 ? rnd.toString(16) : rnd.toString()
RND1.push(rndstr.substr(0, 2))
RND1.push(rndstr.substr(2, 4))
}
})()
console.log("RND1", JSON.stringify(RND1))

// Procedure 2: Group
// Format: 33 a b c d 32 j k l
let kfile = []
;(function () {
let area = 0, file = ''
for (let rnd of RND1) {
rnd = parseInt(rnd, 16)
if (rnd < 90) {
if (file !== '') {
area = parseInt(area.toString(16))
kfile.push([area, file])
file = ''
}
area = rnd
} else {
file += String.fromCharCode(rnd) // 91=a, 92=b, ...
}
}
kfile.push([area, file]) // Seems last item needn't to convert
})()
console.log("kfile", JSON.stringify(kfile))

// Procedure 3: Find
// Find No.`mapNo` of `kfile[i][0] == areaID`, and its file name is `kfile[i][1]`
;(function () {
let i = 0
for (let _ of kfile) {
let area = _[0], file = _[1]
if (area === mapArea) {
i += 1
if (i === mapNo)
console.log(`${mapArea}-${mapNo}: ${file}`)
}
}
})()

舰船语音的文件名 (mp3)

Last Update: 2016-03-17

角川于 2016 冬活结束的维护(2016-02-24)中,对所有舰船语音的文件名进行了混淆,但仍保留旧的文件(1~51.mp3)。于 2016-02-29 的维护中,更改了混淆算法的 key 并删除了旧的文件,但没有修改混淆算法。

该混淆算法位于 Core.swf/common.util.SoundUtil/createFileName。该算法极为简单,且算法本身未有任何混淆。

整理后的混淆算法:

1
2
3
4
5
6
7
8
9
10
11
12
# vcKey is extracted from Core.swf/common.util.SoundUtil
const vcKey = [604825,607300,613847,615318,624009,631856,635451,637218,
640529,643036,652687,658008,662481,669598,675545,685034,
687703,696444,702593,703894,711191,714166,720579,728970,
738675,740918,743009,747240,750347,759846,764051,770064,
773457,779858,786843,790526,799973,803260,808441,816028,
825381,827516,832463,837868,843091,852548,858315,867580,
875771,879698,882759,885564,888837,896168]
# shipId: api_start2 . api_mst_ships [] . api_id
# voiceId: 1 ~ 51
convertFilename = (shipId, voiceId) =>
(shipId + 7) * 17 * (vcKey[voiceId] - vcKey[voiceId - 1]) % 99173 + 100000

战果排名的战果值混淆

Last Update: 2016-08-01

角川于 2016-7-15 的维护更新中,对战果排名中的玩家战果值(api_rate)进行了混淆。此次混淆对不同玩家采用了不同的混淆值,且算法本身亦经过一定规模的混淆。角川与 2016-8-1 的维护更新中,对战果 API URL 和其字段进行了混淆,同时小幅更新了战果值的混淆算法。

本文不涉及战果 API URL 和其字段的混淆分析,请参考其他信息

此混淆算法涉及到四个文件:核心部分为 Core.swf/core.apis._APIBaseS_RecoreMain.swf/scene.record.models.RankData/RankData,次要部分为 RecordMain.swf/scene.record.models.RankingData/setDataRecordMain.swf/connection.api_req_ranking.RankingListAPI/_handleLoadComplete

因为角川几乎在每次游戏维护都会修改混淆算法的 key,因此以下算法仅供了解原理,请前往 poi 我变强了 查看最新的混淆 key。

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
#!/usr/bin/env python3

import sys
from math import sqrt

# `Il` is extracted from 'core.apis._APIBaseS_/Il'
Il = [4999, 2257, 7351, 2039, 6604, 4132653, 1033183, 2570, 4979, 6314, 13,
5478, 3791, 10, 9640, 6707, 1000, 1875979]
# Maigc number is calculated from 'core.apis._APIBaseS_/I1'
MAGIC_13 = str(sqrt(13))

if sys.version_info < (3, 0):
# Python 2.x hard code
MAGIC_13 = '%.15f' % sqrt(13)

MAGIC = [-1] * 10
for i in range(10):
n = 0
while MAGIC_13[n] != str(i):
n += 1
MAGIC[i] = Il[n]

# `_l_` is refactored from 'core.apis._APIBaseS_/_l_'
# Get the first two digits of the magic number
def _l_(id):
return int(str(MAGIC[id % 10])[0:2])

# Magic R is extracted from 'scene.record.models.RankData/RankData'
MAGIC_R = [8831, 1201, 1175, 555, 4569, 4732, 3779, 4568,
5695, 4619, 4912, 5669, 6569]
# `real_rate` is refactored from 'scene.record.models.RankData/RankData'
# We assume that it always is even divisible.
def real_rate(login_member_id, rank_no, rank_rate):
return rank_rate / MAGIC_R[rank_no % 13] / _l_(login_member_id) - 73 - 18

ocserv + Let's Encrypt + 证书认证

本文需要基础 Linux 知识与网络知识。
请自行替换文内的所有全大写字段。

简要原理

以下包含口胡,欢迎指正。

TLS/SSL 证书认证

TLS/SSL 协议的允许连接双方都对端做身份认证。
对服务器端认证一般采用证书认证的,对客户端认证一般采用用户名+密码的认证。
由于每次都输入密码较为繁琐,因此对客户端亦采用证书认证的方式能更方便。

Let’s Encrypt 签发流程 (WebRoot)

  1. 客户端生成验证文件,存放到 WEBROOT/.well-known/acme-challenge/
  2. 客户端告知 Let’s Encrypt 服务器开始验证
  3. 服务器读取 http://DOMAIN/.well-known/acme-challenge/ 进行验证
  4. 客户端向服务器查询验证是否成功
  5. 若验证成功,向服务器获取证书

其中 1、3 为可能出现问题的地方,若获取证书失败,建议优先检查此部分。
如:WEBROOT 不可被 letsencrypt 客户端写入;Let’s Encrypt 服务器无法解析 DOMAIN 的 DNS;DOMAIN 对应的 IP 非 Let’s Encrypt 客户端写入验证文件的主机等诸多问题。

服务器证书

自建 CA 并签发服务器证书固然是可行方案,但需要在每台设备上都信任该自建 CA,较为麻烦且不安全。
因此我采用 Let’s Encrypt 来获取合法的服务器证书。

以下大部分命令需要 root 权限,但可以通过配置目录的读写权限绕过。
建议同时阅读 Let’s Encrypt User Guide

事前准备

  1. 将域名 DOMAIN 的 A/AAAA 记录指向当前主机。
  2. 配置 HTTP 服务器,使 WEBROOT/.well-known/acme-challenge/ 可被访问。

获取证书

测试时建议加上 --test-cert 以免用完 Let’s Encrypt 的证书获取速率限制。

1
2
yum install certbot
certbot certonly --webroot -w WEBROOT -d DOMAIN

自动更新证书

添加 cron 脚本至 /etc/cron.monthly/certbot,实现每月自动更新。

1
2
3
4
5
6
7
8
#!/bin/bash

WD="/root/certbot"
LOG="${WD}/cron.log"

mkdir -p $WD
date >> $LOG
certbot renew >> $LOG

用户证书

用户证书只需 ocserv 信任 CA 即可,因此使用自建 CA 签发证书。
将以下脚本保存到 /etc/ocserv/certs/,然后运行 ./ocm generate USERNAME 即可直接生成用户证书 USERNAME.p12

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
#!/bin/bash

init() {
WORK="./work"
CA_TMPL="${WORK}/ca.tmpl"
CA_KEY="${WORK}/ca-key.pem"
CA_CERT="./ca.pem"
USER="$1"
USER_TMPL="${WORK}/${USER}.tmpl"
USER_KEY="${WORK}/${USER}-key.pem"
USER_CERT="${WORK}/${USER}.pem"
USER_P12="./${USER}.p12"
REVOKED_CERT="${WORK}/revoked.pem"
CRL_TMPL="${WORK}/crl.tmpl"
CRL_CERT="./crl.pem"

# Ensure working directory
[[ -d $WORK ]] || mkdir -p $WORK

# CA Template
[[ -f $CA_TMPL ]] || cat << _EOF_ > $CA_TMPL
cn = "VPN CA"
serial = 1
expiration_days = 3650
ca
signing_key
cert_signing_key
crl_signing_key
_EOF_

# CA Private Key
[[ -f $CA_KEY ]] || certtool --generate-privkey --outfile $CA_KEY

# CA Certificate
[[ -f $CA_CERT ]] || certtool --generate-self-signed --load-privkey $CA_KEY --template $CA_TMPL --outfile $CA_CERT
}

generate() {
# User Template
cat << _EOF_ > $USER_TMPL
cn = "$USER"
expiration_days = 3650
signing_key
tls_www_client
_EOF_

# User Private Key
certtool --generate-privkey --outfile $USER_KEY

# User Certificate
certtool --generate-certificate --load-privkey $USER_KEY --load-ca-certificate $CA_CERT --load-ca-privkey $CA_KEY --template $USER_TMPL --outfile $USER_CERT

# Export User Certificate
certtool --to-p12 --pkcs-cipher 3des-pkcs12 --load-privkey $USER_KEY --load-certificate $USER_CERT --outfile $USER_P12 --outder
}

revoke() {
# Copy User Certificate to Revoked Certificate
cat $USER_CERT >> $REVOKED_CERT

# CRL Template
[[ -f $CRL_TMPL ]] || cat << _EOF_ > $CRL_TMPL
crl_next_update = 3650
crl_number = 1
_EOF_

# CRL Certificate
certtool --generate-crl --load-certificate $REVOKED_CERT --load-ca-privkey $CA_KEY --load-ca-certificate $CA_CERT --template $CRL_TMPL --outfile $CRL_CERT
}

case $1 in
generate)
init $2
generate
;;
revoke)
init $2
revoke
;;
*)
echo "\
Usage:
$0 generate USER
$0 revoke USER
"
esac

ocserv

编译安装 ocserv

注意安装依赖,阅读 README.md 即可。

1
2
3
4
5
6
7
8
VERSION='0.10.11'
cd /opt
wget ftp://ftp.infradead.org/pub/ocserv/ocserv-${VERSION}.tar.xz
tar xvf ocserv-${VERSION}.tar.xz
cd ocserv-${VERSION}
./configure
make
make install

配置 ocserv

复制配置文件:cp /opt/ocserv-${VERSION}/doc/sample.config /etc/ocserv/ocserv.conf
修改以下项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 打开 PMTUD
try-mtu-discovery = true

# 以 CN 为用户 ID。(用户证书认证)
cert-user-oid = 2.5.4.3

# 服务器证书与密钥(Let's Encrypt)
server-cert = /etc/letsencrypt/live/DOMAIN/fullchain.pem
server-key = /etc/letsencrypt/live/DOMAIN/privkey.pem

# 如有需要,可修改 VPN 端口
tcp-port = 443
udp-port = 443

# 修改 VPN 子网网段(避免和常用内网网段相同)
ipv4-network = 192.168.111/24

# 修改 DNS
dns = 8.8.8.8
dns = 8.8.4.4

# 注释掉所有的 route,让服务器成为 gateway
#route = 192.168.1.0/255.255.255.0

配置服务器

修改 /etc/sysctl.conf 中的 net.ipv4.ip_forward=1,然后刷新配置 sysctl -p /etc/sysctl.conf
修改 iptables,注意对 iptables 做持久化。

1
2
3
4
5
6
7
# 若强化了服务器安全,需要开放 443 端口。
iptables -A INPUT -p tcp -m state --state NEW --dport 443 -j ACCEPT
iptables -A INPUT -p udp -m state --state NEW --dport 443 -j ACCEPT
iptables -D FORWARD -j DROP

# 打开 NAT
iptables -t nat -A POSTROUTING -j MASQUERADE

测试 ocserv

修改 ocserv.conf 中的 auth = "plain[passwd=/etc/ocserv/passwd]"
创建用户 ocpasswd -c /etc/ocserv/passwd your-username
运行 ocserv ocserv -f -d 1,在手机上尝试连接。

配置证书认证

修改 /etc/ocserv/ocserv.conf 中的以下项:

1
2
auth = "certificate"
ca-cert = /etc/ocserv/certs/ca-cert.pem

重新运行、测试连接。

附录

以 service 运行 ocserv (Ubuntu)

执行以下指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ln -s /lib/init/upstart-job /etc/init.d/ocserv

cat << _EOF_ > /etc/init/ocserv.conf
#!upstart
description "OpenConnect Server"

start on runlevel [2345]
stop on runlevel [06]

respawn
respawn limit 20 5

script
exec start-stop-daemon --start --pidfile /var/run/ocserv.pid --exec /usr/local/sbin/ocserv -- -f >> /dev/null 2>&1
end script
_EOF_

启动服务:service ocserv start
停止服务:service ocserv stop

方便的安装用户证书(iOS)

  1. 将生成的用户证书 USER.p12 复制到 WEBROOT
  2. 打开 AnyConnect 客户端,切换到 Diagnostics 页。
  3. 点击 Certificates 项,点击 Import User Certiticate…
  4. 输入 http://DOMAIN/USER.p12,然后输入密码。

参考资料

历史记录

20160215:Init
20161018:使用 certbot(CentOS)