ZEXOR-v2.0
Crackme writeup that disables anti-debugging and exploits a nibble-swap bug to recover ZEXOR's license PRNG seed.
Description
ZEXOR v0.2
Description This app obfuscates and encrypts its own code. A license key is generated randomly and is 16 characters long (Latin letters and Arabic numerals). Each failed attempt reduces the number of remaining tries(by 5).
Goal Bypass or deactivate the license check so the program accepts any input.
In this challenge we are given a Windows binary with a 16-character license check and aggressive anti-debugging. The path to the solution is the following:
- neutralize the anti-debug checks,
- understand how
license.datis parsed and decrypted, - exploit a bug in the nibble swap to recover the PRNG seed.
Debugging function
When the program starts it performs several checks to ensure it is not being debugged or analyzed. The checks are pervasive:
Timing checks via
__rdtsc()IsDebuggerPresent()callsChecking a debugger presence with the thread context:
bool __fastcall hw_brk_point_check1() { HANDLE CurrentThread; // rax __m128i v1; // xmm0 _CONTEXT Context; // [rsp+20h] [rbp-4D8h] BYREF memset(&Context, 0, sizeof(Context)); Context.ContextFlags = 1048592; CurrentThread = GetCurrentThread(); GetThreadContext(CurrentThread, &Context); v1 = _mm_or_si128(_mm_loadu_si128((const __m128i *)&Context.Dr0), _mm_loadu_si128((const __m128i *)&Context.Dr2)); return _mm_or_si128(v1, _mm_srli_si128(v1, 8)).m128i_u64[0] != 0; }Process scanning (the program refuses to run if Ghidra/IDA and other programs are open):
__int64 __fastcall process_scanner() { HANDLE Toolhelp32Snapshot_0; // rbp CHAR v1; // al PROCESSENTRY32 *p_pe; // rdx PROCESSENTRY32 pe; // [rsp+20h] [rbp-158h] BYREF Toolhelp32Snapshot_0 = CreateToolhelp32Snapshot_0(2u, 0); if ( Toolhelp32Snapshot_0 == (HANDLE)-1LL ) return 0; memset(&pe, 0, sizeof(pe)); pe.dwSize = 304; if ( !Process32First(Toolhelp32Snapshot_0, &pe) ) { LABEL_15: CloseHandle(Toolhelp32Snapshot_0); return 0; } while ( 1 ) { v1 = pe.szExeFile[0]; if ( pe.szExeFile[0] ) { p_pe = &pe; do { if ( (unsigned __int8)(v1 - 65) <= 0x19u ) p_pe->szExeFile[0] = v1 + 32; v1 = p_pe->szExeFile[1]; p_pe = (PROCESSENTRY32 *)((char *)p_pe + 1); } while ( v1 ); } if ( strstr_0(pe.szExeFile, "x64dbg") || strstr_0(pe.szExeFile, "x32dbg") || strstr_0(pe.szExeFile, "ollydbg") || strstr_0(pe.szExeFile, "ida") || strstr_0(pe.szExeFile, "windbg") || strstr_0(pe.szExeFile, "ghidra") ) { break; } if ( !Process32Next(Toolhelp32Snapshot_0, &pe) ) goto LABEL_15; } CloseHandle(Toolhelp32Snapshot_0); return 1; }
Rather than describe every technique, the key point is that all of them funnel through a single function:
_BOOL8 __fastcall anti_debugger()
{
v0 = 0;
v23 = 0;
do
{
v1 = v0++;
v23 += v1 ^ 0x23;
}
while ( v0 != 100 );
v2 = IsDebuggerPresent();
if ( v2 )
return 1;
do
{
v3 = v2;
v4 = v2++;
v27[v4] = v3 ^ 0xAA;
}
while ( v2 != 64 );
v28[0] = 0;
CurrentProcess = GetCurrentProcess();
_IAT_start__(CurrentProcess, v28);
if ( v28[0] )
return 1;
v7 = __rdtsc();
v8 = 10;
v25 = 0;
v9 = v7;
do
{
++v25;
--v8;
}
while ( v8 );
v10 = __rdtsc();
if ( (unsigned int)(v10 - v9) > 0xC350 )
return 1;
if ( *(_BYTE *)(*((_QWORD *)NtCurrentTeb() + 12) + 2LL) )
return 1;
v11 = 0;
do
{
v12 = v11;
v13 = v11++;
v27[v13 + 64] = v12 ^ 0xAA;
}
while ( v11 != 64 );
if ( (*(_BYTE *)(*((_QWORD *)NtCurrentTeb() + 12) + 188LL) & 0x70) != 0 )
return 1;
if ( HW_BRK_POINT_CHECK1() )
return 1;
v14 = __rdtsc();
v15 = 10;
v26 = 0;
v16 = v14;
do
{
++v26;
--v15;
}
while ( v15 );
v17 = __rdtsc();
if ( (unsigned int)(v17 - v16) > 0xC350 )
return 1;
if ( (unsigned __int8)process_scanner() )
return 1;
v18 = 0;
do
{
v19 = v18;
v20 = v18++;
*((_BYTE *)v28 + v20) = v19 ^ 0xAA;
}
while ( v18 != 64 );
if ( (unsigned __int8)fn_d7() )
return 1;
result = fn_d8();
if ( result )
return 1;
v24 = 0;
for ( i = 0; i != 100; ++i )
{
v22 = i;
v24 += v22 ^ 0x1B;
}
return result;
}
This function is annoying, but it is also a perfect patch point: I patched the prologue to return zero immediately, so none of the timing checks (or any other checks) execute.
License Check Function
With anti-debug out of the way, we can follow the normal startup flow in WinMain:
int __stdcall WinMain(HINSTANCE hInst, HINSTANCE hPreInst, LPSTR lpszCmdLine, int nCmdShow)
{
unsigned __int64 v6; // rax
HWND Window; // rax
HWND v8; // rbx
size_t v10; // rax
int v11; // edx
__int64 v12; // rax
size_t v13; // rax
int v14; // edx
__int64 v15; // rax
size_t v16; // rax
int v17; // edx
__int64 v18; // rax
size_t v19; // rax
int v20; // edx
__int64 v21; // rax
MSG Msg; // [rsp+60h] [rbp-A8h] BYREF
WNDCLASSEXA v23; // [rsp+90h] [rbp-78h] BYREF
v6 = __rdtsc();
g_t1 = v6;
if ( ANTI_DEBUGGER() ) {
v10 = strlen(g_s1.m128i_i8);
v11 = v10;
if ( !v10 ) goto LABEL_17;
if ( (unsigned int)(v10 - 1) <= 0xE ) {
v12 = 0;
} else {
g_s1 = _mm_xor_si128(_mm_shuffle_epi32(_mm_cvtsi32_si128(0x71717171u), 0), g_s1);
if ( v10 == 16 ) goto LABEL_17;
v12 = 16;
}
do
g_s1.m128i_i8[v12++] ^= 0x71u;
while ( v11 > (int)v12 );
LABEL_17:
v13 = strlen(g_s2);
v14 = v13;
if ( v13 )
{
if ( (unsigned int)(v13 - 1) <= 6 ) {
v15 = 0;
do
LABEL_21:
g_s2[v15++] ^= 0x71u;
while ( v14 > (int)v15 );
goto LABEL_22;
}
*(_QWORD *)g_s2 = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)g_s2), _mm_loadl_epi64((const __m128i *)&qword_1400D43B0)).m128i_u64[0];
if ( v13 != 8 ) {
v15 = 8;
goto LABEL_21;
}
}
LABEL_22:
MessageBoxA(0, g_s1.m128i_i8, g_s2, 0x10u);
return 1;
}
if ( !(unsigned __int8)license_check() ) {
fn_x2(INMemKey);
g_cnt1 = 20;
g_cnt2 = 0;
fn_sv();
}
v23.cbSize = 80;
memset(&v23.style, 0, 36);
memset((char *)&v23.hbrBackground + 4, 0, 28);
v23.lpfnWndProc = (WNDPROC)WndProc; <-- Nice to know
v23.hInstance = hInst;
v23.hbrBackground = (HBRUSH)6;
v23.hCursor = LoadCursorA(0, (LPCSTR)0x7F00);
v23.lpszClassName = "ZEXOR v0.2";
if ( RegisterClassExA(&v23) ) {
Window = CreateWindowExA(0x200u, "ZEXOR v0.2", "ZEXOR v0.2", 0xCA0000u, 0x80000000, 0x80000000, 420, 280, 0, 0, hInst, 0);
v8 = Window;
if ( Window ) {
ShowWindow(Window, nCmdShow);
UpdateWindow(v8);
while ( GetMessageA(&Msg, 0, 0, 0) > 0 ) {
if ( ANTI_DEBUGGER() ) {
v16 = strlen(g_s1.m128i_i8);
v17 = v16;
if ( v16 ) {
if ( (unsigned int)(v16 - 1) <= 0xE ) {
v18 = 0;
goto LABEL_30;
}
g_s1 = _mm_xor_si128(_mm_shuffle_epi32(_mm_cvtsi32_si128(0x71717171u), 0), g_s1);
if ( v16 != 16 ) {
v18 = 16;
do
LABEL_30:
g_s1.m128i_i8[v18++] ^= 0x71u;
while ( v17 > (int)v18 );
}
}
v19 = strlen(g_s2);
v20 = v19;
if ( v19 ) {
if ( (unsigned int)(v19 - 1) <= 6 ) {
v21 = 0;
goto LABEL_35;
}
*(_QWORD *)g_s2 = _mm_xor_si128(_mm_loadl_epi64((const __m128i *)g_s2),_mm_loadl_epi64((const __m128i *)&qword_1400D43B0)).m128i_u64[0];
if ( v19 != 8 ) {
v21 = 8;
do
LABEL_35:
g_s2[v21++] ^= 0x71u;
while ( v20 > (int)v21 );
}
}
MessageBoxA(0, g_s1.m128i_i8, g_s2, 0x10u);
ExitProcess(0);
}
TranslateMessage(&Msg);
DispatchMessageA(&Msg);
}
return Msg.wParam;
} else {
MessageBoxA(0, "Window Creation Failed!", "Error", 0x30u);
return 0;
}
} else {
MessageBoxA(0, "Window Registration Failed!", "Error", 0x30u);
return 0;
}
}
WinMain does three things in order:
- runs the anti-debug checks,
- generates
license.datif it is missing or invalid, - creates the window and routes input through
WndProc.
Here is the relevant part for the license generation path:
if ( !(unsigned __int8)license_check() ) { <- Seen latern on
fn_x2(INMemKey);
g_cnt1 = 20;
g_cnt2 = 0;
fn_sv();
}
And here is where the UI gets attached to WndProc:
WNDCLASSEXA v23;
...
v23.lpfnWndProc = (WNDPROC)WndProc; <-- Seen later on
v23.hInstance = hInst;
v23.hbrBackground = (HBRUSH)6;
v23.hCursor = LoadCursorA(0, (LPCSTR)0x7F00);
v23.lpszClassName = "ZEXOR v0.2";
if ( RegisterClassExA(&v23) ) {
...
}
The license check function:
loads the license (
license.dat)decrypts the payload with the simple linear function:
key = 0x7A; do { CurrChar = *StartDstBuffer++; temp = key ^ CurrChar; key = 0x11 * key + 0x1F; *(StartDstBuffer - 1) = (0x10 * temp) | (temp >> 4); } while ( &FileStream != (__int64 (__fastcall ***)())StartDstBuffer );from
license.datloads the hashsum of the binary text region and checks against itself, this is done to prevent anti tampering of the binary.CurrentProcessHandle = (IMAGE_DOS_HEADER *)GetModuleHandleA(0); // SizeOfCode = pNtHeaders->OptionalHeader.SizeOfCode; SizeOfCode_OR_RunningHash = *(unsigned int *)((char *)CurrentProcessHandle->e_res + CurrentProcessHandle->e_lfanew); if ( (_DWORD)SizeOfCode_OR_RunningHash ) { PtrCodeEnd = (IMAGE_DOS_HEADER *)((char *)CurrentProcessHandle + SizeOfCode_OR_RunningHash); LODWORD(SizeOfCode_OR_RunningHash) = 0; do { CurrentByte = LOBYTE(CurrentProcessHandle->e_magic); CurrentProcessHandle = (IMAGE_DOS_HEADER *)((char *)CurrentProcessHandle + 1); LODWORD(SizeOfCode_OR_RunningHash) = __ROL4__(CurrentByte + SizeOfCode_OR_RunningHash, 1); } while ( CurrentProcessHandle != PtrCodeEnd ); } LOBYTE(is_open_OR_BuffCurr) = is_open_OR_BuffCurr == (_DWORD)SizeOfCode_OR_RunningHash; }if all the checks pass, it returns ok
Here is the full function. Before and after tagging it so that you can better understand how it works.
License encryption analysis
I noticed that this license check always returns false because the encryption function is broken. In fact, the encryption/decryption routine has a signedness bug on this line:
d[i] = (d[i] << 4) | (d[i] >> 4);
This is intended to be a nibble swap (swap the left 4 bits with the right 4 bits).
- Left Shift: Moves bits to the left, fills with 0.
- Right Shift: This is the killer.
When you right shift a signed negative number (the high bit is 1), the CPU performs an arithmetic shift. Instead of filling the empty space with 0s, it fills it with 1s to preserve the “sign” of the number.
Example 1:
Input is0xF0 = 1111 0000Left Shift:
1111 0000 -> 0000 0000(Lower 4 bits 0000 moved to top).Right Shift:
Because the first bit is 1 (Negative), the CPU fills the new bits with 1.1111 0000 -> 1111 1111(0xFF)0000 0000 | 1111 1111 = 1111 1111(Result: 0xFF)Example 2:
Input is0xF1 = 1111 0001Left Shift:
1111 0001 = 0001 0000(Lower 4 bits 0001 moved to top).Right Shift:
First bit is 1 (Negative), so fill with 1.1111 0001 = 1111 1111(0xFF).0001 0000 | 1111 1111 = 1111 1111(Result: 0xFF)
This breaks the license saved to disk, but not all bytes are corrupted.
Bytes are encrypted correctly iff
char XOR keyis positive.
We also know that the key is updated deterministically with key = 0x11 * key + 0x1F;,
this makes the key generation deterministic, being the key seed the same the
sequence of keys is always the same.
Given that, we have two useful facts:
- The decryption algorithm is easily reversible
- Some bytes (and we can identify which) are encrypted correctly
That suggests a strategy: recover the remaining bytes by guessing the PRNG seed.
Breaking the PRNG
At the beginning we saw that if no file is present a license is generated. Now we know that even with a license.dat
present the license is regenerated because the encrypted data is broken and never matches.
This means we can try all seeds until we generate a license that matches the known-good bytes.
These three partial licenses can help you visualize what i mean.
ER8Q[?]E[?][?]NI[?]03[?][?]QIB6[?][?]M[?][?]8O[?]ZS[?][?]8Q0B[?][?]X[?][?]TL[?]TJ[?][?]
This function is the one that generates the license and as we can see the random
seed is v2 which is 32 bits (easily bruteforceable).
char *__fastcall fn_x2(char *a1)
{
int v1; // ebx
DWORD v2; // ecx
__int64 v3; // rbx
char *result; // rax
char v5[72]; // [rsp+30h] [rbp-48h] BYREF
char *v6; // [rsp+80h] [rbp+8h] BYREF
strcpy(v5, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
v6 = a1;
v1 = time64(0);
v2 = v1 ^ GetTickCount() ^ (unsigned int)&v6;
v3 = 0;
srand_0(v2);
do
v6[v3++] = v5[rand_0() % 0x24uLL];
while ( v3 != 16 );
result = v6;
v6[16] = 0;
return result;
}
Final analysis
Lastly, the program resets the license after a certain number of failed tries,
and each time a license is generated it is saved to license.dat.
This logic is centralized in the WndProc handler: it validates the input,
decrements the counter, writes the updated state to disk, and triggers a reset
when the tries hit zero.
// Inside: LRESULT __fastcall WndProc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
if ( ValidateLicense(String) ) {
SetWindowTextA(g_h3, &byte_1400D4178);
MessageBoxA(hWnd, "Congratulations! You've successfully cracked it!", "Success", 0x40u);
DeleteFileA("license.dat");
ExitProcess(0);
}
--g_cnt1;
fn_sv(); // <-- Saved to file
_mingw_sprintf(buf, "Tries remaining: %d", g_cnt1);
SetWindowTextA(g_h4, buf);
if ( g_cnt1 <= 0 )
{
SetWindowTextA(g_h3, "You failed. You could not find the right license key.");
MessageBoxA(
hWnd,
"You failed. You could not find the right license key.\n"
"\n"
"Click OK to reset with a new license (each time you fail you get fewer tries)",
"Failed",
0x10u);
fn_rst(); // <-- Reset after a certain amount of time (g_cnt1)
SetWindowTextA(g_h1, &byte_1400D4139);
_mingw_sprintf(buf, "Tries remaining: %d", g_cnt1);
SetWindowTextA(g_h4, buf);
SetWindowTextA(g_h3, "Application has been reset.\nNew license generated.");
}
else
{
_mingw_sprintf(buf, &byte_1400D4300);
SetWindowTextA(g_h3, buf);
}
}
Full WndProc for context:
LRESULT __fastcall WndProc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
{
char buf[128]; // [rsp+60h] [rbp-198h] BYREF
__m128i String[17]; // [rsp+E0h] [rbp-118h] BYREF
if ( a2 == 16 )
{
DestroyWindow(hWnd);
return 0;
}
else
{
if ( a2 > 0x10 )
{
if ( a2 == 273 )
{
if ( (_WORD)a3 == 2 )
{
if ( ANTI_DEBUGGER() )
{
fn_ds(g_s1.m128i_i8);
fn_ds(g_s2);
MessageBoxA(hWnd, g_s1.m128i_i8, g_s2, 0x10u);
ExitProcess(0);
}
GetWindowTextA(g_h1, String[0].m128i_i8, 256);
if ( strlen(String[0].m128i_i8) == 16 ) {
if ( ValidateLicense(String) ) {
SetWindowTextA(g_h3, &byte_1400D4178);
MessageBoxA(hWnd, "Congratulations! You've successfully cracked it!", "Success", 0x40u);
DeleteFileA("license.dat");
ExitProcess(0);
}
--g_cnt1;
fn_sv();
_mingw_sprintf(buf, "Tries remaining: %d", g_cnt1);
SetWindowTextA(g_h4, buf);
if ( g_cnt1 <= 0 )
{
SetWindowTextA(g_h3, "You failed. You could not find the right license key.");
MessageBoxA(
hWnd,
"You failed. You could not find the right license key.\n"
"\n"
"Click OK to reset with a new license (each time you fail you get fewer tries)",
"Failed",
0x10u);
fn_rst();
SetWindowTextA(g_h1, &byte_1400D4139);
_mingw_sprintf(buf, "Tries remaining: %d", g_cnt1);
SetWindowTextA(g_h4, buf);
SetWindowTextA(g_h3, "Application has been reset.\nNew license generated.");
}
else
{
_mingw_sprintf(buf, &byte_1400D4300);
SetWindowTextA(g_h3, buf);
}
}
else
{
SetWindowTextA(g_h3, "License must be exactly 16 characters!");
}
}
return 0;
}
return DefWindowProcA(hWnd, a2, a3, a4);
}
if ( a2 != 1 )
{
if ( a2 == 2 )
{
PostQuitMessage(0);
return 0;
}
return DefWindowProcA(hWnd, a2, a3, a4);
}
if ( ANTI_DEBUGGER() )
{
fn_ds(g_s1.m128i_i8);
fn_ds(g_s2);
MessageBoxA(0, g_s1.m128i_i8, g_s2, 0x10u);
ExitProcess(0);
}
CreateWindowExA(0, "STATIC", "Enter License Key:", 0x50000000u, 20, 20, 300, 20, hWnd, 0, 0, 0);
g_h1 = CreateWindowExA(0x200u, "EDIT", &byte_1400D4139, 0x50000088u, 20, 50, 360, 25, hWnd, (HMENU)1, 0, 0);
g_h2 = (__int64)CreateWindowExA(0, "BUTTON", "CHECK", 0x50000000u, 20, 90, 140, 30, hWnd, (HMENU)2, 0, 0);
g_h4 = CreateWindowExA(0, "STATIC", &byte_1400D4139, 0x50000000u, 180, 95, 200, 20, hWnd, 0, 0, 0);
g_h3 = CreateWindowExA(0, "STATIC", &byte_1400D4139, 0x50000001u, 20, 140, 360, 60, hWnd, 0, 0, 0);
_mingw_sprintf(String[0].m128i_i8, "Tries remaining: %d", g_cnt1);
SetWindowTextA(g_h4, String[0].m128i_i8);
return 0;
}
}
The script below implements the broken nibble swap, extracts the partial pattern
from license.dat, brute-forces the PRNG seed,
and reconstructs the full 16-character license.
import sys
import time
LCG_A = 214013
LCG_C = 2531011
LCG_M = 0xFFFFFFFF
CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def simulate_c_corruption(char_code, key):
# Mirror the buggy signed nibble-swap behavior in the binary.
val = char_code ^ key
if val > 127: signed_val = val - 256
else: signed_val = val
left = (signed_val << 4) & 0xFF
right = (signed_val >> 4) & 0xFF
return left | right
def extract_partial_pattern(hex_data):
# Recover candidates per position by matching the corrupted bytes.
try:
data = bytes.fromhex(hex_data)
except ValueError:
print("[-] Error: Invalid Hex String")
sys.exit(1)
if len(data) < 28:
print("[-] Error: Data too short")
sys.exit(1)
key = 0x7A
pattern_list = []
print("[1] Decrypting file and analyzing collisions for pattern:")
for i in range(64):
if 12 <= i < 28:
target_byte = data[i]
candidates = []
for char in CHARSET:
if simulate_c_corruption(ord(char), key) == target_byte:
candidates.append(char)
pattern_list.append(candidates)
key = (17 * key + 31) & 0xFF
if i >= 27: break
pretty_str = ""
for p in pattern_list:
if len(p) == 0: pretty_str += "?"
elif len(p) == 1: pretty_str += p[0]
else: pretty_str += f"[?]"
print(f" {pretty_str}")
return pattern_list
def crack_rng_seed(pattern_list):
# PRNG is MSVC-style LCG; prune candidates using known-good positions.
print("[2] Cracking PRNG Seed based on unambiguous characters")
first_char_candidates = pattern_list[0]
first_char_indices = [CHARSET.index(c) for c in first_char_candidates]
valid_high_bits = []
for x in range(32768):
if (x % 36) in first_char_indices:
valid_high_bits.append(x)
print(f" {len(valid_high_bits) * 65536 / 1000000:.1f}M possible states")
start_time = time.time()
for high in valid_high_bits:
tops = [(high << 16), ((high | 0x8000) << 16)]
for base_state in tops:
for low in range(65536):
state = base_state | low
temp_state = state
match = True
for k in range(1, 16):
temp_state = (temp_state * LCG_A + LCG_C) & LCG_M
val = ((temp_state >> 16) & 0x7FFF) % 36
generated_char = CHARSET[val]
if generated_char not in pattern_list[k]:
match = False
break
if match:
# Recover the original seed from the first internal state.
inv_A = pow(LCG_A, -1, 2**32)
seed = ((state - LCG_C) * inv_A) & LCG_M
print(f"\n[+] Seed found: {seed} in {time.time() - start_time:.2f}s")
return seed
print("[-] Failed to find seed. Data might be too corrupted or header changed.")
return None
def generate_full_key(seed):
# Recreate the full 16-char license from the seed.
state = seed
full_key = ""
for _ in range(16):
state = (state * LCG_A + LCG_C) & LCG_M
val = ((state >> 16) & 0x7FFF) % 36
full_key += CHARSET[val]
return full_key
if __name__ == "__main__":
# Read the saved license, recover the seed, then rebuild the key.
hex = open("license.dat", "rb").read().hex()
pattern = extract_partial_pattern(hex)
seed = crack_rng_seed(pattern)
if seed is not None:
final_key = generate_full_key(seed)
print("-" * 40)
print(f"FINAL RECOVERED LICENSE: {final_key}")
print("-" * 40)
Output of my program:
$ python assets/solution/solve.py [1] Decrypting file and analyzing collisions for pattern: 8Q0B[?][?]X[?][?]TL[?]TJ[?][?] [2] Cracking PRNG Seed based on unambiguous characters 59.6M possible states [+] Seed found: 411352552 in 20.23s ---------------------------------------- FINAL RECOVERED LICENSE: 8Q0BFEXRPTLQTJSJ ----------------------------------------