Skip to content

Commit 64b0dc6

Browse files
committed
feat: add baseline anti-cheat protection for issue #2
Closes #2 Payouts: - EVM: 0xe744f6791a685b0A0cC316ED44375B69361c837F - SOL: 8BsByR6rPqxDPku6dYtdoiSk6bdgE9YETbLQF2RGSw1C PoA-Signature: poa_6c8f67fc40a11300
1 parent 8796d97 commit 64b0dc6

6 files changed

Lines changed: 210 additions & 2 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
*.py[cod]

‎AntiCheat.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <windows.h>
66
#include <tlhelp32.h>
7+
#include <intrin.h>
78
#include <iostream>
89
#include <fstream>
910
#include <vector>
@@ -134,7 +135,7 @@ int main() {
134135
if (JAVELIN_EXPECTED_CRC32 != 0u) {
135136
if (!checkSelfIntegrity(JAVELIN_EXPECTED_CRC32)) {
136137
std::cerr << kTag << "Integrity check failed (CRC mismatch). Exiting.\n";
137-
return 0xCRC; // custom code (note: non-standard, may be truncated)
138+
return 0x1CC; // non-zero error code for failed integrity checks
138139
}
139140
}
140141

‎README.md‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,44 @@
1-
# py-workedtask
1+
# py-workedtask
2+
3+
Minimal anti-cheat guards for the Javelin project.
4+
5+
## Included protections
6+
7+
- `AntiCheat.cpp`: debugger detection plus suspicious process scanning for the native client.
8+
- `anti_cheat.py`: debugger detection plus suspicious process scanning for the Python monitor.
9+
10+
Known suspicious tools currently blocked in both paths:
11+
12+
- `cheatengine.exe`
13+
- `ollydbg.exe`
14+
- `x64dbg.exe`
15+
- `httpdebuggerui.exe`
16+
- `ida.exe`
17+
- `ida64.exe`
18+
- `scylla.exe`
19+
- `processhacker.exe`
20+
21+
## Run the Python monitor
22+
23+
```powershell
24+
python anti_cheat.py
25+
```
26+
27+
Exit codes:
28+
29+
- `0x6B` when a debugger is detected
30+
- `0x6C` when a suspicious process is detected
31+
32+
## Test the Python monitor
33+
34+
```powershell
35+
python -m unittest discover -s . -p "test_*.py" -v
36+
```
37+
38+
## Build the C++ client
39+
40+
Example with MSVC:
41+
42+
```powershell
43+
cl /EHsc AntiCheat.cpp
44+
```

‎anti_cheat.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
3+
import ctypes
4+
import subprocess
5+
import sys
6+
from typing import Iterable, Optional
7+
8+
TAG = "[Javelin AntiCheat]"
9+
DEBUGGER_EXIT_CODE = 0x6B
10+
SUSPICIOUS_PROCESS_EXIT_CODE = 0x6C
11+
SUSPICIOUS_PROCESSES = {
12+
"cheatengine.exe",
13+
"ollydbg.exe",
14+
"x64dbg.exe",
15+
"httpdebuggerui.exe",
16+
"ida.exe",
17+
"ida64.exe",
18+
"scylla.exe",
19+
"processhacker.exe",
20+
}
21+
22+
23+
def is_windows() -> bool:
24+
return sys.platform.startswith("win")
25+
26+
27+
def debugger_attached(kernel32=None) -> bool:
28+
if not is_windows():
29+
return False
30+
31+
kernel32 = kernel32 or ctypes.windll.kernel32
32+
return bool(kernel32.IsDebuggerPresent())
33+
34+
35+
def list_process_names(tasklist_output: Optional[str] = None) -> list[str]:
36+
if not is_windows():
37+
return []
38+
39+
if tasklist_output is None:
40+
result = subprocess.run(
41+
["tasklist", "/fo", "csv", "/nh"],
42+
capture_output=True,
43+
check=True,
44+
text=True,
45+
)
46+
tasklist_output = result.stdout
47+
48+
names: list[str] = []
49+
for raw_line in tasklist_output.splitlines():
50+
line = raw_line.strip()
51+
if not line:
52+
continue
53+
if line.startswith('"'):
54+
line = line[1:]
55+
name = line.split('","', 1)[0].strip('"').lower()
56+
if name:
57+
names.append(name)
58+
return names
59+
60+
61+
def suspicious_process_found(
62+
process_names: Optional[Iterable[str]] = None,
63+
) -> Optional[str]:
64+
if process_names is None:
65+
process_names = list_process_names()
66+
67+
for name in process_names:
68+
lowered = name.lower()
69+
if lowered in SUSPICIOUS_PROCESSES:
70+
return lowered
71+
return None
72+
73+
74+
def run_checks() -> int:
75+
print(f"{TAG} starting checks...")
76+
77+
if debugger_attached():
78+
print(f"{TAG} Debugger detected. Exiting.")
79+
return DEBUGGER_EXIT_CODE
80+
81+
bad_process = suspicious_process_found()
82+
if bad_process:
83+
print(f"{TAG} Suspicious process detected: {bad_process}. Exiting.")
84+
return SUSPICIOUS_PROCESS_EXIT_CODE
85+
86+
print(f"{TAG} All clear. Continue.")
87+
return 0
88+
89+
90+
if __name__ == "__main__":
91+
raise SystemExit(run_checks())

‎tests/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎tests/test_anti_cheat.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import subprocess
2+
import unittest
3+
from unittest import mock
4+
5+
import anti_cheat
6+
7+
8+
class AntiCheatTests(unittest.TestCase):
9+
def test_list_process_names_parses_tasklist_csv(self) -> None:
10+
sample = '"explorer.exe","1234","Console","1","11,000 K"\n"x64dbg.exe","999","Console","1","4,096 K"\n'
11+
with mock.patch.object(anti_cheat, "is_windows", return_value=True):
12+
self.assertEqual(
13+
anti_cheat.list_process_names(sample),
14+
["explorer.exe", "x64dbg.exe"],
15+
)
16+
17+
def test_list_process_names_returns_empty_off_windows(self) -> None:
18+
with mock.patch.object(anti_cheat, "is_windows", return_value=False):
19+
self.assertEqual(anti_cheat.list_process_names(""), [])
20+
21+
def test_debugger_attached_uses_kernel32_on_windows(self) -> None:
22+
kernel32 = mock.Mock()
23+
kernel32.IsDebuggerPresent.return_value = 1
24+
with mock.patch.object(anti_cheat, "is_windows", return_value=True):
25+
self.assertTrue(anti_cheat.debugger_attached(kernel32=kernel32))
26+
27+
def test_debugger_attached_is_false_off_windows(self) -> None:
28+
kernel32 = mock.Mock()
29+
kernel32.IsDebuggerPresent.return_value = 1
30+
with mock.patch.object(anti_cheat, "is_windows", return_value=False):
31+
self.assertFalse(anti_cheat.debugger_attached(kernel32=kernel32))
32+
33+
def test_suspicious_process_found_detects_known_tool(self) -> None:
34+
found = anti_cheat.suspicious_process_found(["explorer.exe", "CheatEngine.exe"])
35+
self.assertEqual(found, "cheatengine.exe")
36+
37+
def test_suspicious_process_found_returns_none_when_clean(self) -> None:
38+
self.assertIsNone(anti_cheat.suspicious_process_found(["explorer.exe", "python.exe"]))
39+
40+
def test_run_checks_returns_debugger_exit_code(self) -> None:
41+
with mock.patch.object(anti_cheat, "debugger_attached", return_value=True):
42+
self.assertEqual(anti_cheat.run_checks(), anti_cheat.DEBUGGER_EXIT_CODE)
43+
44+
def test_run_checks_returns_suspicious_process_exit_code(self) -> None:
45+
with mock.patch.object(anti_cheat, "debugger_attached", return_value=False):
46+
with mock.patch.object(
47+
anti_cheat,
48+
"suspicious_process_found",
49+
return_value="x64dbg.exe",
50+
):
51+
self.assertEqual(
52+
anti_cheat.run_checks(),
53+
anti_cheat.SUSPICIOUS_PROCESS_EXIT_CODE,
54+
)
55+
56+
def test_run_checks_returns_success_when_clean(self) -> None:
57+
with mock.patch.object(anti_cheat, "debugger_attached", return_value=False):
58+
with mock.patch.object(anti_cheat, "suspicious_process_found", return_value=None):
59+
self.assertEqual(anti_cheat.run_checks(), 0)
60+
61+
def test_tasklist_command_failure_propagates(self) -> None:
62+
with mock.patch.object(anti_cheat, "is_windows", return_value=True):
63+
with mock.patch("anti_cheat.subprocess.run") as run:
64+
run.side_effect = subprocess.CalledProcessError(1, "tasklist")
65+
with self.assertRaises(subprocess.CalledProcessError):
66+
anti_cheat.list_process_names()
67+
68+
69+
if __name__ == "__main__":
70+
unittest.main()

0 commit comments

Comments
 (0)