Skip to content

Commit

Permalink
feat: add flag generation capability for each Challenge Scenario on D…
Browse files Browse the repository at this point in the history
…emand
  • Loading branch information
pandatix committed Jan 31, 2024
1 parent c1787ca commit 92f9943
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 3 deletions.
27 changes: 27 additions & 0 deletions DESIGN_DOCUMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Table of content:
- [Internals](#internals)
- [High Availability](#high-availability)
- [Timeouts](#timeouts)
- [Security](#security)
- [Authentication](#authentication) <!-- TODO add authentication discussion -->
- [Shareflag](#shareflag)
- [Deployment](#deployment)
- [Local deployment for developers](#local-deployment-for-developers)
- [Production deployment](#production-deployment)
Expand Down Expand Up @@ -153,6 +156,30 @@ Nevertheless, people may want to get their resources to run longer than what has

In the end, our proposal gets another interesting feature by design that is configurable to the _Ops_ and in favor of both the _Ops_ and players.

### Security

#### Authentication

#### Shareflag

An obstacle to a good event is cheating. People's interest may tend to this when focused on winning rather than learning or having a good time.
In this approach, they may lure or deal with other participants to share a flag to solve a challenge.

A good solution to avoid that behavior is by having a unique flag per team, but this imply a lot of work for the _ChallMaker_ to build and debug the challenge, and a long time adding them all in the CTF platform for the _Ops_. Moreover, this does not scale: horizontally by increasing the number of teams and/or players, vertically by increasing the number of challenges.
For instance, a reverse engineering themed challenge would imply to the _ChallMaker_ to either compile one binary that contains all those flags and hope everyone successfully falls into the its specific Call Graph Tree to find its flag, either compile _n_ binaries with the flag updated.
In the end, this approach is very limited, costfull, and operationally most likely not used.

In our proposal, as we already provide a solution to _Challenge Scenario on Demand_, we think we have to go further and make use of this design to enable automating it.
Indeed, once a challenge scenario request is performed, the factory can randomise a hardcoded flag value using the identity as a seed to a PRNG and provide it to the resources that is suppose to expose it. The flag variation algorithm walks on each character, if it is contained in a list of variations it randomly select one of its variations (could be itself), and then mutate it. This list of mutations has been manually built over the printable extended ascii table thus should be acceptable per each CTF platform. Variations are supposed to represent the same character (e.g. `e` could mutate to ``, `È` or `3`).

Technically, this flag is returned by the Pulumi factory (_Challenge Scenario_) as an exported output throuh the key `flag`, as a string, and is non-mandatory. If provided, the CTF platform could add it on the fly to the list of acceptable flags for the challenge, or reuse a pivot structure between the team (or user) and the challenge, side to the connection information. If not provided, the CTF platform should only validate the flags of its Challenge resource.
Integration won't be further discussed as it is platform-specific and is trusted out of scope.

Through this design, we use a reproducible identity as a seed for a pseudo-random number generator, always perform the same simple operation, hence provide a reproducible way of randomising a flag per user (or team).

In addition to the contribution on generalizing the _Challenge Scenario on Demand_ problem, we integrate an anti-cheat (shareflag) protection by design.
Nevertheless, this approach is not sufficient to block people from sharing hints or writeups, but could build upon our current work at generalizing the approach to _Challenge Scenario on Demand_ in another work. We invite anyone interested to make a proposal and contribution in this way.

### Deployment

When deploying resources to a Kubernetes cluster with the necessity of high availability and security, a beginner can only focus on getting the things work. We do not want that because in the design of the chall-manager itself, code is run from distant inputs we can't trust by default (no authentication is part of the chall-manager nor does we want to).
Expand Down
7 changes: 6 additions & 1 deletion api/v1/launch/launch.proto
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,13 @@ message LaunchResponse {
// Timestamp the challenge is expected to run until.
// Due to latencies it is not guaranteed, but could happen at any time.
google.protobuf.Timestamp until = 2;
// The flag is a non-mantdatory response specific to the Challenge Scenario on
// Demand, and is the payload to validate this challenge.
// It enables variability thus shareflag (does not natively block from sharing
// writeups or anything similar).
optional string flag = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"CTFER{ÏnƒRåŠ7rÜ©†ûrè 㧠©Ödè}\""}, (google.api.field_behavior) = OPTIONAL];
// The error message, if anything failed.
string error = 3;
string error = 4;
}

// RetrieveLaunchRequest is a scenario launch information request.
Expand Down
1 change: 1 addition & 0 deletions api/v1/launch/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func untilFromNow(reqDates any) *time.Time {
func response(st *state.State) *LaunchResponse {
res := &LaunchResponse{
ConnectionInfo: st.Outputs.ConnectionInfo,
Flag: st.Outputs.Flag,
}
if st.Metadata.Until != nil {
res.Until = timestamppb.New(*st.Metadata.Until)
Expand Down
5 changes: 5 additions & 0 deletions pkg/state/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func New(ctx context.Context, stack auto.Stack, metadata StateMetadata, outputs
}
}
state.Outputs.ConnectionInfo = coninfo.Value.(string)
flag, ok := outputs["flag"]
if ok {
fstr := flag.Value.(string)
state.Outputs.Flag = &fstr
}

return state, nil
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/state/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ type StateMetadata struct {
type StateOutputs struct {
// ConnectionInfo to the Challenge Scenario on Demand.
ConnectionInfo string `json:"connection_info"`
// Flag specific to the Challenge Scenario on Demand.
// Avoid shareflag.
Flag *string `json:"flag,omitempty"`
}
1 change: 1 addition & 0 deletions sdk/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func Run(f Factory) {
}

ctx.Export("connection_info", resp.ConnectionInfo)
ctx.Export("flag", resp.Flag)
return nil
})
}
88 changes: 88 additions & 0 deletions sdk/flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package sdk

import (
"encoding/binary"
"math/rand"
"slices"
)

// vars is the list of all the characters that contains vars.
// if not contained, don't variate the character.
// please don't add vars that are not part of the printable extended ascii table.
var vars = [][]rune{
{'a', 'A', '4', '@', 'ª', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'à', 'á', 'â', 'ã', 'ä', 'å'},
{'b', 'B', '8', 'ß'},
{'c', 'C', '(', '¢', '©', 'Ç', 'ç'},
{'d', 'D', 'Ð'},
{'e', 'E', '€', '&', '£', 'È', 'É', 'Ê', 'Ë', 'è', 'é', 'ê', 'ë', '3'},
{'f', 'F', 'ƒ'},
{'g', 'G'},
{'h', 'H', '#'},
{'i', 'I', '1', '!', 'Ì', 'Í', 'Î', 'Ï', 'ì', 'í', 'î', 'ï'},
{'j', 'J'},
{'k', 'K'},
{'l', 'L'},
{'m', 'M'},
{'n', 'N', 'Ñ', 'ñ'},
{'o', 'O', '0', '¤', '°', 'º', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'ø', 'ò', 'ó', 'ô', 'õ', 'ö', 'ð'},
{'p', 'P'},
{'q', 'Q'},
{'r', 'R', '®'},
{'s', 'S', '5', '$', 'š', 'Š', '§'},
{'t', 'T', '7', '†'},
{'u', 'U', 'µ', 'Ù', 'Ú', 'Û', 'Ü', 'ù', 'ú', 'û', 'ü'},
{'v', 'V'},
{'w', 'W'},
{'x', 'X', '×'},
{'y', 'Y', 'Ÿ', '¥', 'Ý', 'ý', 'ÿ'},
{'z', 'Z', 'ž', 'Ž'},
{' ', '-', '_', '~'},
}

// VariateFlag builds a PRNG with the given the 8 first characters of the seed (zeros
// are appended if necessary) then travels through all the content and variate each
// character that could be, then returns it. In the end, the result should still be
// understandable by the player thus not altering his soul.
// For instance the flag "super-flag" could become "$uPer-Fl@g" or "sUP&r-fLag".
//
// This process is explainable and reproducible thus enables CTF events to be
// completly reproducibles as required by many organizers.
// Mutated characters have decimal representations over 32 and under 127 (see ASCII
// table for more info), variations remains in this interval.
func VariateFlag(identity string, flag string) string {
// Append zeros if necessary (should not happen if proper identity provided).
p := 8 - len(identity)
if p > 0 {
for i := 0; i < p; i++ {
identity += "0"
}
}

// Create PRNG
sb := []byte(identity)
s := int64(binary.BigEndian.Uint64(sb))
prng := rand.New(rand.NewSource(s))

// Compute variations
buf := make([]rune, 0, len(flag))
for _, r := range flag {
vars := getVars(r)
// If no variants available, don't do it
if vars == nil {
buf = append(buf, r)
continue
}
idx := prng.Int() % len(vars)
buf = append(buf, vars[idx])
}
return string(buf)
}

func getVars(r rune) []rune {
for _, vars := range vars {
if slices.Contains(vars, r) {
return vars
}
}
return nil
}
3 changes: 1 addition & 2 deletions sdk/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ type Request struct {
// respond to the chall-manager API call once IaC ran.
type Response struct {
ConnectionInfo pulumi.StringOutput
Flag pulumi.StringOutput
}

// Configuration is the struct that contains the flattened configuration
// from a chall-manager stack up.
type Configuration struct {
Identity string
SourceID string
}

// Load flatten the Pulumi stack configuration into a ready-to-use struct.
Expand All @@ -34,6 +34,5 @@ func Load(ctx *pulumi.Context, project string) *Configuration {

return &Configuration{
Identity: cfg.Get("identity"),
SourceID: cfg.Get("source_id"),
}
}

0 comments on commit 92f9943

Please sign in to comment.