CTF - robotrickster - Reverse - Tinkoff

Ссылка на задачу
Робошулер

Описание
В космопоездах беда: появился обаятельный андроид-шулер, который обыгрывает всех в наперстки и обирает до нитки
Совершите маленькую шалость — выиграйте у робошулера весь миллион монет, которые звенят в его карманах.

Android приложение
robotrickster.zip

Выполним декомпиляцию приложения
Можно воспользоваться любым онлайн декомпилятором, например .JAR and .Class to Java decompiler
После докомпиляции получаем архив с .java файлами

Используются 3 эндпоинта:

  • PUT /start/{uuid}
    • Инициализация игры, в качестве uuid используется uuid, который заполняется случайным образом как UUID.randomUUID().toString()
  • GET /next/{uuid}
    • Переход к следующему раунду
  • GET /accept/{uuid}/{answer}, где answer это число
    • Выбор пользователем правильного наперстка

GameService.java

public interface GameService {
    @GET("/accept/{uuid}/{answer}")
    Call<GameState> accept(@Path("uuid") String str, @Path("answer") Integer num);

    @GET("/next/{uuid}")
    Call<List<List<Integer>>> getNext(@Path("uuid") String str);

    @PUT("/start/{uuid}")
    Call<ResponseBody> start(@Path("uuid") String str);
}

Эндпоинты относятся к хосту t-trickster-jbi8aw9z.spbctf.ru, который задается при создании GameService

GameServiceFactory.java

public class GameServiceFactory {
    public GameService create() {
        return (GameService) new Retrofit.Builder()
            .baseUrl("https://t-trickster-jbi8aw9z.spbctf.ru/")
            .addConverterFactory(GsonConverterFactory.create()).build()
            .create(GameService.class);
    }
}

В классе FlagController есть есть функция decrypt, которая использует симметричное шифрование флага с помощью xor
Ключ передается в качестве аргумента

FlagController.java

public class FlagController {
    public byte[] flag = new byte[0];
    private GameService gameService;

    public FlagController(GameService gameService2) {
        this.gameService = gameService2;
        gameService2.start(UUIDController.uuid).enqueue(new Callback<ResponseBody>() {
            public void onFailure(Call<ResponseBody> call, Throwable th) {
            }

            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                try {
                    FlagController.this.flag = response.body().bytes();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    public String decrypt(byte[] bArr) {
        byte[] bArr2 = new byte[this.flag.length];
        int i = 0;
        while (true) {
            byte[] bArr3 = this.flag;
            if (i >= bArr3.length) {
                return new String(bArr2, StandardCharsets.UTF_8);
            }
            bArr2[i] = (byte) (bArr3[i] ^ bArr[i % bArr.length]);
            i++;
        }
    }
}

В функции clickByCup происходит вызов эндпоинта GET /accept/{uuid}/{answer}, где в качестве awswer указывается выбранный пользователем наперсток. В случае успеха обновляется счет и вызывается функция update. Функция update выводит расшифрованный флаг, если ключ не пустой

GameViewModel.java

public class GameViewModel extends AndroidViewModel {
    private MutableLiveData<String> flag = new MutableLiveData<>("");
    private FlagController flagController = new FlagController(this.gameService);

    // ...

    public void startGame() {
        this.blockPlayButton.setValue(false);
        this.score.setValue(0);
    }

    public List<Integer> getCurrentPosition() {
        return this.currentPosition.getValue();
    }

    public void getNextTransposition(Callback<List<List<Integer>>> callback) {
        this.blockPlayButton.setValue(true);
        this.gameService.getNext(UUIDController.uuid).enqueue(callback);
    }

    public void clickByCup(int i) {
        if (this.canTryAnswer) {
            stopChooseAnswer();
            this.gameService.accept(UUIDController.uuid, Integer.valueOf(this.currentPosition.getValue().lastIndexOf(Integer.valueOf(i)))).enqueue(new Callback<GameState>() {
                public void onResponse(Call<GameState> call, Response<GameState> response) {
                    GameViewModel.this.blockPlayButton.setValue(false);
                    GameViewModel.this.score.setValue(Integer.valueOf(response.body().getScore()));
                    GameViewModel.this.update(response.body().getKey());
                }

                public void onFailure(Call<GameState> call, Throwable th) {
                    GameViewModel.this.blockPlayButton.setValue(false);
                    GameViewModel.this.update(new byte[0]);
                }
            });
        }
    }

    public void update(byte[] bArr) {
        if (bArr != null && bArr.length != 0) {
            this.blockPlayButton.setValue(true);
            this.flag.setValue("Шулер-андроид повержен!\nЕго последние слова:\n" + this.flagController.decrypt(bArr));
        }
    }

    // ...
}
  • Начинаем новую игру с помощью PUT /start/{uuid}
  • Пока не получим ключ повторяем
    • Запускаем перемешивание с помощью GET /next/{uuid}
    • Узнаем текущую позицию шара и указываем ее в GET /accept/{uuid}/{answer}
  • С помощью ключа расшифровываем флаг

Напишем программу на Go:

main.go

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/google/uuid"
)

type ScoreDTO struct {
	Key []int `json:"key,omitempty"`
}

const serverURL = "https://t-trickster-jbi8aw9z.spbctf.ru"

func start() (string, []byte, error) {
	uuid := uuid.NewString()
	flag, err := request("PUT", serverURL+"/start/"+uuid)
	return uuid, flag, err
}

func next(uuid string) ([]byte, error) {
	data, err := request("GET", serverURL+"/next/"+uuid)
	return data, err
}

func accept(uuid string, answer string) ([]byte, error) {
	data, err := request("GET", serverURL+"/accept/"+uuid+"/"+answer)
	return data, err
}

func request(method, url string) ([]byte, error) {
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		return nil, err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode >= http.StatusBadRequest {
		return nil, fmt.Errorf("request: invalid status: %v", resp.Status)
	}

	bs, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	return bs, nil
}

func calcAnswer(bs []byte, answer string) string {
	switch {
	case string(bs[len(bs)-3]) == answer:
		return "2"
	case string(bs[len(bs)-5]) == answer:
		return "1"
	case string(bs[len(bs)-7]) == answer:
		return "0"
	default:
		return "X"
	}
}

func encryptFlag(flag, data []byte) string {
	var score ScoreDTO
	err := json.Unmarshal(data, &score)
	if err != nil {
		return ""
	}

	if len(score.Key) == 0 {
		return ""
	}

	flagEncrypted := ""
	for i := range flag {
		flagEncrypted += string(byte(int(flag[i]) ^ score.Key[i%len(score.Key)]))
	}

	return flagEncrypted
}

func robotrickster() error {
	uuid, flagEncrypted, err := start()
	if err != nil {
		return err
	}

	fmt.Printf("uuid: %v\n", uuid)
	fmt.Printf("flag: %v\n", flagEncrypted)

	answer := "1"

	for {
		data, err := next(uuid)
		if err != nil {
			return err
		}

		fmt.Printf("positions: %v\n", string(data))

		answer = calcAnswer(data, answer)

		data, err = accept(uuid, answer)
		if err != nil {
			return err
		}

		fmt.Printf("score:    %v\n", string(data))

		flag := encryptFlag(flagEncrypted, data)
		if flag != "" {
			fmt.Printf("flag: %v\n", flag)
			break
		}
	}

	return nil
}

func main() {
	err := robotrickster()
	if err != nil {
		fmt.Println("ERROR:", err)
	}
}

Запускаем написанную на Go программу:

output.txt

[amyasnikov@ubuntu:~]$ go run main.go

uuid: b3eb04b4-03fc-4888-97b1-5446de3156d2
flag: [188 231 38 149 94 210 245 103 248 240 13 128 82 145 171 97 164 225 32 172 86 195 183 54 251 231 58 150 86 255 177 109 251 234 13 145 87 197 164 110 253 219 54 195 82 206 184]
positions: [[2,1,0],[0,2,1],[0,1,2],[1,2,0],[0,1,2],[2,1,0]]
score:    {"score":1,"key":null}
positions: [[1,0,2],[0,2,1],[1,0,2],[2,0,1],[0,1,2],[2,1,0],[0,1,2],[2,1,0]]
score:    {"score":2,"key":null}
positions: [[2,1,0],[0,1,2],[1,0,2],[2,0,1],[2,1,0],[0,1,2]]
score:    {"score":3,"key":null}
positions: [[0,1,2],[1,2,0],[0,1,2],[1,2,0],[2,1,0],[1,2,0]]
score:    {"score":4,"key":null}
positions: [[0,2,1],[0,1,2],[0,2,1],[2,0,1],[0,1,2],[2,1,0],[0,2,1]]
score:    {"score":5,"key":null}
positions: [[0,2,1],[1,0,2],[2,1,0],[2,0,1],[0,2,1],[1,0,2],[2,0,1]]
score:    {"score":6,"key":null}
positions: [[0,1,2],[2,1,0],[2,0,1],[1,0,2],[1,2,0],[0,2,1],[2,1,0]]
score:    {"score":7,"key":null}
positions: [[1,2,0],[0,1,2],[1,0,2],[1,2,0],[1,0,2],[1,2,0]]
score:    {"score":8,"key":null}
positions: [[1,2,0],[1,0,2],[2,0,1],[0,2,1],[1,0,2]]
score:    {"score":9,"key":null}
positions: [[2,0,1],[2,1,0],[0,1,2],[2,1,0],[1,2,0],[2,1,0],[0,1,2],[0,2,1]]
score:    {"score":10,"key":null}
positions: [[0,1,2],[2,0,1],[1,0,2],[1,2,0],[0,2,1],[1,2,0]]
score:    {"score":11,"key":null}
positions: [[1,0,2],[0,2,1],[1,0,2],[2,0,1],[1,0,2],[0,2,1]]
score:    {"score":12,"key":null}
positions: [[1,0,2],[0,1,2],[2,1,0],[0,2,1],[2,1,0],[1,2,0],[0,1,2],[0,2,1]]
score:    {"score":13,"key":null}
positions: [[2,1,0],[0,2,1],[0,1,2],[1,0,2],[1,2,0],[2,1,0],[1,0,2],[2,1,0]]
score:    {"score":14,"key":null}
positions: [[0,1,2],[1,0,2],[1,2,0],[2,0,1],[1,0,2],[2,1,0]]
score:    {"score":15,"key":null}
positions: [[2,1,0],[2,0,1],[0,1,2],[1,0,2],[2,1,0],[1,0,2]]
score:    {"score":16,"key":null}
positions: [[2,0,1],[1,2,0],[2,1,0],[2,0,1],[1,0,2],[0,1,2],[2,1,0]]
score:    {"score":17,"key":null}
positions: [[2,0,1],[2,1,0],[2,0,1],[1,2,0],[2,0,1],[0,2,1]]
score:    {"score":18,"key":null}
positions: [[1,0,2],[1,2,0],[0,2,1],[2,1,0],[1,2,0],[2,0,1],[0,2,1],[2,1,0]]
score:    {"score":19,"key":null}
positions: [[1,2,0],[0,1,2],[0,2,1]]
score:    {"score":20,"key":[-56,-124,82,-13,37,-96,-59,5]}
flag: tctf{r0b0t_sw1ndler_scr33ches_th3n_break5_d0wn}

Получили флаг tctf{r0b0t_sw1ndler_scr33ches_th3n_break5_d0wn}

Похожее