Init. commit

This commit is contained in:
Caplag
2026-03-02 21:44:22 +03:00
committed by Ivan Z
commit 9511b38280
38 changed files with 4397 additions and 0 deletions

162
tesseract-reverse/README.md Normal file
View File

@@ -0,0 +1,162 @@
# Tesseract
Забудьте про статику: истина доступна только в динамике.
## Решение
Подсказка в задании сразу задает направление: **"забудь про статику, смотри на динамику"**.
![Интерфейс задания](image.png)
![Подсказка](image-1.png)
Первым делом загружаем сэмпл в **Detect It Easy**. Видим **VMProtect**, anti-debug. Т.к. это *.NET*, открываем в **DnSpy**, понимаем что смылсла в статике нет. Везде сильная обфускация, Control-Flow + Сильная анти-дебаг защита. Поэтому лучше смотреть, что происходит во время работы программы: какие строки появляются в памяти и через какие WinAPI проходят данные.
Ниже разберем решение пошагово, тем же путем, которым обычно идет участник.
## Что понадобится
- `frida` и `frida-tools` на Windows.
- PowerShell
- Process Hacker / Process Explorer - посмотреть TCP-соединения.
- WinAPI Monitor - понять, какие WinAPI реально вызываются (SSPI/Schannel, Winsock и т.п.).
Готовые скрипты решения:
- стадия 1 (получение пароля): `stage_1_findpassword.js`
- стадия 2 (получение флага): `stage_2_decryptmessage.js`
## Что видно при запуске
После старта видим простое окно:
![Окно программы](image-2.png)
- поле ввода
- кнопка `Check`
- подпись `status:`
Если вводить случайные строки и нажимать `Check`, появляется сообщение **`Invalid Password`**.
Из этого делаем два практических вывода:
1. внутри есть сравнение с правильным паролем;
2. правильная строка где-то в процессе все-таки появляется, хотя бы на короткое время.
## Стадия 1: достаем пароль динамически
### Почему не `strings` и не статический реверс
Если мы запустим программу, укажем любой пароль и посмотрим какие строки лежат в памяти процесса, то ничего полезного не увидем. Сам верный пароль может лежать в памяти, но не долго.
В задачах такого типа пароль часто:
- вычисляется на лету;
- расшифровывается в память на долю секунды;
- сразу затирается;
- скрывается за виртуализацией/обфускацией.
Поэтому здесь быстрее работать через рантайм.
### Идея
Мы не знаем пароль, но знаем строку, которая точно рисуется на экране: `Invalid Password`.
План такой:
1. поймать момент, когда приложение рисует `Invalid Password`;
2. получить указатель на эту строку;
3. найти memory range, где лежит этот указатель;
4. просканировать соседнюю область памяти на строки-кандидаты;
5. выбрать наиболее похожую на пароль.
В этом таске строка рисуется через `user32!DrawTextExW`, поэтому это удобная точка для хука.
### Как автоматизировать клики
Чтобы не нажимать кнопку вручную, скрипт:
- находит `EDIT` и кнопку `Check`;
- шлет `WM_SETTEXT` в поле;
- шлет `BM_CLICK` в кнопку;
- повторяет это циклом.
Поиск контролов сделан через `EnumWindows` и `EnumChildWindows`.
### Что делает `stage_1_findpassword.js`
По сути в нем четыре шага:
1. Минимально отключает антидебаг (`IsDebuggerPresent`, `CheckRemoteDebuggerPresent`).
2. Автоматически гоняет ввод и нажатие `Check`.
3. Хукает `DrawTextExW` и ловит вызов с текстом `Invalid Password`.
4. От найденного адреса сканирует память, собирает строки-кандидаты и ранжирует их по простым эвристикам.
![Результат первой стадии](image-3.png)
в выводе видно что-то такое:
```
[CAND 1] ... VMP_Is_Watching_Y0u
[+] BEST GUESS: VMP_Is_Watching_Y0u
```
Эту строку и вводим в GUI как пароль.
### Запуск стадии 1
```powershell
frida -f .\tesseract.exe -l .\stage_1_findpassword.js --runtime=v8
```
Дальше ждем строку `BEST GUESS`.
## Стадия 2: достаем флаг при TLS и pinning
![Сетевое поведение](image-4.png)
После ввода верного пароля ничего не происходит, происходит авторизация и все. Запустим proc hacker и видим, что после ввода пароля приложение создает какое-то подключение. Так же это видно через API Monitor
Интуитивный вариант - снять трафик в Wireshark/Burp/mitmproxy - здесь не помогает:
- трафик зашифрован TLS;
- pinning ломает MITM даже с подставленным сертификатом.
### Идея
Смотрим не в сеть, а в процесс. Любой TLS-клиент внутри себя в какой-то момент получает уже расшифрованные данные.
В Windows это обычно связка SSPI/Schannel. В API Monitor это видно по вызовам:
- `InitializeSecurityContextW` (handshake);
- `DecryptMessage` (расшифровка прикладных данных).
![Следы Schannel в API Monitor](image-5.png)
Значит, хукаем `DecryptMessage`, и после вызова читаем буферы `SECBUFFER_DATA`. Там лежит plaintext, который приложение уже расшифровало для себя.
Так мы обходим pinning без MITM: просто забираем готовые данные из памяти процесса.
### Что делает `stage_2_decryptmessage.js`
Скрипт:
1. при необходимости гасит базовые антидебаг-проверки;
2. ждет загрузку `secur32.dll` / `sspicli.dll`;
3. ставит хук на `DecryptMessage`;
4. на `onLeave` парсит `SecBufferDesc` и связанные `SecBuffer`;
5. читает все буферы типа `SECBUFFER_DATA` и печатает расшифрованный текст.
![Результат второй стадии](image-6.png)
В выводе получаем флаг:
```
[+] FLAG: caplag{D0uble_H00k_And_Tim3_Warp_Cr4ck}
```
### Запуск стадии 2
```powershell
frida -f .\tesseract.exe -l .\stage_2_decryptmessage.js --runtime=v8
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
tesseract-reverse/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,351 @@
console.log('[*] solve.js: anchor-based heap extractor loaded');
function getExport(mod, name) {
var m = Process.findModuleByName(mod);
if (!m) return null;
try {
return m.getExportByName(name);
} catch (_) {
return null;
}
}
function safeReadUtf16(p, maxChars) {
if (p.isNull()) return null;
try {
if (maxChars !== undefined) return p.readUtf16String(maxChars);
return p.readUtf16String();
} catch (_) {
return null;
}
}
function isPrintableAscii(c) {
return c >= 0x20 && c <= 0x7e;
}
function readUtf16Around(addr, maxChars) {
var p = addr;
try {
for (var i = 0; i < 128; i++) {
var prev = p.sub(2).readU16();
if (prev === 0) break;
if (!isPrintableAscii(prev)) break;
p = p.sub(2);
}
} catch (_) {}
var s = safeReadUtf16(p, maxChars || 96);
return { base: p, str: s };
}
function analyzeString(s) {
var hasU = false;
var hasL = false;
var hasD = false;
var underscores = 0;
for (var i = 0; i < s.length; i++) {
var ch = s.charCodeAt(i);
if (ch === 0x5f) underscores++;
if (ch >= 0x41 && ch <= 0x5a) hasU = true;
else if (ch >= 0x61 && ch <= 0x7a) hasL = true;
else if (ch >= 0x30 && ch <= 0x39) hasD = true;
}
var segs = s.split('_').length;
return { hasU: hasU, hasL: hasL, hasD: hasD, underscores: underscores, segs: segs };
}
function looksKeyLike(s) {
if (!s) return false;
if (s.length < 8 || s.length > 64) return false;
// Filter obvious noise.
if (s.indexOf('=') !== -1) return false;
if (s.indexOf('\\') !== -1) return false;
if (s.indexOf(':') !== -1) return false;
if (s.indexOf('.') !== -1) return false;
if (s.indexOf(' ') !== -1) return false;
for (var i = 0; i < s.length; i++) {
var c = s.charCodeAt(i);
if (!isPrintableAscii(c)) return false;
}
for (var i = 0; i < s.length; i++) {
var c = s.charCodeAt(i);
var ok =
(c >= 0x30 && c <= 0x39) ||
(c >= 0x41 && c <= 0x5a) ||
(c >= 0x61 && c <= 0x7a) ||
c === 0x5f;
if (!ok) return false;
}
if (s.indexOf('_') === -1) return false;
var a = analyzeString(s);
if (!(a.hasU && a.hasL)) return false;
return true;
}
function scoreCandidate(s) {
var a = analyzeString(s);
var score = 0;
if (s.length >= 12 && s.length <= 32) score += 50;
score += Math.max(0, 40 - Math.abs(s.length - 20));
if (a.segs >= 3 && a.segs <= 5) score += 30;
else score -= Math.abs(a.segs - 4) * 5;
if (a.hasD) score += 10;
score += a.underscores * 2;
return score;
}
function absPtrDelta(a, b) {
try {
var d = a.sub(b).toInt32();
if (d < 0) d = -d;
return d;
} catch (_) {
return 0x7fffffff;
}
}
function installAntiAntiDebug() {
function hookRet0(mod, name) {
var addr = getExport(mod, name);
if (!addr) return;
try {
Interceptor.replace(
addr,
new NativeCallback(function () {
return 0;
}, 'int', [])
);
console.log('[*] anti-anti-debug: ' + mod + '!' + name + ' -> 0');
} catch (_) {}
}
function hookCheckRemoteDebuggerPresent() {
var mod = 'kernel32.dll';
var name = 'CheckRemoteDebuggerPresent';
var addr = getExport(mod, name);
if (!addr) return;
try {
Interceptor.replace(
addr,
new NativeCallback(function (hProcess, pbDebuggerPresent) {
try {
if (!pbDebuggerPresent.isNull()) pbDebuggerPresent.writeU32(0);
} catch (_) {}
return 1;
}, 'int', ['pointer', 'pointer'])
);
console.log('[*] anti-anti-debug: ' + mod + '!' + name + ' -> TRUE, out=0');
} catch (_) {}
}
hookRet0('kernel32.dll', 'IsDebuggerPresent');
hookCheckRemoteDebuggerPresent();
}
function automateUi() {
var user32 = 'user32.dll';
var pEnumWindows = getExport(user32, 'EnumWindows');
var pEnumChildWindows = getExport(user32, 'EnumChildWindows');
var pGetWindowThreadProcessId = getExport(user32, 'GetWindowThreadProcessId');
var pGetWindowTextW = getExport(user32, 'GetWindowTextW');
var pGetClassNameW = getExport(user32, 'GetClassNameW');
var pSendMessageW = getExport(user32, 'SendMessageW');
if (!pEnumWindows || !pEnumChildWindows || !pGetWindowThreadProcessId || !pGetWindowTextW || !pGetClassNameW || !pSendMessageW) {
return;
}
var EnumWindows = new NativeFunction(pEnumWindows, 'int', ['pointer', 'pointer']);
var EnumChildWindows = new NativeFunction(pEnumChildWindows, 'int', ['pointer', 'pointer', 'pointer']);
var GetWindowThreadProcessId = new NativeFunction(pGetWindowThreadProcessId, 'uint', ['pointer', 'pointer']);
var GetWindowTextW = new NativeFunction(pGetWindowTextW, 'int', ['pointer', 'pointer', 'int']);
var GetClassNameW = new NativeFunction(pGetClassNameW, 'int', ['pointer', 'pointer', 'int']);
var SendMessageW = new NativeFunction(pSendMessageW, 'pointer', ['pointer', 'uint', 'pointer', 'pointer']);
var pidBuf = Memory.alloc(4);
function getText(hwnd) {
var buf = Memory.alloc(2 * 512);
buf.writeU16(0);
var n = GetWindowTextW(hwnd, buf, 512);
if (n <= 0) return '';
return safeReadUtf16(buf, 512) || '';
}
function getClass(hwnd) {
var buf = Memory.alloc(2 * 256);
buf.writeU16(0);
var n = GetClassNameW(hwnd, buf, 256);
if (n <= 0) return '';
return safeReadUtf16(buf, 256) || '';
}
function findMainWindow() {
var best = NULL;
var cb = new NativeCallback(function (hwnd, lParam) {
pidBuf.writeU32(0);
GetWindowThreadProcessId(hwnd, pidBuf);
var pid = pidBuf.readU32();
if (pid !== Process.id) return 1;
var title = getText(hwnd);
if (best.isNull()) best = hwnd;
if (title.indexOf('Tesseract') !== -1) {
best = hwnd;
return 0;
}
return 1;
}, 'int', ['pointer', 'pointer']);
EnumWindows(cb, ptr(0));
return best;
}
function findControls() {
var hwnd = findMainWindow();
if (hwnd.isNull()) return null;
var btn = NULL;
var edit = NULL;
var cb = new NativeCallback(function (child, lParam) {
var txt = getText(child);
var cls = getClass(child);
if (btn.isNull() && txt === 'Check') btn = child;
if (edit.isNull() && cls.indexOf('EDIT') !== -1) edit = child;
return 1;
}, 'int', ['pointer', 'pointer']);
EnumChildWindows(hwnd, cb, ptr(0));
return { hwnd: hwnd, btn: btn, edit: edit };
}
var WM_SETTEXT = 0x000c;
var BM_CLICK = 0x00f5;
var cached = null;
setInterval(function () {
try {
if (!cached || cached.btn.isNull() || cached.edit.isNull()) {
cached = findControls();
if (!cached) return;
console.log('[*] UI: hwnd=' + cached.hwnd + ' edit=' + cached.edit + ' btn=' + cached.btn);
if (cached.btn.isNull() || cached.edit.isNull()) return;
}
var input = Memory.allocUtf16String('a');
SendMessageW(cached.edit, WM_SETTEXT, ptr(0), input);
SendMessageW(cached.btn, BM_CLICK, ptr(0), ptr(0));
} catch (_) {}
}, 300);
}
var dumped = false;
function dumpCandidatesAround(anchorPtr) {
if (dumped) return;
dumped = true;
console.log('[*] anchor ptr=' + anchorPtr);
var r = Process.findRangeByAddress(anchorPtr);
if (!r) {
console.log('[!] findRangeByAddress failed');
return;
}
var radius = 0x100000;
var lo = r.base;
var hi = r.base.add(r.size);
var start = anchorPtr.sub(radius);
if (start.compare(lo) < 0) start = lo;
var end = anchorPtr.add(radius);
if (end.compare(hi) > 0) end = hi;
var size = end.sub(start).toInt32();
console.log('[*] scan window base=' + start + ' size=' + size + ' (range base=' + r.base + ' size=' + r.size + ')');
var needle = '5F 00';
var seen = {};
var cands = [];
Memory.scan(start, size, needle, {
onMatch: function (address, sz) {
var res = readUtf16Around(address, 96);
var s = res.str;
if (!looksKeyLike(s)) return;
if (seen[s]) return;
seen[s] = 1;
var dist = absPtrDelta(res.base, anchorPtr);
var score = scoreCandidate(s);
cands.push({ s: s, addr: res.base, dist: dist, score: score });
},
onError: function (reason) {},
onComplete: function () {
cands.sort(function (a, b) {
if (b.score !== a.score) return b.score - a.score;
return a.dist - b.dist;
});
console.log('[*] candidates found: ' + cands.length);
var top = Math.min(15, cands.length);
for (var i = 0; i < top; i++) {
var c = cands[i];
console.log('[CAND ' + (i + 1) + '] score=' + c.score + ' dist=' + c.dist + ' @' + c.addr + ' ' + c.s);
}
if (cands.length > 0) {
console.log('[+] BEST GUESS: ' + cands[0].s);
} else {
console.log('[!] No candidates.');
}
}
});
}
function installAnchorHook() {
var addr = getExport('user32.dll', 'DrawTextExW');
if (!addr) {
console.log('[!] missing user32!DrawTextExW');
return;
}
Interceptor.attach(addr, {
onEnter: function (args) {
if (dumped) return;
var pText = args[1];
var n = args[2].toInt32();
if (pText.isNull()) return;
var s = safeReadUtf16(pText, n === -1 ? 512 : Math.min(n, 512));
if (!s) return;
if (s.indexOf('Invalid Password') !== -1) {
console.log('[*] saw Invalid Password');
dumpCandidatesAround(pText);
}
}
});
console.log('[*] hooked user32!DrawTextExW');
}
setTimeout(function () {
installAntiAntiDebug();
installAnchorHook();
automateUi();
console.log('[*] ready; waiting for Invalid Password render...');
}, 500);

View File

@@ -0,0 +1,176 @@
var installed = false;
var msgNo = 0;
function getExport(mod, name) {
var m = Process.findModuleByName(mod);
if (!m) return null;
try {
return m.getExportByName(name);
} catch (_) {
return null;
}
}
function findExportBySubstring(mod, needle) {
var m = Process.findModuleByName(mod);
if (!m) return null;
try {
var ex = m.enumerateExports();
var lowNeedle = needle.toLowerCase();
for (var i = 0; i < ex.length; i++) {
var n = (ex[i].name || "").toLowerCase();
if (n.indexOf(lowNeedle) !== -1) return ex[i].address;
}
} catch (_) {}
return null;
}
function bytesToEscapedText(bytes) {
var out = "";
for (var i = 0; i < bytes.length; i++) {
var b = bytes[i] & 0xff;
if (b === 0x0a) out += "\n";
else if (b === 0x0d) out += "\r";
else if (b === 0x09) out += "\t";
else if (b >= 0x20 && b <= 0x7e) out += String.fromCharCode(b);
else {
var h = b.toString(16);
if (h.length === 1) h = "0" + h;
out += "\\x" + h;
}
}
return out;
}
function readSecBufferDesc(pDesc) {
var ptrSize = Process.pointerSize;
if (pDesc.isNull()) return [];
var cBuffers = 0;
var pBuffers = NULL;
try {
cBuffers = pDesc.add(4).readU32();
pBuffers = pDesc.add(8).readPointer();
} catch (_) {
return [];
}
if (cBuffers === 0 || cBuffers > 32) return [];
if (pBuffers.isNull()) return [];
var out = [];
var secBufSize = 8 + ptrSize; // 12 on x86, 16 on x64
for (var i = 0; i < cBuffers; i++) {
try {
var p = pBuffers.add(i * secBufSize);
var cb = p.readU32();
var type = p.add(4).readU32();
var pv = p.add(8).readPointer();
out.push({ cb: cb, type: type, pv: pv });
} catch (_) {}
}
return out;
}
function dumpPlaintext(bytes) {
if (!bytes || bytes.length === 0) return;
msgNo++;
console.log("");
console.log("[*] ===== DecryptMessage plaintext #" + msgNo + " (" + bytes.length + " bytes) =====");
// Prefer single print to preserve original line breaks. Fall back to chunking
// only for very large buffers.
var MAX_SINGLE_LOG = 256 * 1024;
if (bytes.length <= MAX_SINGLE_LOG) {
console.log(bytesToEscapedText(bytes));
return;
}
var CHUNK = 256 * 1024;
for (var off = 0; off < bytes.length; off += CHUNK) {
var end = off + CHUNK;
if (end > bytes.length) end = bytes.length;
console.log("[*] --- chunk " + off + ".." + end + " ---");
console.log(bytesToEscapedText(bytes.subarray(off, end)));
}
}
function installAntiAntiDebug() {
function hookRet0(mod, name) {
var a = getExport(mod, name);
if (!a) return;
try {
Interceptor.replace(a, new NativeCallback(function () { return 0; }, "int", []));
console.log("[*] anti-anti-debug: " + mod + "!" + name + " -> 0");
} catch (_) {}
}
function hookCheckRemoteDebuggerPresent() {
var a = getExport("kernel32.dll", "CheckRemoteDebuggerPresent");
if (!a) return;
try {
Interceptor.replace(
a,
new NativeCallback(function (hProcess, pbDebuggerPresent) {
try { if (!pbDebuggerPresent.isNull()) pbDebuggerPresent.writeU32(0); } catch (_) {}
return 1;
}, "int", ["pointer", "pointer"])
);
console.log("[*] anti-anti-debug: kernel32!CheckRemoteDebuggerPresent -> TRUE,out=0");
} catch (_) {}
}
hookRet0("kernel32.dll", "IsDebuggerPresent");
hookCheckRemoteDebuggerPresent();
}
function installHooksOnce() {
if (installed) return true;
var addr =
getExport("secur32.dll", "DecryptMessage") ||
findExportBySubstring("secur32.dll", "DecryptMessage") ||
getExport("sspicli.dll", "DecryptMessage") ||
findExportBySubstring("sspicli.dll", "DecryptMessage");
if (!addr) return false;
installed = true;
console.log("[*] Hooking DecryptMessage @ " + addr);
Interceptor.attach(addr, {
onEnter: function (args) {
this.pDesc = args[1];
},
onLeave: function (retval) {
try {
var bufs = readSecBufferDesc(this.pDesc);
for (var i = 0; i < bufs.length; i++) {
var b = bufs[i];
if (b.type !== 1) continue;
if (b.cb === 0) continue;
if (b.pv.isNull()) continue;
var ab = b.pv.readByteArray(b.cb);
if (ab === null) continue;
dumpPlaintext(new Uint8Array(ab));
}
} catch (_) {}
}
});
return true;
}
setTimeout(function () {
installAntiAntiDebug();
}, 100);
setInterval(function () {
if (!installed) {
var ok = installHooksOnce();
if (ok) console.log("[*] DecryptMessage hook installed. Use the app normally and wait for output...");
}
}, 50);