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 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 1,000,000 pulls (100万 is 100*10000 in japanese).

We need to reverse the game to bypass those 1,000,000 pulls.

2. Unity reversing

As mentioned 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 resources 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 seem to be useful for us: GachaManager and GameState.

2. Modifying GameState

The class GameState defines an 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 pulls to 999,999, 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 sends 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 that 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 successfully 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.