# Doki Doki Anticheat

Heyo stranger,  
I really need ur help! My PC hasn't been working for the past few days and the
only thing I'm left with are my savefiles (I always have them on my USB-Stick,
just in case). I need to know what's next in my favorite video game, could you
please load these savefiles and tell me the following dialogue, please, I
can't wait any longer!

Here's a link to the game, you can even run it easily on Linux:
https://teamsalvato.itch.io/ddlc

I don't know how to contact you, so just text me the dialogue as a flag, ok?  
So if the dialogue says:"Sayori, what is up with you.." Then just send
flag{Sayori,_what_is_up_with_you..}  
I'd be really REALLY thankful if you'd do that!

## Solution

RenPy save games are stored in `%AppData%/RenPy/<gameName>/` on Windows, so if
we copy the files we got there we can see the following:

![](img/doki.png)

But thanks to [this random online save game editor
website](https://www.saveeditonline.com/)

![](img/dokicheat.png)

We can see some suspicious options - so if we turn them off:

![](img/dokinocheat.png)

This gives us the flag to copy

![](img/dokisolve.png)

```  
flag{...There_is_no_way_I'm_going_to_your_club.}  
```

Note here, that it seems that the `anticheat` value actually matters and
entering 1337 just seems to randomly work.  
That makes this quite the scuffed writeup, but it seems other people have just
extracted the next said sentence from the RenPy save directly,  
if you know what you are looking for then it's very visible.

![](img/dokialt.png)

# Cube Hash

I build a super secure hash function and encoded the hash in my login checker.
But by my bad luck I lost the password. Do you dare to recover it?

## Solution

The program itself comes with symbols so there isn't much in detail low-level
reverse engineering going on.

```python  
CUBE_LENGTH = 0x28  
CUBE_LENGTH_M1 = CUBE_LENGTH-1

def convChar(c):  
rcx = (c - 0x30)&0xff  
if rcx <= 0x4d:  
if rcx >= 0 and rcx <= 9:  
return (c-0x30)&0xff  
if rcx >= 0x11 and rcx <= 0x2a:  
return (c-0x37)&0xff  
if rcx >= 0x31 and rcx <= 0x4a:  
return (c-0x3d)&0xff  
if rcx == 0x4d:  
return 0x3f  
if (((((((((((((((((((((((((((rcx == 0x11 or rcx == 0x12) or rcx == 0x13) or
rcx == 0x14) or rcx == 0x15) or rcx == 0x16) or rcx == 0x17) or rcx == 0x18)
or rcx == 0x19) or rcx == 0x1a) or rcx == 0x1b) or rcx == 0x1c) or rcx ==
0x1d) or rcx == 0x1e) or rcx == 0x1f) or rcx == 0x20) or rcx == 0x21) or rcx
== 0x22) or rcx == 0x23) or rcx == 0x24) or rcx == 0x25) or rcx == 0x26) or
rcx == 0x27) or rcx == 0x28) or rcx == 0x29) or rcx == 0x2a) or rcx == 0x4b)):  
return 0x3e  
return None

def read_a(buf, i, j, x, y):  
if y == 0:  
return buf[i][j][x]  
elif y == 1:  
return buf[i][x][j]  
elif y == 2:  
return buf[x][i][j]  
  
def write_a(buf, i, j, x, y, v):  
if y == 0:  
buf[i][j][x] = v  
elif y == 1:  
buf[i][x][j] = v  
elif y == 2:  
buf[x][i][j] = v

def rotate_cub(buf, x, y):  
for i in range(CUBE_LENGTH//2):  
for j in range(i, CUBE_LENGTH_M1-i):  
v = read_a(buf, i, j, x, y)  
write_a(buf, i, j, x, y, read_a(buf, j, (CUBE_LENGTH_M1 - i), x, y))  
write_a(buf, j, (CUBE_LENGTH_M1 - i), x, y, read_a(buf, (CUBE_LENGTH_M1 - i),
(CUBE_LENGTH_M1 - j), x, y))  
write_a(buf, (CUBE_LENGTH_M1 - i), (CUBE_LENGTH_M1 - j), x, y, read_a(buf,
(CUBE_LENGTH_M1 - j), i, x, y))  
write_a(buf, (CUBE_LENGTH_M1 - j), i, x, y, v)

def hashCube(s):  
hashVal = [convChar(ord(s[i])) for i in range(len(s))]  
bigBuffer = makeCube()  
for x in range(len(s)):  
for y in range(3):  
k3 = (hashVal[x] >> (y*2))&3  
for k in range(k3):  
rotate_cub(bigBuffer, x, y)  
return bigBuffer  
```

Notable here are how the rotates are simplified in `read_a` and `write_a`.

To what is happening is that each character encodes how many spins (0-3) to do
on every axis.

![](img/cubeorder.png)

The amount of input that can be "hashed" is equal to the CUBE_LENGTH. Each
characters "starting position" is `cube[i][i][i]` for a given index `i`.  
Of course unlike an actual Rubik's cube we have cubes within and we do not
care about faces but cube positions (but I still think they are good for
visualization).

![](img/cube1.png)

For `i=1` the black bars here demonstrate the cubes that will be rotates (plus
all the inner cubes).

![](img/cube2.png)

And here for `i=2`.  
First thing to notice is that `cube[i][i][i]` is only ever getting rotated at
index `i`. So the values in the fully "hashed" cube leak information about
individual characters.  
Even more so, something my teammate [duk](https://nothing-ever.works/@duk)
noticed (and these images make quite clear) is that for each index we have
"subcubes" (going `cube[i-y][i-z][i-x]` for `0 < x,y,z < i+1`) that are never
touched again and stay as they are - so in the "hashed" cube we have even more
leaked information.  
  
```python  
def unhashCube(targetCube, x=0, already="", ln=CUBE_LENGTH):  
if x == ln:  
return already  
convCubeT = hashCube(already)

sols = []  
for c in range(0x30, 0x7f):  
conv = convChar(c)  
if conv == None: continue

convCube = copyCube(convCubeT)  
for y in range(3):  
k3 = (conv >> (y*2))&3  
for k in range(k3):  
rotate_cub(convCube, x, y)

if convCube[x][x][x] != targetCube[x][x][x]:  
continue  
  
bad = False  
for i in range(x+1):  
for j in range(x+1):  
for k in range(x+1):  
if convCube[i][j][k] != targetCube[i][j][k]:  
bad = True  
break  
if bad:  
break  
if bad:  
break  
if not bad:  
sols.append(chr(c))  
print(x, sols)  
for c in sols:  
res = unhashCube(targetCube, x+1, already+c, ln)  
if res is not None:  
return res  
return None  
  
def makeCubeFromBinary():  
f = open("chall", "rb")  
data = f.read()[0x0002150:]  
f.close()  
import struct  
return [[[struct.unpack("<I", data[((y*0x28*0x28)+(z*0x28)+x)*4:][:4])[0] for
x in range(CUBE_LENGTH)] for z in range (CUBE_LENGTH)] for y in
range(CUBE_LENGTH)]

hashed = makeCubeFromBinary()  
print(unhashCube(hashed))  
```

Using this information we can bruteforce our way through the cube hash, by
first filtering candidates by the `cube[i][i][i]` property and then using the
inner "subcube" to filter even more.  
Effectively for everything except the first character (where there are no
previous "subcubes") we reduce the possible character amount to 1 making this
almost linear.

This will give us `flag{LuckyYouFoundMyPasswordInThisCube}0` as output, we
ignore the `0` as it serves as filler and is equal to a null byte.

```  
$ ./chall  
Input the password: flag{LuckyYouFoundMyPasswordInThisCube}  
Nice  
```  

# Ghost

A wild ghost appeared! We can try our luck at scaring him away. Since you
don't know how to use those magic spells yet, we asked our elders for a
manual... which... they gave us? But.. Can you help me understand it and show
your mastery of the Ghost-scaring by scaring this guy 50 times in a row?

## Solution

When running the ghost binary without arguments we are given quite a bit of
information

```  
Usage: ./ghost_no_flag <total_numer_of_games> <minimax_search_depth>  
Typical examples values would be:  
total_numer_of_games: 1000  
minimax_search_depth: 32  
Exiting due to incorrect command-line arguments.  
```

Notable here are the amount of games and minimax_search_depth.

![](img/ghost.png)

The main function calculates a random seed, prints it out and then starts
games until all games have been played. Then it prints the flag - on the
remote server it prints the real flag.

![](img/ghostshuffle.png)

The actual game function does quite a lot of seemingly weird stuff, mostly
because of optimized code.  
First of all an array gets shuffled based on the randomSeed, the current game
index and the turn index.

![](img/ghostchoice.png)

This shuffled array is then used to communicate a possibly random choice the
binary takes.

We as a user are offered the options between 1 and 12 to enter but the binary
actually just takes `(inputNumber-1)%9` and puts our choice in that array
spot.

![](img/ghostwin.png)

After each turn the game checks whether a win conditions has been fulfilled
and either makes the player, the program or nobody win.  
If the program wins the program exits - we need to prevent this from
happening.

Now the interesting observation is that this is in fact a Tic-Tac-Toe game
(which is also hinted at by the minimax parameter as it's a popular algorithm
to solve the game).

So to solve this challenge we need to re implement the shuffle logic to keep
track of the enemy turns and then use our own minimax implementation to always
play a winning game or a draw.

A nice overview of how to implement this algorithm can be found
[Here](https://thesharperdev.com/coding-the-perfect-tic-tac-toe-bot/ ).

```python  
def swap(shuffle, a, xorStuff, m):  
tmp = shuffle[a]  
shuffle[a] = shuffle[xorStuff%m]  
shuffle[xorStuff%m] = tmp  
  
  
def shuffeArray(gameIndex, iterationIndex, seed):  
shuffle = [i for i in range(13)]  
xorStuff = gameIndex^seed^iterationIndex

for i in range(12, 0, -1):  
swap(shuffle, i, xorStuff, i+1)  
return shuffle  
  
spookyArray = [  
"*OoooOOOoooo*",  
"*Booooo-hoooo*",  
"*Eeeeek*",  
"*Hoooowl*",  
"*Sliiither*",  
"*Waaail*",  
"*Woooosh*",  
"*Eeeerie*",  
"*Creeeeeeak*",  
"*Haauuunt*",  
"*Woooo-woooo*",  
"*Gaaaasp*",  
"*Shiiivver*"  
]

def getEnemyTurn(msg, iterationIndex, gameIndex, seed):  
shuffle = shuffeArray(gameIndex, iterationIndex, seed)  
unshuffle = [0 for _ in range(0xd)]  
for i in range(0xd):  
unshuffle[shuffle[i]] = i  
  
unspookyMap = {}

for i in range(len(spookyArray)):  
unspookyMap[spookyArray[i]] = unshuffle[i]

return unspookyMap[msg]  
```

This is the implemented logic to translate the spooky noises given seed, game
index and turn index.  
The full script is [here](ghost.py).

Running it against the remote gives the flag

```  
SUCCESS! flag{ghosts_can_play_tic_tac_toe_too}  
```

# The Password Game

Please choose a password.

## Solution

The password game provides us with a website which executes a complex circuit
through CSS magic.  
Our goal is to enter something that matches 11 hidden rules.

![](img/pwanim.png)

Going through the `game-animator.min.js` and giving things more useful names
shows that this is a system with 8 16-bit registers and 0x20000 bytes of RAM.

One interesting thing is that the css "animation" is triggered many times
until a halt condition is reached.  
Each cycle represents something like an instruction and in between there is
the very convenient `window.animationHook` function that is called that we can
overwrite (of course we could also just modify the source for this).

```javascript  
function decimalToHex(d) {  
var hex = Number(d).toString(16);  
hex = "0000".substr(0, 4 - hex.length) + hex;  
return hex;  
}  
window.animationHook = function() {  
var out = F.reduceRight((acc, cur) => cur[0]+": "+decimalToHex(R[cur[0]])+",
"+acc, "")+" ha: "+B.ha;  
if(B.lf) {  
out = out+", lf: "+B.lf+", la: "+decimalToHex(R.la)+", ld:
"+decimalToHex(M[R.la]);  
}  
if(B.sf) {  
out = out+", sf: "+B.sf+", sa: "+decimalToHex(R.sa)+", sd:
"+decimalToHex(R.sd);  
}  
console.log(out);  
}  
```

Through this little added code (e.g. in the console or actually appended to
the code) we get program traces from the password matching!

```  
R1: 1000, R2: 0002, R3: 0000, R4: 0000, R5: 0000, R6: 0001, R7: 0000, R8:
0009, ha: 0, lf: 1, la: 1000, ld: 0002  
R1: 0000, R2: 0002, R3: 0000, R4: 0000, R5: 0000, R6: 0001, R7: 0000, R8:
000a, ha: 0  
R1: 0024, R2: 0002, R3: 0000, R4: 0000, R5: 0000, R6: 0001, R7: 0000, R8:
000b, ha: 0  
R1: 0024, R2: 0002, R3: 0000, R4: 0000, R5: 0000, R6: 0001, R7: 0000, R8:
000c, ha: 0  
```

For example when we enter 2 characters, we fail very early and get the
following trace.  
The password is stored in the format `[pwlen] [password]` at `0x1000`, so this
reads the lengths and then fails.  
Notably here is that there is another number that is a viable length - 36.  
If we enter 36 characters we pass Rule 1 and a lot more computation happens
directly afterwards.

Using this method of failing traces, and trying to make the program behave
differently we can very easily deduce the first 8 rules:

Rule 1: The password has to be 36 characters  
Rule 2: The password needs to contain a number  
Rule 3: The password needs to contain an uppercase letter  
Rule 4: Probably requirement for `{` and `}` letters  
Rule 5: The numbers in the password need to add up to 9  
Rule 6: The password needs to end with `}`  
Rule 7: The password starts with `flag{`  
Rule 8: All characters are valid printable ASCII letters

Rule 9 is a bit more complex.

It iterates for the following addresses and characters for each flag
character:

```  
lf: 1, la: 0105, ld: 007b  
lf: 1, la: 0107, ld: 0031  
lf: 1, la: 0109, ld: 0073  
lf: 1, la: 010b, ld: 0075  
lf: 1, la: 010d, ld: 0079  
lf: 1, la: 010f, ld: 0030  
lf: 1, la: 0111, ld: 0065  
lf: 1, la: 0113, ld: 0042  
lf: 1, la: 0115, ld: 0078  
lf: 1, la: 0117, ld: 0064  
lf: 1, la: 0119, ld: 0057  
lf: 1, la: 011b, ld: 0038  
lf: 1, la: 011d, ld: 004b  
```

And if they match a certain other character has to follow.  
Extracted this looks like this:

```python  
def applyRule9(arr):  
for i in range(len(arr)-1):  
if arr[i] == 0x7b: arr[i+1] = arr[i] ^ 15  
elif arr[i] == 0x31: arr[i+1] = arr[i] ^ 66  
elif arr[i] == 0x73: arr[i+1] = arr[i] ^ 44  
elif arr[i] == 0x75: arr[i+1] = arr[i] ^ 25  
elif arr[i] == 0x79: arr[i+1] = arr[i] ^ 38  
elif arr[i] == 0x30: arr[i+1] = arr[i] ^ 66  
elif arr[i] == 0x65: arr[i+1] = arr[i] ^ 58  
elif arr[i] == 0x42: arr[i+1] = arr[i] ^ 58  
elif arr[i] == 0x78: arr[i+1] = arr[i] ^ 28  
elif arr[i] == 0x64: arr[i+1] = arr[i] ^ 51  
elif arr[i] == 0x57: arr[i+1] = arr[i] ^ 111  
elif arr[i] == 0x38: arr[i+1] = arr[i] ^ 115  
elif arr[i] == 0x4b: arr[i+1] = arr[i] ^ 54  
```

Now to Rule 10:

This was a bit more trick to reverse.

On the following index pairs `[(6, 7), (0xa, 0xb), (0xe, 0xf), (0x13, 0x14),
(0xd, 0x11), (0x1c, 0x1d)]` we are run through this checksum function to match
the following hashes `[0xb9fe, 0xe249, 0x5d06, 0xa9df, 0x362c, 0x08ff]`.

```python  
def checksum(a, b):  
v0 = data191[a^0xff]  
v1 = data191[(((~v0)>>8)&0xff)^b]  
res = v1^((v0<<8)&0xff00)  
return res  
```

This lookup table at address 0x191 can be dumped with `for(var
i=0;i<0x100;i++) console.log(M[0x191+i]);` in the javascipt console.

The last Rule 11:

We currently have the following input `flag{th1s_is_truly_h0r??????mBxdW8K}`.  
So we are missing 6 characters. This last rule is difficult to do by trace
analysis because in reality it creates instructions for the underlying machine
from these letters.  
So instead of solving it "legit" we cheese!

First of all, from the `applyRule9` rules, all have been applied except the
rule for the character `e`, which will unroll to `e_`.  
Looking at the words, the flag probably wants to say `this is truly horrible`.  
So we are at `flag{th1s_is_truly_h0r????e_mBxdW8K}`, missing `r`, `i`, `b` and
`l`.  
The obvious one where they are all lowercase does not work, but there are only
16 options so we can just bruteforce them all.

In the end `flag{th1s_is_truly_h0rrIbLe_mBxdW8K}` is the correct flag.

![](img/pwflag.png)

Original writeup
(https://github.com/Pusty/writeups/tree/master/HackLuCTF2023#the-password-
game).