# Subleq64 - zer0pts CTF 2021 ###### tags: `zer0pts CTF 2021` `misc` ## 概要 なんかOISCのesolangコードが渡される。 実行するとだんだん出力が遅くなり、36hで終わるか不明。 ``` $ ./a.out test.sq GO zer0pts{OISC_1s_ ``` ## 解析 出力時に対象となるメモリアドレスを調べると、6170であると分かる。 まずはここに書き込みしているやつを調べる。 書き込みはBレジスタを使うので、その参照を見る。 ``` print(pc, a, b, hex(memory[a]), hex(memory[b])) ``` こんな感じ ``` 609 6170 6170 0x0 0x0 627 6168 6170 -0x17d9 0x0 654 6170 6170 0x17d9 0x17d9 660 6174 6170 -0x22 0x0 675 6174 6170 0x0 0x22 747 6170 6170 0x22 0x22 765 6169 6170 0x0 0x0 1757 6170 6170 0x0 0x0 1775 6168 6170 -0x5851f42d4c957f2d 0x0 2006 6170 6170 0x5851f42d4c957f2d 0x5851f42d4c957f2d ... ... ... ``` 0x5851f42d4c957f2dってのはコード先頭の方にある6364136223846793005から来ているはず。 見ると最後に6168から6170にデータが移ってる。 ``` 3240 6168 6170 -0x1454568c2ff9bb7a 0x0 ``` ので今度は6168を見る。同様に6189, 6174, 6169と追う。ここで始めてmov以外の操作がある。 ``` 1085 6174 6169 -0x22 0x0 1091 6171 6169 -0x1454568c2ff9bb58 0x22 ``` まずは0x22の出処を追う。 最初にこの値が現れる箇所を調べる。 ``` if memory[b] == 0x22 or memory[b] == -0x22: print(pc, a, b, memory[b], memory[a]) ``` すると6105からやってきたと分かる。固定データかを調べるためこの周辺を見る。 ``` print(memory[6105:6105+10]) >> [34, 70, 194, 23, 42, 149, 207, 118, 189, 238] ``` これは明らかにメモリアドレスっぽくないので、たぶんこれの配列$\delta_i$が次のように使われているんだと想像が付く。 0以上の整数$x$を受け取る関数$F(x)$が存在し、 $$\mathit{flag}_i = F(i) - \delta_i$$ によりフラグが作られるんじゃないかな。 次に-0x1454568c2ff9bb58の出処を追う。なんとなく上に辿ると ``` 237 6 6169 0x14057b7ef767814f 0x2859d20b27613ca7 ``` ってのがあり、このsubで-0x1454568c2ff9bb58が作られている。 0x14057b7ef767814fは6番目にある固定データ。 ところでこの固定データでぐぐると、こいつがヒットする。 https://en.wikipedia.org/wiki/Linear_congruential_generator もう一個の1442695040888963407からしてMMIXってのがそれっぽい。そして-802458212630453242はシードと思われる。 試しに回す。 ``` #include <stdio.h> int main() { long a = 6364136223846793005; long b = 1442695040888963407; long x = -802458212630453242; x = a * x + b; printf("%ld\n", x); x = a * x + b; printf("%ld\n", x); x = a * x + b; printf("%ld\n", x); return 0; } ``` ``` $ ./a.out -7904187076687966115 -1464890938902559576 -5755922903835546921 ``` ``` hex(-1464890938902559576) '-0x1454568c2ff9bb58' ``` 2つ目のやつはどこかで見覚えがある。 ということで、単純な乱数と差分の配列を使っているらしい。あとは何回乱数を回しているか。だんだん遅くなる挙動からして、乱数の生成回数が徐々に増えていってそう。 LCG自体は一般項の公式が求まるので、生成回数さえ分かればO(1)で計算が終わる。 最初の方の`zer0pts{OISC_1s`あたりまでは知っているので、それを元に生成回数を逆算する。 ``` #include <stdio.h> int delta[] = {34, 70, 194, 23, 42, 149, 207, 118, 189, 238, 163, 162, 97, 218, 119, 98, 74, 85, 43, 70, 80, 230, 160, 128, 176, 104, 30, 159, 74, 155, 201, 140, 10, 173, 175, 42, 219, 49, 244, 125, 63, 57, 83, 175, 211, 55, 200, 153, 185, 94, 239, 55, 190, 255, 18, 129, 9, 172, 186, 12}; int main() { const char *known = "zer0pts{OISC_1s"; for (int i = 0; i < 14; i++) { long a = 6364136223846793005; long b = 1442695040888963407; long x = -802458212630453242; for (int j = 0; j < 0x10000; j++) { x = a * x + b; if (((delta[i] - x) & 0xff) == known[i]) { printf("%c: %i\n", known[i], j); break; } } } return 0; } ``` ふーん ``` z: 1 e: 4 r: 9 0: 18 p: 35 t: 68 s: 133 {: 6 O: 7 I: 8 S: 9 C: 10 _: 11 1: 12 ``` 最初のは1, 2, 4, 8, 16, ...とも取れる。そのあとなぜ6,7,8になってるかはよく分からん。 ``` #include <stdio.h> int delta[] = {34, 70, 194, 23, 42, 149, 207, 118, 189, 238, 163, 162, 97, 218, 119, 98, 74, 85, 43, 70, 80, 230, 160, 128, 176, 104, 30, 159, 74, 155, 201, 140, 10, 173, 175, 42, 219, 49, 244, 125, 63, 57, 83, 175, 211, 55, 200, 153, 185, 94, 239, 55, 190, 255, 18, 129, 9, 172, 186, 12}; int main() { const char *known = "zer0pts{OISC_1s"; int lp[] = {1, 4, 9, 18, 35, 68, 133, 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}; for (int i = 0; i < sizeof(lp)/4; i++) { long a = 6364136223846793005; long b = 1442695040888963407; long x = -802458212630453242; for (int j = 0; j < lp[i] + 1; j++) { x = a * x + b; } printf("%c", (delta[i] - x) & 0xff); } putchar('\n'); return 0; } ``` おわり ``` zer0pts{OISC_1s_th3_futur3~0xbuyd0g3!0xb1tc01n$vCQW04nFV+Wz} ```