CTF Writeup: Return of the EmojASM
It looks like we have another EmojiASM problem!
Note: Depending on what editor/browser you view this on, the emoji’s might not be shown properly. That means copying and pasting the emojis might not work well. Try using a browser.
Problem Description
Impressive work. But the emojasm gods have provided you with one more foul challenge. Once again, the web interface should be above.
Info from the EmojiASM Webpage
Architecture
- General-purpose registers X and Y (both 8 bits wide)
- Accumulator Register A (8 bits wide)
- 3x Tape Drives, T0 T1 and T2 (descibed below), each with the following:
- 1 input buffer register (TnI)
- 1 ouput buffer register (TnO)
- 1 write flag (TnW)
- Obviously each tape drive has a position on the tape as well, which changes as the tape gets moved. (TnP)
“n” here refers to the tape id, so for instance the input buffer register of tape 2 (T2) is known as T2I.
Tape drive details
Each tape drive supports the following operations:
- Forward - move the tape forward one space (and other things, described below)
- Backward - move the tape backward one space
- Rewind - reset the tape’s position to 0, and clear both buffers and the write flag.
- Set-write - write a byte to the output buffer and set the write flag.
The forward operation is special because it also does the following:
- Reads the data in the space it has just passed over into the input buffer register
- If the write flag is set:
- Write the data in the output buffer register to the byte just passed over
- Clear the write flag
None of those things happen when going backwards, and this is the only way to read and write to or from any tape.
Each tape drive is 256 bytes long. Trying to move forwards or backwards past the end of the tape has no effect.
Read more about EmojiASM here.
Approach
Basically we just have to xor each byte from tape0 with a byte from tape1.
Unfortunately we EmojiASM doesn’t have an xor instruction. All the bitwise operations we have are &
and |
.
Fortunately, we can use a combination of the given operations to create something that’s equivalent to the xor operation.
After some searching, I come across this stack overflow post.
After scrolling through the various answers, it seems like this is the easiest to implement in emojiasm.
We’ll use this to replace xor
a ^ b = (a | b) - (a & b)
Read data off of Tape0 and Tape1
We’ll be using the practice mode first to make sure our asm actually works (make sure to check the “Include debug output” option).
First, let’s see if we can read some of the data on tape0 and tape1.
Let’s read from T1 first:
β‘οΈποΈ Move T1 forward puts one character into T1I
ποΈποΈ A <- T1I move the character in T1I into the Accumulator Register A
π€ output <- chr(A) print out what's in A
We can put all the instructions in a compact line like this β‘οΈποΈποΈποΈπ€
.
Let’s see what’s on T1 by stringing a bunch of these together:
β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€β‘οΈποΈποΈποΈπ€
We get the following output
Your program has been run successfully!
The output was:
xorkeyxork
Let’s try doing the same for T0:
β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€β‘οΈπΌποΈπΌπ€
Output:
Your program has been run successfully!
The output was:
β½β½β½β½β½
β½
It looks like Tape0 has non ASCII characters. Let’s add 0x30 to each character from tape0 to make them viewable.
For example, if tape0 had 0
in the first byte, we would add 0x30 before displaying it. chr(0 + 0x30) = "0"
. Then we would see "0"
instead of β½
.
β‘οΈπΌ Move T0 forward Puts one byte into T0I
ποΈπΌ A <- T0I Move the byte in T0I into the Accumulator Register A
π¦π¨ X <- A Put what's in A into X
βοΈππ A <- 0x30 Puts 0x30 into A
βπ¨ A <- A + X Adds X to A
π€ out <- chr(A) Outputs A
The pseudocode below is equivalenet to the above
A = T0I + 0x30
print(A)
Make the above emojiasm more compact: β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€
And string a bunch together to read tape1:
β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€β‘οΈπΌποΈπΌπ¦π¨βοΈππβπ¨π€
Output:
Your program has been run successfully!
The output was:
:>AO32<:1O
So the first byte on tape0 is 10 since ord(":") - 0x30 = 10
Let’s see what the first character of the flag should be by xoring the first byte from tape0 (10) with the first byte from tape1 (ord(“x”)):
$ python -c 'print chr(10 ^ ord("x"))'
r
As we expected, the first character of the flag is r
.
Implement Subtraction
Now we can use a ^ b = (a | b) - (a & b)
to implement an xor function. However, emojiasm doesn’t have a subtract operation, only a decrement op.
So we have to first create a way to subtract before we make implement xor.
The following code subtracts 0x02 from 0x43 and stores the result in T2:
01: βοΈππ A <- 0x43
02: βοΈπ₯ T2O <- A
03: β‘οΈπ₯ T2 <- T2O
04: T2[0] = 0x43
05:
06: βοΈππ A <- 0x02
07: π¦π¨ X <- A
08: X = 0x2
09:
10: βοΈππ A <- 0
11: π¦βοΈ Y <- A
12: Y = 0
13:
14: βͺπ₯ Rewind T2
15: β‘οΈπ₯ποΈπ₯ A <- T2
16: π¦ποΈ A <- A - 1
17: βͺπ₯ Rewind T2
18: βοΈπ₯ T2O <- A
19: β‘οΈπ₯ T2 <- T2O
20: A = T2[0]
21: A -= 1
22: T2[0] = A
23:
24: π‘βοΈ Y <- Y + 1
25: πβοΈ A <- Y
26: βπ¨ flags β cmp(X, A)
27: πππππ
RJMP <- value
28: π·οΈ {if flags.EQ not set} PC β RJMP
29: Y += 1
30: if y != A:
31: goto 14
An uncommented version of the above:
βοΈππ
βοΈπ₯
β‘οΈπ₯
βοΈππ
π¦π¨
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
Let’s run the above asm:
Your program has been run successfully!
The output was:
--Debug output
1-4: OP.LDA with args: ['π', 'π']
6-8: OP.TOUT with args: π₯
10-12: OP.TF with args: π₯
14-17: OP.LDA with args: ['οΏ½', 'π']
18-20: OP.TAR with args: π¨
21-23: OP.TRW with args: π₯
25-27: OP.TF with args: π₯
28-30: OP.TIN with args: π₯
31-34: OP.DEC with args: ποΈ
35-37: OP.TRW with args: π₯
39-41: OP.TOUT with args: π₯
43-45: OP.TF with args: π₯
46-49: OP.INC with args: βοΈ
50-53: OP.TRA with args: βοΈ
54-56: OP.CMP with args: π¨
57-62: OP.LDJMP with args: ['οΏ½', 'οΏ½', 'π', 'π
']
[OP.LDJMP] - Setting RJMP to 21 for ldjmp [0x0, 0x0, 0x1, 0x5]
64-65: OP.JMPNEQ with args: None
[OP.JMPNEQ] - Setting PC to 21
21-23: OP.TRW with args: π₯
25-27: OP.TF with args: π₯
28-30: OP.TIN with args: π₯
31-34: OP.DEC with args: ποΈ
35-37: OP.TRW with args: π₯
39-41: OP.TOUT with args: π₯
43-45: OP.TF with args: π₯
46-49: OP.INC with args: βοΈ
50-53: OP.TRA with args: βοΈ
54-56: OP.CMP with args: π¨
57-62: OP.LDJMP with args: ['οΏ½', 'οΏ½', 'π', 'π
']
[OP.LDJMP] - Setting RJMP to 21 for ldjmp [0x0, 0x0, 0x1, 0x5]
64-65: OP.JMPNEQ with args: None
Halting (pc: 65, maxidx: 64)
Jumping:
Program memory offsets are the number of characters (unicode codepoints) from the start of the file. This takes into account emojis that are actually multiple codepoints, so beware.
If you have any problems or errors running the above code, make sure to remove any extraneous spaces. Any extra character will throw the jump offset off (see the above quote). Here we set the jump to 21 since the first OP.TRW (rewind) instruction is at 21.
If you have extra spaces or extra characters, you might need to adjust the offset to whereever the first rewind instruction is.
βͺπ₯ Rewind tape2
β‘οΈπ₯ποΈπ₯ A <- one chr of tape2
π€ out <- chr(A)
The full subtraction code with output:
βοΈππ
βοΈπ₯
β‘οΈπ₯
βοΈππ
π¦π¨
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
Output:
Your program has been run successfully!
The output was:
A
The program prints out A
, which is the expected result since 0x43 - 2 = 0x41 and chr(0x41) = “A”.
Implement XOR
Now that our test subtraction program works, we use it to implement xor.
We’ll basically use the following forumla and imlement that: a ^ b = (a | b) - (a & b)
Note: In the pseudocode below, i
is just the current index or current position on the tape
When the program first starts, the i = 0, since the program starts at the beginning of each tape.
Everytime we read, i is incremented since the position on the tape is also incremented.
01: β‘οΈπΌποΈπΌ A <- one chr of tape0
02: π¦π¨ X <- A
03: β‘οΈποΈποΈποΈ A <- one chr of tape1
04: π·π¨ A <- A | X
05: βͺπ₯ Rewind Tape2
06: βοΈπ₯ T2O <- A
07: β‘οΈπ₯ T2 <- T2O (output buf of T2)
08: T2[0] = tape1[i] | tape0[i]
09:
10: β¬
οΈπΌβ¬
οΈποΈ Move T1 and T0 backwards by 1 since reading from the tapes moves the position forward
11: β‘οΈπΌποΈπΌ A <- one chr of tape0
12: π¦π¨ X <- A
13: β‘οΈποΈποΈποΈ A <- one chr of tape1
14: π΄π¨ A <- A & X
15: π¦π¨ X <- A
16: βοΈππ A <- 0
17: π¦βοΈ Y <- A
18: X = T1[i] & T0[i]
20: Y = 0
21:
22: βͺπ₯ Rewind T2
23: β‘οΈπ₯ποΈπ₯ A <- T2
24: π¦ποΈ A <- A - 1
25: βͺπ₯ Rewind T2
26: βοΈπ₯ T2O <- A
27: β‘οΈπ₯ T2 <- T2O
28: A = T2[0]
29: T2[0] = A - 1
30:
31: π‘βοΈ Y <- Y + 1
32: πβοΈ A <- Y
33: βπ¨ flags β cmp(X, A)
34: πππ RJMP <- value
35: π·οΈ {if flags.EQ not set} PC β RJMP
36: Y += 1
37: if X != Y:
38: goto 22
Uncommented version of above:
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π·π¨
βͺπ₯
βοΈπ₯
β‘οΈπ₯
β¬
οΈπΌβ¬
οΈποΈ
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π΄π¨
π¦π¨
βοΈππ
π¦βοΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
Just like in the subtraction program, we set the jump offset to where the first rewind instruction is (OP.TRW) which in this case is 75 or 0x4b.
Now let’s make sure the program is actually working by printing out the first byte in T2. We’ll use this code snippet from earlier to print out the first byte of T2:
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
Now combine the output code with the xor implementation:
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π·π¨
βͺπ₯
βοΈπ₯
β‘οΈπ₯
β¬
οΈπΌβ¬
οΈποΈ
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π΄π¨
π¦π¨
βοΈππ
π¦βοΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
Ouput
Your program has been run successfully!
The output was:
r
We’ve got the first letter of the flag!
Now all we have to do is copy and paste the above code snippet about 4 times:
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π·π¨
βͺπ₯
βοΈπ₯
β‘οΈπ₯
β¬
οΈπΌβ¬
οΈποΈ
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π΄π¨
π¦π¨
βοΈππ
π¦βοΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π·π¨
βͺπ₯
βοΈπ₯
β‘οΈπ₯
β¬
οΈπΌβ¬
οΈποΈ
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π΄π¨
π¦π¨
βοΈππ
π¦βοΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π·π¨
βͺπ₯
βοΈπ₯
β‘οΈπ₯
β¬
οΈπΌβ¬
οΈποΈ
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π΄π¨
π¦π¨
βοΈππ
π¦βοΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π·π¨
βͺπ₯
βοΈπ₯
β‘οΈπ₯
β¬
οΈπΌβ¬
οΈποΈ
β‘οΈπΌποΈπΌ
π¦π¨
β‘οΈποΈποΈποΈ
π΄π¨
π¦π¨
βοΈππ
π¦βοΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π¦ποΈ
βͺπ₯
βοΈπ₯
β‘οΈπ₯
π‘βοΈ
πβοΈ
βπ¨
πππππ
π·οΈ
βͺπ₯
β‘οΈπ₯ποΈπ₯
π€
Output
Your program has been run successfully!
The output was:
ractf{testing_flag}
The expected output was:
ractf{testingflag}
This was a match.
We’ve got the whole flag!
Running the code just 4 times is able to produce the whole flag since the jump offset is constant at 75, which means that after we execute the 1st block, we’ll loop to the beginning.
Runnning the above code on the actual challenge works and gives us the real flag.
2020-06-16 01:01 +0000 (Last updated: 2020-12-05 19:18 +0000)