Programmatically generate three of the five in-game codes

This commit is contained in:
RunasSudo 2017-02-05 22:11:55 +10:30
parent 80a3dbab99
commit dbc021967a
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 221 additions and 0 deletions

View File

@ -111,3 +111,144 @@ In other words, a similar string-handling subroutine is called, but instead of `
Now we have everything we need to [extract these encrypted (double-encrypted??) strings](https://github.com/RunasSudo/synacor.py/blob/master/tools/decrypt_strings.py) from the binary!
Only the self-test completion code appears to be stored there, though, so I'm not sure what the point of encrypting those was…
## The codes
We may not have the codes themselves, but we can now easily locate where they are printed. The tablet code, for example, is conveniently sandwiched between strings `6ed1` and `6ef1`. Thus the code we are looking for is:
1290 set R1 1092
1293 set R2 650a
1296 set R3 7fff
1299 set R4 6eed
129c call 0731
Looking into `0731`:
0731 push R4
0733 push R5
0735 push R6
0737 push R7
0739 set R7 0001
073c add R5 R4 R7
0740 rmem R5 R5
0743 add R6 17ed R7
0747 wmem R6 R5
074a add R7 R7 0001
074e rmem R6 17ed
0751 gt R5 R7 R6
0755 jf R5 073c
0758 set R4 0000
075b set R5 0000
075e rmem R6 17ed
0761 mod R6 R5 R6
0765 add R6 R6 17ed
0769 add R6 R6 0001
076d rmem R7 R6
0770 mult R7 R7 1481
0774 add R7 R7 3039
0778 wmem R6 R7
077b push R1
077d push R2
077f set R2 R7
0782 call 084d
0784 set R7 R1
0787 pop R2
0789 pop R1
078b rmem R6 R2
078e mod R7 R7 R6
0792 add R7 R7 0001
0796 gt R6 R7 R3
079a jt R6 07a0
079d set R4 0001
07a0 add R7 R7 R2
07a4 rmem R7 R7
07a7 add R5 R5 0001
07ab add R6 R5 17f1
07af wmem R6 R7
07b2 rmem R6 17f1
07b5 eq R6 R5 R6
07b9 jf R6 075e
07bc jf R4 0758
07bf push R1
07c1 set R1 17f1
07c4 call 05ee
07c6 pop R1
07c8 pop R7
07ca pop R6
07cc pop R5
07ce pop R4
07d0 ret
Umm… Sorry, could you repeat that?
Rewriting this again in more friendly terms:
```c
// R1: A seed of sorts - the same for all users
// R2: The length and alphabet to use, usually 650a, but 653f for the mirror
// R3: Usually 7fff, but 0004 for the mirror
// R4: An initialisation vector of sorts - contents different for every user - points to the length, but this is always 3
0731(R1, R2, R3, R4) {
// copy the string at R4 to 17ed
R7 = 0001;
do {
R5 = R4 + R7;
R5 = [R5];
R6 = 17ed + R7;
[R6] = R5;
R7 = R7 + 0001;
R6 = [17ed]; // 3, the length - this never seems to change
} while (R7 <= R6);
// the string at 17ed is now what was at R4
do {
R4 = 0000; // done flag
R5 = 0000; // index
do {
R6 = [17ed]; // 3, the length
R6 = R5 % R6;
R6 = R6 + 17ed;
R6 = R6 + 0001; // will cycle through three addresses of 17ed/R4 string: 17ee, 17ef, 17f0
R7 = [R6];
R7 = R7 * 1481;
R7 = R7 + 3039;
[R6] = R7; // mutate that value of the 17ed string
R7 = R1 ^ R7; // combine R1 into R7
R6 = [R2]; // length of the alphabet
R7 = R7 % R6;
R7 = R7 + 0001; // calculate index in alphabet
if (R7 <= R3) {
R4 = 0001; // we are done with the entire code - this returns immediately for all except the mirror
}
R7 = R7 + R2; // calculate address of letter to use
R7 = [R7]; // the letter to use
R5 = R5 + 0001; // increment the index
R6 = R5 + 17f1; // index of new letter in code
[R6] = R7; // set the letter
R6 = [17f1]; // length of the code: twelve letters
} while (R5 != R6); // loop until we've generated all the letters
} while (!R4);
print(17f1);
}
```
Re-implementing this in Python, we can now extract the code for the tablet directly from the binary!
Unfortunately, each of the other codes uses an `R1` based on the solution to the puzzle. In the case of the maze code:
0f03 rmem R1 0e8e
The value at `0e8e` is derived from which rooms are visited in the maze, as mentioned in the main note file. Armed with our [trusty map](https://github.com/RunasSudo/synacor.py/blob/master/tools/graph.py), and cross-referencing the callbacks with the files, we identify:
* Twisty passages entrance, `0949`: Calls `0e9e`, resets `0e8e` to `0000`.
* West to `095d`: Calls `0ec0`: `OR`s `0e8e` with `0008`.
* South to `0926`: Calls `0eca`: `OR`s `0e8e` with `0010`.
* North to `096c`: Calls `0ede`: `OR`s `0e8e` with `0040`.
Putting it all together, the final value at `0e8e` is `0008 | 0010 | 0040` = `0058`.
Similarly, the `R1` for the teleporter code is the value of `R8` from the Ackermann function, which we determined earlier to be `6486`:
1592 set R1 R8
The remaining two codes, for the coins and the vault, are more complicated still, but follow the same pattern of determining `R1` based on the player's history.

View File

@ -24,6 +24,7 @@ SYN_REG = [0] * 8
SYN_STK = []
SYN_STDIN_BUF = []
DBG_FLAG = False
DBG_CSTK = []
class OpLiteral:
@ -72,6 +73,9 @@ else:
# Begin execution
while True:
if DBG_FLAG:
import pdb; pdb.set_trace()
instruction = swallowOp().get()
if instruction == 21: #NOP

76
tools/generate_codes.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
# synacor.py - An implementation of the Synacor Challenge
# Copyright © 2017 RunasSudo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import struct
import sys
IV_LEN = 3
CODE_LEN = 12
def generate_code(R1, R2, R3, R4):
R2data = SYN_MEM[R2+1:R2+1+SYN_MEM[R2]]
R4data = SYN_MEM[R4+1:R4+1+SYN_MEM[R4]]
assert len(R4data) == IV_LEN
done = False
while not done:
code = ''
for i in range(CODE_LEN):
R6 = i % IV_LEN
R7 = R4data[R6]
R7 = (R7 * 0x1481) % 0x8000
R7 = (R7 + 0x3039) % 0x8000
R4data[R6] = R7
R7 = R1 ^ R7
R7 = R7 % len(R2data)
if R7 + 1 <= R3:
done = True
R7 = R2data[R7]
code += chr(R7)
return code
# Read code into memory
SYN_MEM = [0] * 32768
with open(sys.argv[1], 'rb') as data:
i = 0
while True:
byteData = data.read(2)
if len(byteData) < 2:
break
SYN_MEM[i] = struct.unpack('<H', byteData)[0]
i += 1
# Emulate 06bb
for R2 in range(0x17b4, 0x7562):
R1 = SYN_MEM[R2]
R1 ^= pow(R2, 2, 32768)
R1 ^= 0x4154
SYN_MEM[R2] = R1
# Look for calls to 0731
CODE_PARAMS = [
(0x0058, 0x650a, 0x7fff, 0x6e8b), # R1 from the maze
(0x1092, 0x650a, 0x7fff, 0x6eed),
(0x6486, 0x650a, 0x7fff, 0x7239), # R1 is R8 from Ackermann
# 162e is a bit tricky
# 1691 is a bit tricky
]
for cp in CODE_PARAMS:
print(generate_code(*cp))