add initial implementation of GCNet packet decryptor with pcapng support
This commit is contained in:
commit
acf58a7085
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# =====================
|
||||||
|
# Java / Maven
|
||||||
|
# =====================
|
||||||
|
target/
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# IDE - JetBrains (IntelliJ, etc.)
|
||||||
|
# =====================
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Eclipse
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# NetBeans
|
||||||
|
nbproject/
|
||||||
|
nbbuild/
|
||||||
|
dist/
|
||||||
|
nbdist/
|
||||||
|
.nb-gradle/
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# OS Files
|
||||||
|
# =====================
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Test / Capture Files
|
||||||
|
# =====================
|
||||||
|
*.pcapng
|
||||||
|
*.pcap
|
||||||
|
*.cap
|
||||||
|
*.dump
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Miscellaneous
|
||||||
|
# =====================
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.recommenders
|
||||||
134
README.md
Normal file
134
README.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# GCNet Packet Decryptor
|
||||||
|
|
||||||
|
A Java tool to decrypt and analyze GCNet (Grand Chase) packets from pcapng capture files.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This tool reads pcapng files containing network captures of Grand Chase game traffic, filters TCP packets on a specified port (default: 8501), and decrypts them using the GCNet security protocol. It automatically:
|
||||||
|
|
||||||
|
1. Parses pcapng file format
|
||||||
|
2. Extracts TCP segments and filters by port
|
||||||
|
3. Detects the initial key exchange packet (opcode 1) to obtain session keys
|
||||||
|
4. Decrypts all subsequent packets using DES-CBC
|
||||||
|
5. Validates packet integrity using MD5-HMAC ICV
|
||||||
|
6. Decompresses zlib-compressed payloads
|
||||||
|
7. Displays decrypted packet contents in human-readable format
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates two JAR files in `target/`:
|
||||||
|
- `gcnet-decryptor-1.0.0.jar` - Standalone JAR (requires dependencies)
|
||||||
|
- `gcnet-decryptor-1.0.0-jar-with-dependencies.jar` - Fat JAR with all dependencies (recommended)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar <pcapng-file> [port]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `<pcapng-file>`: Path to the pcapng capture file (required)
|
||||||
|
- `[port]`: TCP port to filter on (default: 8501)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Decrypt packets on default port 8501
|
||||||
|
java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar capture.pcapng
|
||||||
|
|
||||||
|
# Decrypt packets on custom port
|
||||||
|
java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar capture.pcapng 9001
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### GCNet Protocol Structure
|
||||||
|
|
||||||
|
The GCNet protocol has two main layers:
|
||||||
|
|
||||||
|
#### 1. Security Layer
|
||||||
|
- **Size** (2 bytes): Total security layer size
|
||||||
|
- **SPI** (2 bytes): Security Parameters Index
|
||||||
|
- **Sequence Number** (4 bytes): Packet counter
|
||||||
|
- **IV** (8 bytes): DES initialization vector
|
||||||
|
- **Encrypted Payload** (variable): DES-CBC encrypted data
|
||||||
|
- **ICV** (10 bytes): Integrity check value (MD5-HMAC truncated)
|
||||||
|
|
||||||
|
#### 2. Payload Layer
|
||||||
|
- **Opcode** (2 bytes): Packet type identifier
|
||||||
|
- **Content Size** (4 bytes): Size of content
|
||||||
|
- **Compression Flag** (1 byte): Whether content is zlib-compressed
|
||||||
|
- **Content** (variable): Actual data (possibly compressed)
|
||||||
|
- **Null Padding** (4 bytes): End padding
|
||||||
|
|
||||||
|
### Key Exchange
|
||||||
|
|
||||||
|
The first packet (opcode 1) contains the session keys:
|
||||||
|
- Sent by server using default keys
|
||||||
|
- Contains new SPI, authentication key, and encryption key
|
||||||
|
- All subsequent packets use these new keys
|
||||||
|
|
||||||
|
**Default Keys:**
|
||||||
|
- Encryption Key: `C7 D8 C4 BF B5 E9 C0 FD`
|
||||||
|
- Authentication Key: `C0 D3 BD C3 B7 CE B8 B8`
|
||||||
|
|
||||||
|
### Encryption
|
||||||
|
|
||||||
|
- **Algorithm**: DES in CBC mode
|
||||||
|
- **Padding**: Custom GCNet padding scheme (incrementing bytes)
|
||||||
|
- **Integrity**: MD5-HMAC truncated to 10 bytes
|
||||||
|
|
||||||
|
### Compression
|
||||||
|
|
||||||
|
- **Algorithm**: zlib
|
||||||
|
- **Header**: `78 01`
|
||||||
|
- **Structure**: First 4 bytes indicate decompressed size (little-endian)
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
For each packet, the tool displays:
|
||||||
|
- Source/destination IP and port
|
||||||
|
- TCP sequence number
|
||||||
|
- SPI and IV values
|
||||||
|
- ICV validation status
|
||||||
|
- Opcode and content size
|
||||||
|
- Hex dump of decrypted content
|
||||||
|
- Extracted readable strings
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
gcnet-decryptor/
|
||||||
|
├── pom.xml
|
||||||
|
└── src/main/java/com/gcnet/decryptor/
|
||||||
|
├── GCNetDecryptor.java # Main application
|
||||||
|
├── pcapng/
|
||||||
|
│ ├── PcapngParser.java # pcapng file parser (wraps pcapngdecoder)
|
||||||
|
│ └── TcpPacketParser.java # TCP segment extractor
|
||||||
|
├── security/
|
||||||
|
│ └── GCNetSecurityAssociation.java # Decryption & ICV validation
|
||||||
|
└── payload/
|
||||||
|
└── GCNetPayloadParser.java # Payload parser & decompression
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **[pcapng-decoder](https://github.com/bertrandmartel/pcapng-decoder)** by Bertrand Martel (MIT License) - Pure Java pcapng file parser
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
Based on the [GCNet](https://github.com/frihet/GCNet) library by Gabriel F. (Frihet Dev), licensed under AGPL-3.0.
|
||||||
|
|
||||||
|
Protocol documentation:
|
||||||
|
- [The Security Layer](../GCNet/docs/en/The%20Security%20Layer.md)
|
||||||
|
- [The Cryptography](../GCNet/docs/en/The%20Cryptography.md)
|
||||||
|
- [The Payload Layer](../GCNet/docs/en/The%20Payload%20Layer.md)
|
||||||
|
- [The Security Protocol Setup](../GCNet/docs/en/The%20Security%20Protocol%20Setup.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided as-is for educational and analysis purposes.
|
||||||
213
docs/ADDING_PACKET_PARSERS.md
Normal file
213
docs/ADDING_PACKET_PARSERS.md
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# Adding New Packet Parsers
|
||||||
|
|
||||||
|
This guide explains how to implement parsers for new GCNet packet types.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The packet parsing system uses several design patterns:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ GCNetPacketProcessor │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||||
|
│ │ PacketParserFactory (Factory) ││
|
||||||
|
│ │ ┌────────────┐ ┌────────────┐ ┌──────────────────────────┐ ││
|
||||||
|
│ │ │ PingParser │ │ PongParser │ │ VerifyAccountRequestParser│ ││
|
||||||
|
│ │ └────────────┘ └────────────┘ └──────────────────────────┘ ││
|
||||||
|
│ │ Strategy Pattern ││
|
||||||
|
│ └─────────────────────────────────────────────────────────────┘│
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ PacketContext │
|
||||||
|
│ • decrypted payload bytes │
|
||||||
|
│ • TCP segment (src/dst IP:port) │
|
||||||
|
│ • server port for direction detection │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design Patterns Used
|
||||||
|
|
||||||
|
| Pattern | Class | Purpose |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| **Strategy** | `PacketParser` interface | Each opcode has its own parsing strategy |
|
||||||
|
| **Factory** | `PacketParserFactory` | Creates the correct parser for each opcode |
|
||||||
|
| **Context** | `PacketContext` | Immutable context with payload + connection metadata |
|
||||||
|
| **Registry** | `GCNetOpcode` enum | Maps opcode numbers to metadata and direction hints |
|
||||||
|
| **Null Object** | `GenericPayloadParser` | Fallback for unimplemented opcodes |
|
||||||
|
|
||||||
|
## Step-by-Step: Adding a New Parser
|
||||||
|
|
||||||
|
### 1. Define the Opcode
|
||||||
|
|
||||||
|
Add the opcode to `GCNetOpcode.java`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// In packets.com.gcpp.GCNetOpcode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: Player join request.
|
||||||
|
* Structure: Player name (string), Character class (int), Level (short)
|
||||||
|
*/
|
||||||
|
ENU_PLAYER_JOIN_REQ(50, "ENU_PLAYER_JOIN_REQ",
|
||||||
|
"Player join request", Direction.CLIENT_TO_SERVER),
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Direction` enum indicates expected traffic flow:
|
||||||
|
- `CLIENT_TO_SERVER` — Sent from client **to** server port (dstPort == serverPort)
|
||||||
|
- `SERVER_TO_CLIENT` — Sent from server **from** server port (srcPort == serverPort)
|
||||||
|
- `BIDIRECTIONAL` — Valid both ways (e.g., PING/PONG)
|
||||||
|
|
||||||
|
### 2. Create the Parser
|
||||||
|
|
||||||
|
Create a new class in `packets/parsers/`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.gcnet.decryptor.packets.parsers;
|
||||||
|
|
||||||
|
import packets.com.gcemu.gcpp.GCNetOpcode;
|
||||||
|
import packets.com.gcemu.gcpp.PacketContext;
|
||||||
|
import packets.com.gcemu.gcpp.PacketParser;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for ENU_PLAYER_JOIN_REQ packets (opcode 50).
|
||||||
|
*
|
||||||
|
* <h3>Packet Structure:</h3>
|
||||||
|
* <table border="1">
|
||||||
|
* <tr><th>Offset</th><th>Size</th><th>Type</th><th>Description</th></tr>
|
||||||
|
* <tr><td>0</td><td>2</td><td>ushort (BE)</td><td>Opcode (50)</td></tr>
|
||||||
|
* <tr><td>2</td><td>4</td><td>int (BE)</td><td>Content size</td></tr>
|
||||||
|
* <tr><td>6</td><td>1</td><td>bool</td><td>Compression flag</td></tr>
|
||||||
|
* <tr><td>7</td><td>4</td><td>int (LE)</td><td>Player name byte length</td></tr>
|
||||||
|
* <tr><td>11</td><td>var</td><td>string (UTF-16LE)</td><td>Player name</td></tr>
|
||||||
|
* <tr><td>var</td><td>4</td><td>int (LE)</td><td>Character class ID</td></tr>
|
||||||
|
* <tr><td>var</td><td>2</td><td>short (LE)</td><td>Character level</td></tr>
|
||||||
|
* </table>
|
||||||
|
*
|
||||||
|
* <p>Direction: {@link GCNetOpcode.Direction#CLIENT_TO_SERVER}</p>
|
||||||
|
*/
|
||||||
|
@PacketParser.PacketParserFor(opcode = 50)
|
||||||
|
public class PlayerJoinRequestParser implements PacketParser {
|
||||||
|
|
||||||
|
private static final int CONTENT_OFFSET = 7;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParseResult parse(PacketContext context) {
|
||||||
|
byte[] payload = context.decryptedPayload();
|
||||||
|
|
||||||
|
if (payload.length < CONTENT_OFFSET + 10) {
|
||||||
|
return ParseResult.empty(GCNetOpcode.ENU_PLAYER_JOIN_REQ, "Client → Server");
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = CONTENT_OFFSET;
|
||||||
|
Map<String, String> fields = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Parse player name
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(payload, offset, 4)
|
||||||
|
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
|
||||||
|
int nameLen = bb.getInt();
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
if (offset + nameLen <= payload.length) {
|
||||||
|
String playerName = new String(payload, offset, nameLen, java.nio.charset.StandardCharsets.UTF_16LE);
|
||||||
|
fields.put("Player Name", playerName);
|
||||||
|
offset += nameLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse character class
|
||||||
|
bb = ByteBuffer.wrap(payload, offset, 4)
|
||||||
|
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
|
||||||
|
int charClass = bb.getInt();
|
||||||
|
fields.put("Character Class", String.valueOf(charClass));
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
// Parse level
|
||||||
|
bb = ByteBuffer.wrap(payload, offset, 2)
|
||||||
|
.order(java.nio.ByteOrder.LITTLE_ENDIAN);
|
||||||
|
short level = bb.getShort();
|
||||||
|
fields.put("Level", String.valueOf(level));
|
||||||
|
|
||||||
|
return ParseResult.parsed(
|
||||||
|
GCNetOpcode.ENU_PLAYER_JOIN_REQ,
|
||||||
|
"Client → Server",
|
||||||
|
"ENU_PLAYER_JOIN_REQ { name: string_utf16_le, class_id: int32, level: int16 }",
|
||||||
|
fields,
|
||||||
|
formatContentHex(payload),
|
||||||
|
"player=\"" + fields.getOrDefault("Player Name", "?") + "\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatContentHex(byte[] data) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
if (i % 16 == 0 && i > 0) sb.append("\n");
|
||||||
|
sb.append(String.format("%02X ", data[i]));
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register the Parser
|
||||||
|
|
||||||
|
Add registration in `PacketParserFactory.registerBuiltInParsers()`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private void registerBuiltInParsers() {
|
||||||
|
// ... existing registrations ...
|
||||||
|
|
||||||
|
// Player Management
|
||||||
|
registerParser(50, new PlayerJoinRequestParser());
|
||||||
|
registerParser(51, new PlayerJoinAckParser());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Direction Detection
|
||||||
|
|
||||||
|
The `PacketContext` automatically determines direction:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public boolean isClientToServer() {
|
||||||
|
return segment.dstPort() == serverPort; // Destination is the server
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isServerToClient() {
|
||||||
|
return segment.srcPort() == serverPort; // Source is the server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a server on port 9501:
|
||||||
|
- `10.0.1.10:51094 → 10.0.1.10:9501` → **Client → Server** (dstPort == 9501)
|
||||||
|
- `10.0.1.10:9501 → 10.0.1.10:51094` → **Server → Client** (srcPort == 9501)
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/java/com/gcnet/decryptor/packets/
|
||||||
|
├── GCNetOpcode.java ← Opcode registry (add new opcodes here)
|
||||||
|
├── PacketContext.java ← Immutable context object
|
||||||
|
├── PacketParser.java ← Strategy interface + annotation
|
||||||
|
├── PacketParserFactory.java ← Factory (register new parsers here)
|
||||||
|
└── parsers/
|
||||||
|
├── GenericPayloadParser.java ← Fallback for unknown opcodes
|
||||||
|
├── KeyExchangeParser.java ← Opcode 1
|
||||||
|
├── PingParser.java ← Opcode 39
|
||||||
|
├── PongParser.java ← Opcode 40
|
||||||
|
├── VerifyAccountRequestParser.java ← Opcode 34
|
||||||
|
└── VerifyAccountAckParser.java ← Opcode 35
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the decryptor with your capture file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar capture.pcapng 9501
|
||||||
|
```
|
||||||
|
|
||||||
|
New parsers will automatically be invoked for their registered opcodes, producing structured field output instead of raw hex dumps.
|
||||||
85
pom.xml
Normal file
85
pom.xml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.gcemu</groupId>
|
||||||
|
<artifactId>gcpp</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.44</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>fr.bmartel</groupId>
|
||||||
|
<artifactId>pcapngdecoder</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.44</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.gcemu.gcpp.GCPacketParser</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.gcemu.gcpp.GCPacketParser</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>make-assembly</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
37
src/main/java/com/gcemu/gcpp/CliArguments.java
Normal file
37
src/main/java/com/gcemu/gcpp/CliArguments.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package com.gcemu.gcpp;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class CliArguments {
|
||||||
|
private static final int DEFAULT_PORT = 9501;
|
||||||
|
|
||||||
|
private final File pcapngFile;
|
||||||
|
private final int targetPort;
|
||||||
|
|
||||||
|
public CliArguments(String[] args) {
|
||||||
|
if (args.length < 1) {
|
||||||
|
printUsage();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pcapngFile = new File(args[0]);
|
||||||
|
this.targetPort = args.length > 1 ? Integer.parseInt(args[1]) : DEFAULT_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validate() {
|
||||||
|
if (!pcapngFile.exists() || !pcapngFile.isFile()) {
|
||||||
|
System.err.println("Error: File not found or not accessible: " + pcapngFile.getPath());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printUsage() {
|
||||||
|
System.out.println("Usage: java -jar gcnet-decryptor.jar <pcapng-file> [port]");
|
||||||
|
System.out.println(" <pcapng-file>: Path to the pcapng capture file");
|
||||||
|
System.out.println(" [port]: TCP port to filter on (default: " + DEFAULT_PORT + ")");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/main/java/com/gcemu/gcpp/GCPacketParser.java
Normal file
86
src/main/java/com/gcemu/gcpp/GCPacketParser.java
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package com.gcemu.gcpp;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.pcapng.TcpPacketParser;
|
||||||
|
|
||||||
|
public class GCPacketParser {
|
||||||
|
private final PacketExtractor extractor = new PacketExtractor();
|
||||||
|
private final OutputFormatter formatter = new OutputFormatter();
|
||||||
|
|
||||||
|
private final CliArguments cliArgs;
|
||||||
|
private final PacketProcessor processor;
|
||||||
|
|
||||||
|
public GCPacketParser(String[] args) {
|
||||||
|
this.cliArgs = new CliArguments(args);
|
||||||
|
this.processor = new PacketProcessor(cliArgs.getTargetPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
cliArgs.validate();
|
||||||
|
|
||||||
|
formatter.printHeader(cliArgs.getPcapngFile().getName(), cliArgs.getTargetPort());
|
||||||
|
|
||||||
|
try {
|
||||||
|
var extractionResult = extractor.extract(cliArgs.getPcapngFile(), cliArgs.getTargetPort());
|
||||||
|
formatter.printParsingPhase(extractionResult.totalCaptured());
|
||||||
|
formatter.printExtractionPhase(extractionResult.tcpSegmentsFound(), extractionResult.filteredSegments().size());
|
||||||
|
|
||||||
|
if (extractionResult.filteredSegments().isEmpty()) {
|
||||||
|
formatter.printNoSegmentsFound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.printDecryptionPhase();
|
||||||
|
processPackets(extractionResult.filteredSegments());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Error processing file: " + e.getMessage());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processPackets(java.util.List<TcpPacketParser.TcpSegment> segments) {
|
||||||
|
var packetCount = 0;
|
||||||
|
var decryptedCount = 0;
|
||||||
|
var failedCount = 0;
|
||||||
|
|
||||||
|
for (TcpPacketParser.TcpSegment segment : segments) {
|
||||||
|
packetCount++;
|
||||||
|
formatter.printPacketHeader(packetCount, segment, segment.payload().length);
|
||||||
|
|
||||||
|
var result = processor.process(segment);
|
||||||
|
|
||||||
|
if (result.isTooSmall()) {
|
||||||
|
formatter.printSkippedPacket();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
formatter.printDecryptionError(result.errorMessage());
|
||||||
|
failedCount++;
|
||||||
|
System.out.println();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.printDecryptionResult(result.decryptionResult());
|
||||||
|
|
||||||
|
if (result.isKeyExchangeDetected()) {
|
||||||
|
formatter.printKeyExchangeDetected();
|
||||||
|
if (processor.isKeyExchangeProcessed()) {
|
||||||
|
formatter.printKeyExchangeSuccess();
|
||||||
|
} else {
|
||||||
|
formatter.printKeyExchangeFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.printParsedPacket(result.parseResult());
|
||||||
|
decryptedCount++;
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.printSummary(packetCount, decryptedCount, failedCount, processor.isKeyExchangeProcessed());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void main(String[] args) {
|
||||||
|
new GCPacketParser(args).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/main/java/com/gcemu/gcpp/OutputFormatter.java
Normal file
148
src/main/java/com/gcemu/gcpp/OutputFormatter.java
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package com.gcemu.gcpp;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.packets.PacketParser;
|
||||||
|
import com.gcemu.gcpp.pcapng.TcpPacketParser;
|
||||||
|
import com.gcemu.gcpp.security.SecurityAssociation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles formatted console output for the parser.
|
||||||
|
*/
|
||||||
|
public class OutputFormatter {
|
||||||
|
public void printHeader(String fileName, int targetPort) {
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("GCEmu Packet Parser");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("Input file: " + fileName);
|
||||||
|
System.out.println("Target port: " + targetPort);
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printParsingPhase(int totalPackets) {
|
||||||
|
System.out.println("[1/4] Parsing pcapng file...");
|
||||||
|
System.out.println(" Total packets captured: " + totalPackets);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printExtractionPhase(int tcpFound, int filteredCount) {
|
||||||
|
System.out.println("[2/4] Extracting TCP segments...");
|
||||||
|
System.out.println(" TCP segments found: " + tcpFound);
|
||||||
|
System.out.println(" Segments on target port: " + filteredCount);
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printNoSegmentsFound() {
|
||||||
|
System.out.println("No TCP segments found on target port");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printDecryptionPhase() {
|
||||||
|
System.out.println("[3/4] Parsing packets...");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printPacketHeader(int packetNum, TcpPacketParser.TcpSegment segment, int payloadSize) {
|
||||||
|
System.out.println("────────────────────────────────────");
|
||||||
|
System.out.println("Packet #" + packetNum);
|
||||||
|
System.out.println("────────────────────────────────────");
|
||||||
|
System.out.println(segment);
|
||||||
|
System.out.println("TCP Payload Size: " + payloadSize + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printSkippedPacket() {
|
||||||
|
System.out.println(" [SKIP] Too small to be a GCNet packet");
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printDecryptionResult(SecurityAssociation.DecryptionResult result) {
|
||||||
|
System.out.println(" SPI: 0x" + String.format("%04X", result.spi()));
|
||||||
|
System.out.println(" IV: " + bytesToHex(result.iv()));
|
||||||
|
System.out.println(" ICV Valid: " + result.icvValid());
|
||||||
|
System.out.println(" Decrypted Payload Size: " + result.decryptedPayload().length + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printKeyExchangeDetected() {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" *** KEY EXCHANGE PACKET DETECTED (Opcode 1) ***");
|
||||||
|
System.out.println(" Extracting session keys for subsequent packets...");
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printKeyExchangeSuccess() {
|
||||||
|
System.out.println(" [OK] Session keys extracted - all following packets will use these");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printKeyExchangeFailure() {
|
||||||
|
System.out.println(" [WARN] Failed to parse key exchange, using default keys");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printParsedPacket(PacketParser.ParseResult parseResult) {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" ┌─────────────────────────────────────────────────");
|
||||||
|
System.out.println(" │ Packet: " + parseResult.opcodeName());
|
||||||
|
System.out.println(" │ Opcode: 0x" + String.format("%04X", parseResult.opcode().getOpcode()) +
|
||||||
|
" (" + parseResult.opcode().getOpcode() + ")");
|
||||||
|
System.out.println(" │ Direction: " + parseResult.direction());
|
||||||
|
System.out.println(" │ Structure: " + parseResult.structure());
|
||||||
|
System.out.println(" ├─────────────────────────────────────────────────");
|
||||||
|
|
||||||
|
if (!parseResult.fields().isEmpty()) {
|
||||||
|
System.out.println(" │ Fields:");
|
||||||
|
for (var entry : parseResult.fields().entrySet()) {
|
||||||
|
var value = entry.getValue();
|
||||||
|
|
||||||
|
// Truncate long values like hex strings
|
||||||
|
if (value.length() > 64) {
|
||||||
|
value = value.substring(0, 60) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println(" │ " + entry.getKey() + ": " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseResult.readableText().isEmpty()) {
|
||||||
|
System.out.println(" ├─────────────────────────────────────────────────");
|
||||||
|
System.out.println(" │ Extracted: " + parseResult.readableText());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseResult.rawHex().isEmpty()) {
|
||||||
|
System.out.println(" ├─────────────────────────────────────────────────");
|
||||||
|
System.out.println(" │ Raw Hex:");
|
||||||
|
|
||||||
|
var lines = parseResult.rawHex().split("\n");
|
||||||
|
for (var line : lines) {
|
||||||
|
System.out.println(" │ " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println(" └─────────────────────────────────────────────────");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printDecryptionError(String error) {
|
||||||
|
System.out.println(" [ERROR] Parsing failed: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void printSummary(int total, int decrypted, int failed, boolean keyExchangeProcessed) {
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("[4/4] Summary");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("Total packets processed: " + total);
|
||||||
|
System.out.println("Successfully parsed: " + decrypted);
|
||||||
|
System.out.println("Failed: " + failed);
|
||||||
|
System.out.println("Key exchange processed: " + keyExchangeProcessed);
|
||||||
|
System.out.println("========================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String bytesToHex(byte[] bytes) {
|
||||||
|
return bytesToHex(bytes, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String bytesToHex(byte[] bytes, int length) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var end = Math.min(length, bytes.length);
|
||||||
|
|
||||||
|
for (var i = 0; i < end; i++) {
|
||||||
|
sb.append(String.format("%02X ", bytes[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/com/gcemu/gcpp/PacketExtractor.java
Normal file
43
src/main/java/com/gcemu/gcpp/PacketExtractor.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.gcemu.gcpp;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.pcapng.PcapngParser;
|
||||||
|
import com.gcemu.gcpp.pcapng.TcpPacketParser;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts TCP segments from pcapng files filtered by port.
|
||||||
|
*/
|
||||||
|
public class PacketExtractor {
|
||||||
|
private final PcapngParser pcapngParser = new PcapngParser();
|
||||||
|
private final TcpPacketParser tcpParser = new TcpPacketParser();
|
||||||
|
|
||||||
|
public ExtractionResult extract(File file, int targetPort) throws Exception {
|
||||||
|
var rawPackets = pcapngParser.parseFile(file);
|
||||||
|
|
||||||
|
var packetsByLinkLayer = rawPackets.stream()
|
||||||
|
.collect(Collectors.groupingBy(PcapngParser.Packet::linkLayerType));
|
||||||
|
|
||||||
|
List<TcpPacketParser.TcpSegment> allTcpSegments = new ArrayList<>();
|
||||||
|
for (var entry : packetsByLinkLayer.entrySet()) {
|
||||||
|
var packets = entry.getValue();
|
||||||
|
var segments = packets.stream()
|
||||||
|
.map(packet -> tcpParser.parsePacket(packet.packetData()))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
allTcpSegments.addAll(segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filteredSegments = TcpPacketParser.filterByPort(allTcpSegments, targetPort);
|
||||||
|
|
||||||
|
return new ExtractionResult(rawPackets.size(), allTcpSegments.size(), filteredSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ExtractionResult(int totalCaptured, int tcpSegmentsFound,
|
||||||
|
List<TcpPacketParser.TcpSegment> filteredSegments) {
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/main/java/com/gcemu/gcpp/PacketProcessor.java
Normal file
80
src/main/java/com/gcemu/gcpp/PacketProcessor.java
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package com.gcemu.gcpp;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.packets.PacketContext;
|
||||||
|
import com.gcemu.gcpp.packets.PacketParser;
|
||||||
|
import com.gcemu.gcpp.packets.PacketParserFactory;
|
||||||
|
import com.gcemu.gcpp.pcapng.TcpPacketParser;
|
||||||
|
import com.gcemu.gcpp.security.SecurityAssociation;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PacketProcessor {
|
||||||
|
private static final int MIN_GCNET_PACKET_SIZE = 28;
|
||||||
|
|
||||||
|
private final PacketParserFactory parserFactory = new PacketParserFactory();
|
||||||
|
|
||||||
|
private SecurityAssociation securityAssociation = new SecurityAssociation();
|
||||||
|
private final int serverPort;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private boolean keyExchangeProcessed = false;
|
||||||
|
|
||||||
|
public ProcessingResult process(TcpPacketParser.TcpSegment segment) {
|
||||||
|
var tcpPayload = segment.payload();
|
||||||
|
|
||||||
|
if (tcpPayload.length < MIN_GCNET_PACKET_SIZE) {
|
||||||
|
return ProcessingResult.tooSmall();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var result = securityAssociation.decryptPacket(tcpPayload);
|
||||||
|
var newKeyExchangeDetected = false;
|
||||||
|
|
||||||
|
// Check for key exchange packet (only process once)
|
||||||
|
if (!keyExchangeProcessed && SecurityAssociation.isInitialKeyExchange(result.decryptedPayload())) {
|
||||||
|
var newAssociation = SecurityAssociation.parseInitialKeyExchange(result.decryptedPayload());
|
||||||
|
|
||||||
|
if (nonNull(newAssociation)) {
|
||||||
|
securityAssociation = newAssociation;
|
||||||
|
keyExchangeProcessed = true;
|
||||||
|
newKeyExchangeDetected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context and parse with appropriate parser
|
||||||
|
var context = new PacketContext(result.decryptedPayload(), segment, serverPort);
|
||||||
|
var opcode = context.getOpcode();
|
||||||
|
var parser = parserFactory.getParser(opcode.getOpcode());
|
||||||
|
var parseResult = parser.parse(context);
|
||||||
|
|
||||||
|
return ProcessingResult.success(result, parseResult, newKeyExchangeDetected);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ProcessingResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ProcessingResult(
|
||||||
|
boolean isSuccess,
|
||||||
|
boolean isTooSmall,
|
||||||
|
boolean isKeyExchangeDetected,
|
||||||
|
SecurityAssociation.DecryptionResult decryptionResult,
|
||||||
|
PacketParser.ParseResult parseResult,
|
||||||
|
String errorMessage
|
||||||
|
) {
|
||||||
|
public static ProcessingResult tooSmall() {
|
||||||
|
return new ProcessingResult(false, true, false, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProcessingResult success(SecurityAssociation.DecryptionResult decryption,
|
||||||
|
PacketParser.ParseResult parseResult, boolean keyExchangeDetected) {
|
||||||
|
return new ProcessingResult(true, false, keyExchangeDetected, decryption, parseResult, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProcessingResult error(String message) {
|
||||||
|
return new ProcessingResult(false, false, false, null, null, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/com/gcemu/gcpp/packets/Opcode.java
Normal file
38
src/main/java/com/gcemu/gcpp/packets/Opcode.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package com.gcemu.gcpp.packets;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum Opcode {
|
||||||
|
KEY_EXCHANGE(1, "KEY_EXCHANGE", "Initial security key exchange", Direction.SERVER_TO_CLIENT),
|
||||||
|
|
||||||
|
UNKNOWN(-1, "UNKNOWN", "Unknown opcode", Direction.BIDIRECTIONAL);
|
||||||
|
|
||||||
|
private final int opcode;
|
||||||
|
private final String name;
|
||||||
|
private final String description;
|
||||||
|
private final Direction direction;
|
||||||
|
|
||||||
|
private static final Map<Integer, Opcode> OPCODE_MAP = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (var opcode : values()) {
|
||||||
|
OPCODE_MAP.put(opcode.opcode, opcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Opcode valueOf(int opcode) {
|
||||||
|
return OPCODE_MAP.getOrDefault(opcode, UNKNOWN);
|
||||||
|
}
|
||||||
|
public enum Direction {
|
||||||
|
CLIENT_TO_SERVER,
|
||||||
|
SERVER_TO_CLIENT,
|
||||||
|
BIDIRECTIONAL
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/com/gcemu/gcpp/packets/PacketContext.java
Normal file
38
src/main/java/com/gcemu/gcpp/packets/PacketContext.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package com.gcemu.gcpp.packets;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.pcapng.TcpPacketParser;
|
||||||
|
|
||||||
|
public record PacketContext(
|
||||||
|
byte[] decryptedPayload,
|
||||||
|
|
||||||
|
TcpPacketParser.TcpSegment segment,
|
||||||
|
|
||||||
|
int serverPort
|
||||||
|
) {
|
||||||
|
public boolean isClientToServer() {
|
||||||
|
return segment.dstPort() == serverPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isServerToClient() {
|
||||||
|
return segment.srcPort() == serverPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Opcode getOpcode() {
|
||||||
|
if (decryptedPayload.length < 2) {
|
||||||
|
return Opcode.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
var opcode = ((decryptedPayload[0] & 0xFF) << 8) | (decryptedPayload[1] & 0xFF);
|
||||||
|
return Opcode.valueOf(opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s [%d bytes] %s:%d -> %s:%d",
|
||||||
|
getOpcode().getName(),
|
||||||
|
decryptedPayload.length,
|
||||||
|
segment.srcIp(), segment.srcPort(),
|
||||||
|
segment.dstIp(), segment.dstPort()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/java/com/gcemu/gcpp/packets/PacketParser.java
Normal file
46
src/main/java/com/gcemu/gcpp/packets/PacketParser.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package com.gcemu.gcpp.packets;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface PacketParser {
|
||||||
|
ParseResult parse(PacketContext context);
|
||||||
|
|
||||||
|
record ParseResult(
|
||||||
|
Opcode opcode,
|
||||||
|
String opcodeName,
|
||||||
|
String direction,
|
||||||
|
String structure,
|
||||||
|
Map<String, String> fields,
|
||||||
|
String rawHex,
|
||||||
|
String readableText
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static ParseResult empty(Opcode opcode, String direction) {
|
||||||
|
return new ParseResult(
|
||||||
|
opcode,
|
||||||
|
opcode.getName(),
|
||||||
|
direction,
|
||||||
|
"<unparsed>",
|
||||||
|
Collections.emptyMap(),
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ParseResult parsed(Opcode opcode, String direction,
|
||||||
|
String structure, Map<String, String> fields,
|
||||||
|
String rawHex, String readableText) {
|
||||||
|
return new ParseResult(
|
||||||
|
opcode, opcode.getName(), direction, structure,
|
||||||
|
fields, rawHex, readableText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE)
|
||||||
|
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
|
||||||
|
@interface PacketParserFor {
|
||||||
|
int opcode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.gcemu.gcpp.packets;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.packets.parsers.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class PacketParserFactory {
|
||||||
|
private final Map<Integer, PacketParser> parsers = new HashMap<>();
|
||||||
|
private final PacketParser defaultParser = new GenericPayloadParser();
|
||||||
|
|
||||||
|
public PacketParserFactory() {
|
||||||
|
registerBuiltInParsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketParser getParser(int opcode) {
|
||||||
|
return parsers.getOrDefault(opcode, defaultParser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerParser(int opcode, PacketParser parser) {
|
||||||
|
parsers.put(opcode, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerBuiltInParsers() {
|
||||||
|
// Security Protocol
|
||||||
|
registerParser(1, new KeyExchangeParser());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.gcemu.gcpp.packets.parsers;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.packets.PacketContext;
|
||||||
|
import com.gcemu.gcpp.packets.PacketParser;
|
||||||
|
import com.gcemu.gcpp.packets.PacketParserFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic fallback parser for unrecognized or unimplemented opcodes.
|
||||||
|
*
|
||||||
|
* <p>This parser provides a basic hex dump and string extraction for any
|
||||||
|
* packet, even if no specific parser is registered for its opcode. It
|
||||||
|
* follows the <strong>Null Object Pattern</strong>, providing sensible
|
||||||
|
* defaults instead of null or error states.</p>
|
||||||
|
*
|
||||||
|
* <p>When adding support for a new opcode, replace this parser in
|
||||||
|
* {@link PacketParserFactory} with a specific implementation.</p>
|
||||||
|
*/
|
||||||
|
public class GenericPayloadParser implements PacketParser {
|
||||||
|
@Override
|
||||||
|
public ParseResult parse(PacketContext context) {
|
||||||
|
var opcode = context.getOpcode();
|
||||||
|
var direction = resolveDirection(context);
|
||||||
|
|
||||||
|
return ParseResult.empty(opcode, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveDirection(PacketContext context) {
|
||||||
|
if (context.isClientToServer()) {
|
||||||
|
return "Client → Server";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.isServerToClient()) {
|
||||||
|
return "Server → Client";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
package com.gcemu.gcpp.packets.parsers;
|
||||||
|
|
||||||
|
import com.gcemu.gcpp.packets.Opcode;
|
||||||
|
import com.gcemu.gcpp.packets.PacketContext;
|
||||||
|
import com.gcemu.gcpp.packets.PacketParser;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for KEY_EXCHANGE packets (opcode 1).
|
||||||
|
*
|
||||||
|
* <h3>Packet Structure:</h3>
|
||||||
|
* <table border="1">
|
||||||
|
* <tr><th>Offset</th><th>Size</th><th>Type</th><th>Description</th></tr>
|
||||||
|
* <tr><td>0</td><td>2</td><td>ushort (BE)</td><td>Opcode (1)</td></tr>
|
||||||
|
* <tr><td>2</td><td>4</td><td>int (BE)</td><td>Content size (38)</td></tr>
|
||||||
|
* <tr><td>6</td><td>1</td><td>bool</td><td>Compression flag (false)</td></tr>
|
||||||
|
* <tr><td>7</td><td>2</td><td>ushort (BE)</td><td>New SPI for session</td></tr>
|
||||||
|
* <tr><td>9</td><td>4</td><td>int (BE)</td><td>Auth key length</td></tr>
|
||||||
|
* <tr><td>13</td><td>var</td><td>byte[]</td><td>Authentication key</td></tr>
|
||||||
|
* <tr><td>var</td><td>4</td><td>int (BE)</td><td>Crypto key length</td></tr>
|
||||||
|
* <tr><td>var</td><td>var</td><td>byte[]</td><td>Encryption key</td></tr>
|
||||||
|
* <tr><td>var</td><td>4</td><td>int (BE)</td><td>Sequence number (initial)</td></tr>
|
||||||
|
* <tr><td>var</td><td>4</td><td>int (BE)</td><td>Last sequence number</td></tr>
|
||||||
|
* <tr><td>var</td><td>4</td><td>int (BE)</td><td>Replay window mask</td></tr>
|
||||||
|
* </table>
|
||||||
|
*
|
||||||
|
* <p>This is the first packet in every session, sent by the server using
|
||||||
|
* default encryption keys. It establishes the session-specific SPI,
|
||||||
|
* authentication key, and encryption key for all subsequent packets.</p>
|
||||||
|
*
|
||||||
|
* <p>Direction: {@link Opcode.Direction#SERVER_TO_CLIENT}</p>
|
||||||
|
*/
|
||||||
|
@PacketParser.PacketParserFor(opcode = 1)
|
||||||
|
public class KeyExchangeParser implements PacketParser {
|
||||||
|
private static final int CONTENT_OFFSET = 7;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParseResult parse(PacketContext context) {
|
||||||
|
var payload = context.decryptedPayload();
|
||||||
|
var direction = "Server → Client";
|
||||||
|
|
||||||
|
if (payload.length < CONTENT_OFFSET + 38) {
|
||||||
|
return ParseResult.empty(Opcode.KEY_EXCHANGE, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var content = parseContent(payload);
|
||||||
|
return ParseResult.parsed(
|
||||||
|
Opcode.KEY_EXCHANGE,
|
||||||
|
direction,
|
||||||
|
"KEY_EXCHANGE { spi: ushort, auth_key: bytes, crypto_key: bytes, seq_num: uint, last_seq: uint, replay_mask: uint }",
|
||||||
|
content.fields(),
|
||||||
|
formatContentHex(payload),
|
||||||
|
content.readableText()
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ParseResult.empty(Opcode.KEY_EXCHANGE, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParsedContent(Map<String, String> fields, String readableText) {}
|
||||||
|
|
||||||
|
private ParsedContent parseContent(byte[] payload) {
|
||||||
|
Map<String, String> fields = new LinkedHashMap<>();
|
||||||
|
var readable = new StringBuilder();
|
||||||
|
var offset = CONTENT_OFFSET;
|
||||||
|
|
||||||
|
// Parse SPI
|
||||||
|
var bb = ByteBuffer.wrap(payload, offset, 2)
|
||||||
|
.order(java.nio.ByteOrder.BIG_ENDIAN);
|
||||||
|
var spi = bb.getShort();
|
||||||
|
offset += 2;
|
||||||
|
fields.put("SPI", String.format("0x%04X", spi));
|
||||||
|
|
||||||
|
// Parse auth key (with length prefix)
|
||||||
|
bb = ByteBuffer.wrap(payload, offset, 4).order(java.nio.ByteOrder.BIG_ENDIAN);
|
||||||
|
var authKeyLen = bb.getInt();
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
if (offset + authKeyLen <= payload.length) {
|
||||||
|
var authKey = new byte[authKeyLen];
|
||||||
|
System.arraycopy(payload, offset, authKey, 0, authKeyLen);
|
||||||
|
fields.put("Auth Key", bytesToHex(authKey));
|
||||||
|
offset += authKeyLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse crypto key (with length prefix)
|
||||||
|
bb = ByteBuffer.wrap(payload, offset, 4).order(java.nio.ByteOrder.BIG_ENDIAN);
|
||||||
|
var cryptoKeyLen = bb.getInt();
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
if (offset + cryptoKeyLen <= payload.length) {
|
||||||
|
var cryptoKey = new byte[cryptoKeyLen];
|
||||||
|
System.arraycopy(payload, offset, cryptoKey, 0, cryptoKeyLen);
|
||||||
|
fields.put("Crypto Key", bytesToHex(cryptoKey));
|
||||||
|
offset += cryptoKeyLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse sequence numbers
|
||||||
|
if (offset + 12 <= payload.length) {
|
||||||
|
bb = ByteBuffer.wrap(payload, offset, 12).order(java.nio.ByteOrder.BIG_ENDIAN);
|
||||||
|
var seqNum = bb.getInt();
|
||||||
|
var lastSeq = bb.getInt();
|
||||||
|
var replayMask = bb.getInt();
|
||||||
|
|
||||||
|
fields.put("Seq Num", String.valueOf(seqNum));
|
||||||
|
fields.put("Last Seq Num", String.valueOf(lastSeq));
|
||||||
|
fields.put("Replay Window Mask", String.format("0x%08X", replayMask));
|
||||||
|
}
|
||||||
|
|
||||||
|
readable.append("spi=0x").append(String.format("%04X", spi));
|
||||||
|
return new ParsedContent(fields, readable.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bytesToHex(byte[] bytes) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var b : bytes) {
|
||||||
|
sb.append(String.format("%02X", b));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatContentHex(byte[] data) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
if (i % 16 == 0 && i > 0) {
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(String.format("%02X ", data[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/java/com/gcemu/gcpp/pcapng/PcapngParser.java
Normal file
37
src/main/java/com/gcemu/gcpp/pcapng/PcapngParser.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package com.gcemu.gcpp.pcapng;
|
||||||
|
|
||||||
|
import fr.bmartel.pcapdecoder.PcapDecoder;
|
||||||
|
import fr.bmartel.pcapdecoder.structure.types.IPcapngType;
|
||||||
|
import fr.bmartel.pcapdecoder.structure.types.inter.IEnhancedPacketBLock;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PcapngParser {
|
||||||
|
public static final int LINK_TYPE_ETHERNET = 1;
|
||||||
|
|
||||||
|
public List<Packet> parseFile(File file) throws Exception {
|
||||||
|
List<Packet> packets = new ArrayList<>();
|
||||||
|
|
||||||
|
var decoder = new PcapDecoder(file.getAbsolutePath());
|
||||||
|
decoder.decode();
|
||||||
|
|
||||||
|
var sectionList = decoder.getSectionList();
|
||||||
|
|
||||||
|
for (IPcapngType block : sectionList) {
|
||||||
|
if (block instanceof IEnhancedPacketBLock epb) {
|
||||||
|
var data = epb.getPacketData();
|
||||||
|
packets.add(new Packet(
|
||||||
|
data,
|
||||||
|
LINK_TYPE_ETHERNET
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Packet(byte[] packetData, int linkLayerType) {
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/main/java/com/gcemu/gcpp/pcapng/TcpPacketParser.java
Normal file
173
src/main/java/com/gcemu/gcpp/pcapng/TcpPacketParser.java
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package com.gcemu.gcpp.pcapng;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.Objects.isNull;
|
||||||
|
|
||||||
|
public class TcpPacketParser {
|
||||||
|
private static final int ETHERNET_HEADER_SIZE = 14;
|
||||||
|
private static final short ETHERNET_TYPE_IPV4 = 0x0800;
|
||||||
|
private static final byte IP_PROTOCOL_TCP = 6;
|
||||||
|
|
||||||
|
public TcpSegment parsePacket(byte[] packetData) {
|
||||||
|
int ipOffset;
|
||||||
|
|
||||||
|
// Auto-detect packet format by examining the data
|
||||||
|
// This handles cases where the pcapng library reports wrong link type
|
||||||
|
ipOffset = detectIpOffset(packetData);
|
||||||
|
|
||||||
|
if (ipOffset < 0) {
|
||||||
|
return null; // Failed to parse
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseIPv4AndTCP(packetData, ipOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect where the IP header starts in the packet data.
|
||||||
|
* Handles various link layer types and raw captures.
|
||||||
|
*/
|
||||||
|
private int detectIpOffset(byte[] packetData) {
|
||||||
|
if (isNull(packetData) || packetData.length < 20) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to detect IPv4 header: first byte should be 0x45 (version 4, IHL 5)
|
||||||
|
for (var offset = 0; offset <= Math.min(40, packetData.length - 20); offset++) {
|
||||||
|
var versionAndIhl = packetData[offset];
|
||||||
|
var version = (versionAndIhl >> 4) & 0x0F;
|
||||||
|
var ihl = versionAndIhl & 0x0F;
|
||||||
|
|
||||||
|
if (version == 4 && ihl >= 5) {
|
||||||
|
// Looks like IPv4 header, verify protocol field is TCP
|
||||||
|
var headerLength = ihl * 4;
|
||||||
|
if (offset + headerLength + 13 <= packetData.length) {
|
||||||
|
var protocol = packetData[offset + 9];
|
||||||
|
if (protocol == IP_PROTOCOL_TCP) {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try standard Ethernet header
|
||||||
|
if (packetData.length >= ETHERNET_HEADER_SIZE + 20) {
|
||||||
|
var bb = ByteBuffer.wrap(packetData).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.position(12);
|
||||||
|
var etherType = bb.getShort();
|
||||||
|
|
||||||
|
if (etherType == ETHERNET_TYPE_IPV4) {
|
||||||
|
return ETHERNET_HEADER_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Linux SLL header (16 bytes)
|
||||||
|
if (packetData.length >= 16 + 20) {
|
||||||
|
var bb = ByteBuffer.wrap(packetData, 14, 2).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
var protocolType = bb.getShort();
|
||||||
|
|
||||||
|
if (protocolType == ETHERNET_TYPE_IPV4) {
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TcpSegment parseIPv4AndTCP(byte[] packetData, int ipHeaderStart) {
|
||||||
|
if (packetData.length < ipHeaderStart + 20) {
|
||||||
|
System.err.println(" [DEBUG] IPv4: packet too small for IP header (" + packetData.length + " bytes, need " + (ipHeaderStart + 20) + ")");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipBb = ByteBuffer.wrap(packetData, ipHeaderStart, packetData.length - ipHeaderStart)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
var versionAndIhl = ipBb.get();
|
||||||
|
var ipHeaderLength = (versionAndIhl & 0x0F) * 4;
|
||||||
|
ipBb.get(); // DSCP/ECN (TOS)
|
||||||
|
ipBb.getShort();
|
||||||
|
ipBb.getShort(); // Identification
|
||||||
|
ipBb.getShort(); // Flags + Fragment Offset
|
||||||
|
ipBb.get(); // TTL
|
||||||
|
var protocol = ipBb.get();
|
||||||
|
ipBb.getShort(); // Header checksum
|
||||||
|
|
||||||
|
var srcIp = ipBb.getInt();
|
||||||
|
var dstIp = ipBb.getInt();
|
||||||
|
|
||||||
|
if (protocol != IP_PROTOCOL_TCP) {
|
||||||
|
return null; // Not TCP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse TCP header
|
||||||
|
var tcpHeaderStart = ipHeaderStart + ipHeaderLength;
|
||||||
|
if (packetData.length < tcpHeaderStart + 20) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcpBb = ByteBuffer.wrap(packetData, tcpHeaderStart, packetData.length - tcpHeaderStart)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
var srcPort = tcpBb.getShort() & 0xFFFF;
|
||||||
|
var dstPort = tcpBb.getShort() & 0xFFFF;
|
||||||
|
var seqNum = tcpBb.getInt();
|
||||||
|
tcpBb.getInt();
|
||||||
|
|
||||||
|
var dataOffsetAndFlags = tcpBb.get();
|
||||||
|
var tcpHeaderLength = ((dataOffsetAndFlags >> 4) & 0x0F) * 4;
|
||||||
|
|
||||||
|
if (tcpHeaderLength < 20 || tcpHeaderStart + tcpHeaderLength > packetData.length) {
|
||||||
|
return null; // Invalid TCP header
|
||||||
|
}
|
||||||
|
|
||||||
|
var payloadStart = tcpHeaderStart + tcpHeaderLength;
|
||||||
|
var payloadLength = packetData.length - payloadStart;
|
||||||
|
|
||||||
|
if (payloadLength <= 0) {
|
||||||
|
return null; // No payload
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new byte[payloadLength];
|
||||||
|
System.arraycopy(packetData, payloadStart, payload, 0, payloadLength);
|
||||||
|
|
||||||
|
return new TcpSegment(
|
||||||
|
intToIp(srcIp),
|
||||||
|
srcPort,
|
||||||
|
intToIp(dstIp),
|
||||||
|
dstPort,
|
||||||
|
seqNum,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<TcpSegment> filterByPort(List<TcpSegment> segments, int targetPort) {
|
||||||
|
List<TcpSegment> filtered = new ArrayList<>();
|
||||||
|
for (var segment : segments) {
|
||||||
|
if (segment.srcPort() == targetPort || segment.dstPort() == targetPort) {
|
||||||
|
filtered.add(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String intToIp(int ip) {
|
||||||
|
return String.format("%d.%d.%d.%d",
|
||||||
|
(ip >> 24) & 0xFF,
|
||||||
|
(ip >> 16) & 0xFF,
|
||||||
|
(ip >> 8) & 0xFF,
|
||||||
|
ip & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TcpSegment(String srcIp, int srcPort, String dstIp, int dstPort, int sequenceNumber, byte[] payload) {
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s:%d -> %s:%d [SEQ=%d] %d bytes",
|
||||||
|
srcIp, srcPort, dstIp, dstPort, sequenceNumber, payload.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/main/java/com/gcemu/gcpp/security/SecurityAssociation.java
Normal file
200
src/main/java/com/gcemu/gcpp/security/SecurityAssociation.java
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package com.gcemu.gcpp.security;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.DESKeySpec;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityAssociation {
|
||||||
|
private static final byte[] DEFAULT_CRYPTO_KEY = {
|
||||||
|
(byte) 0xC7, (byte) 0xD8, (byte) 0xC4, (byte) 0xBF,
|
||||||
|
(byte) 0xB5, (byte) 0xE9, (byte) 0xC0, (byte) 0xFD
|
||||||
|
};
|
||||||
|
private static final byte[] DEFAULT_AUTH_KEY = {
|
||||||
|
(byte) 0xC0, (byte) 0xD3, (byte) 0xBD, (byte) 0xC3,
|
||||||
|
(byte) 0xB7, (byte) 0xCE, (byte) 0xB8, (byte) 0xB8
|
||||||
|
};
|
||||||
|
|
||||||
|
private final byte[] cryptoKey;
|
||||||
|
private final byte[] authKey;
|
||||||
|
private final short spi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a security association with default keys (for initial packet).
|
||||||
|
*/
|
||||||
|
public SecurityAssociation() {
|
||||||
|
this(DEFAULT_CRYPTO_KEY, DEFAULT_AUTH_KEY, (short) 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecryptionResult decryptPacket(byte[] secureBuffer) {
|
||||||
|
return decryptPacket(secureBuffer, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecryptionResult decryptPacket(byte[] secureBuffer, int startIndex) {
|
||||||
|
var length = readShort(secureBuffer, startIndex);
|
||||||
|
var spiIndex = startIndex + 2;
|
||||||
|
var ivIndex = startIndex + 8;
|
||||||
|
var payloadIndex = startIndex + 16;
|
||||||
|
var icvIndex = startIndex + length - 10;
|
||||||
|
|
||||||
|
var packetSpi = readShort(secureBuffer, spiIndex);
|
||||||
|
var iv = Arrays.copyOfRange(secureBuffer, ivIndex, ivIndex + 8);
|
||||||
|
|
||||||
|
var payloadLength = length - 16 - 10; // Total - header - ICV
|
||||||
|
var encryptedPayload = Arrays.copyOfRange(secureBuffer, payloadIndex, payloadIndex + payloadLength);
|
||||||
|
|
||||||
|
var storedIcv = Arrays.copyOfRange(secureBuffer, icvIndex, icvIndex + 10);
|
||||||
|
|
||||||
|
var authData = Arrays.copyOfRange(secureBuffer, spiIndex, icvIndex);
|
||||||
|
var icvValid = validateIcv(authData, storedIcv);
|
||||||
|
|
||||||
|
var decryptedPayload = decryptPayload(encryptedPayload, iv);
|
||||||
|
|
||||||
|
return new DecryptionResult(packetSpi, iv, decryptedPayload, icvValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateIcv(byte[] authData, byte[] storedIcv) {
|
||||||
|
try {
|
||||||
|
var calculatedIcv = calculateIcv(authData);
|
||||||
|
return Arrays.equals(calculatedIcv, storedIcv);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("ICV validation error: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] calculateIcv(byte[] authData) {
|
||||||
|
try {
|
||||||
|
var md = MessageDigest.getInstance("MD5");
|
||||||
|
|
||||||
|
var key = new byte[64];
|
||||||
|
System.arraycopy(authKey, 0, key, 0, Math.min(authKey.length, 64));
|
||||||
|
|
||||||
|
var ipad = new byte[64];
|
||||||
|
var opad = new byte[64];
|
||||||
|
|
||||||
|
for (var i = 0; i < 64; i++) {
|
||||||
|
ipad[i] = (byte) (key[i] ^ 0x36);
|
||||||
|
opad[i] = (byte) (key[i] ^ 0x5C);
|
||||||
|
}
|
||||||
|
|
||||||
|
md.update(ipad);
|
||||||
|
var innerHash = md.digest(authData);
|
||||||
|
|
||||||
|
var md2 = MessageDigest.getInstance("MD5");
|
||||||
|
md2.update(opad);
|
||||||
|
var fullHmac = md2.digest(innerHash);
|
||||||
|
|
||||||
|
return Arrays.copyOf(fullHmac, 10);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to calculate ICV", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] decryptPayload(byte[] encryptedPayload, byte[] iv) {
|
||||||
|
try {
|
||||||
|
var keySpec = new DESKeySpec(cryptoKey);
|
||||||
|
var keyFactory = SecretKeyFactory.getInstance("DES");
|
||||||
|
var key = keyFactory.generateSecret(keySpec);
|
||||||
|
|
||||||
|
var cipher = Cipher.getInstance("DES/CBC/NoPadding");
|
||||||
|
var ivSpec = new IvParameterSpec(iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
|
||||||
|
|
||||||
|
var decrypted = cipher.doFinal(encryptedPayload);
|
||||||
|
|
||||||
|
// Remove padding
|
||||||
|
var paddingLength = decrypted[decrypted.length - 1] + 1;
|
||||||
|
|
||||||
|
if (paddingLength > 0 && paddingLength <= decrypted.length) {
|
||||||
|
return Arrays.copyOf(decrypted, decrypted.length - paddingLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to decrypt payload", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private short readShort(byte[] buffer, int offset) {
|
||||||
|
// Little-endian
|
||||||
|
return (short) ((buffer[offset] & 0xFF) | ((buffer[offset + 1] & 0xFF) << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the initial key exchange packet (opcode 1) to extract new security parameters.
|
||||||
|
* <p>
|
||||||
|
* Initial packet content structure (big-endian):
|
||||||
|
* - New SPI (2 bytes)
|
||||||
|
* - Authentication Key (8 bytes)
|
||||||
|
* - Encryption Key (8 bytes)
|
||||||
|
* - Sequence Number (4 bytes)
|
||||||
|
* - Last Sequence Number (4 bytes)
|
||||||
|
* - Replay Window Mask (4 bytes)
|
||||||
|
*/
|
||||||
|
public static SecurityAssociation parseInitialKeyExchange(byte[] decryptedPayload) {
|
||||||
|
try {
|
||||||
|
// Payload header: opcode (2 bytes), content size (4 bytes), compression flag (1 byte)
|
||||||
|
int offset = 7;
|
||||||
|
|
||||||
|
// Parse content (big-endian)
|
||||||
|
var bb = ByteBuffer.wrap(decryptedPayload, offset, decryptedPayload.length - offset);
|
||||||
|
bb.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
var newSpi = bb.getShort();
|
||||||
|
|
||||||
|
var authKeyLen = bb.getInt();
|
||||||
|
var newAuthKey = new byte[authKeyLen];
|
||||||
|
bb.get(newAuthKey);
|
||||||
|
|
||||||
|
var cryptoKeyLen = bb.getInt();
|
||||||
|
var newCryptoKey = new byte[cryptoKeyLen];
|
||||||
|
bb.get(newCryptoKey);
|
||||||
|
|
||||||
|
var seqNum = bb.getInt();
|
||||||
|
bb.getInt();
|
||||||
|
bb.getInt();
|
||||||
|
|
||||||
|
System.out.println("Extracted security parameters from key exchange:");
|
||||||
|
System.out.printf(" SPI: 0x%04X%n", newSpi);
|
||||||
|
System.out.printf(" Auth Key (%d bytes): %s%n", authKeyLen, bytesToHex(newAuthKey));
|
||||||
|
System.out.printf(" Crypto Key (%d bytes): %s%n", cryptoKeyLen, bytesToHex(newCryptoKey));
|
||||||
|
System.out.printf(" Seq Num: %d%n", seqNum);
|
||||||
|
|
||||||
|
return new SecurityAssociation(newCryptoKey, newAuthKey, newSpi);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to parse initial key exchange: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInitialKeyExchange(byte[] decryptedPayload) {
|
||||||
|
if (decryptedPayload.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Big-endian opcode
|
||||||
|
short opcode = (short) (((decryptedPayload[0] & 0xFF) << 8) | (decryptedPayload[1] & 0xFF));
|
||||||
|
return opcode == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (var b : bytes) {
|
||||||
|
sb.append(String.format("%02X ", b));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DecryptionResult(short spi, byte[] iv, byte[] decryptedPayload, boolean icvValid) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user