Skip to content

Commit 3a5dc62

Browse files
committed
Merge branch 'release/1.0.0'
2 parents 243ab6b + bf87ed0 commit 3a5dc62

9 files changed

Lines changed: 46 additions & 10 deletions

File tree

.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ DROIDGROUND_START_SERVICE_DISABLED=false # Feature enabled by default if not set
2323
DROIDGROUND_TERMINAL_DISABLED=false # Feature enabled by default if not set otherwise
2424
DROIDGROUND_RESET_DISABLED=false # Feature enabled by default if not set otherwise
2525
DROIDGROUND_EXPLOIT_APP_DURATION=10 # The time (in seconds) the exploit app will be active before the target app is restarted. This field makes sense only if the App Manager is enabled. Default value is 10
26+
DROIDGROUND_EXPLOIT_APP_MAX_SIZE=50 # The max size (in MB) of the exploit app. This field makes sense only if the App Manager is enabled. Default value is 50
2627
DROIDGROUND_NUM_TEAMS=0 # Number of teams playing, this enables the usage of team-based tokens/keys to lock down the usage of installed apps and log servers
2728
DROIDGROUND_TEAM_TOKEN_1=RANDOMSTRING # Token for team #1, this only makes sense if DROIDGROUND_NUM_TEAMS is higher than 0. If a team token is not explicitly defined it'll be randomly generated on boot and present in the output logs
2829
DROIDGROUND_IP_STATIC=192.168.1.1 # Shows a static IP address in the Exploit Server page, this takes precedence over DROIDGROUND_IP_IFACE

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<a href="https://github.com/SECFORCE/droidground/blob/main/README.md"><img src="https://img.shields.io/badge/Documentation-complete-green.svg?style=flat"></a>
1111
<a href="https://github.com/SECFORCE/droidground/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-GPL3-blue.svg"></a>
1212
<a href="https://blackhat.com/eu-25/arsenal/schedule/index.html#droidground-a-flexible-playground-for-android-ctf-challenges-47803"><img src="./docs/blackhat-2025.svg"></a>
13+
<br />
14+
<a href="https://droidground.com" target="_blank">Website</a> |
15+
<a href="https://droidground.com/demo" target="_blank">Demo</a>
1316
</p>
1417

1518
In traditional **CTF challenges**, it's common to hide flags in files on a system, requiring attackers to exploit vulnerabilities to retrieve them. However, in the Android world, this approach doesn't work well. APK files are easily downloadable and reversible, so **placing a flag on the device usually makes it trivial** to extract using static analysis or emulator tricks. This severely limits the ability to create realistic, runtime-focused challenges.
@@ -104,6 +107,7 @@ The `.env.sample` file in the root directory is a good starting point. This is t
104107
| `DROIDGROUND_TERMINAL_DISABLED` | Disable terminal | `false` |
105108
| `DROIDGROUND_RESET_DISABLED` | Disable reset | `false` |
106109
| `DROIDGROUND_EXPLOIT_APP_DURATION` | The time (in seconds) the exploit app will be active | `10` |
110+
| `DROIDGROUND_EXPLOIT_APP_MAX_SIZE` | The max size (in MB) of the exploit app | `50` |
107111
| `DROIDGROUND_NUM_TEAMS` | The number of teams playing simultaneously | - |
108112
| `DROIDGROUND_TEAM_TOKEN_<N>` | The token for the nth team. Auto-generated if missing | - |
109113
| `DROIDGROUND_IP_STATIC` | The static IP address to display. It takes precedence over `DROIDGROUND_IP_IFACE` | - |

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "droidground",
3-
"version": "0.3.5",
3+
"version": "1.0.0",
44
"type": "module",
55
"author": "Angelo Delicato",
66
"scripts": {

src/server/api/controller.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,16 @@ class APIController {
508508
}
509509

510510
const manager = ManagerSingleton.getInstance();
511+
const features = manager.getConfig().features;
512+
513+
// Check if file size exceeds max size
514+
if (req.file.size > features.exploitAppMaxSize * 1024 * 1024) {
515+
throw new Error(`Exploit App size exceeds the limit of ${features.exploitAppMaxSize} MB.`);
516+
}
511517

512518
// Check if teamToken is needed and valid
513519
if (
514-
manager.getConfig().features.teamModeEnabled &&
520+
features.teamModeEnabled &&
515521
(!Object.keys(req.body).includes("teamToken") || !manager.isTeamTokenValid(req.body.teamToken))
516522
) {
517523
throw new Error("Missing or invalid Team Token.");
@@ -550,7 +556,7 @@ class APIController {
550556

551557
manager.exploitApps.push(packageName);
552558
// Duplicate but it shouldn't be a big problem
553-
if (manager.getConfig().features.teamModeEnabled) {
559+
if (features.teamModeEnabled) {
554560
manager.linkExploitAppToTeam(req.body.teamToken, packageName);
555561
}
556562

src/server/manager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ScrcpyMediaStreamConfigurationPacket } from "@yume-chan/scrcpy";
1010
import { AdbServerNodeTcpConnector } from "@yume-chan/adb-server-node-tcp";
1111
import Logger from "@shared/logger";
1212
import { randomString, sleep } from "@shared/helpers";
13-
import { DroidGroundConfig, DroidGroundTeam, FridaState, StreamMetadata } from "@shared/types";
13+
import { DroidGroundConfig, DroidGroundTeam, FridaState, StreamMetadata, DroidGroundFrame } from "@shared/types";
1414
import { AppStatus, WebsocketClient } from "@server/utils/types";
1515
import { setupFrida } from "@server/utils/frida";
1616
import { setupScrcpy } from "@server/utils/scrcpy";
@@ -44,11 +44,17 @@ export class ManagerSingleton {
4444
public exploitApps: string[] = [];
4545
// Exploit App Run Queue
4646
public queue;
47+
// Last Scrcpy keyframe
48+
public lastKeyframe: DroidGroundFrame | null = null;
49+
public lastFrame: DroidGroundFrame | null = null;
4750

4851
private constructor() {
4952
// private constructor prevents direct instantiation
5053
const port: any = process.env.DROIDGROUND_ADB_PORT ?? "";
5154
const exploitAppDuration: any = process.env.DROIDGROUND_EXPLOIT_APP_DURATION ?? "";
55+
const exploitAppmaxSizeEnv: any = process.env.DROIDGROUND_EXPLOIT_APP_MAX_SIZE ?? "";
56+
const exploitAppMaxSize: number =
57+
isNaN(exploitAppmaxSizeEnv) || exploitAppmaxSizeEnv.trim().length === 0 ? 50 : parseInt(exploitAppmaxSizeEnv);
5258
// Check if IP address should be displayed
5359
const ipStatic = process.env.DROIDGROUND_IP_STATIC ?? undefined;
5460
const iface = process.env.DROIDGROUND_IP_IFACE ?? "";
@@ -92,6 +98,7 @@ export class ManagerSingleton {
9298
fridaType: process.env.DROIDGROUND_FRIDA_TYPE === "full" ? "full" : "jail",
9399
exploitAppDuration:
94100
isNaN(exploitAppDuration) || exploitAppDuration.trim().length === 0 ? 10 : parseInt(exploitAppDuration),
101+
exploitAppMaxSize: exploitAppMaxSize,
95102
ipAddress: `${ipAddress}:${backendPort}`,
96103
logoLink: parseValidUrl(process.env.DROIDGROUND_LOGO_LINK ?? ""),
97104
},

src/server/utils/scrcpy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ export const setupScrcpy = async () => {
105105
keyframe: packet.keyframe,
106106
pts: packet.pts ? packet.pts.toString() : null,
107107
};
108+
// Store frame
109+
singleton.lastFrame = { metadata: metadata, data: packet.data };
110+
if (metadata.keyframe) {
111+
singleton.lastKeyframe = { metadata: metadata, data: packet.data };
112+
}
108113
broadcastForPhase(wsStreamingClients, StreamingPhase.RENDER, {
109114
type: WSMessageType.DATA,
110115
metadata: metadata,

src/server/ws/scrcpy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@ export const setupScrcpyWss = (wssStreaming: WebSocketServer) => {
3535
}
3636
break;
3737
case WSMessageType.CONFIGURATION_ACK:
38-
const nextState: StreamingPhase =
38+
let nextState: StreamingPhase =
3939
singleton.sharedVideoMetadata?.hardwareType === "hardware"
4040
? StreamingPhase.KEYFRAME
4141
: StreamingPhase.RENDER;
42+
// Send last frames if available
43+
if (nextState == StreamingPhase.RENDER && singleton.lastFrame) {
44+
sendStructuredMessage(ws, WSMessageType.DATA, singleton.lastFrame.metadata, singleton.lastFrame.data);
45+
} else if (nextState == StreamingPhase.KEYFRAME && singleton.lastKeyframe) {
46+
sendStructuredMessage(ws, WSMessageType.DATA, singleton.lastKeyframe.metadata, singleton.lastKeyframe.data);
47+
nextState = StreamingPhase.RENDER;
48+
}
4249
wsStreamingClients.set(id, { ...(currentClientData as WebsocketClient), state: nextState });
4350
break;
4451
default:

src/shared/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface DroidGroundFeatures {
2020
unlimitedTeams: boolean;
2121
fridaType: "full" | "jail";
2222
exploitAppDuration: number;
23+
exploitAppMaxSize: number;
2324
ipAddress: string;
2425
logoLink: string | null;
2526
}
@@ -76,6 +77,11 @@ export interface WSMessage {
7677
data: Uint8Array;
7778
}
7879

80+
export interface DroidGroundFrame {
81+
metadata: WSMetadata;
82+
data: Uint8Array;
83+
}
84+
7985
export type WSCallback = (metadata: WSMetadata, binaryBuf: Uint8Array<ArrayBuffer>) => void;
8086

8187
// ADB types

0 commit comments

Comments
 (0)