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.
Resolution
For this challenge, I worked on Windows.
1. Overview
The attachment is a gacha game made with Unity.
Our goal is to get the limited character but we have several issues:
- we only have one pull (try)
- the probability of getting the limited character is 0%!
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:
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:
And we successfully got the limited character and the flag.
The flag seems to be in a base64 encoded PNG (starting with iV
in base64).
Once decoded:
We got the flag: SEKAI{D0N7_73LL_53G4_1_C0P13D_7H31R_G4M3}