Points: 200

Description

Great work finding those files! Barry shares the files you extracted with the blue team who share it back to Aaliyah and her team. As a first step, she ran strings across all the files found and noticed a reference to a known DIB, “Guardian Armaments” She begins connecting some dots and wonders if there is a connection between the software and the hardware tokens. But what is it used for and is there a viable threat to Guardian Armaments (GA)?

She knows the Malware Reverse Engineers are experts at taking software apart and figuring out what it’s doing. Aaliyah reaches out to them and keeps you in the loop. Looking at the email, you realize your friend Ceylan is touring on that team! She is on her first tour of the Computer Network Operations Development Program

Barry opens up a group chat with three of you. He wants to see the outcome of the work you two have already contributed to. Ceylan shares her screen with you as she begins to reverse the software. You and Barry grab some coffee and knuckle down to help.

Figure out how the APT would use this software to their benefit

Downloads:

  • Executable from ZFS filesystem (server)
  • Retrieved from the facility, could be important? (shredded.jpg)

Prompt:

  • Enter a valid JSON that contains the (3 interesting) keys and specific values that would have been logged if you had successfully leveraged the running software. Do ALL your work in lower case.

Solution

The first thing to note is the picture they provide us with. It is upside down, but we can just rotate it to be able to read it.

It provides us with the clue JASPER_0. If we remember back to Task 1, the email used for Guardian Armaments was [email protected], so this may be helpful later.

The next thing to do is to look at the provided server executable and start the reverse engineering process.

Looking at it, we can easily see that it’s a 64-bit Go (NSA’s favorite language) binary and it’s not stripped which will make reversing it much easier.

cobra@arch:~/codebreaker/task3$ file server
server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=hnJQ9IsBLehzpfPkH8lC/NDcZM7prk0dN8gIWtohi/Nd_RZ66BNigzeu9KJbFv/t6cvCDa1S1yjcfmp3UGe, with debug_info, not stripped

Let’s start with dynamic analysis then move into static analysis once we get a better idea of what the binary is doing.

Running the binary with --help shows that it’s some kind of seed generation service for the Guardian Armaments contractor.

root@247dd025944d:/challenge# chmod +x server
root@247dd025944d:/challenge# ./server --help
Starting the Guardian Armaments OTP seed generation service!  Please ensure that this software can reach the authentication service to register any generated seeds!  Otherwise your token will not authenticate you to the network after you program it with this seedUsage of ./server:
  -auth-ip string
        Set the IP address of the auth server (default "127.0.0.1")
  -loglevel string
        Set the logging level (debug, info, warn, error) (default "info")

Looks like it connects to an external authentication server and also lets us set the log level which we can set to debug for as much information as possible.

When runnning it, we can see that it tries to connect to port 50052.

root@247dd025944d:/challenge# ./server -loglevel debug
Starting the Guardian Armaments OTP seed generation service!  Please ensure that this software can reach the authentication service to register any generated seeds!  Otherwise your token will not authenticate you to the network after you program it with this seed{"time":"2025-01-03T23:21:10.357459071Z","level":"INFO","msg":"Connected to auth server"}
{"time":"2025-01-03T23:21:10.357940494Z","level":"ERROR","msg":"Failed to ping the auth service","ping_response":null,"err":"rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 127.0.0.1:50052: connect: connection refused\""}

Some quick research online shows that this port is typically associatead with gRPC, but we can also setup a Netcat listener to check.

root@247dd025944d:/challenge# nc -lvnp 50052
root@247dd025944d:/challenge# ./server -loglevel debug 
Starting the Guardian Armaments OTP seed generation service!  Please ensure that this software can reach the authentication service to register any generated seeds!  Otherwise your token will not authenticate you to the network after you program it with this seed{"time":"2025-01-03T23:32:26.66366691Z","level":"INFO","msg":"Connected to auth server"}
listening on [any] 50052 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 50410
PRI * HTTP/2.0

SM

gRPC uses HTTP/2 for transport so this is most likely gRPC, especially considering it is commonly used with Go.

In order to get an understanding of what is being sent across gRPC, we can attempt to extract protocol buffer definitions from the binary.

There’s a great tool called protodump that assists with this. This article explains how it works in depth.

It instantly works to extract all defintions.

root@247dd025944d:/challenge# protodump -file server -output ./extracted-proto
Wrote extracted-proto/google/protobuf/any.proto
Wrote extracted-proto/google/protobuf/duration.proto
Wrote extracted-proto/google/protobuf/timestamp.proto
Wrote extracted-proto/google.golang.org/genproto/googleapis/rpc/status/status.proto
Wrote extracted-proto/seed_generation.proto
Wrote extracted-proto/auth.proto
Wrote extracted-proto/grpc/binlog/v1/binarylog.proto

Of these, auth.proto and seed_generation.proto seem to be relevant.

Since we need to provide the auth server in order to move forward, let’s look at auth.proto first.

syntax = "proto3";

package auth_service;

option go_package = "./auth_service";

service AuthService {
  rpc Authenticate (.auth_service.AuthRequest) returns (.auth_service.AuthResponse) {}
  rpc RegisterOTPSeed (.auth_service.RegisterOTPSeedRequest) returns (.auth_service.RegisterOTPSeedResponse) {}
  rpc VerifyOTP (.auth_service.VerifyOTPRequest) returns (.auth_service.VerifyOTPResponse) {}
  rpc RefreshToken (.auth_service.RefreshTokenRequest) returns (.auth_service.RefreshTokenResponse) {}
  rpc Logout (.auth_service.LogoutRequest) returns (.auth_service.LogoutResponse) {}
  rpc Ping (.auth_service.PingRequest) returns (.auth_service.PingResponse) {}
}

message AuthRequest {
  string username = 1;
  string password = 2;
}

message AuthResponse {
  bool success = 1;
}

message RegisterOTPSeedRequest {
  string username = 1;
  int64 seed = 2;
}

message RegisterOTPSeedResponse {
  bool success = 1;
}

message VerifyOTPRequest {
  string username = 1;
  int64 otp = 2;
}

message VerifyOTPResponse {
  bool success = 1;
  string token = 2;
}

message RefreshTokenRequest {
  string token = 1;
}

message RefreshTokenResponse {
  string token = 1;
}

message LogoutRequest {
  string token = 1;
}

message LogoutResponse {
  bool success = 1;
}

message PingRequest {
  int64 ping = 1;
}

message PingResponse {
  int64 response = 1;
}

We can see what parameters every response and request should have, so we can write our own auth server in Go accordingly. Since gRPC supports a lot of different languages, we could use something like Python for simplicity, but it is probably better to match the structure of the server which we know is using Go. For now, the only function we should need to get it running is a ping function.

After some reserach, I used the following layout when creating my auth server. I’ll walk through creating all of these files.

grpc_server
├── api
|    └── auth.pb.go
├── proto
|    └── auth.proto
├── server
|    ├── handlers.go
|    └── main.go
├── go.mod
└── go.sum

After creating the above directory structure, we can start by compiling the exfiltrated auth.proto protocol buffer. A guide to achieve this can be found here.

Aftering placing auth.proto in grpc_server/proto, we can run protoc to compile it for us. This will place auth.pb.go in grpc_server/api.

root@05fa30350cb0:/challenge# protoc/bin/protoc -I=./grpc_server --go_out=. proto/auth.proto

We can start by defining our main function. This will just listen for a gRPC connection on port 50052 and call our PingHandler function when the Ping method is hit.

// main.go

package main

import (
	"fmt"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	lis, err := net.Listen("tcp", ":50052")
	if err != nil {
		fmt.Printf("Failed to listen: %v\n", err)
		return
	}

	authService := &AuthServiceImpl{}

	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	grpcServer.RegisterService(&grpc.ServiceDesc{
		ServiceName: "auth_service.AuthService",
		HandlerType: (*AuthService)(nil),
		Methods: []grpc.MethodDesc{
			{
				MethodName: "Ping",
				Handler:    authService.PingHandler,
			},
		},
		Streams:  []grpc.StreamDesc{},
		Metadata: "auth_service",
	}, authService)

	fmt.Println("gRPC server running on port 50052")

	if err := grpcServer.Serve(lis); err != nil {
		fmt.Printf("Failed to serve: %v\n", err)
	}
}

The PingHandler function itself can be defined in handlers.go. This will just listen for any pings and respond by reflecting the ping data back to the server.

// handlers.go

package main

import (
	"context"
	"fmt"
	pb "grpc_server/api"

	"google.golang.org/grpc"
)

type AuthService interface {
	PingHandler(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error)
}

type AuthServiceImpl struct{}

func (s *AuthServiceImpl) PingHandler(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error) {
	var req pb.PingRequest

	if err := dec(&req); err != nil {
		return nil, err
	}

	fmt.Println("Received Ping!")

	resp := &pb.PingResponse{
		Response: req.Ping,
	}

	return resp, nil
}

To configure the Go environment, we also need to setup the module.

root@8bbcf549341f:/challenge/grpc_server# go mod init grpc_server
go: creating new go.mod: module grpc_server
go: to add module requirements and sums:
        go mod tidy
root@8bbcf549341f:/challenge/grpc_server# go mod tidy
go: finding module for package google.golang.org/grpc/credentials/insecure
go: finding module for package google.golang.org/protobuf/runtime/protoimpl
go: finding module for package google.golang.org/grpc
go: finding module for package google.golang.org/protobuf/reflect/protoreflect
go: found google.golang.org/protobuf/reflect/protoreflect in google.golang.org/protobuf v1.36.3
go: found google.golang.org/protobuf/runtime/protoimpl in google.golang.org/protobuf v1.36.3
go: found google.golang.org/grpc in google.golang.org/grpc v1.69.4
go: found google.golang.org/grpc/credentials/insecure in google.golang.org/grpc v1.69.4

Now we can finally run our auth server and listen/respond to the incoming ping from the binary.

root@8bbcf549341f:/challenge/grpc_server/server# go run .
gRPC server running on port 50052

As soon as we run the binary, we can see it hit our server and the debug log indicates the pong it received back from its ping, meaning we are succesful.

root@8bbcf549341f:/challenge# ./server -loglevel debug
Starting the Guardian Armaments OTP seed generation service!  Please ensure that this software can reach the authentication service to register any generated seeds!  Otherwise your token will not authenticate you to the network after you program it with this seed{"time":"2025-01-15T20:03:03.414560227Z","level":"INFO","msg":"Connected to auth server"}
{"time":"2025-01-15T20:03:03.415775388Z","level":"DEBUG","msg":"Auth Service Pong ","pong":123}
{"time":"2025-01-15T20:03:03.415799925Z","level":"INFO","msg":"Seedgen Server running on port 50051"}
Received Ping!

Now that we have the seed generation server up running, we can start sending messages to it. The easiest way to do this is with a tool called gRPCurl.

We can look at seed_generation.proto from before to look at what methods we can call.

syntax = "proto3";

package seed_generation;

option go_package = "./seedgen";

service SeedGenerationService {
  rpc GetSeed (.seed_generation.GetSeedRequest) returns (.seed_generation.GetSeedResponse) {}
  rpc Ping (.seed_generation.PingRequest) returns (.seed_generation.PingResponse) {}
  rpc StressTest (.seed_generation.StressTestRequest) returns (.seed_generation.StressTestResponse) {}
}

message GetSeedRequest {
  string username = 1;
  string password = 2;
}

message GetSeedResponse {
  int64 seed = 1;
  int64 count = 2;
}

message StressTestRequest {
  int64 count = 1;
}

message StressTestResponse {
  int64 response = 1;
}

message PingRequest {
  int64 ping = 1;
}

message PingResponse {
  int64 response = 1;
}

Let’s start by sending a ping request.

Note that we need to include the retrieved proto file with all gRPCurl commands so that gRPCurl knows how to format the requests. Trying without it will show an error that the seed generation server does not support the reflection API which would have allowed gRPCurl to query protocol buffers directly from the server.

Upon performing the ping request however, we can immediately see that its not implemented.

root@8bbcf549341f:/challenge# grpcurl -plaintext -proto extracted-proto/seed_generation.proto -d '{"ping":12345}' localhost:50051 seed_generation.SeedGenerationService.Ping
ERROR:
  Code: Unimplemented
  Message: method Ping not implemented

Since we confirmed we are able to get a response though, we can jump into the most interesting looking method, GetSeedRequest.

We just need to provide a username and password. We get an unimplemented response again, but this time its unimplemented on our auth server instead of the seed generation server.

root@8bbcf549341f:/challenge# grpcurl -plaintext -proto extracted-proto/seed_generation.proto -d '{"username":"test","password":"test"}' localhost:50051 seed_generation.SeedGenerationService.GetSeed
ERROR:
  Code: Unimplemented
  Message: unknown method RegisterOTPSeed for service auth_service.AuthService

There are also some interesting messages in the seed generation server log.

{"time":"2025-01-15T20:16:06.014632384Z","level":"DEBUG","msg":"got a GetSeed","username":"test","password":"test"}
{"time":"2025-01-15T20:16:06.014649226Z","level":"INFO","msg":"Authenticating","username":"test","password":"test"}
{"time":"2025-01-15T20:16:06.014659425Z","level":"DEBUG","msg":"test user authenticated, but has no privileges in network so no need to authenticate with Auth Service!"}
{"time":"2025-01-15T20:16:06.014662481Z","level":"INFO","msg":"Registered OTP seed with authentication service","username":"test","seed":5389613538379169202,"count":1}
{"time":"2025-01-15T20:16:06.014906209Z","level":"WARN","msg":"failed to register OTP seed","username":"test","response":null}

What’s weird here is that it doesn’t seem to even authenticate the user with out auth server. It just immediately registers a seed and calls the auth server’s RegisterOTPSeed method.

If we try a username other than test, we get a failed to authenticate message as expected since we haven’t actually implemented the Authenticate method.

root@8bbcf549341f:/challenge# grpcurl -plaintext -proto extracted-proto/seed_generation.proto -d '{"username":"asdf","password":"asdf"}' localhost:50051 seed_generation.SeedGenerationService.GetSeed 
ERROR:
  Code: Unknown
  Message: failed to authenticate
{"time":"2025-01-15T20:20:31.642805366Z","level":"DEBUG","msg":"got a GetSeed","username":"asdf","password":"asdf"}
{"time":"2025-01-15T20:20:31.642820966Z","level":"INFO","msg":"Authenticating","username":"asdf","password":"asdf"}
{"time":"2025-01-15T20:20:31.642825454Z","level":"DEBUG","msg":"Authenticating with auth service"}
{"time":"2025-01-15T20:20:31.643051509Z","level":"ERROR","msg":"Failed to authenticate client with service"}
{"time":"2025-01-15T20:20:31.643060596Z","level":"WARN","msg":"failed to authenticate user","username":"asdf","password":"asdf"}

So it seems that the test user bypasses legimiate authentication. It also works regardless of the user’s password.

Additionally, after attempting to authenticate a second time, the test user no longer works and receives a failed to authenticate message like any other user. This is probably a good thing to start investigating once we start static analysis of the binary.

Let’s go ahead an create the RegisterOTPSeed and Authenticate methods so that we have all the functions we need for our auth server.

We can just follow the same steps as before to modify main.go and handlers.go. For these methods, it may also be helpful to log the request data since its no longer just a simple ping.

// main.go

package main

import (
	"fmt"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	lis, err := net.Listen("tcp", ":50052")
	if err != nil {
		fmt.Printf("Failed to listen: %v\n", err)
		return
	}

	authService := &AuthServiceImpl{}

	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	grpcServer.RegisterService(&grpc.ServiceDesc{
		ServiceName: "auth_service.AuthService",
		HandlerType: (*AuthService)(nil),
		Methods: []grpc.MethodDesc{
			{
				MethodName: "Ping",
				Handler:    authService.PingHandler,
			},
			{
				MethodName: "Authenticate",
				Handler:    authService.Authenticate,
			},
			{
				MethodName: "RegisterOTPSeed",
				Handler:    authService.RegisterOTPSeed,
			},
		},
		Streams:  []grpc.StreamDesc{},
		Metadata: "auth_service",
	}, authService)

	fmt.Println("gRPC server running on port 50052")

	if err := grpcServer.Serve(lis); err != nil {
		fmt.Printf("Failed to serve: %v\n", err)
	}
}

Since we want to simulate an attacker most likely not knowing any valid credentials, we can configure Authenticate to always return false. This is easily modifiable to allow any user to login or even specific ones if needed later.

// handlers.go

package main

import (
	"context"
	"fmt"
	pb "grpc_server/api"
	"strconv"

	"google.golang.org/grpc"
)

type AuthService interface {
	PingHandler(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error)
	Authenticate(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error)
	RegisterOTPSeed(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error)
}

type AuthServiceImpl struct{}

func (s *AuthServiceImpl) PingHandler(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error) {
	var req pb.PingRequest

	if err := dec(&req); err != nil {
		return nil, err
	}

	fmt.Println("Received Ping!")

	resp := &pb.PingResponse{
		Response: req.Ping,
	}

	return resp, nil
}

func (s *AuthServiceImpl) Authenticate(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error) {
	var req pb.AuthRequest

	if err := dec(&req); err != nil {
		return nil, err
	}

	fmt.Println("Received Authenticate request: username=" + req.Username + ", password=" + req.Password)

	resp := &pb.AuthResponse{
		Success: false,
	}

	return resp, nil
}

func (s *AuthServiceImpl) RegisterOTPSeed(srv interface{}, ctx context.Context, dec func(interface{}) error, _ grpc.UnaryServerInterceptor) (interface{}, error) {
	var req pb.RegisterOTPSeedRequest

	if err := dec(&req); err != nil {
		return nil, err
	}

	fmt.Println("Received RegisterOTPSeed request: username=" + req.Username + ", seed=" + strconv.FormatInt(req.Seed, 10))

	resp := &pb.RegisterOTPSeedResponse{
		Success: true,
	}

	return resp, nil
}

Now, all users still return an authentication failure like before except when the first user to authenticate has the username test.

root@8bbcf549341f:/challenge# grpcurl -plaintext -proto extracted-proto/seed_generation.proto -d '{"username":"test","password":"test"}' localhost:50051 seed_generation.SeedGenerationService.GetSeed
{
  "seed": "5389613538379169202",
  "count": "1"
}

We also receive a RegisterOTPSeed request on our server to actually associate the seed with the username, meaning authentication was completely bypassed with this user.

Received RegisterOTPSeed request: username=test, seed=5389613538379169202

This is potentially interesting. However, it only works for the very first authentication request, so we still need to dig deeper.

The next step here is to start static analysis of the seed generation server binary. Luckily, since the binary has debug symbols included, it shouldn’t be too difficult to dechipher.

I personally used Binary Ninja for this challenge, but you could also use Ghidra. Binary Ninja has a hefty price tag of $299 for the personal license, but you can get it for $74 with a student discount. You can also use the free version although I believe it doesn’t support plugins.

With debug symbols, we can easily see a lot of functions to start digging into, but given what we have learned so far, we want to investigate this test user first.

main.(*SeedgenAuthClient).auth seems to be the function where this occurs.

The function takes in the provided username and password then performs some weird XOR operations in a loop.

image

image

After the loop completes, the value of rdx_2 is compared to a constant value of 0xa4f828ea and if it matches, the rest of the code is bypassed and the auth server is never called which is what happens when we use the username test.

image

Since rdx_2 is what is actually being checked, the main line to pay attention to is the XOR operation in the loop. This is somehow linked to letting the test user bypass authentication.

rdx_2 ^= r15_1

We can look at some the initial values before the loop to get a better idea of what is going on.

image

It’s a little bit confusing where the initial value of rdx_2 is coming from. It’s assigned to the value of rdx which comes from a memory offset of rax.

We can jump into GDB (with GEF) to break here and get a better idea of what’s going on.

We can break at the address where rdx is initially assigned then take one step forward to see its value.

gef➤  b *0x007cf4d6
gef➤  r -loglevel debug
gef➤  si

Matching, the variable name, GDB shows the value is stored in the rdx register.

0x7cf4d6 <main.(*SeedgenAuthClient).auth+0056> mov    rdx, QWORD PTR [rax+0x10]

Looking in the register table we can see the following value.

$rdx   : 0x4288bfded08b4d9e  

Interestingly enough, this value isn’t hardcoded anywhere and just seems to magically appear after digging deeper.

However, looking at the lines below it, we can see that the memory where this value comes from is subsequently replaced with the result of a rand function.

007cf4e0        rax_1, x, zmm15 = math/rand.Int63(~r0: c)
007cf4ed        *(username.str + 0x10) = rax_1

username.str is the same memory address as rax which is offset by 0x10 in the initial rdx assignment.

int32_t rdx = (*(rax + 0x10)).d

So it seems like a random value is being stored in memory after the username string. Perhaps the initial value is random as well and just assigned further back in the code?

To check this, we first need to retrieve the seed and then generate our own values.

We can look at the Go math/rand package documentation here.

One function we can try to break at is rand.Seed.

gef➤  b math/rand.Seed

As soon as we run the program like before, we can immediately see the seed pop up.

[#0] 0x49d10e → math/rand.Seed(seed=0x432449ee78e0f)

So, now we know the seed value is 0x432449ee78e0f. Repeating this process multiple times also produces the same value, so we know its a consistent value.

With the seed, we can generate random numbers similar to the server with the Int63 function.

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	rand.Seed(0x432449ee78e0f)
	fmt.Printf("0x%x\n", rand.Int63())
}

Running it, we get the same value as what we found to be assigned to rdx.

root@3f37f97c37b1:/challenge# go run test.go
0x4288bfded08b4d9e

So now, linking this value back to rdx_2, we know its initial value and where it comes from. Additionally, we also know the subsequent values since the memory is just assigned to the value of Int63 after its previous value is copied out. This means that the following authentication attempts will just continue using the next random values based on the seed which is why the test user only works for the first attempt.

Looking back at the XOR operation, we now need to figure out where r15_1 is coming from.

rdx_2 ^= r15_1

We can see that its initially declared in the loop itself then assigned differently across varying cases.

Notably, it’s always assigned to the value of len_2’s memory address offset by i.

len_2 is initially assigned to username.len. Let’s analyze this in memory to see what values are associated with this address.

We can break and step like before then call the GetSeed method with the test user.

gef➤  b *0x007cf602

The value of the operation is stored in the r12 register.

0x7cf602 <main.(*SeedgenAuthClient).auth+0182> mov    r12, QWORD PTR [rsp+0xb0]

Looking at the value, we can see our username and password directly at the memory address.

$r12   : 0x000000c000014208  →  "testtestZ\t./seedgen"  

So, username.len is actually pointing to the memory address of our username which is what len_2 is assigned to.

All of the assignments to r15_1 in the loop are just offsets to different parts of the username string. Since the loop is incrementing i by 4, it appears that it accessing 4 bytes of the username at a time before performing additional XOR operations.

Piecing everything together, the code starts with a random value then XORs it 4 bytes at a time with the provided username. The final result of these XOR operations is compared to a constant value of only 4 bytes and if a match occurs, authentication is bypassed. Additionally, in the process of this authentication attempt, a new random initial value for the next attempt is generated and then utilized subsequently.

At this point, the question to ask is how the APT could of abused this behavior?

Since it uses the same seed every time, the sequence of random values is predictable. So, although the code is masked with the function of allowing a test user to authenticate for the first iteration, additional users may be able to authenticate as long the series of XOR operations with the current random value result in the comparison to the constant being succesful.

Looking back at the beginning of the challenge, the image we were provided with hinted towards the user jasper_03038. Well, given enough iterations, there should eventually be some random initial value that when XORed with that username, will result in the test user authentication bypass being triggered.

This shouldn’t take too many iterations for any username since we only need 4 bytes to match up. However, bruteforcing the server with this username until it eventually suceeds will still take a very long time.

Instead, we can work backwards. We know that we want the result to be 0xa4f828ea. So, we can XOR each chunk of the username with this value until we get what the random initial value needs to be.

This achievable with a few lines of Python code. Since the username is 12 bytes long, we will need to perform 3 XOR operations for each chunk of 4 bytes.

username = b'jasper_03038'
const_hex = 0xa4f828ea

first_iter = const_hex ^ int.from_bytes(username[0:4], 'little')
second_iter = first_iter ^ int.from_bytes(username[4:8], 'little')
third_iter = second_iter ^ int.from_bytes(username[8:12], 'little')

print(hex(third_iter))

Running it, we get the value 0xdce70bd6.

root@3f37f97c37b1:/challenge# python3 find_rand.py
0xdce70bd6

Knowing this value, we can just iterate through the values of the Int63 function until one matches which is way faster than sending endless requests to the server.

However, it is worth noting that the challenge prompt says to provide 3 interesting keys that are logged in the server’s JSON. We can correlate the count key to the number of iterations it takes us to get to the above value, and we can provide the username. However, the most useful thing to be able to provide here would be the seed that the APT generated for the username.

Well, if we look back at authenticating as the test user, the first provided seed is always the same: 5389613538379169202.

If we modify our auth server handler to allow any user to login, the subsequent seeds are also always the same: 985638661823938680, 4440018162547453673, etc.

These values are most likely derived from the same random values that are being XORed with the provided username.

We can expand our test script from earlier to get a list of the first few random values generated by the binary’s seed and compare those to the list of seeds that the program outputs after each succesful authentication.

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	rand.Seed(0x432449ee78e0f)
	fmt.Printf("0x%x\n", rand.Int63())
	fmt.Printf("0x%x\n", rand.Int63())
	fmt.Printf("0x%x\n", rand.Int63())
}
root@3f37f97c37b1:/challenge# go run test.go
0x4288bfded08b4d9e
0x4acbc0f52fb9fdb2
0xdadb1240ae84078

Converting the seeds output by the seed generation server to hex yields the same values offset by one.

5389613538379169202 -> 0x4acbc0f52fb9fdb2
985638661823938680 -> 0xdadb1240ae84078
4440018162547453673 -> 0x3d9e1cfe9eec6ee9

So now, once we find the number of iterations needed for the username jasper_03038, we will also know the seed provided by the seed generation server.

We can write this all out in a single bruteforce script.

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	r := rand.New(rand.NewSource(int64(0x432449ee78e0f)))

	targetSuffix := int64(0xdce70bd6)

	initialXor := r.Int63()
	count := 1

	var seed int64

	for {
		seed = r.Int63()

		if initialXor&0xffffffff == targetSuffix {
			fmt.Println("Found match!")
			fmt.Printf("{\"username\":\"jasper_03038\",\"seed\":%d,\"count\":%d}\n", seed, count)

			break
		}

		initialXor = seed
		count++
	}
}

Running it produces the result in just a couple of seconds.

root@3f37f97c37b1:/challenge# go run brute.go
Found match!
{"username":"jasper_03038","seed":3053067984830347147,"count":1224080480}

Submitting this succesfully solves the task.

Result

So that’s how they leveraged their tokens!