64 ビット PE 無殻。main 関数を開く。
問題の出所:xctf
まず、入力の長さが 31 ビットであるかどうかを判断します。そうでない場合はフリーズします。
scanf("%s",input);
lVar5 = -1;
do {
str_len = lVar5 + 1;
lVar4 = lVar5 + 1;
lVar5 = str_len;
} while (input[lVar4] != '\0');
if (str_len != 31) {
do {
Sleep(1000);
} while( true );
}
他の師匠の wp に基づいて、以下は最後の文字から比較を始めて分析します。
while( true ) {
if ("1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;\'ASDFGHJKL:\"ZXCVBNM<>?zxcvbnm, ./"
[(int)(&out_DAT_1400056c0)[lVar5] % 0x17] != (&DAT_140003478)[lVar5]) {
/* WARNING: Subroutine does not return */
_exit(_Code);
}
if ("1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;\'ASDFGHJKL:\"ZXCVBNM<>?zxcvbnm, ./"
[(int)(&out_DAT_1400056c0)[lVar5] / 0x17] !=
"55565653255552225565565555243466334653663544426565555525555222"[lVar5]) break;
_Code = _Code + 1;
lVar5 = lVar5 + 1;
if (0x3d < _Code) {
printf("flag{MD5(your input)}\n");
__security_check_cookie(uVar3 ^ (ulonglong)auStack_58);
return extraout_EAX_00;
}
}
DAT_140003478
の値をコピーしてスクリプトを書く。
_Code = 0
lVar5 = 0
out_DAT_1400056c0 = [" "] * 0x3e
DAT_140003478 = [ 0x28, 0x5f, 0x40, 0x34, 0x36, 0x32, 0x30, 0x21, 0x30, 0x38, 0x21, 0x36, 0x5f, 0x30, 0x2a, 0x30, 0x34, 0x34, 0x32, 0x21, 0x40, 0x31, 0x38, 0x36, 0x25, 0x25, 0x30, 0x40, 0x33, 0x3d, 0x36, 0x36, 0x21, 0x21, 0x39, 0x37, 0x34, 0x2a, 0x33, 0x32, 0x33, 0x34, 0x3d, 0x26, 0x30, 0x5e, 0x33, 0x26, 0x31, 0x40, 0x3d, 0x26, 0x30, 0x39, 0x30, 0x38, 0x21, 0x36, 0x5f, 0x30, 0x2a, 0x26 ]
while True:
ans = 0
print(DAT_140003478[lVar5])
for i in range(128):
print(i, end=" ")
if ord("""1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;\'ASDFGHJKL:\"ZXCVBNM<>?zxcvbnm,. /"""[i % 0x17]) != DAT_140003478[lVar5]:
continue
if ord("""1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;\'ASDFGHJKL:\"ZXCVBNM<>?zxcvbnm,. /"""[i // 0x17]) != ord("55565653255552225565565555243466334653663544426565555525555222"[lVar5]):
continue
ans = i
break
out_DAT_1400056c0[_Code] = ans
_Code = _Code + 1;
lVar5 = lVar5 + 1;
print()
if (0x3d < _Code):
print("".join(map(chr,out_DAT_1400056c0)))
break
得られたのはprivate: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)
さらに前に進む:
UnDecorateSymbolName(symbol_name,&out_DAT_1400056c0,0x100,0);
lVar5 = -1;
do {
lVar4 = lVar5 + 1;
pcVar1 = &DAT_1400056c1 + lVar5;
lVar5 = lVar4;
} while (*pcVar1 != '\0');
if (lVar4 != 62) {
this = FUN_1400018a0((longlong *)cout_exref);
std::basic_ostream<char,struct_std::char_traits<char>_>::operator<<
((basic_ostream<char,struct_std::char_traits<char>_> *)this,FUN_140001a60);
__security_check_cookie(uVar3 ^ (ulonglong)auStack_58);
return extraout_EAX;
}
UnDecorateSymbolName については、コンパイラがシンボルを人間が理解できない形式に変換することはよく知られています(以前、mangle という言葉を見たことがあります)(ecosia で mangled という言葉を検索しないでください、血なまぐさい画像が出てきます、うぇうぇうぇ)(ああ、これは gcc と clang のものです、見てくださいhttp://web.mit.edu/tibbetts/Public/inside-c/www/mangling.html)
話が逸れましたが、vs のルールはhttps://zh.wikipedia.org/wiki/Visual_C%2B%2B 名前修飾で確認できます。しかし、より便利な方法はこの関数を直接書き出し、__FUNCDNAME__
マクロを使用して出力することです。
#include <cstdio>
class R0Pxx {
public:
R0Pxx() { My_Aut0_PWN(nullptr); }
private:
char* __thiscall My_Aut0_PWN(unsigned char* a) {
printf(__FUNCDNAME__);
return nullptr;
}
};
int main() {
auto a = R0Pxx();
}
得られたのは?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z
もう一つ言いたいのは、0 と O を区別できるフォントを必ず使用してください。cmd のフォントはドットフォントに設定できます(なぜ vs の出力ウィンドウのフォントは選べるものが少ないのか不明ですが、他のものに設定するとフォントが見つからず、宋体に戻ります)。
最後に残ったのはこの部分です:
iVar2 = FUN_140001280(input);
root = (Node *)CONCAT44(extraout_var,iVar2);
symbol_name = &sym_name_DAT_1400057c0;
if (root != (Node *)0x0) {
dfs(root->left);
dfs(root->right);
symbol_name[i_DAT_1400057e0] = root->v;
i_DAT_1400057e0 = i_DAT_1400057e0 + 1;
}
省略:動的デバッグにより、ある位置での置換が発見され、その方法で戻すことができます。(ただし、この二叉木を分析しました)
Node*
型は私たちが定義した構造体です(なぜ「たち」かというと、ghidra の自動生成が一部手伝ってくれたからです)。
オフセット | 長さ | 型 | 名前 |
---|---|---|---|
0 | 1 | char | v |
1~7 | 1 | 未定義 | |
8 | 8 | Node * | left |
16 | 8 | Node * | right |
次は dfs 関数で、名前を dfs に変更する前は理解できませんでした。
しかし、今は明らかです。これは後順遍歴で、sym_name
に保存されます。
void dfs(Node *param_1)
{
if (param_1 != (Node *)0x0) {
dfs(param_1->left);
dfs(param_1->right);
(&sym_name_DAT_1400057c0)[i_DAT_1400057e0] = param_1->v;
i_DAT_1400057e0 = i_DAT_1400057e0 + 1;
}
return;
}
ida で root の構造を確認し、木がどのように構築されたかを見てみましょう:
(ida はメモリ内の変数を 2 つのレジスタに分ける操作を認識できない場合があります。例えば、ここでは v6 で右クリックして v4 にマッピングする必要があります。)
この二叉木は何と呼ばれるか忘れましたが、要するに層の順序で左から右に並んでいます。
ついでに f# でデータ構造を練習します。
まず深さ優先探索でデータを埋め込み、その後幅優先探索で木を再構築します。
open System.Collections.Generic
type Node =
{ v: char
l: Node option
r: Node option }
let mutable cnt = 0
let s = "?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z"
let rec dfs (dep: int) (p: Node) : Node =
if dep <= 5 then
let r =
{ p with
l = Some({ v = ' '; l = None; r = None } |> dfs (dep + 1))
r = Some({ v = ' '; l = None; r = None } |> dfs (dep + 1))
v = s[cnt] }
cnt <- cnt + 1
r
else
p
let root = dfs 1 { v = ' '; l = None; r = None }
let q = new Queue<Node>()
q.Enqueue root
while q.Count > 0 do
let t = q.Dequeue()
printf "%c" t.v
if t.l.IsSome then
q.Enqueue t.l.Value
if t.r.IsSome then
q.Enqueue t.r.Value
得られたのは:Z0@tRAEyuP@xAAA?M_A0_WNPx@@EPDP
最後に md5 を取れば大丈夫です。
雑談:f# で二叉木を書くことを試みたとき、最初はノードを可変にしようとしました。結果、type Node ={ v: char;l: Node ref option;r: Node ref option}
になりました。
中のNode
を取得するには?まず 2 層のラッパーを解かなければなりません。そして、参照渡しの最初は Node ref 型を書いていましたが、書き終わったら青い警告が出て、今は byref で書くことを知りました。
戻るときはメンバーのアドレスを直接取得できず、まず let バインディングをしなければなりませんでした。
その結果、私は崩壊しました。調べたら、ノードを変更する場合は、直接捨てて新しいものを返す必要があることがわかりました。ああ、関数型の書き方を使うのを忘れていました。(しかし、私はまだカウントのためにグローバル変数を使用しました。)
実際、この問題は私がランダムに見つけたもので、難易度に沿って進めているときに、突然いくつかの新しい問題が出てきて、強迫観念を満たせないと感じ、ここで打破することにしました。そして、難易度 6 の問題にランダムに出くわしました。木を構築する関数に入ったとき、驚かされました。そして、wp を見て、これはサインインの問題だと教えてくれました。うぇうぇうぇ。