Post

Project SEKAI CTF 2023 - Azusawa’s Gacha World

Description

Category: Forensic

https://azusawa.world/#/2023/03/02

Author: enscribe

❖ Note The website only contains the challenge description, and is not needed to solve the challenge.

dist.zip

Resolution

For this challenge, I worked on Windows.

1. Overview

The attachment is a gacha game made with Unity.

Game

Our goal is to get the limited character but when we have several issues:

  1. we only have one pull (try)
  2. the probability of getting the limited character is 0%!

Pull

However, the character is guarenteed after 1000000 pulls (100万 is 100*10000 in japanese).

We need to reverse the game to bypass make those 1000000 pulls.

2. Unity reversing

As mentionned before, this is a Unity game and there are plenty of tutorials on how to reverse Unity game.

I followed this one: Hacking Unity games - Vaibhav Choudhari.

In short, we can disassemble the file \dist\Asusawa's Gacha World_Data\Managed\Assembly-CSharp.dll which contains all the compiled ressources of the game.

I used dnSpy to disassemble the file.

Once Assembly-CSharp.dll is disassembled, we can see several classes:

![Alt text](image-3.png)

There are two classes which seems to be useful fo us: GachaManager and GameState.

2. Modifying GameState

The class GameState defines internal counter for the number of pulls but also the cost.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using UnityEngine;

// Token: 0x02000007 RID: 7
public class GameState : MonoBehaviour
{
	// Token: 0x06000019 RID: 25 RVA: 0x0000266B File Offset: 0x0000086B
	public void SpendCrystals(int numPulls)
	{
		this.crystals -= ((numPulls == 1) ? 100 : 1000);
		this.pulls += numPulls;
	}

	// Token: 0x0400001C RID: 28
	private const int OnePullCost = 100;

	// Token: 0x0400001D RID: 29
	private const int TenPullCost = 1000;

	// Token: 0x0400001E RID: 30
	public int crystals = 1000;

	// Token: 0x0400001F RID: 31
	public int pulls;
}

I modified the cost of a pull and set the current number of pull to 999999 but it didn’t work.

2. Exploiting GachaManager.SendGachaRequest

In the class GachaManager there is a very interesting method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public IEnumerator SendGachaRequest(int numPulls)
{
  string json = JsonUtility.ToJson(new GachaRequest(this.gameState.crystals, this.gameState.pulls, numPulls));
  using (UnityWebRequest request = this.CreateGachaWebRequest(json))
  {
    yield return request.SendWebRequest();
    if (request.result == UnityWebRequest.Result.Success)
    {
      this.HandleGachaResponse(request.downloadHandler.text, numPulls);
      GachaResponse gachaResponse = JsonUtility.FromJson<GachaResponse>(request.downloadHandler.text);
      base.StartCoroutine(this.uiManager.DisplaySplashArt(gachaResponse.characters));
    }
    else
    {
      this.uiManager.GenericModalHandler(this.uiManager.failedConnectionModal, this.uiManager.failedConnectionModalCloseButton);
      AudioController.Instance.PlaySFX("Open");
    }
  }
  UnityWebRequest request = null;
  yield break;
  yield break;
}

This method send a request to an external server which handles the pulls.

We can see that the sent data are not protected so we can overwrite it with what we want.

Moreover, the request is simply a POST request which we can craft easily.

1
2
3
4
5
6
7
8
9
10
11
private UnityWebRequest CreateGachaWebRequest(string json)
{
  byte[] bytes = Encoding.UTF8.GetBytes(json);
  string s = "aHR0cDovLzE3Mi44Ni42NC44OTozMDAwL2dhY2hh";
  UnityWebRequest unityWebRequest = new UnityWebRequest(Encoding.UTF8.GetString(Convert.FromBase64String(s)), "POST");
  unityWebRequest.uploadHandler = new UploadHandlerRaw(bytes);
  unityWebRequest.downloadHandler = new DownloadHandlerBuffer();
  unityWebRequest.SetRequestHeader("Content-Type", "application/json");
  unityWebRequest.SetRequestHeader("User-Agent", "SekaiCTF");
  return unityWebRequest;
}

The endpoint is base64 encoded, once decoded we have it: http://172.86.64.89:3000/gacha.

We send a POST request using Insomnia:

![Alt text](image-4.png)

![Alt text](image-5.png)

And we sucessfully got the limited character and the flag.

![Alt text](image-6.png)

The flag seems to be in a base64 encoded PNG (starting with iV in base64).

Once decoded:

Flag

We got the flag: SEKAI{D0N7_73LL_53G4_1_C0P13D_7H31R_G4M3}

This post is licensed under CC BY 4.0 by the author.