Init. commit
162
tesseract-reverse/README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Tesseract
|
||||
|
||||
Забудьте про статику: истина доступна только в динамике.
|
||||
|
||||
## Решение
|
||||
|
||||
Подсказка в задании сразу задает направление: **"забудь про статику, смотри на динамику"**.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Первым делом загружаем сэмпл в **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`
|
||||
|
||||
## Что видно при запуске
|
||||
|
||||
После старта видим простое окно:
|
||||
|
||||

|
||||
|
||||
- поле ввода
|
||||
- кнопка `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. От найденного адреса сканирует память, собирает строки-кандидаты и ранжирует их по простым эвристикам.
|
||||
|
||||

|
||||
|
||||
в выводе видно что-то такое:
|
||||
|
||||
```
|
||||
[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
|
||||
|
||||

|
||||
|
||||
После ввода верного пароля ничего не происходит, происходит авторизация и все. Запустим proc hacker и видим, что после ввода пароля приложение создает какое-то подключение. Так же это видно через API Monitor
|
||||
|
||||
Интуитивный вариант - снять трафик в Wireshark/Burp/mitmproxy - здесь не помогает:
|
||||
|
||||
- трафик зашифрован TLS;
|
||||
- pinning ломает MITM даже с подставленным сертификатом.
|
||||
|
||||
### Идея
|
||||
|
||||
Смотрим не в сеть, а в процесс. Любой TLS-клиент внутри себя в какой-то момент получает уже расшифрованные данные.
|
||||
|
||||
В Windows это обычно связка SSPI/Schannel. В API Monitor это видно по вызовам:
|
||||
|
||||
- `InitializeSecurityContextW` (handshake);
|
||||
- `DecryptMessage` (расшифровка прикладных данных).
|
||||
|
||||

|
||||
|
||||
Значит, хукаем `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` и печатает расшифрованный текст.
|
||||
|
||||

|
||||
|
||||
В выводе получаем флаг:
|
||||
|
||||
```
|
||||
[+] FLAG: caplag{D0uble_H00k_And_Tim3_Warp_Cr4ck}
|
||||
```
|
||||
|
||||
### Запуск стадии 2
|
||||
|
||||
```powershell
|
||||
frida -f .\tesseract.exe -l .\stage_2_decryptmessage.js --runtime=v8
|
||||
```
|
||||
BIN
tesseract-reverse/image-1.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
tesseract-reverse/image-2.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
tesseract-reverse/image-3.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
tesseract-reverse/image-4.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
tesseract-reverse/image-5.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
tesseract-reverse/image-6.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
tesseract-reverse/image.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
351
tesseract-reverse/stage_1_findpassword.js
Normal 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);
|
||||
176
tesseract-reverse/stage_2_decryptmessage.js
Normal 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);
|
||||