Python is Hard
Last week, I participated in the Cyber Jawara International CTF 2024 with my university’s merged team. During the competition, I managed to solve only one reversing challenge, as I was more focused on a Pyjail challenge (which I didn’t manage to solve, and no one solved it until the CTF was over). Despite my focus, I found all the reversing challenges quite difficult overall.
Understand js2py VM
We are given python file that looks like compressed with zlib and encoded in base64, after recovering the source code we'll see js2py VM.
chall.py (with some adjustment)
import pyjsparser
from js2py.internals.space import Space
from js2py.internals import fill_space
from js2py.internals.byte_trans import ByteCodeGenerator
from js2py.internals.code import Code
from js2py.internals.simplex import *
from js2py.internals.opcodes import *
a = ByteCodeGenerator(Code(debug_mode=True))
s = Space()
a.exe.space = s
s.exe = a.exe
d = pyjsparser.parse("")
a.emit(d)
fill_space.fill_space(s,
a)
a.exe.tape = [LOAD_UNDEFINED(),
LOAD_STRING(input("Input flag: "),),
STORE('inp',),
POP(),
LOAD_ARRAY(0,),
STORE('a',),
POP(),
LOAD_ARRAY(0,),
STORE('b',),
POP(),
LOAD_ARRAY(0,),
STORE('c',),
POP(),
LOAD_ARRAY(0,),
STORE('d',),
POP(),
LOAD_NUMBER(0.0,),
STORE('i',),
POP(),
JUMP(3,),
...altered because of long text...
LABEL(21,),
JUMP_IF_FALSE(19,),
POP(),
LOAD('console',),
LOAD_STRING('congrats',),
LOAD_N_TUPLE(1,),
CALL_METHOD_DOT('log',),
JUMP(20,),
LABEL(19,),
POP(),
LOAD('console',),
LOAD_STRING('failed',),
LOAD_N_TUPLE(1,),
CALL_METHOD_DOT('log',),
LABEL(20,)]
a.exe.compile()
a.exe.run(a.exe.space.GlobalObj)
By reading the opcode, we could intuitively imagine what the JS code will be like, but for the sake of correctness, we could actually look at the actual implementation at GitHub.
https://github.com/PiotrDabkowski/Js2Py
If we look at the github page we could see how they generate those opcode :
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
So after reading the opcode from the challenge itself i try to mimic it by creating JS code and compare it with the actual opcode.
We could see some pattern on the opcode so once you get it it was pretty easy to convert back to JS.
Like for this example this opcode is a for loop
LABEL(1,),
LOAD_NUMBER(4.0,),
STORE_OP('i', '+'),
POP(),
LABEL(3,),
LOAD('i',),
LOAD_NUMBER(36.0,),
BINARY_OP('<',),
JUMP_IF_FALSE(2,),
LOAD_NUMBER(0.0,),
STORE('tmp',),
POP(),
LOAD_NUMBER(0.0,),
STORE('j',),
POP(),
JUMP(6,),
LABEL(4,),
POSTFIX(1, -1, 'j'),
POP(),
LABEL(6,),
LOAD('j',),
LOAD_NUMBER(4.0,),
BINARY_OP('<',),
JUMP_IF_FALSE(5,),
POP(),
LOAD('inp',),
LOAD('i',),
LOAD('j',),
BINARY_OP('+',),
LOAD_N_TUPLE(1,),
CALL_METHOD_DOT('charCodeAt',),
LOAD('j',),
LOAD_NUMBER(8.0,),
BINARY_OP('*',),
BINARY_OP('<<',),
STORE_OP('tmp', '|'),
JUMP(4,),
LABEL(5,),
POP(),
LOAD('a',),
LOAD('tmp',),
LOAD_N_TUPLE(1,),
CALL_METHOD_DOT('push',),
JUMP(1,),
And this how they look like in js
This what i have at the end
There's some equation that i don't write back to JS because i directly try to solve the equation with z3.
POC
After knowing that this is just a z3 because there's lot of equation and check i write this solve script
sol.py :
But i think i got one of the equation wrong since it returns me an invalid flag
or
As we can see we fucked up some chars, but we could do some guess work from it. In the end it was