⚠️⚠️⚠️ This is a solution to the challenge. This post will be full of spoilers.
Download the binaries here: https://github.com/mlesterdampios/huntress-2024-binary-challenges
THIS IS ONE OF MY FAVORITE CHALLEGE FOR HUNTRESS 2024 AS THIS REMIND’S ME OF VERITASIUM’S “MATH’S FUNDAMENTAL FLAW”
Upon initial triage, the binary is built from protobuf and every tick is saved in file named game_state.pb
. Upon observation, there are 12 X
and O
below the game screen. Sometimes they switch. Based from inference, we must at least meet all 12 to be O
. A single O
, means a condition was met, so we must investigate to get the conditions so we can probably win this game.
We start first by extracting the protobuf definitions from the binary using https://github.com/arkadiyt/protodump.
syntax = "proto3";
package thats_life;
option go_package = "github.com/HuskyHacks/thats_life/pb;pb";
message Cell {
bool alive = 1;
int32 color = 2;
}
message CellRow {
repeated Cell cells = 1;
}
message Grid {
int32 width = 1;
int32 height = 2;
repeated CellRow rows = 3;
}
We are able to get the protobuf definition. Now we try to parse the game_state.pb
// cmd/deserialize.go
package main
import (
"fmt"
"io/ioutil"
"log"
"strings"
"example.com/m/v2/pb"
"google.golang.org/protobuf/proto"
)
func main() {
// Path to the serialized Grid file
filePath := "game_state.pb"
// Read the serialized Grid from the file
data, err := ioutil.ReadFile(filePath)
if err != nil {
log.Fatalf("Failed to read file %s: %v", filePath, err)
}
// Create an empty Grid object
var grid pb.Grid
// Deserialize the data into the Grid object
err = proto.Unmarshal(data, &grid)
if err != nil {
log.Fatalf("Failed to deserialize Grid: %v", err)
}
// Generate Go code representation
goCode := formatGridAsGoCode("grid", &grid)
// Print the generated Go code
fmt.Println(goCode)
}
// formatGridAsGoCode formats the Grid object into a Go code snippet
func formatGridAsGoCode(varName string, grid *pb.Grid) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("%s := &pb.Grid{\n", varName))
sb.WriteString(fmt.Sprintf(" Width: %d,\n", grid.Width))
sb.WriteString(fmt.Sprintf(" Height: %d,\n", grid.Height))
sb.WriteString(" Rows: []*pb.CellRow{\n")
for _, row := range grid.Rows {
sb.WriteString(" {\n")
sb.WriteString(" Cells: []*pb.Cell{\n")
for _, cell := range row.Cells {
sb.WriteString(fmt.Sprintf(" {Alive: %t, Color: %d},\n", cell.Alive, cell.Color))
}
sb.WriteString(" },\n")
sb.WriteString(" },\n")
}
sb.WriteString(" },\n")
sb.WriteString("}\n")
return sb.String()
}
We have successfully deserialized the pb file. Therefore we can create a solution too by forging our own data based on winning conditions and serialize it.
Upon further reverse engineering, there is a win criteria generation in the binary.
Upon investigating the qwordWinCriteria
, it seems like it stores the data for the win condition.
Entry 0:
- Field 0 (Row): 0x0A00000000000000 -> 10
- Field 8 (Column): 0x0F00000000000000 -> 15
- Field 16 (Value): 0x1F00000000000000 -> 31
Entry 1:
- Field 0 (Row): 0x1400000000000000 -> 20
- Field 8 (Column): 0x1900000000000000 -> 25
- Field 16 (Value): 0x2000000000000000 -> 32
... and so on.
With these information, we are now ready to craft the solution.
// serialize.go
package main
import (
"log"
"os"
"example.com/m/v2/pb"
"google.golang.org/protobuf/proto"
)
func main() {
// Define the win criteria
winCriteria := []struct {
Row int32
Column int32
Color int32
}{
{10, 15, 31},
{20, 25, 32},
{30, 35, 33},
{40, 45, 34},
{25, 50, 35},
{5, 55, 36},
{15, 60, 37},
{35, 65, 31},
{45, 70, 32},
{0, 75, 33},
{1, 80, 34},
{2, 85, 35},
}
// Initialize the grid
width := int32(400)
height := int32(50)
grid := &pb.Grid{
Width: width,
Height: height,
Rows: make([]*pb.CellRow, height),
}
// Initialize all cells to dead and color 0
for i := int32(0); i < height; i++ {
row := &pb.CellRow{
Cells: make([]*pb.Cell, width),
}
for j := int32(0); j < width; j++ {
row.Cells[j] = &pb.Cell{
Alive: false,
Color: 0,
}
}
grid.Rows[i] = row
}
// Apply the win criteria
for _, wc := range winCriteria {
if wc.Row >= 0 && wc.Row < height && wc.Column >= 0 && wc.Column < width {
grid.Rows[wc.Row].Cells[wc.Column].Alive = true
grid.Rows[wc.Row].Cells[wc.Column].Color = wc.Color
} else {
log.Fatalf("Win criteria position out of bounds: (%d, %d)", wc.Row, wc.Column)
}
}
// Serialize the Grid to binary format
data, err := proto.Marshal(grid)
if err != nil {
log.Fatalf("Failed to serialize Grid: %v", err)
}
// Write to a file
file, err := os.Create("game_state.pb") // The game expects this filename
if err != nil {
log.Fatalf("Failed to create file: %v", err)
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
log.Fatalf("Failed to write data to file: %v", err)
}
log.Println("Grid serialized to game_state.pb successfully.")
}