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 的自動生成幫了一部分忙)。
Offset | Length | Type | Name |
---|---|---|---|
0 | 1 | char | v |
1~7 | 1 | undefined | |
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 可能無法識別把一個內存中的變量分到兩個寄存器這個操作,比如這裡需要在 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
?先解兩層包。然後引用傳參最開始寫的是 Node ref 類型,寫完給我個藍色 warning,然後才知道現在都是寫 byref。然後返回的時候還不能直接取成員地址,讓我先 let 綁定。
然後我就崩潰了,一查才知道原來是要修改結點的時候直接扔掉返回一個新的。啊,我忘了該用函數式寫法了。(然而我還是用了個全局變量計數。)
其實這道題是我隨機到的,就是感覺按難度挨著做,中間突然冒出幾道新題,無法滿足強迫症的感覺,乾脆從這裡打破吧。然後隨機到這個難度 6 的題。然後點進建樹的函數被嚇出來。然後開擺看 wp。然後告訴我這是簽到題。呜呜呜。