This is the cops' challenge. To post a robber, go here.
In this challenge, cops will invent a (likely simple) programming language, and write an interpreter, transpiler, or compiler that allows you to run it. Robbers will write a program in this language that manages to inject arbitrary code into the interpreter, transpiler, or compiler.
Cops
Cops should design a language. This language should be "pure"; its programs can take input and/or produce output, but cannot affect files, make network requests, behave non-deterministically (based on something like randomness or the system time), or otherwise impact or be impacted by the outside world (within reason).
Cops must then implement this language, with one of:
- An interpreter: This will take the code and input as arguments/input, and produce the programs's output as output.
- A transpiler: This will take the code and input, and produce code in some other language which does the same task. When run using that language's interpreter/compiler with input, the correct output is produced.
- A compiler: Similar to a transpiler, but with machine code instead of another language.
There must be an intended "crack", or way to inject arbitrary code.
Robbers
Robbers should find a way to do one of the following:
- Write a program that, when interpreted, transpiled, or compiled, can run arbitrary code as the interpreter/transpiler/compiler (e.g., if written in Python, you could run arbitrary Python, manipulate files, and so on)
- Write a program that, when transpiled or compiled, injects arbitrary code into the resulting program/machine code (e.g., if your transpiler converts the language to Node.js, you could inject arbitrary JS into the transpiler output)
Cop Posts
Cops should include the interpreter/transpiler/compiler code, and some documentation for the language. For example:
Print1Lang
function transpile(code) { return code.replace(/^.+/g, "print(1)"); }Print1Lang takes code as input, and returns a Python program that prints
1.
Cops should be designed with some exploit in mind that allows injecting any code, in some language or situation that allows performing arbitrary computations and interacting with the computer's files, running shell commands, system calls, non-deterministic code, or anything like that. Simply printing a nondeterministic value does not count, as it does not give you unrestricted access to the environment the code or transpilation/compilation output is being run or transpiled/compiled in.
For an interpreter, this is likely going to be running code in the language the interpreter is written in. For a transpiler or compiler, the vulnerability can occur at either compile time or run time, and for the latter, the vulnerability would most likely consist of being able to inject arbitrary code or machine code into the compiled code.
Other
All the boring stuff. Nothing malicious, no crypto stuff.
Winner for cops is the user with the most safe cops, winner for robbers is the user who cracks the most cops.
Note:
This challenge is intended for languages to be designed for the challenge specifically, but using existing languages is allowed. In this case, you can define a subset of the language such that it follows the rules about non-deterministic commands.
9 Answers 9
Exceptionally (cracked by emanresu A)
Exceptionally is a toy language I invented for this challenge. It is inspired by, implemented in, and transpiled to Whython (pxeger's modified version of Python 3 with an added exception-handling operator).
The language
A program in Exceptionally contains of a series of lines, each consisting of a command (or multiple commands connected with the Rescue operator; see below under Exceptions). The lines are executed one by one. When the instruction pointer reaches the end of the program, it wraps around to the beginning. The program thus forms an infinite loop; the only way to break out of the loop is by causing an error. Every Exceptionally program either does not terminate or terminates with an exception.
Each command consists of a symbol, optionally followed by an argument. The argument can be an integer literal, a string literal, or a variable name.
Most commands modify the value of a register, initially set to 0. A command without an explicit argument uses the value of the register as its argument. For example, the command *3 multiplies the register by 3, while the command * multiplies the register by itself.
Commands
Here is the full list of commands:
{: Load (copy value into register)}: Store (copy register into variable)+: Add (value to register)-: Subtract (value from register)*: Multiply (register by value)/: Divide (register by value--floating point division)%: Mod (register by value)^: Pow (take register to the power of value--result may be floating point):: Item (at given index in register)[: Slice From (given index to end of register)]: Slice To (given index from beginning of register)@: Find (value's index in register)#: Count (occurrences of value in register)|: Split (register by value)$: Join (register on value)<: Print (value)>: Input (line of stdin into variable)=: Equal (assert that register is equal to value)!: Skip (skip execution of the given number of lines)\: Func (apply the given function to the register)
The Skip command ! provides simple control flow. Think of it as a goto, relative to the current line.
The Func command \ takes a string as its argument and does one of several things:
\"int": Cast the register to an integer\"str": Cast the register to a string\"ord": Convert a single-character register value to its character code\"chr": Convert an integer register value to the corresponding character\"elems": Convert a string or list register value to a list of its elements\"len": Get the length of the register\"sum": Sum/concatenate the register\"range": Get the range from 0 to the register's value (exclusive)\"wrap": Wrap the register's value in a singleton list\"inv": Reverse the register's value, or negate it if it's a number
Exceptions
Most commands are capable of triggering an exception in some way. For example, dividing by 0 will cause an exception, as will an out-of-bounds index, as will trying to add a string and a number. These exceptions are the only way to end the program, but they don't have to end the program. They can be caught using the Rescue operator ?, borrowed from Whython.
A command may be followed by ? and an additional command. If the first command succeeds, execution continues to the next line. If the first command causes an exception, the second command is executed instead. A line can contain any number of commands chained with ?. If the last command is reached and it also causes an exception, then the program halts.
For example, consider the line /x ? <"Division by zero". The command /x attempts to divide the register by the value of x. If x is zero, this operation will trigger an exception, in which case the second command <"Division by zero" is executed, printing an error message (and leaving the value of the register unchanged).
The ? operator is the only conditional construct in Exceptionally. Different commands can be used to trigger exceptions under specific circumstances, and ! can be used to jump to different points in the program depending on the results. For example, in =5 ? !4, the = command raises an exception if the register does not equal 5; in this case the ! command is executed, skipping the next four lines. Or again, the following two lines:
-5
/ ? !4
will skip four lines if the register equals 5: -5 subtracts 5 from the register, and / divides it by itself, resulting in 1 if it is nonzero or an exception if it is zero.
Miscellaneous
Exceptionally has comments that start with ' and go until the next newline.
Whitespace is generally unimportant in Exceptionally. This program to square an input number and halt:
>
\"int"
*
<
/0
could also be written as >\"int"*</0. Newlines that end comments, and whitespace in strings, are the only significant whitespace.
The transpiler
Here is the Exceptionally transpiler, written in Whython:
import re
import sys
COMMANDS = {
"{": "reg := %s", # Load
"}": "%s := reg", # Store
"+": "reg := reg + %s", # Add
"-": "reg := reg - %s", # Sub
"*": "reg := reg * %s", # Mul
"/": "reg := reg / %s", # Div
"%": "reg := reg %% %s", # Mod
"^": "reg := reg ** %s", # Pow
":": "reg := reg[%s]", # Item
"[": "reg := reg[%s:]", # SliceFrom
"]": "reg := reg[:%s]", # SliceTo
"@": "reg := reg.index(%s)", # Find
"#": "reg := reg.count(%s)", # Count
"|": "reg := reg.split(%s)", # Split
"$": "reg := %s.join(reg)", # Join
"<": "print(%s)", # Print
">": "%s := input()", # Input
"=": "1 / (reg == %s)", # Equal
"!": "ip := ip + %s", # Skip
"\\": "reg := FUNCS[%s](reg)", # Func
}
PROGRAM_TEMPLATE = """program = %s
FUNCS = {
"int": int,
"str": str,
"chr": chr,
"ord": ord,
"elems": list,
"len": len,
"sum": lambda x: sum(x) ? sum(x, []) ? "".join(x),
"range": lambda x: list(range(x)),
"wrap": lambda x: [x],
"inv": lambda x: x[::-1] ? -x,
}
ip = 0
reg = 0
while True:
eval(program[ip])
ip = (ip + 1) %% len(program)"""
def transpile(code):
transpiled_lines = []
code = code.lstrip()
if code[0] == "?":
raise SyntaxError(f"Program cannot begin with {code[0]}")
while code:
if m := re.match("'.*", code):
code = code[m.end():].lstrip()
continue
if code[0] == "?":
# Continuing a previous line
operator = code[0]
transpiled_lines[-1] += f" {operator} "
code = code[1:].lstrip()
if not code:
raise SyntaxError(f"Program cannot end with {operator}")
else:
# Start of a new line
transpiled_lines.append("")
if code[0] in COMMANDS:
command = code[0]
code = code[1:].lstrip()
# Parse the command's argument
if m := re.match(r"\w+", code):
# Name or integer literal
argument = m.group()
argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
code = code[m.end():].lstrip()
elif m := re.match(r'"[^"]*"', code):
# String literal
argument = repr(m.group())
code = code[m.end():].lstrip()
else:
# No argument, defaults to register
argument = "'reg'"
argument = eval(argument) ? f"eval({argument})"
translation = "(" + COMMANDS[command] % argument + ")"
transpiled_lines[-1] += translation
else:
raise SyntaxError(f"Expected command, found: {code[0]}")
return PROGRAM_TEMPLATE % transpiled_lines
Attempt This Online!
You can run the transpiler for yourself at Attempt This Online. Put your Exceptionally code in the Input box and click Execute; the program will output the transpiled Whython code.
Here is a version that immediately executes the transpiled code: Attempt This Online.
Example program
Here is a commented program that outputs the orbit of an input number under the Collatz function. (This is the program used in the second ATO link above.)
' Read initial number from stdin
>
' Convert to an integer
\"int"
' Store a copy in n
}n
' Output
<
' Minus 1
-1
' Divide by itself; if n is 1, (n-1)/(n-1) is division by zero and the program halts
/
' Otherwise, keep going; load n back into the register
{n
' Mod 2
%2
' Is this equal to 0? If not, skip the next three lines
=0 ? !3
' Load n
{n
' Divide by 2
/2
' Skip the next three lines
!3
' Load n
{n
' Multiply by 3
*3
' Add 1
+1
' Skip the next line (that is, skip the first line when the program loops)
!1
-
1\$\begingroup\$ +1 for first new lang made for this challenge and also cause it looks fun \$\endgroup\$thejonymyster– thejonymyster2022年01月31日 14:26:22 +00:00Commented Jan 31, 2022 at 14:26
-
\$\begingroup\$ Cracked! \$\endgroup\$emanresu A– emanresu A2022年01月31日 18:57:39 +00:00Commented Jan 31, 2022 at 18:57
Vyxal 2.7 (safe)
Yes, I found another another one.
No, I'm not JoKing.
As the rules call for deterministic languages, commands involving randomness are not allowed. Also, the Eˆ commands are not allowed, since those are designed to execute arbitrary code.
Code and documentation can be found at the repo.
Solution:
If you look at the recent releases and commits in the GitHub repo, you may notice that we don't like SymPy anymore. There's a reason for that. It turns out that it doesn't play nicely with strings. More specifically, a lot of the time, when you pass a string to it, you can make it do unintended stuff. For example, if you do
sympy.nsimplify("f'{print(69)}'"), it will evaluate the f-string contained within the string, resulting in it printing 69. The ACE was originally found usingøḋ, but there were actually a lot of commands that used it. This has now been fixed.
-
6\$\begingroup\$ This is basically free labour and I like it. \$\endgroup\$2022年01月29日 04:35:32 +00:00Commented Jan 29, 2022 at 4:35
-
5\$\begingroup\$ Plot twist: I didn’t actually find another ACE, I’m just hoping somebody else will find it for me. \$\endgroup\$Aaroneous Miller– Aaroneous Miller2022年01月29日 06:56:07 +00:00Commented Jan 29, 2022 at 6:56
Exceptionally v0.3 (safe)
Exceptionally is a toy language I invented for this challenge. It is inspired by, implemented in, and transpiled to Whython (pxeger's modified version of Python 3 with an added exception-handling operator).
This is the third (and (削除) hopefully (削除ここまで) final) version of the language, but the only difference is in the implementation; the language spec is the same as before. See the original post for a full description.
New transpiler
Here is the Exceptionally v0.3 transpiler, written in Whython:
import re
import sys
COMMANDS = {
"{": "reg := %s", # Load
"}": "%s := reg", # Store
"+": "reg := reg + %s", # Add
"-": "reg := reg - %s", # Sub
"*": "reg := reg * %s", # Mul
"/": "reg := reg / %s", # Div
"%": "reg := reg %% %s", # Mod
"^": "reg := reg ** %s", # Pow
":": "reg := reg[%s]", # Item
"[": "reg := reg[%s:]", # SliceFrom
"]": "reg := reg[:%s]", # SliceTo
"@": "reg := reg.index(%s)", # Find
"#": "reg := reg.count(%s)", # Count
"|": "reg := reg.split(%s)", # Split
"$": "reg := %s.join(reg)", # Join
"<": "print(%s)", # Print
">": "%s := input()", # Input
"=": "1 / (reg == %s)", # Equal
"!": "ip := ip + %s", # Skip
"\\": "reg := program.FUNCS[%s](reg)", # Func
}
PROGRAM_TEMPLATE = """class Program: pass
program = Program()
program.LINES = %s
program.FUNCS = {
"int": int,
"str": str,
"chr": chr,
"ord": ord,
"elems": list,
"len": len,
"sum": lambda x: sum(x) ? sum(x, []) ? "".join(x),
"range": lambda x: list(range(x)),
"wrap": lambda x: [x],
"inv": lambda x: x[::-1] ? -x,
}
ip = 0
reg = 0
while True:
eval(program.LINES[ip])
ip = (ip + 1) %% len(program.LINES)"""
BUILTIN_NAMES = dir(__builtins__)
def transpile(code):
transpiled_lines = []
code = code.lstrip()
if code[0] == "?":
raise SyntaxError(f"Program cannot begin with {code[0]}")
while code:
if m := re.match("'.*", code):
code = code[m.end():].lstrip()
continue
if code[0] == "?":
# Continuing a previous line
operator = code[0]
transpiled_lines[-1] += f" {operator} "
code = code[1:].lstrip()
if not code:
raise SyntaxError(f"Program cannot end with {operator}")
else:
# Start of a new line
transpiled_lines.append("")
if code[0] in COMMANDS:
command = code[0]
code = code[1:].lstrip()
# Parse the command's argument
if m := re.match(r"\w+", code):
# Name or integer literal
argument = m.group()
if argument in BUILTIN_NAMES and command == "}":
raise SyntaxError(f"Cannot overwrite built-in name {argument}")
argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
code = code[m.end():].lstrip()
elif m := re.match(r'"[^"\\]*"', code):
# String literal
argument = repr(m.group())
code = code[m.end():].lstrip()
else:
# No argument, defaults to register
argument = "'reg'"
argument = eval(argument) ? f"eval({argument})"
translation = "(" + COMMANDS[command] % argument + ")"
transpiled_lines[-1] += translation
else:
raise SyntaxError(f"Expected command, found: {code[0]}")
return PROGRAM_TEMPLATE % transpiled_lines
Changelog
The second version was cracked thanks to unescaped backslashes in string literals. To close this loophole, backslashes are no longer allowed in string literals. A string can contain any character that is not \ or ".
I've also closed another unintended loophole: previously, you could overwrite range with exec and then do \"range" to execute an arbitrary string. Now you can no longer use a Whython builtin as the argument for the } command.
Attempt This Online!
You can run the transpiler for yourself at Attempt This Online. Put your Exceptionally code in the Input box and click Execute; the program will output the transpiled Whython code.
Here is a version that immediately executes the transpiled code: Attempt This Online.
Solution
There are two suspiciously convoluted lines in the transpiler. First,
argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
which does this:
Try to cast
argumentto an integer. If that works and the integer is nonzero, remove any leading 0s fromargument; if the integer is zero, use"0"; ifargumentcannot be converted to an integer, wrap it in single quotes. (Note also that literal strings arerepr'd, and the register as an argument is'reg'rather than justreg.)
Then,
argument = eval(argument) ? f"eval({argument})"
If the argument was a name, it should be wrapped in quotes at this point, so
evalgives just the name as a string. If the argument was a number,evalgives the number as a Python int. If the argument was a string,evalreverses thereprwe did earlier. Ifevalfails (???), then we substituteargumentinto a string that has the effect ofeval'ing it at post-transpilation runtime (???).
What kind of input will trigger this case?
We need an argument matching the regex
\w+that does not error when you pass it toint, but does error when you strip leading zeros and pass it toeval. Solution: Python 3 (and therefore Whython) allows underscores as digit-group separators in numeric literals. Thus,0_1is a legal int literal (with a value of 1); but when we strip the leading zero,_1is no longer a legal int literal, but rather a name. When we try toevalit, it errors because_1is not defined. So instead of1or_1being placed in the transpiled code, we geteval(_1).
So the exploit is:
If we set the variable
_1to some string value, any reference to0_1will cause it to beeval'd. Here's a simple example:
{"exec('import os; print(os.getcwd()); exit(0)')" }_1 <0_1
Transpiled, this becomes (in essence):
reg := "exec('import os; print(os.getcwd()); exit(0)')"
_1 := reg
print(eval(_1))
Attempt This Online!
Javastack, Cracked by tsh
Not much of a language but it should be enough. Crappy programming for the win!
Code + docs is in the repo.
Since this must be deterministic, random builtins are banned.
As tsh pointed out, there's a small chance loop variables may coincide, but that chance is so small that you may simply consider it to not happen.
-
\$\begingroup\$ But won't this language be non-deterministically as there is a probability when
secret === secret2? \$\endgroup\$tsh– tsh2022年01月29日 01:58:34 +00:00Commented Jan 29, 2022 at 1:58 -
\$\begingroup\$ @tsh True. It's incredibly unlikely though. \$\endgroup\$emanresu A– emanresu A2022年01月29日 02:02:43 +00:00Commented Jan 29, 2022 at 2:02
-
\$\begingroup\$ codegolf.stackexchange.com/a/241959/44718 \$\endgroup\$tsh– tsh2022年01月29日 02:28:09 +00:00Commented Jan 29, 2022 at 2:28
Jyxal, Cracked by redwolf
Jyxal is a half-baked JS implementation of Vyxal. While it has most core features (loops, functions, lambdas etc), it has almost no elements, but it's still possible to ACE it.
Vyxal docs should apply for most of it, but for some parts you may have to read the code.
Intended solution:
@=1,console.log("ACE"),process.exit(),'+',d|;
Quite similar to Redwolf's, this gets compiled into
var arity = '=1,console.log("ACE"),process.exit(),d';FUNC_=1,console.log("ACE"),process.exit(),d = <meaningless junk>
Just like this Vyxal ACE, this exploits how arbitrary code can be inserted into functions.
-
\$\begingroup\$ codegolf.stackexchange.com/a/241995/79857 \$\endgroup\$2022年01月29日 20:46:44 +00:00Commented Jan 29, 2022 at 20:46
Exceptionally v0.2 (cracked by emanresu A)
Exceptionally is a toy language I invented for this challenge. It is inspired by, implemented in, and transpiled to Whython (pxeger's modified version of Python 3 with an added exception-handling operator).
This is the second version of the language, but the only difference is in the implementation; the language spec is the same as before. See the original post for a full description.
New transpiler
Here is the Exceptionally v0.2 transpiler, written in Whython:
import re
import sys
COMMANDS = {
"{": "reg := %s", # Load
"}": "%s := reg", # Store
"+": "reg := reg + %s", # Add
"-": "reg := reg - %s", # Sub
"*": "reg := reg * %s", # Mul
"/": "reg := reg / %s", # Div
"%": "reg := reg %% %s", # Mod
"^": "reg := reg ** %s", # Pow
":": "reg := reg[%s]", # Item
"[": "reg := reg[%s:]", # SliceFrom
"]": "reg := reg[:%s]", # SliceTo
"@": "reg := reg.index(%s)", # Find
"#": "reg := reg.count(%s)", # Count
"|": "reg := reg.split(%s)", # Split
"$": "reg := %s.join(reg)", # Join
"<": "print(%s)", # Print
">": "%s := input()", # Input
"=": "1 / (reg == %s)", # Equal
"!": "ip := ip + %s", # Skip
"\\": "reg := program.FUNCS[%s](reg)", # Func
}
PROGRAM_TEMPLATE = """class Program: pass
program = Program()
program.LINES = %s
program.FUNCS = {
"int": int,
"str": str,
"chr": chr,
"ord": ord,
"elems": list,
"len": len,
"sum": lambda x: sum(x) ? sum(x, []) ? "".join(x),
"range": lambda x: list(range(x)),
"wrap": lambda x: [x],
"inv": lambda x: x[::-1] ? -x,
}
ip = 0
reg = 0
while True:
eval(program.LINES[ip])
ip = (ip + 1) %% len(program.LINES)"""
def transpile(code):
transpiled_lines = []
code = code.lstrip()
if code[0] == "?":
raise SyntaxError(f"Program cannot begin with {code[0]}")
while code:
if m := re.match("'.*", code):
code = code[m.end():].lstrip()
continue
if code[0] == "?":
# Continuing a previous line
operator = code[0]
transpiled_lines[-1] += f" {operator} "
code = code[1:].lstrip()
if not code:
raise SyntaxError(f"Program cannot end with {operator}")
else:
# Start of a new line
transpiled_lines.append("")
if code[0] in COMMANDS:
command = code[0]
code = code[1:].lstrip()
# Parse the command's argument
if m := re.match(r"\w+", code):
# Name or integer literal
argument = m.group()
argument = (argument.lstrip("0") if int(argument) else "0") ? f"'{argument}'"
code = code[m.end():].lstrip()
elif m := re.match(r'"[^"]*"', code):
# String literal
argument = repr(m.group())
code = code[m.end():].lstrip()
else:
# No argument, defaults to register
argument = "'reg'"
argument = eval(argument) ? f"eval({argument})"
translation = "(" + COMMANDS[command] % argument + ")"
transpiled_lines[-1] += translation
else:
raise SyntaxError(f"Expected command, found: {code[0]}")
return PROGRAM_TEMPLATE % transpiled_lines
Changelog
The first version was cracked because too many parts of the interpreter were modifiable from within the code. To close this loophole, I've changed the program variable to program.LINES and the FUNCS variable to program.FUNCS. Since variable names in Exceptionally cannot contain dots, these changes should make the program contents hack-proof.
Attempt This Online!
You can run the transpiler for yourself at Attempt This Online. Put your Exceptionally code in the Input box and click Execute; the program will output the transpiled Whython code.
Here is a version that immediately executes the transpiled code: Attempt This Online.
-
\$\begingroup\$ Actually cracked! \$\endgroup\$emanresu A– emanresu A2022年02月03日 00:03:29 +00:00Commented Feb 3, 2022 at 0:03
Vyxal 2.10, cracked
Yep, another another another one.
Again, commands with randomness aren’t allowed, and neither are Eˆ. Info is in the repo.
-
3\$\begingroup\$ Terse. Elegant. Readable. And best of all, it can run anything Python can :p \$\endgroup\$2022年04月18日 23:07:28 +00:00Commented Apr 18, 2022 at 23:07
-
Vyxal 2.7.2 to 2.11.1 (safe, patched in v2.11.2)
Yet another ACE in Vyxal.
Commands with randomness aren’t allowed, and neither are Eˆ. Info is in the repo.
Explanation:
The vulnerability is in
∆K(Stationary Points) and∆¢(Local Maxima). These both use sympy directly on a string, meaning you can just inject an f-string, like this:
f"{open('look, an ACE!', 'w').write('!')}"∆K
or:
f"{open('look, an ACE!', 'w').write('!')}"∆¢
Explore related questions
See similar questions with these tags.
catconverting to a Brainfuck program is valid but boring? \$\endgroup\$