diff --git a/.gitignore b/.gitignore index 24b64db..61311f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /challenge.bin /dumps +/tools/ackermann.o diff --git a/notes.md b/notes.md index 28d62ba..069e7ec 100644 --- a/notes.md +++ b/notes.md @@ -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*2 + *d*3 - 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) diff --git a/synacor.py b/synacor.py index c329c93..50e1112 100755 --- a/synacor.py +++ b/synacor.py @@ -16,7 +16,7 @@ # along with this program. If not, see . import struct # for bytes<-->word handling -import sys # for stdin +import sys # for args, stdin SYN_PTR = 0 SYN_MEM = [0] * 32768 diff --git a/tools/ackermann.c b/tools/ackermann.c new file mode 100644 index 0000000..e62b732 --- /dev/null +++ b/tools/ackermann.c @@ -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 . +*/ + +#include +#include +#include +#include + +#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"); +} diff --git a/tools/disasm.py b/tools/disasm.py new file mode 100755 index 0000000..1d5ebc7 --- /dev/null +++ b/tools/disasm.py @@ -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 . + +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('