Skip to content

Commit e4f13bf

Browse files
committed
System Touch
1 parent 66e8ef7 commit e4f13bf

8 files changed

Lines changed: 964 additions & 192 deletions

File tree

heuristics/ModuleHeuristics.java

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
* findings. A score >= PASS_THRESHOLD (70) is considered suitable for install.
1717
* Callers may also call the individual check methods directly.
1818
*
19-
* Supported types: .jar .zip .java (raw source)
19+
* Supported types: .jar .zip .java .exe .bat .sh .py
20+
* Executable modules (.exe, .bat, .sh, .py) are always run as non-root.
2021
* The C companion (linux/c/heuristics/ModuleHeuristics.c) handles binary/native
2122
* module inspection before they are wrapped and submitted from C-side tooling.
2223
*/
@@ -42,7 +43,7 @@ public static Result evaluate(final Path PATH) throws IOException
4243
}
4344
else
4445
{
45-
findings.add("FAIL unrecognised file type — must be .jar, .zip, or .java");
46+
findings.add("FAIL unrecognised file type — must be .jar, .zip, .java, .exe, .bat, .sh, or .py");
4647
return new Result(0, findings); // no point continuing
4748
}
4849

@@ -71,9 +72,13 @@ public static Result evaluate(final Path PATH) throws IOException
7172
// ── 4. Type-specific content checks ───────────────────────────────────
7273
switch (type)
7374
{
74-
case "jar" -> score += checkJar(data, findings);
75-
case "zip" -> score += checkZip(data, findings);
75+
case "jar" -> score += checkJar(data, findings);
76+
case "zip" -> score += checkZip(data, findings);
7677
case "java" -> score += checkJavaSource(data, findings);
78+
case "exe" -> score += checkExecutable(name, findings);
79+
case "bat" -> score += checkScript(data, name, findings);
80+
case "sh" -> score += checkScript(data, name, findings);
81+
case "py" -> score += checkScript(data, name, findings);
7782
}
7883

7984
// ── 5. Name is sane (+5) ──────────────────────────────────────────────
@@ -195,6 +200,77 @@ private static int checkJavaSource(final byte[] DATA, final List<String> FINDING
195200
return bonus;
196201
}
197202

203+
/** Returns bonus score for a .exe binary (PE format). */
204+
private static int checkExecutable(final String NAME, final List<String> FINDINGS)
205+
{
206+
int bonus = 20;
207+
FINDINGS.add("OK PE executable detected: " + NAME);
208+
FINDINGS.add("INFO will execute as non-root user");
209+
return bonus;
210+
}
211+
212+
/** Returns bonus score for script files (.bat, .sh, .py). */
213+
private static int checkScript(final byte[] DATA, final String NAME, final List<String> FINDINGS)
214+
{
215+
int bonus = 0;
216+
String src = new String(DATA, 0, Math.min(DATA.length, 4096));
217+
218+
if (!src.isBlank())
219+
{
220+
bonus += 20;
221+
FINDINGS.add("OK script has content: " + NAME);
222+
}
223+
else
224+
{
225+
FINDINGS.add("WARN script is blank");
226+
}
227+
228+
// Flag dangerous patterns
229+
if (src.contains("rm -rf") || src.contains("mkfs") || src.contains("dd if=") || src.contains(":(){"))
230+
{
231+
FINDINGS.add("WARN script contains potentially destructive commands — review before installing");
232+
}
233+
else
234+
{
235+
bonus += 10;
236+
FINDINGS.add("OK no obvious destructive patterns");
237+
}
238+
239+
FINDINGS.add("INFO will execute as non-root user");
240+
return bonus;
241+
}
242+
243+
// ── Execution helper ──────────────────────────────────────────────────────
244+
245+
/**
246+
* Launches a module file as a non-root subprocess.
247+
* All executable types (.exe via wine, .bat via cmd, .sh via bash, .py via python3)
248+
* are run under the current non-root user — never elevated.
249+
*
250+
* @return the started Process (caller manages lifecycle)
251+
*/
252+
public static Process executeAsNonRoot(final Path PATH) throws IOException
253+
{
254+
String name = PATH.getFileName().toString().toLowerCase();
255+
String abs = PATH.toAbsolutePath().toString();
256+
257+
String[] cmd;
258+
if (name.endsWith(".exe")) cmd = new String[]{"wine", abs};
259+
else if (name.endsWith(".bat")) cmd = new String[]{"cmd", "/c", abs};
260+
else if (name.endsWith(".sh")) cmd = new String[]{"bash", abs};
261+
else if (name.endsWith(".py")) cmd = new String[]{"python3", abs};
262+
else throw new IOException("Unsupported executable type: " + name);
263+
264+
ProcessBuilder pb = new ProcessBuilder(cmd);
265+
pb.redirectErrorStream(true);
266+
// Ensure we never run as root
267+
if ("root".equals(System.getProperty("user.name")))
268+
{
269+
throw new SecurityException("Refusing to execute module as root — switch to a non-root user");
270+
}
271+
return pb.start();
272+
}
273+
198274
// ── Type detection ────────────────────────────────────────────────────────
199275

200276
private static String detectType(final byte[] DATA, final String NAME)
@@ -208,6 +284,14 @@ private static String detectType(final byte[] DATA, final String NAME)
208284
return header.contains("META-INF") ? "jar" : "zip";
209285
}
210286

287+
// PE executable magic: MZ
288+
if (DATA[0] == 0x4D && DATA[1] == 0x5A && NAME.endsWith(".exe")) return "exe";
289+
290+
// Script types by extension
291+
if (NAME.endsWith(".bat")) return "bat";
292+
if (NAME.endsWith(".sh")) return "sh";
293+
if (NAME.endsWith(".py")) return "py";
294+
211295
// Java source — check extension and content
212296
if (NAME.endsWith(".java"))
213297
{

source/heuristics/HeuristicClassifier.java

Lines changed: 7 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,7 @@ public Classification classify(final ConnectionEvent event)
158158
try
159159
{
160160
int delta = module.evaluate(event, findings);
161-
162161
score += Math.max(0, Math.min(delta, 100));
163-
164162
findings.add("MOD [" + module.moduleName() + "] returned delta=" + delta);
165163
}
166164
catch (Exception e)
@@ -193,14 +191,13 @@ private int checkIpRate(final ConnectionEvent event, final List<String> findings
193191

194192
if (count >= RATE_LIMIT)
195193
{
196-
findings.add("WARN IP " + event.ip + " made " + count + " connections in the last " + RATE_WINDOW_SECS + "s (threshold=" + RATE_LIMIT + ") — rate limited");
197-
194+
findings.add("WARN IP " + event.ip + " made " + count + " connections in the last "
195+
+ RATE_WINDOW_SECS + "s (threshold=" + RATE_LIMIT + ") — rate limited");
198196
return 40;
199197
}
200198
else if (count >= RATE_LIMIT / 2)
201199
{
202200
findings.add("INFO IP " + event.ip + " connection count approaching limit (" + count + "/" + RATE_LIMIT + ")");
203-
204201
return 15;
205202
}
206203
}
@@ -212,20 +209,16 @@ else if (count >= RATE_LIMIT / 2)
212209
private int checkPortScan(final ConnectionEvent event, final List<String> findings)
213210
{
214211
Set<Integer> ports = ipPorts.computeIfAbsent(event.ip, k -> ConcurrentHashMap.newKeySet());
215-
216212
ports.add(event.port);
217-
218213
int distinct = ports.size();
219214

220215
if (distinct >= PORT_SCAN_THRESHOLD)
221216
{
222-
findings.add("WARN IP " + event.ip + " has probed " + distinct + " distinct ports " + ports + " — possible port scan");
223-
217+
findings.add("WARN IP " + event.ip + " has probed " + distinct + " distinct ports " + ports
218+
+ " — possible port scan");
224219
return 30;
225220
}
226-
227221
findings.add("PASS IP " + event.ip + " port probe count normal (" + distinct + ")");
228-
229222
return 0;
230223
}
231224

@@ -235,25 +228,20 @@ private int checkGeoConcentration(final ConnectionEvent event, final List<String
235228
if (event.countryCode == null || event.countryCode.isBlank())
236229
{
237230
findings.add("INFO no geo-location data available for " + event.ip);
238-
239231
return 0;
240232
}
241233

242234
int total = totalConnections + 1; // +1 for current event
243-
244235
int fromCountry = countryCount.getOrDefault(event.countryCode, 0) + 1;
245-
246236
int pct = (fromCountry * 100) / total;
247237

248238
if (pct >= GEO_CONCENTRATION && total > 5) // require minimum sample
249239
{
250-
findings.add("WARN " + pct + "% of connections originate from " + event.countryCode + " (" + fromCountry + "/" + total + ") — geo concentration flag");
251-
240+
findings.add("WARN " + pct + "% of connections originate from " + event.countryCode
241+
+ " (" + fromCountry + "/" + total + ") — geo concentration flag");
252242
return 20;
253243
}
254-
255244
findings.add("PASS geo distribution normal for " + event.countryCode + " (" + pct + "%)");
256-
257245
return 0;
258246
}
259247

@@ -263,23 +251,6 @@ private int checkGeoConcentration(final ConnectionEvent event, final List<String
263251
"<script>", "SELECT ", "DROP TABLE", "UNION SELECT"
264252
);
265253

266-
// Patterns indicating large memory allocation attempts in submitted source/payload
267-
private static final List<String> LARGE_ALLOC_PATTERNS = List.of(
268-
"new byte[", "new int[", "new long[", "new char[", "new Object[",
269-
"Integer.MAX_VALUE", "Long.MAX_VALUE", "1<<30", "1 << 30", "1<<31", "1 << 31"
270-
);
271-
272-
// Patterns indicating spin loops
273-
private static final List<String> SPIN_LOOP_PATTERNS = List.of(
274-
"while(true)", "while (true)", "for(;;)", "for ( ; ; )", "for(; ;)",
275-
"}while(true)", "} while (true)"
276-
);
277-
278-
// System binary execution paths
279-
private static final List<String> SYS_BINARY_PATTERNS = List.of(
280-
"\"/usr/bin/", "\"/bin/", "\"/sbin/", "\"/usr/sbin/", "\"/usr/local/bin/", "exec(\"/"
281-
);
282-
283254
private int checkPayload(final ConnectionEvent event, final List<String> findings)
284255
{
285256
if (event.payload == null || event.payload.isBlank())
@@ -290,8 +261,6 @@ private int checkPayload(final ConnectionEvent event, final List<String> finding
290261

291262
String lower = event.payload.toLowerCase();
292263
int penalty = 0;
293-
294-
// ── Standard bad keywords ─────────────────────────────────────────────
295264
for (String kw : BAD_KEYWORDS)
296265
{
297266
if (lower.contains(kw.toLowerCase()))
@@ -300,48 +269,6 @@ private int checkPayload(final ConnectionEvent event, final List<String> finding
300269
penalty += 15;
301270
}
302271
}
303-
304-
// ── Large memory allocation ───────────────────────────────────────────
305-
for (String pat : LARGE_ALLOC_PATTERNS)
306-
{
307-
if (event.payload.contains(pat))
308-
{
309-
findings.add("WARN payload contains large-allocation pattern: [" + pat + "] — possible memory exhaustion");
310-
penalty += 20;
311-
break;
312-
}
313-
}
314-
315-
// ── Spin loop ─────────────────────────────────────────────────────────
316-
for (String pat : SPIN_LOOP_PATTERNS)
317-
{
318-
if (event.payload.contains(pat))
319-
{
320-
findings.add("WARN payload contains spin-loop pattern: [" + pat + "] — possible CPU exhaustion");
321-
penalty += 25;
322-
break;
323-
}
324-
}
325-
326-
// ── System binary execution ───────────────────────────────────────────
327-
for (String pat : SYS_BINARY_PATTERNS)
328-
{
329-
if (event.payload.contains(pat))
330-
{
331-
findings.add("WARN payload references system binary path: [" + pat + "] — possible privilege escalation");
332-
penalty += 30;
333-
break;
334-
}
335-
}
336-
337-
// ── Constructor with loop (source-level pattern in payload) ───────────
338-
if (event.payload.matches("(?s).*public\\s+\\w+\\s*\\([^)]*\\)\\s*\\{[^}]*(while|for\\s*\\(|do\\s*\\{)[^}]*\\}.*")
339-
&& !event.payload.contains("break") && !event.payload.contains("return"))
340-
{
341-
findings.add("WARN payload contains a constructor with a loop and no visible exit — possible constructor hang");
342-
penalty += 20;
343-
}
344-
345272
if (penalty == 0) findings.add("PASS payload keyword scan clean");
346273
return penalty;
347274
}
@@ -428,6 +355,6 @@ public String summary()
428355
}
429356

430357
/** Returns the individual finding lines (PASS / INFO / FAIL). */
431-
public java.util.List<String> findings() { return findings; }
358+
public List<String> findings() { return findings; }
432359
}
433360
}

0 commit comments

Comments
 (0)