Two weeks ago I played 0CTF/TCTF 2021 Quals CTF with my team. As every year, it was a pretty fun CTF, though in all honesty I observed only one challenge - pypypypy. Without going into too many details, it was a Python sandbox escape challenge where the player controlled only Python bytecode (limited to 1000 bytes - that's 500 instructions). Yes - only bytecode, i.e. no constants, no local or global variable names, etc (with a minor exception, but that's besides the point). Given this, I had to come up with a way to make strings out of thin air, and before that, make integers out of thin air as well.

Making integers isn't actually that hard, since it's quite easy to get False or True on the stack (e.g. with 2 times BUILD_LIST, 0 followed by a COMPARE), and these get implicitly converted to 0 and 1 respectively when used in any math operations. Having a 1, I implemented a method that just kept left-shifting it (to get 1, 2, 4, 8, 16, 32, 64, ...) and selectively duplicating the values on the stack to add them later - this is basically how the binary system works anyway (and it's quite similar to Russian peasant multiplication method). This worked, but...

As I mentioned above, the total number of instructions was effectively limited to 500, and the above method – while it worked – generated waay too long operation sequences. So I had to figure out a new approach.

In the end I decided not to do it. Or rather, I decided to let my computer figure out what's the shortest way to create a constant number having the value 1 on the stack (technically it should have been True instead, but whatever).

To achieve this I used /dev/urandom a genetic algorithm that created sequences of instructions (limited to math/bitwise/stack operators), then run this "program" on a trivial Python bytecode VM implementation, and if it didn't crash, plus it finished with just one value on the stack, I compared the generated "program" for the given constant to the previously known one (if any). And this worked surprisingly well, allowing me to cut down the per-constant instruction sequence length by anything between 25% and 50%!

Now, the fascinating thing about programs made by genetic algorithms is how eerie they are! A few examples:

  • To make the value 0 (which I already had, but I forgot to tell this to the genetic algorithm), it created this expression: 1 % 1 → 0. Is this 0? Yes! Would I have thought of that? Nope – I would just use 1 - 1 instead.
  • To make the value 14 it first made the value -2 on the stack with ~1 operation, and then made the value 16 by using power and multiplication, to finally arrive at -2 % 16 → 14. While I'm well aware how negative modulo behaves in various languages, again it's not something I would come up with.

If you're curious, and would like to explore all generated the numbers, you can use this interactive widget - just put a number from 0 to 200 on the top left, and then you can click through the generated Python opcodes to see how the expression is formed (or scroll down for the full list):

Number (editable, 0–200)
Reversed Expression (green denotes a duplicated value (DUP_TOP))
Stack (Before)
Expression Stack (Before)
Opcodes (click)
Stack (After)
Expression Stack (After)

I must admit that this was both a pretty fun exercise, and a pretty fun CTF challenge. Apart from automated code generation itself (almost like GitHub Copilot, ey?), writing a trivial "decompiler" that gets the bytecode and returns an expression was interesting as well – I didn't have a chance to do this before.

You can find my source code here (this is CTF-quality code, I am NOT paying for your therapist - you have been warned!):
https://github.com/gynvael/random-stuff/tree/master/pypypypy-genetic

P.S. If you like this kind of stuff, I greatly recommend checking out the Hacker's Delight book.

If you'd like to see the full list of expressions, here it is:

01%1
1-(-1)
21+1
31-~1
4~1*-2
5-(~1)-~2
6(1-~1)+3
7~(~1<<--2)
8-(~1)<<2
9~(1<<1)*-3
101+1|2<<2
11~(~(1<<1)<<~-3)
12~1*(-2^-2*-2)
13-(~(~1*(-2^-2*-2)))
14~1%(-2)**(-2*-2)
15~(-((~1*-2)*4))
16(~1*-2)*4
171-(~1<<1--2)
18~(1<<1)*-3+9
19~((~1*-2)*~4)
20~1*-2+4*4
21(3<<3)-(1-~1)
22~1^(-2*-2)*(-2-4)
23~(-(1-~1<<3))
241-~1<<3
25~(~1*-2)*-5
26~(-(1-~1)**3)
27(1-~1)**3
28-(~(1-~1)**3)
29~1%~(-2<<-2*-2)
30~1^-2<<-2*-2
31~(~1<<-2*-2)
32(1<<1)<<2+2
331|1+1<<(1<<2)
34(1+1)+(2<<2*2)
35(1+1)-~(2<<2**2)
36((1-~1)+3)*6
37-(~(((1-~1)+3)*6))
38~((~1*-2)*~4)+19
39(1-~1)+(3+3)*6
40(2|2<<2)<<1+1
41~(((1-~1)+3)*~6)
42((1-~1)+3)+6*6
43~(~1*-2)*-5^25+25
44(1-~1)**3^27-~27
45(1-~1)**3^27+27
46~(-(1-~1<<3))+23
47~(-((1-~1<<3)+24))
48(1-~1<<3)+24
49~(~1<<--2)*7
50~(~1*-2)*-5+25
51~(~1*-2)*-5-~25
52~(-(1-~1)**3)+26
53~(-((1-~1)**3+27))
54(1-~1)**3+27
55(1-~1)**3-~27
56(-(~1)<<2)*~(-8)
57-(~(1-~1)**3)-~28
58~1%~(-2<<-2*-2)+29
59~(~1*-2-(4<<4))
60(~1^-2<<-2*-2)+30
61~(-(~1%(-2*-2<<4)))
62~1%(-2*-2<<4)
63~(-(~1*-2<<4))
64~1*-2<<4
65-(~(~1*-2<<4))
66-(~1)^2**2<<4
67~(-(~1*-2^4<<4))
68~1*-2^4<<4
69~1*-2-~(4<<4)
70~(-(~((-(~1)<<2)*~8)))
71~((-(~1)<<2)*~8)
72(1-~1)*(3<<3)
73(-(~1)<<2)-~(8*8)
74~(-((1-~1)+3*(3<<3)))
75(1-~1)+3*(3<<3)
76-(~((1-~1)+3*(3<<3)))
77~3+(1-~1)*3**3
783*3**3-(1-~1)
79~(~(~1*-2)<<~-5)
80-(~4<<~1*-2)
81(~(1<<1)*-3)*9
821+(~(1<<1))**(1--3)
831-~1|3*3**3
84(1-~1)+3*3**3
85-(~((1-~1)+3*3**3))
86(~(~1*-2)*-5^25+25)+43
87(1-~1)*~(3*~(3*3))
88~(1<<1)*-3^9*9
89~((~(1<<1)*-3)*~9)
90(1-~1)*(3+3**3)
91-(~((1-~1)*(3+3**3)))
92~(1-~1)-~-4*(-4<<3)
93(1-~1)*(3|-(~3**3))
94~1%-(-2--2*-2<<-2--6)
95~(-(1-~1<<(3^3+3)))
961-~1<<(3^3+3)
97((1-~1<<3)+24)-~48
98(1<<1)-(~2<<2--3)
99~(-((1+1|2<<2)*10))
100(1+1|2<<2)*10
101-(~((1+1|2<<2)*10))
102-(~1)^(2|2<<2)*10
103~(~1<<--2)|7*(7+7)
104(~(-(1-~1)**3)+26)+52
105~(~1<<--2)*(7|7+7)
106~(-((1-~1)**3+27))+53
107~(-(((1-~1)**3+27)+54))
108((1-~1)**3+27)+54
109((1-~1)**3+27)-~54
110((1-~1)**3-~27)+55
111((1-~1)**3-~27)-~55
112~(-2<<2)<<(~1)**(--2)
113(-(~1)<<2)*~(-8)-~56
114~(~(1<<1)<<~-3)^11*11
115~(~1<<--2)*7|49+49
116-4*(1-~1^~3<<3)
117(~1%~(-2<<-2*-2)+29)-~58
118~(~1*-2-(4<<4))+59
119~(~1*-2-(4<<4))-~59
120~(~1*-2)^-5*(-5*-5)
121~(1+1|2<<2)*-11
122~1^-2*-2+(-2<<4--2)
123~(-(~1%(-2*-2<<4)+62))
124~1%(-2*-2<<4)+62
125(-(~1)-~2)*(5*5)
126~1^(-2)**(~(-2<<--2))
127~(~1<<--2--2*2)
128(~1*-2<<4)+64
129(~1*-2<<4)-~64
130-(~(~1*-2<<4))+65
131-(~(~1*-2<<4))-~65
132~1*-2^4<<-(~4)
133(-(~1)^2**2<<4)-~66
134~(-(~1*-2^4<<4))+67
135~((-(~1)<<2)*~(8+8))
136(~1*-2^4<<4)+68
137(~1*-2^4<<4)-~68
138(~1*-2-~(4<<4))+69
139(~1*-2-~(4<<4))-~69
140(-(~1)-~2)*(5^5*5)
141~(-(~((-(~1)<<2)*~8)+71))
142~((-(~1)<<2)*~8)+71
143~(-((~1*(-2^-2*-2))*12))
144(~1*(-2^-2*-2))*12
145-(~((~1*(-2^-2*-2))*12))
146((-(~1)<<2)-~(8*8))+73
147~(~1<<--2)*(7+(7+7))
148~(-((1-~1)+3*(3<<3)))+74
149~(-(((1-~1)+3*(3<<3))+75))
150((1-~1)+3*(3<<3))+75
151((1-~1)+3*(3<<3))-~75
152-(~((1-~1)+3*(3<<3)))+76
153~(-(~((-(~1)-~2)-(5<<5))))
154~((-(~1)-~2)-(5<<5))
155~(~1*-2)+(--5<<5)
156(~1*(-2^-2*-2))*-(~12)
157~(-(~1%(--2-~2<<5)))
158~1%(--2-~2<<5)
159~(~(~1*-2)<<--5)
160-(~1)-~2<<5
161-(~(-(~1)-~2<<5))
162(~(1<<1)*-3)*9+81
163(~(1<<1)*-3)*9-~81
164~1*-2|-(~4)<<5
165-(~1)-~2|5<<5
166(-(~1)-~2)-~(5<<5)
167(1-~1|3*3**3)-~83
168((1-~1)+3*3**3)+84
169~(~1*(-2^-2*-2))*-13
170(-(~1)-~2)+(5^5<<5)
171~(1<<1)*-3+9*(9+9)
172(1+1|2<<2)*10^100+100
173~1*(-2^-2*-2)|~12*-13
174(1-~1)*~(3*~(3*3))+87
175~(-((~(1<<1)*-3^9*9)+88))
176(~(1<<1)*-3^9*9)+88
177(~(1<<1)*-3^9*9)-~88
178~((~(1<<1)*-3)*~9)+89
179~((~(1<<1)*-3)*~9+-90)
180(1-~1)*(-4^(~3)**3)
181(1-~1)*(3+3**3)-~90
182(~1%(-2)**(-2*-2))*~(-14)
183-(~((~1%(-2)**(-2*-2))*~(-14)))
184~1%(-2)**(-2*-2)^-14-14*-14
1855<<5|(-(~1)-~2)*5
186~1%(-2*-2<<4)+(62+62)
187~1%(-2*-2<<4)-~(62+62)
188~1%-(-2--2*-2<<-2--6)+94
189~(-(~1^~(--2)<<-2*-3))
190~1^~(--2)<<-2*-3
191~(-(1-~1<<3+3))
1921-~1<<3+3
193-(~(1-~1<<3+3))
194-(~1)^-(~2)<<2*3
195(1-~1)+(3<<3+3)
196(~1%(-2)**(-2*-2))*14
197-(~((~1%(-2)**(-2*-2))*14))
198~(-((1+1|2<<2)*10))+99
199~(-((1+1)*((2+(2<<2))*10)))
200(1+1)*((2+(2<<2))*10)

Till next time!

Comments:

2021-08-11 19:56:29 = vamastah
{
I do not know exactly why, but it reminds me of procedural generation of Sokoban levels. Your way of solving the CTF could also be used for compile-time optimizations, it would be nice to know if a similar algorithm is already implemented in GCC or Clang.
}

Add a comment:

Nick:
URL (optional):
Math captcha: 6 ∗ 8 + 8 =