はまやんはまやんはまやん

hamayanhamayan's blog

2-variable Function [AtCoder Beginner Contest 246 D]

https://atcoder.jp/contests/abc246/tasks/abc246_d

前提知識

解説

https://atcoder.jp/contests/abc246/submissions/30680524

なかなか難しいというか、競プロ的なアプローチを含んだ問題。
二分探索が分かっていない場合はそちらを先に学習してきてほしい。

何から始めるのか

サンプルを自力でうまく解くような方法が無いかを探してみる。
入力1に対して答えを見つけようとした場合、Xの候補である9, 10, 11, ...が条件を満たすかどうかを
判定して、満たす最小のものを答える方法が考えられる。
しかし、とあるXに対して、そのXが条件を満たすかどうかを判定するのはやや難しく、
別のアプローチが無いかを考えてみることにする。

候補が全列挙できるのでは?

ここから競技プログラミングで良く出てくる候補の全列挙をベースに考えてみる。
とあるXが条件を満たすかを判定するのは難しいので、生成の元となっているa,bの列挙から攻めてみる。
3乗が含まれるので、a,bはそれぞれ106が上限であることが分かる。
aは非負整数、かつ、少なくとも a3 ≦ 1018 なので、 a ≦ 106 である必要があるということ。

これでa,bの全列挙が出来れば、条件を満たすXをすべて生成できるので、ここからX以上の最小値を求めればいい。
だが、このままやるとTLE

片方を効率化する

aは[0, 106]の範囲で全探索するとして、bをもっと賢く探索できないかを考えてみる。
条件となっている多項式の計算をf(a,b)と置いて考えてみる。
とあるaについて、
f(a,0), f(a,1), f(a, 2), ...
というのを考えてみると、とある部分を境目としてN以上になっているはずである。
この「とある部分を境目として」という部分から二分探索が使えそうと思い立てば、もう解法は目の前である。

二分探索

bは[0,106]のどこかに最適解が存在するが、N≦f(a,b)を満たすかというのを条件にして境目を見つけよう。
境目が見つかれば、その境界線上のbの大きいほうの値が最適なbになっているはずなのでf(a,b)について
答えの候補として更新していこう。

ll N;

ll f(ll a, ll b) {
    return a * a * a + a * a * b + a * b * b + b * b * b;
}

void _main() {
    cin >> N;

    ll ans = infl;
    rep(a, 0, 1010101) {
        ll ng = -1, ok = 1010101;
        while (ng + 1 != ok) {
            ll md = (ng + ok) / 2;
            if (N <= f(a, md)) ok = md;
            else ng = md;
        }
        chmin(ans, f(a, ok));
    }
    cout << ans << endl;
}

LINE CTF 2022 Web 解説まとめ

CTFtime.org / LINE CTF 2022

自分の復習用に集めた情報をまとめておく。

gotm

SSTI可能。idに{{.}}を与えてやるとJWTの秘密鍵が漏洩するので、それを使ってトークン改ざんしてflagを獲得する。

Memo Drive

バリデーションをbypassしながらLFIして、./flagを読む問題。

bb

環境変数を使ってRCEする方法がこの世に存在するようだ。これを使う。

https://movaxbx.ru/2022/02/22/how-do-i-use-environment-variables-to-inject-and-execute-arbitrary-commands/
(ruドメインの締め出しか知らないが、アクセスできない。VirusTotalとかでIPアドレスを持ってきてhostsに追加する感じで手元で名前解決してやれば見れる。)

online library

app.get("/:t/:s/:e", (req: Express.Request, res: Express.Response): void => {の部分で自由にLFIできるので、これと/proc/self/memを使ってメモリが見れる。
express-sessionではセッション情報がメモリに載るので、usernameにXSSコードを入れてsubmitするとメモリにXSSコードが載るので、それをLFIで取ってきて踏ませる。
すごい方法だけれど、わざわざusernameを報告前に送っているし、領域を指定して取得できる仕様もあるので想定解法なのかもしれない。
ログにpayload乗っけてLFIしてXSS発動と同じ雰囲気。

Discordのwebチャンネルではメモリ経由以外の解法もあると発言している人がいた。この人曰く、この解法ならxssの必要もないとのこと。

Haribote Secure Note

このセクション間違ってるかも。注意。

CSPをbypassして、XSSする問題。以上のように三者三葉

title todo

賢すぎるな。
画像アップロード機能があってキャッシュ機能もあるのだが、キャッシュヒットした場合は特殊なヘッダーが突くことを利用して、初回アクセスか2回目以降アクセスかが分かる。
これをオラクルとして機能させるために、scroll-to-textと画像のloading=lazyをうまく使う。
後はこのオラクルを使ってXSLeakする。

(そういえば去年もXSLeak問ありましたね)

me7-ball

理解できてない。
使ってるライブラリにsecret無しでデータを読みだす方法があって、それを利用すると解けるということだと思う。
多分。

Choose Elements [AtCoder Beginner Contest 245 C]

https://atcoder.jp/contests/abc245/tasks/abc245_c

解説

https://atcoder.jp/contests/abc245/submissions/30483611

やや丁寧めに説明ぞ書いていく。

何から始めていけばいい?

問題への取り組み方として、全列挙してみたり、実験してみたり、色々やってみることが大切である。
条件を満たす数列が存在するかの判定方法の一つとして、数列を列挙してみる方法がある。
以下のサンプルでやってみよう。

5 4  
9 8 7 7 2  
1 6 8 9 5  

先頭から数列の数を決めていくとすると、AかBかから数を選択すればいいので、最初は

9  
1  

の状態がありえる。ここから2番目の数を確定させるには、このありうる状態から8か6をくっつけていけばいい。
だが、前の数から4より大きくは離れることができないので、

98  
96  

が状態として残る。次は78をくっつける。

987  
988  
967  
968  

次は79をくっつけるのだが、このままくっつけると更に倍になって管理が大変である。
倍倍になっていくのは嫌なので、ここで一旦手を止めて考えてみる。
今回は数列が作れるかを判定すればいいので、判定に利用されない最後の数以外はなんでもいい。
よって、「最後に何の数がある候補が残っているか」を管理することで、状態を「圧縮」することができる。

??7  
??8  

このように最後がなんであるかという情報さえあれば数列が存在するかの全列挙が続行できることが分かった。
これならば状態は2以上に増えることはないし、手計算でも何とかなりそうである。

状態の圧縮

このように先頭からシミュレーションのように列挙をしていくのだが、ここまで作ることのできる数列について
「末尾が配列Aのものがあるか」「末尾が配列Bのものがあるか」さえ分かっていれば、そこから数列を伸ばすことができる。
自分の実装では

cur[ab] := ここまでのシミュレーションで数列ab (ab=0ならA, ab=1ならB)が末尾となる数列があるか

を持ちながら

nxt[ab] := 1手後のシミュレーションで数列ab (ab=0ならA, ab=1ならB)が末尾となる数列があるか

を作成している。
このように現在状態を圧縮しながらシミュレーションすることで、最終的に条件を満たす数列が存在するかを判定できる。
配列A,Bを合体して2次元配列としているので実装を読み解くのが難しいかもしれないが、頑張ってほしい。

その先へ

このような状態の圧縮は競技プログラミングでは頻出の考え方である。
とても広く適用できるので、意識していこう。
特に動的計画法では必須な考え方である。
今回のような問題でコツをつかんで、動的計画法に取り組んでもいいかもしれない。
(本質的には変わらないですが、この問題はDPでも解けます)

int N, K, AB[2][201010];
void _main() {
    cin >> N >> K;
    rep(ab, 0, 2) rep(i, 0, N) cin >> AB[ab][i];

    bool cur[2] = { true, true };
    bool nxt[2];
    rep(i, 1, N) {
        nxt[0] = nxt[1] = false;
        rep(from, 0, 2) if (cur[from]) rep(to, 0, 2) {
            if (abs(AB[from][i - 1] - AB[to][i]) <= K) nxt[to] = true;
        }
        cur[0] = nxt[0];
        cur[1] = nxt[1];
    }

    if (cur[0] || cur[1]) cout << "Yes" << endl;
    else cout << "No" << endl;
}

Construct Good Path [AtCoder Beginner Contest 244 G]

https://atcoder.jp/contests/abc244/tasks/abc244_g

解説

https://atcoder.jp/contests/abc244/submissions/30323833

何から始めればいいか困る問題。
構築問題で重要なのはなるべくシンプルなルールで構築を考えていくことである。
今回は特に最適なパス長である必要はないので、若干非効率な方法でも許される。
効率よりも分かりやすさというかシンプルな方法を模索しよう。

どんな条件でも作れる

今回の問題設定では、条件を満たすどんなグラフとSの組であっても良いパスを作ることができるらしい。
ということは与えられたグラフから辺をいくつか削除して、木にしたとしても答えが得られることになる。
この工夫は試行錯誤の結果ではあるのだが、グラフを木にすることでちょっとだけ問題をシンプルにしておこう。

根付き木として考えてみる

スタート地点を1つ決めてそこを根として木を見てみよう。
移動していくと、葉まで行って偶奇が異なる場合はその親に一回戻って葉に再度移れば、偶奇を変更することができる。
これは葉に限らず、他の頂点でも同様である。
つまり、何が言いたいかというと

「現在いる頂点の偶奇を変えたい場合は、親に移動してから自分に戻ってくれば、
 『自分の子孫の偶奇を変更することなく』自分の偶奇を変更可能」

ということである。これは非常に都合がいい。
よって、木をdfsで探索しながら子供の偶奇を全部一致させた後、自分の偶奇を以上のテクで一致させれば、
自分を含めた部分木の偶奇を全部一致させた上で親に戻ることができる。

ここら辺の認識が一番難しい。
実装に落とし込むと割と完結になっているので、実装から思想を吸い上げてもいいかもしれない。

つまり、実装は?

適当に頂点0を始点としてdfsを行う。
訪れた頂点をメモして再度訪れないようにしておけば、無向グラフであっても木上でdfsをしているようなコードが書ける。
各頂点でやることとしては、

  1. 子頂点についてdfsを再度行って、子頂点について偶奇が一致するようにする
  2. 子頂点について偶奇が一致したら、親を使って自分の偶奇を一致させる

を繰り返す。
注意点として頂点0は親を持っていないので偶奇が調整できない。
これは、数列として0→1→2→1→0みたいな数列を作っていると思うが、
最初の0を取り除けばこれまでの偶奇は変わらずに頂点0の偶奇だけを変更できる。

4Nにおさまってる?

正直、未証明ACだが、調整は各頂点で1回ずつしか発生しないし、普通に回るのに2Nかかって調整で2Nかかってで
間に合いそうな雰囲気はある。
(コンテスト中に必ず通すという思想では、始点を0ではなくランダムに設定すると嫌味なケースを回避できるかも)

自分の実装では行ってみて偶奇が合わないなら調整する形をとっているが、そもそも行かなくていいという
ケースを処理すればもっと改善できそう。

int N, M;
vector<int> E[101010];
string S;
//---------------------------------------------------------------------------------------------------
deque<int> ans;
int parity[101010];
void add(int cu) {
    parity[cu] ^= 1;
    ans.push_back(cu);
}
//---------------------------------------------------------------------------------------------------
bool vis[101010];
void dfs(int cu, int pa = -1) {
    vis[cu] = true;

    add(cu);
    fore(to, E[cu]) if(!vis[to]) {
        dfs(to, cu);
        add(cu);
    }

    if (0 <= pa && parity[cu] != S[cu]) {
        add(pa);
        add(cu);
    }
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, M) {
        int u, v;
        cin >> u >> v;
        u--; v--;
        E[u].push_back(v);
        E[v].push_back(u);
    }
    cin >> S;

    fore(c, S) c -= '0';

    dfs(0);
    if (parity[0] != S[0]) ans.pop_front();

    int K = ans.size();
    printf("%d\n", K);
    while (!ans.empty()) {
        if (ans.size() != K) printf(" ");
        printf("%d", ans.front() + 1);
        ans.pop_front();
    }
    printf("\n");
}

Shortest Good Path [AtCoder Beginner Contest 244 F]

https://atcoder.jp/contests/abc244/tasks/abc244_f

前提知識

解説

https://atcoder.jp/contests/abc244/submissions/30322530

今回の問題はbitmaskを状態として利用したbfsで解くことができる。
状態の持ち方としてbitmaskを利用するという方法はbit全列挙やbitDPでも見られる。
bitmaskを利用する方法については説明しないので、少なくともbit全列挙は理解した上で読んでほしい。

何から始めるか

解法を見れば、なるほどと思う人も多いかもしれない。
複雑な問題設定なので、すこし面食らってしまった人もいるかもしれない。

この問題の特徴としてはN≦17である。
Sとして考えられる文字列は全列挙できるので、それぞれに最短の長さを求めて総和をそのまま取ればよさそう。
逆に言えば、Sとして考えられる文字列のそれぞれについて最短距離を求める必要がある。
なので、以下を定義して考察を始めてみよう。

opt[S] := 文字列Sに関する良いパスのうち最短のものの長さ

最短?

パスが伸びていく過程を考えていくと、パスの長さというのは1つずつしか増えない。
+1ずつであり最短距離と言えばbfsである。
bfsで最短距離を計算していく上で、opt[S] -> opt[next_S]を計算したいという前提に立ってみると、
パスの終端は状態に必要になってくる。
よって、保持する状態を変更しておこう。

opt[S][lst] := 文字列Sに関する良いパス、かつ、最後がlstのパスのうち最短のものの長さ

これでopt[S][lst]からの遷移はopt[next_S][to]と遷移が行えるようになった。
これでbfsをしていけば求めたい最短長が求められる。

msk=0の時がちょっと厄介なので、最初の遷移は別途やってしまうことにして、後はbfsをするだけ。
計算量は分かりにくいがO(2N(N+M))のはず。

int N, M;
vector<int> E[17];
int opt[1 << 17][17];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, M) {
        int u, v;
        cin >> u >> v;
        u--; v--;
        E[u].push_back(v);
        E[v].push_back(u);
    }

    queue<pair<int, int>> que;
    rep(i, 0, N) {
        opt[1 << i][i] = 1;
        que.push({ 1 << i, i });
    }

    while (!que.empty()) {
        int msk, lst;
        tie(msk, lst) = que.front();
        que.pop();

        fore(to, E[lst]) {
            int msk2 = msk ^ (1 << to);
            if (opt[msk2][to] == 0) {
                opt[msk2][to] = opt[msk][lst] + 1;
                que.push({ msk2, to });
            }
        }
    }

    int ans = 0;
    rep(msk, 1, 1 << N) {
        int mi = inf;
        rep(lst, 0, N) chmin(mi, opt[msk][lst]);
        ans += mi;
    }
    cout << ans << endl;
}

King Bombee [AtCoder Beginner Contest 244 E]

https://atcoder.jp/contests/abc244/tasks/abc244_e

前提知識

解説

https://atcoder.jp/contests/abc244/submissions/30321733

まず前提としてDPが分かっていないとこの問題を解くことは難しい。
DPが未履修であれば、先に学習してこよう。
DPの入門問題としては向かない。

DP?

mod数え上げは意外と選択肢が無いし、数列の数え上げなのでDP感がすごい。
DPに乗せられるように情報を圧縮していくことを考えよう。

最初は
dp[i] := 数列A[i]まで確定している数列の組み合わせ
からスタート。

数列のA[i]まで確定しているときに次のA[i+1]を決めるには最後の要素が何かだけ分かっていればいい。
なので、状態として最後の要素を含める必要がある。
dp[i][lst] := 数列A[i]まで確定していて、最後の要素がlstである数列の組み合わせ

だが、これでは整数Xの出現回数が分からないので答えを導くことができない。
なので、整数Xの出現回数を含めるのだが、
dp[i][lst][cnt] := 数列A[i]まで確定していて、最後の要素がlstであり、Xがcnt回出現した数列の組み合わせ
としてしまうと状態数がO(KN2)となりちょっとメモリに載らない。
載らないし、計算量もアレなので、問題に即して偶数回出現しているかを保持することにしよう。

dp[i][lst][mod2] :=
数列A[i]まで確定していて、
最後の要素がlstであり、
Xが出現した回数を2で割ったあまりがmod2である
数列の組み合わせ

これがDPの定義になる。
あとはこれを実装する。

実装の注意点

状態のループで既にO(NK)なのであまり無茶はできない。
普通の更新の感じで
dp[i][lst][mod2]からdp[i+1][to][next_mod2]みたいな感じにtoを全探索するとO(N2 K)となって間に合わない。
lst -> toへは辺にあるものだけ実施すればいいのでtoを全探索するのではなく、lstから辺があるtoを全探索しよう。
すると、計算量はO(K(N+M))となって間に合う。

補足:O(K(N+M))??

N+Mの部分が良く分からないと思うのだが、ならし計算量が混ざっている。
例えば rep(lst, 0, N) rep(to, 0, N)とするとこれはO(N2)である。
だが、rep(lst, 0, N) foreach(to, E[lst])とtoをlstから遷移可能なものにforeachで列挙するとO(N+M)となる。
O(N+M)ということはO(N)とO(M)で大きいほうが最終的な(支配的な?専門的な言い方が分からない)計算量となる。

rep(lst, 0, N) foreach(to, E[lst]) { // ここの計算 }のコメント部分が何回評価されるだろうかというのを
考えると、辺があると評価されるので2M回評価されることになる。
「ならし」で2M回評価されるので、処理全体で見るとO(M)ではあるのだが、foreachの評価回数を見るとN回なので、
ループ的にはO(N)で、ネスト内部の評価回数はO(M)となるので、O(N+M)

int N, M, K, S, T, X;
vector<int> E[2010];
mint dp[2010][2010][2];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M >> K >> S >> T >> X;
    S--; T--; X--;

    rep(i, 0, M) {
        int u, v;
        cin >> u >> v;
        u--; v--;
        E[u].push_back(v);
        E[v].push_back(u);
    }

    dp[0][S][0] = 1;
    rep(i, 0, K) rep(lst, 0, N) rep(mod2, 0, 2) {
        fore(to, E[lst]) {
            dp[i + 1][to][mod2 ^ (to == X)] += dp[i][lst][mod2];
        }
    }

    cout << dp[K][T][0] << endl;
}

UMDCTF 2022 Forensicまとめ

CTFtime.org / UMDCTF 2022
久々に出たので、備忘録レベルで残しておく。

Renzik's Case

USBの論理イメージが与えられるので、削除された重要なデータを探す問題。
自分はAutopsyを利用して論理イメージを読み込んで、削除されたデータを探索した。

Xorua

ちょっとした思考パズルを要求される。
Before.png, After.pngというファイルが与えられるのでバイナリエディタで開いて観察してみよう。
以下のことより、Before.pngに対してフラグファイルをxorするとAfter.pngになると予想した。

  • 同じファイルサイズである
  • After.pngの先頭が不自然に0x00になっている
  • 問題名がXorua

「Before.png xor flag = After.png」であるが、XORの性質を利用すると「flag = Before.png xor After.png」であるので、xor結合するpythonコードを適当に書いて実行。

import struct

b1 = open('Before.png', 'rb').read()
b2 = open('After.png', 'rb').read()

b3 = b''
for i in range(len(b1)):
    b3 += struct.pack("B", b1[i] ^ b2[i])

f = open('flag.png', 'wb')
f.write(b3)

運よく予想が当たり、フラグが得られる。

How to Breakdance

USB通信が入ったpcapファイルが与えられる。
通信からキータイプを抜き出す問題だろう。以下のページを参考に抜き出す。
Decoding Mixed Case USB Keystrokes from PCAP

wiresharkのクエリ的には((usb.transfer_type == 0x01) && (frame.len == 72)) && !(usb.capdata == 00:00:00:00:00:00:00:00)で検索して、Leftover Capture Dataを持ってきて、上のページにあるスクリプトに食わせてやればいい。

Blue

ステガノグラフィー解析の問題。画像に情報を埋め込むためのコードと、埋め込まれた後の画像が与えられる。
画像の高さは775であり、こんなに文字長が大きくないだろうので、埋め込まれた後の画像の後半は元々の色になっているはずである。
なので、画像の一番下のピクセルからもともとの色、RGB値を読み込んで、解析に利用する。
埋め込みコードでは、ランダムなx座標とRGBのいずれかに+1をしながら文字を埋め込んでいるので、基準のRGB値からの差をすべてのx座標から計算して総和を取ることで復元する。
これより、以下のコードで復元可能。

from PIL import Image

orig_image = Image.open('bluer.png')
pixels = orig_image.load()
width, height = orig_image.size
orig_pixel = list(orig_image.getpixel((0, height - 1)))

ans = ''
for y in range(height):
    cnt = 0
    for x in range(height):
        pixel = list(orig_image.getpixel((x, y)))
        for c in range(3):
            cnt += pixel[c] - orig_pixel[c]
    if cnt == 0:
        break
    ans += chr(cnt)

print(ans)

Magic Plagueis the Wise

見ると先頭1ビットだけ違っていて、他は全部一緒のファイル群。
先頭だけ抜き出して眺めてみると、Asciiっぽいので、文字起こしするとフラグが書いてあった。

import glob

files = glob.glob("./magic_plagueis_the_wise/*")
l = len(files)
tops = list(range(l))
for file in files:
    name = int(file.split('\\')[-1])
    print(name)
    with open(file, 'rb') as f:
        tops[name - 1] = f.read()[0]

ans = ''
for c in tops:
    ans += chr(c)
print(ans)

jdata 解けなかった

zipファイルが与えられる。解凍するとpngファイルがあり、フラグの末尾が書いてあるが、前半部分が見つからない。
んー、分からない。
0x52C50付近に怪しいものがあったが、繋がらないな…

UMDCTF(writeup) - HHJ :: Import HHJ

解説を見ると、zipはelfファイルらしい。
解凍時にエラー出てたけど無視してたな…いかん。
elfファイルをデコンパイルして中身を見てみるとフラグの前半が書いてある。

Class Project 解けなかった

起動しなくなったVMイメージが与えられるのでトラブルシューティングしよう。
適当にlinuxイメージをUSBで接続して中身を見ていくと、/home/aman_esc/Documents/fork_bomb.bashというのがある。
見慣れぬコマンドだったが、調べてみるとFork爆弾というもので、DoSの一種らしい。
linux - What this command :(){ :|: & };: does? - Super User
これが原因で起動しないのか。削除すると起動するようになる。

んー、その後が分からんな…

CTF-Writeups/UMDCTF_2022/Class Project at main · K1nd4SUS/CTF-Writeups · GitHub

/home/aman_esc/Documents/admin_notesbase64デコードされたフラグが書いてあったららしい。
このファイル見つけたのにデコードしなかったな。なんでかな。

Kernel Infernal 1

カーネルのクラッシュログが与えられる。
第674回 カーネルのクラッシュ情報を解析する:Ubuntu Weekly Recipe|gihyo.jp … 技術評論社
この辺を参考にしながら、crashコマンドで中身を見てみる。

適当にいろんなサイトを巡回しながらやってると、filesで怪しいものが出てくる。

crash> files
PID: 5206   TASK: ffff9b1878e45d00  CPU: 1   COMMAND: "bash"
ROOT: /    CWD: /home/ubuntu/XXXXXXXXXXXXXXXXXXXXXX1SCEhfQ==
 FD       FILE            DENTRY           INODE       TYPE PATH
  0 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1
  1 ffff9b1878f99a00 ffff9b187c093540 ffff9b187c1cfad8 REG  /proc/sysrq-trigger
  2 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1
 10 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1
255 ffff9b1878f87500 ffff9b1852988180 ffff9b18529a2720 CHR  /dev/pts/1

CWDに書いてあるパスに含まれる文字列をbase64デコードするとフラグが得られる。