Add notes to code 7

This commit is contained in:
RunasSudo 2016-06-02 18:04:15 +09:30
parent f6ac40234c
commit d8d2d51eab
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 380 additions and 8 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/challenge.bin
/dumps
/tools/ackermann.o

157
notes.md
View File

@ -2,13 +2,15 @@
Note: Codes seem to be unique for every user.
## Code 1
## The programming codes
### Code 1
Found in `arch-spec`.
== hints ==
- Here's a code for the challenge website: ............
## Code 2
### Code 2
Obtained on initial execution of code. (Or just be lazy like me and extract it from the binary.)
Welcome to the Synacor Challenge!
@ -17,7 +19,7 @@ Obtained on initial execution of code. (Or just be lazy like me and extract it f
Required opcodes: `nop` and `out`
## Code 3
### Code 3
Obtained at the completion of the self-test.
Executing self-test...
@ -27,7 +29,9 @@ Obtained at the completion of the self-test.
Required opcodes: All except `halt` and `in`
## Code 4
## The RPG codes
### Code 4 (Tablet)
At the foothills, the first area of the RPG:
== Foothills ==
@ -41,7 +45,7 @@ At the foothills, the first area of the RPG:
Required opcodes: All except `halt`
## Code 5 (Twisty passages)
### Code 5 (Twisty passages)
After falling down the bridge, there is an `empty lantern` to the `east`, which should be `take`n. To the `west`, there is a passage, where one can take the `ladder` down or venture into the `darkness`. Attempting to venture into the `darkness` at this point will result in being eaten by Grues.
Taking the `ladder` down, then traversing `west`, `south`, `north`, a code is obtained:
@ -54,12 +58,12 @@ Taking the `ladder` down, then traversing `west`, `south`, `north`, a code is ob
There is also a can, which can be `take`n and `use`d to fill the lantern, which can then be `use`d to become lit, and which will keep away Grues.
## Code 6 (Dark passage, ruins and teleporter)
### Code 6 (Dark passage, ruins and teleporter)
Returning to the fork at the passage and venturing to the `darkness`, we now `continue` `west`ward to the ruins. The `north` door is locked, but dotted elsewhere around the ruins are a `red coin` (2 dots), `corroded coin` (triangle = 3 sides), `shiny coin` (pentagon = 5 sides), `concave coin` (7 dots) and `blue coin` (9 dots) which should be `take`n and `look`ed at, and the equation:
_ + _ * _^2 + _^3 - _ = 399
### Mathematical!
#### Mathematical!
In other words, we seek a solution to the equation *a* + *bc*<sup>2</sup> + *d*<sup>3</sup> - e = 399, where {*a*, *b*, *c*, *d*, *e*} = {2, 3, 5, 7, 9}.
Synacor being, of course, a programming-orientated exercise, the usual response to this problem is to code up a quick program to loop through all 5! = 120 permutations of the coins to find which one satisfies the equation. Being a mathematician, however – could you tell from the italics? – I will solve this the [*proper* way](https://xkcd.com/435/): with a scientific calculator and some thinking (\*insert insufferably smug expression here\*).
@ -90,3 +94,142 @@ Proceed to the `north` door and `use` the `teleporter` to obtain the code:
............
After a few moments, you find yourself back on solid ground and a little disoriented.
## The true-believers-only codes
At this point, you will almost certainly need to delve into the code of the challenge, if you haven't already. The code in `challenge.bin` past the self-test is encrypted, so disassembling and analysing the code is most easily done based off a memory dump from a running copy.
### The guts
(Note to self: `pop` takes an operand, *duh*. No wonder everything looked funny…)
Note that at `1808` there is the following data:
1808 data 00b7
1809 data "You find yourself standing at the base of an enormous mountain. ...
(Accompanied by some other familiar-sounding but somewhat garbled strings.) Searching the code for references to `1809` yields nothing, but searching for `1808` yields the following as the only result:
090d data 17fe 1808 6914 6917
0911 halt # (i.e. data 0000)
The first address referred to in the `090d` line has value `0009`, which looks suspiciously like a length. Indeed, this is the exact length of the following string `Foothills`. Continuing in this manner,
17fe data 0009 "Foothills"
1808 data 00b7 "You find yourself standing at the base of an enormous mountain. ...
6914 data 0002 18c0 18c8
6917 data 0002 0917 0912
Following the rabbit hole,
18c0 data 0007 "doorway"
18c8 data 0005 "south"
0917 data 1929 1933 691e 6921 0000
0912 data 18ce 18d8 691a 691c 0000
And unsurprisingly, the `0912` line leads to the data for the screen reached when venturing `south` from the beginning.
Scrolling through the list of rooms beginning `090d`, we notice a peculiarly long line, `0949`:
0949 data
1fe3 # -> string "Twisty passages"
1ff3 # -> string "You are in a maze ...
695b # -> data 0005 206f 2076 207c 2082 2087
6961 # -> data 0005 093f 094e 0953 0958 095d
So far so good, but what's this??
0e9e # -> wmem 0e8e 0000; ret
208c # -> string "Twisty passages"
209c # -> string "You are in a twisty maze ...
...
Aah, so it looks like each room is stored as a block of 5 words, each a pointer to a length of words: a string (the title), a string (the text), a list of pointers to strings (the exit names), a list of pointers to more rooms (the exits), and a memory location to `call` (or `0000`).
Further analysis suggests that this particular call relates to the step counter for the Grues in the maze.
We probably could have reached these same conclusions by analysing the suspicious-looking block of code following the room definitions, but assembly makes my head spin so ¯\_(ツ)_/¯
Now what about items? Looking at a more familiar item, the tablet:
0a6c data 468e 4695 090d 1270
468e data 0006 "tablet"
4695 data 0088 "The tablet seems appropriate for use as a writing surface but is unfortunately blank. Perhaps you should USE it as a writing surface..."
090d ... # the foothills from earlier
1270 ... # a subroutine that presumably prints code 4
### Code 7 (Synacor Headquarters and Teleporter 2: Electric Boogaloo)
Examining the data for the teleporter:
0a94 data 4a55 4a60 099f 1545
4a55 data 000a "teleporter"
4a60 data 0048 "This small device has a button on it and reads \"teleporter\" on the side."
099f ... # the north room in the ruins
1545 ... # aha!
Now, let's see what this `1545` does. C-style, because assembly makes my brain spin.
```c
1545() {
if (R8 == 0)
return 15e5(); // Ground state teleport
05b2(70ac, 05fb, 1807 + 585a); // Secure text print
for(i = 0; i < 5; i++); // Speed up loop, because why not?
if (178b(0004, 0001) != 0006) // The check!
return 15cb();
05b2(7156, 05fb, 1ed6 + 0992);
0731(R8, 650a, 7fff, 7239);
05b2(723d, 05fb, 7c1f + 0146);
mem[0aac] = 09c2;
mem[0aad] = 0000;
mem[0a94 + 0002] = 7fff;
return 1652();
}
178b(R1, R2) {
if (R1 != 0) {
return 1793(R1, R2);
}
return R2 + 0001;
}
1793(R1, R2) {
if (R2 != 0) {
return 17a0(R1, R2);
}
R1 = R1 + 7fff;
R2 = R8;
R1 = 178b(R1, R2);
return R1;
}
17a0(R1, R2) {
R2 = R2 + 7fff;
R2 = 178b(R1, R2);
R1 = R1 + 7fff;
R1 = 178b(R1, R2);
return R1;
}
```
Phew, so in other words, we seek an `R8` such that `178b(4, 1, R8)` equals 6. Let's see if we can't rewrite that function. Note that adding 0x7fff = 32767 to a number modulo 32768 is equivalent to subtracting 1. Thinking through the code, then,
A(R1, R2) = R2 + 1 , if R1 = 0
A(R1 - 1, R8) , if R1 ≠ 0 and R2 = 0
A(R1 - 1, A(R1, R2 - 1)) , if R1 ≠ 0 and R2 ≠ 0
Recognise anything? Well, neither did I the first time, and I'd [already seen the video](https://www.youtube.com/watch?v=i7sm9dzFtEI). It's the [Ackermann function](https://en.wikipedia.org/wiki/Ackermann_function)! With the slight twist that instead of the second line being `A(R1 - 1, 1)`, it's `A(R1 - 1, R8)`.
No mathematical wizardry here, just implementing this and run a brute-force on all possible values of `R8`. And as much as it pains me to admit this, this is a tool for the raw processing efficiency of C, which I am not very proficient in, so I based my solution on [this wonderful code](https://github.com/glguy/synacor-vm/blob/master/teleport.c) by [glguy](https://github.com/glguy). My only contribution is the parallelisation of the computation. (500% speed-up! Whoo!)
gcc ackermann.c -o ackermann -lpthread -O3 && ./ackermann
Running the algorithm, the correct value is revealed to be `0x6486`.
### Code 8 (Beach and vault)

View File

@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import struct # for bytes<-->word handling
import sys # for stdin
import sys # for args, stdin
SYN_PTR = 0
SYN_MEM = [0] * 32768

74
tools/ackermann.c Normal file
View File

@ -0,0 +1,74 @@
/*
synacor.py - An implementation of the Synacor Challenge
Copyright © 2016 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/>.
*/
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define NUM_THREADS 8
static uint16_t A(uint16_t R1, uint16_t R2, uint16_t R8, uint16_t cache[5][0x8000]) {
if (cache[R1][R2] != 0xffff) {
return cache[R1][R2];
}
if (R1 == 0)
return (R2 + 1) & 0x7fff;
if (R2 == 0)
return A(R1 - 1, R8, R8, cache);
return A(R1 - 1, cache[R1][R2 - 1] = A(R1, R2 - 1, R8, cache), R8, cache);
}
static void* bruteThread(void* rmdrVoid) {
uint16_t rmdr;
uint16_t cache[5][0x8000];
uint16_t R8;
rmdr = *((int *) rmdrVoid);
for (R8 = rmdr; R8 <= 0x7fff; R8 += NUM_THREADS) {
memset(cache, 0xff, sizeof(cache)); /* Clear the cache */
if (!(R8 & 0xff))
printf("%04x...\n", R8);
if (A(4, 1, R8, cache) == 6)
printf("Found a solution! %04x...\n", R8);
}
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_args[NUM_THREADS];
int i;
for (i = 0; i < NUM_THREADS; i++) {
printf("Spawning thread %d...\n", i);
thread_args[i] = i; /* rmdr */
pthread_create(&threads[i], NULL, bruteThread, (void*) &thread_args[i]);
}
/* wait */
for (i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Search complete\n");
}

154
tools/disasm.py Executable file
View File

@ -0,0 +1,154 @@
#!/usr/bin/env python3
# synacor.py - An implementation of the Synacor Challenge
# Copyright © 2016 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 # for bytes<-->word handling
import sys # for args
class OpLiteral:
def __init__(self, value):
self.value = value;
def get(self):
return '{:04x}'.format(self.value);
def set(self):
return '{:04x}'.format(self.value);
class OpRegister:
def __init__(self, register):
self.register = register;
def get(self):
return 'R{}'.format(self.register + 1);
def set(self):
return 'R{}'.format(self.register + 1);
def readWord():
byteData = data.read(2)
if len(byteData) < 2:
return None
return struct.unpack('<H', byteData)[0]
def readOp():
word = readWord()
if 0 <= word <= 32767:
return OpLiteral(word)
if 32768 <= word <= 32775:
return OpRegister(word - 32768)
raise Exception('Invalid word {} at {}'.format(word, SYN_PTR))
def escapeChar(char):
return char.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
MODE_OUT = False
MODE_DAT = False #False, 1 (data), 2 (text)
with open(sys.argv[1], 'rb') as data:
while True:
pos = int(data.tell() / 2)
word = readWord()
if word == None:
break
if MODE_OUT and word != 19:
print('"')
MODE_OUT = False
if MODE_DAT and 0 <= word <= 21:
if MODE_DAT == 1:
print()
if MODE_DAT == 2:
print('"')
MODE_DAT = False
if word == 21: #NOP
print('{:04x} nop'.format(pos))
elif word == 0: #HALT
print('{:04x} halt'.format(pos))
elif word == 1: #SET
print('{:04x} set {} {}'.format(pos, readOp().set(), readOp().get()))
elif word == 2: #PUSH
print('{:04x} push {}'.format(pos, readOp().get()))
elif word == 3: #POP
print('{:04x} pop {}'.format(pos, readOp().set()))
elif word == 4: #EQ
print('{:04x} eq {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 5: #GT
print('{:04x} gt {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 6: #JMP
print('{:04x} jmp {}'.format(pos, readOp().get()))
elif word == 7: #JT (jump if not zero)
print('{:04x} jt {} {}'.format(pos, readOp().get(), readOp().get()))
elif word == 8: #JF (jump if zero)
print('{:04x} jf {} {}'.format(pos, readOp().get(), readOp().get()))
elif word == 9: #ADD
print('{:04x} add {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 10: #MULT
print('{:04x} mult {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 11: #MOD
print('{:04x} mod {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 12: #AND
print('{:04x} and {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 13: #OR
print('{:04x} or {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
elif word == 14: #NOT
print('{:04x} not {} {}'.format(pos, readOp().set(), readOp().get()))
elif word == 15: #RMEM
print('{:04x} rmem {} {}'.format(pos, readOp().set(), readOp().get()))
elif word == 16: #WMEM
print('{:04x} wmem {} {}'.format(pos, readOp().set(), readOp().get()))
elif word == 17: #CALL
print('{:04x} call {}'.format(pos, readOp().get()))
elif word == 18: #RET
print('{:04x} ret'.format(pos))
elif word == 19: #OUT
op = readOp()
if isinstance(op, OpLiteral):
if MODE_OUT:
print(escapeChar(chr(op.value)), end='')
else:
print('{:04x} out "{}'.format(pos, escapeChar(chr(op.value))), end='')
MODE_OUT = True
if op.value == 0x0a:
print('"')
MODE_OUT = False # break on newlines
else:
if MODE_OUT:
print('"')
MODE_OUT = False
print('{:04x} out {}'.format(pos, op.get(), end=''))
elif word == 20: #IN
print('{:04x} in {}'.format(pos, readOp().set()))
else:
if 32 <= word <= 126:
# looks like letters - unfortunately "\n" looks like MULT
if MODE_DAT == 2:
print(escapeChar(chr(word)), end='')
else:
if MODE_DAT == 1:
print()
print('{:04x} data "{}'.format(pos, escapeChar(chr(word))), end='')
MODE_DAT = 2
if word == 0x0a:
print('"')
MODE_DAT = False # break on newlines
else:
if MODE_DAT == 1:
print(' {:04x}'.format(word), end='')
else:
if MODE_DAT == 2:
print('"')
print('{:04x} data {:04x}'.format(pos, word), end='')
MODE_DAT = 1