nothoudaifa


writeup for dna chall from smiley ctf 2025

TL;DR

This challenge features a vm that takes your flag, treats it as a vector and multiples it by a matrix of constant values, then compares the resulting vector to another vector, if the comparision is correct it prints “CORRECT!”

Initial Analysis

we are given two files: main.cpython-310.pyc which is our python compiled vm and vm.dna which is the vm code, the first thing i did was open https://pylingual.io to get the decompilation of the pyc, it gave me:

# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: main.py
# Bytecode version: 3.10.0rc2 (3439)
# Source timestamp: 2025-06-06 03:24:45 UTC (1749180285)

import marshal
import sys
s = []
m = {}
nm = {'A': 0, 'T': 1, 'G': 2, 'C': 3}
unlucky = [b'\x8coooooooooooonooolooo,ooo\x9cSooo\x06o\x12o\x1bo\x0bnvo\x13o\x0bmSo\x1bo\x0blvo\x13o\x0bnSo\x1bo\x0bkvo\x13o\x0blSo\x1bo\x0bmvo\x13o\x0bkSo\x13o\x0eo\x0bo<oFj!\xb5n;\xb5n.\xb5n(\xb5n,\xc6n\xb5m\x01\x02\xc6n\xb5l\x1b\x02\x1f\xc6o\x1deooo\x95fS\x1a\x01\x03\x1a\x0c\x04\x16Q\xb5h\x1a\x01\x03\x1a\x0c\x04\x16booo\x9ccoookmcncncncngn', b'\x96uuuuuuuuuuuuruuu}uuu6uuu\x86\x11uuu\x11t\x08u\x11w\x08t\x11v\x08w\x11q\x11p\xf1u\tu1u\xf6t\x08v\tu\tt\tw\x13v1u(n\x08q\x01u\x01t\x01w\xd5v\xd4u\xf6t\xf6t1u(e)w\x08p\x08s\tv\tspulu\x01w\tq\tpluluMuvuIu\x04i\x04g\tv\x14w\x11u&u\\s;\xafq426!\xafq!642\xafq6!24\x16tuuuuuuuuuuuwuuusuuu&uuu\x86ouuu\x1cu\tu(|\x08t\tt\x01u\x01t\xd5w\xd4u\xf6t\xe6w\x04w&u\\u\xdcv\xafv\x06\x00\x18\xafw\x1b\x18\xafs\x03\x14\x19\x00\x10\x06\xdcw\xafw[E\xaft\x16\xdcu\x07xuuu\x8f|I\x00\x1b\x19\x00\x16\x1e\x0cK\xafr\x00\x1b\x19\x00\x16\x1e\x0cnuuu\x86wuuuou\x8fh\x00\x1b\x19\x00\x16\x1e\x0c*G[I\x19\x1a\x16\x14\x19\x06K[I\x11\x1c\x16\x01\x16\x1a\x18\x05K\xdcq\xaf|\x10\x1b\x00\x18\x10\x07\x14\x01\x10\xafs\x06\x1a\x07\x01\x10\x11\x07}uuu\xafq\x1e\x10\x0c\x06\xdcr\xafw\x06D\xafw\x06G\xafw\x06F\xafv\x01\x18\x05\xaft\x06\xaft\x1c\x07yuuu\x07xuuu\x07xuuu\x07{uuu\x07zuuucuuu\x86guuuqwqtqt{t{tmtotw\x8a}w', b"\x8aiiiiiiiiiiiihiiiniiijiii\x9a/iii\x1di\rh\xeah\xe0i\xe1i\xc9h\x1di\rk\xeah\xc9k\rj\rm\xedi\x1dj\xc9m\xc8i\xc8k\xc8hhi.i\xeei\x0fh\rl\ro\xeda\ro\x1dl\xeaj\x14i\x15i\x1dj\xeah\x08j\ri:i@n'\xb3o\x1b\x08\x07\r\x06\x04\xb3`\x0f\x1c\x07\n\x1d\x06\x06\x05\x1a\nkiiiiiiiiiiikiiikiii:iii\x9aaiii\x15i\x15h(i:i@h'\xc0i\xc0k\xb3h\x11\xb3h\x10\x1bliii\x1bliii\x93`U\x1c\x07\x05\x1c\n\x02\x10W\xb3n\x1c\x07\x05\x1c\n\x02\x10Miii\x9akiiiai\x93r\x1c\x07\x05\x1c\n\x02\x106ZGU\x05\x06\n\x08\x05\x1aWGU\x05\x08\x04\x0b\r\x08W\niiiiiiiiiiiiiiiijiiiiiii\x9aCiii\x0ci3h\ri3k\xeei\xeeh\x0fk\rh\rk\xeda3j\xeei\x0fh\rj\rm\xeda3m\xeeimi3l:i@l\x93s\x1c\x07\x05\x1c\n\x02\x106ZGU\x05\x06\n\x08\x05\x1aWG\x1c\x07\x05\x1c\n\x02\x10\nkiiiiiiiiiiimiiiliiiziii\x9a-iii\x1di\xeai\xc9h\x15h\xc8hhi\x1dk\rh\xeah\x14k\xe1h\xc9j\x15k\xc8hhi\x1dm\rk\xeah-i4e\x14j\x15h\x15k\x15jpipi\x15i\rh\x15jpiUi\x18z\ri:i@j'\xb3m(*.=\x80miii\xc0l\xb3l\x1a\x1c\x19\x0c\x1b\xb3a66\x00\x07\x00\x1d66\xb3m\x05\x00\x1a\x1d\xb3n\x1a\x01\x1c\x0f\x0f\x05\x0c\xb3l\x1b\x08\x07\x0e\x0c\xc0m\xb3m\x1a\x0c\x05\x0f\xb3n\x04\x08\x19\x19\x00\x07\x0e\xb3m\x02\x0c\x10\x1a\xb3h\x00\xc0k\xb3`66\n\x05\x08\x1a\x1a66\xb3h\x1b\x1bliii\x1b`iii\x1bciiiOiii\x9aeiiiehahcheh\x7fhm\x96\x93J\x1c\x07\x05\x1c\n\x02\x106ZGU\x05\x06\n\x08\x05\x1aWG\x1c\x07\x05\x1c\n\x02\x10G66\x00\x07\x00\x1d66\nkiiiiiiiiiiiliiiliiiziii\x9a;iii\x1di\rh\xeah\x14k\x1di\rk\xeah\x14j`i\x15k\xc9h\rm\xc8h\x14m\x1dk\xeei\x0fh\rl\ro\xeda\x15j\xc9j\x15m\xc8h\xc9m\xc8i\ri\rn\xeckpi-i\xeah\xeah\x1bA\x1dl\xeai\xc9o\xe1i\xc8h:i\x18`@a'\x1bkiii\xb3n\x01\x08\x1a\x01\x05\x00\x0b=\x80Iiii\nhiiiiiiiiiiikiiimiiiZiii\x9auiii\xe8i\x15i4`\x14h\x15h\x1di\xe1i\xeah\x02k?ihi\x18k\ri:i@h'\xc0h\xb3j\x06\x1b\r\xc0k\xb3kGY\x1bniii\xc0h\xb3j\x02\x0c\x10\x1bliii\x1b`iii\x1bciii[iii\x9amiiik\xe9si\x93P\x1c\x07\x05\x1c\n\x02\x106ZGU\x05\x06\n\x08\x05\x1aWG\x1c\x07\x05\x1c\n\x02\x10G66\x0e\x0c\x1d\x00\x1d\x0c\x0466GU\x05\x06\n\x08\x05\x1aWGU\x0e\x0c\x07\x0c\x11\x19\x1bW\x80hiii\xc0n\xb3c66\x00\x04\x19\x06\x1b\x1d66\xb3`\x1b\x08\x07\r\x0b\x10\x1d\x0c\x1a\xb3j\x08\x05\x05\xb3o\x1a\x01\x08[\\_\xb3o\r\x00\x0e\x0c\x1a\x1d\x1bziii\xb3b66\x0e\x0c\x1d\x00\x1d\x0c\x0466\xc0l\x1bpiii\x1bBiii\xb3m\x01\x05\x00\x0b\xb3m\x1b\x05\x00\x0b\xb3h\x0b\xc0h\x1bwiii\x1bCiii\x1b`iii\x1bciiiDiii\x9agiiiahahkhchAhehk\x94\x93O\x1c\x07\x05\x1c\n\x02\x106ZGU\x05\x06\n\x08\x05\x1aWG\x1c\x07\x05\x1c\n\x02\x10G66\x0e\x0c\x1d\x00\x1d\x0c\x0466\xc0o\xb3a66\x07\x08\x04\x0c66\xb3c66\x04\x06\r\x1c\x05\x0c66\xb3e66\x18\x1c\x08\x05\x07\x08\x04\x0c66\x1b}iii\x1b\\iii\xb3d66\n\x05\x08\x1a\x1a\n\x0c\x05\x0566\x1bliii\xc0h\x1bviii\x1bSiii\x1b`iii\x1bciiiLiii\x9aoiiiaigh}n\x1bciii\xc0o\x1bYiii\xb3m\x1a\x0c\x0c\r\xb3o\x1b\x0c\r\x1c\n\x0c\xb3k\x07\x04\xb3o\x1f\x08\x05\x1c\x0c\x1a\xb3m\r\x00\n\x1d\xc0h\x1bciii\x1bliii\x1b+iii\x1b`iii\x1bciiiHiii\x9aaiiiakwh}hey", b'\x82aaaaaaaaaaaacaaagaaa"aaa\x92]aaa&a\x05`\x05c\xe5a\x05c\x15a\xe2b\x1ca&a\x05b\x05e\xe5a\x05e\x15`\x1da\x05d\xece\x1c`\x15c\x05g\x15`\x15b\xe2`\xfaa\x05f\xfcb\xe2``a\x05a2aHi/\x02aaaaaaaaaaaaaaaabaaaaaaa\x92Iaaa\x04a;`\x05a;c\xe6a\x07`\x05`\x05c\xe5i;b\xe6a\x07`\x05b\x05e\xe5i;e\xe6aea;d2aHd\x9bt\x14\x0f\r\x14\x02\n\x18>UO]\r\x0e\x02\x00\r\x12_O,,\x02eaaaaaaaaaaaeaaagaaaraaa\x92saaa\x15a\xe2a\xc1`\x1da\x1d`\x1dc\x1db\xc0e2aH`/\xc8c\xbbd\x12\x14\x11\x04\x13\xbbf>>\x0f\x04\x16>>\xc8e\xbbb\x02\r\x12\xbbe\x0f\x00\x0c\x04\xbbd\x03\x00\x12\x04\x12\xbbb\x05\x02\x15\xc8`\xbbh>>\x02\r\x00\x12\x12>>\xc8a\x9bh]\x14\x0f\r\x14\x02\n\x18_\xbbf\x14\x0f\r\x14\x02\n\x18Zaaa\x92caaas`\x9b|\x14\x0f\r\x14\x02\n\x18>UO]\r\x0e\x02\x00\r\x12_O,,O>>\x0f\x04\x16>>\x02`aaaaaaaaaaafaaadaaa~aaa\x92\x05aaa\x15a\xe2a\x0b`\x1d`\x08a\x1dc\xc5`\xef`\x1cb\x15c\x1db\xc1b\xc0a\xe2`\x1ce\x1de\x05a\x05a\x05`\xe4bxa\x1de\x05c\x05a\x05`\xe4bxava\x1ce\x15e\x15d\x1db\xc1g\xc0a\xe2`\xe2`%a<k=c\x1cd\x1cg\x1de\x1ddxa\x1db\x1dg]a\x10D\x1db2aHb/\x88caaa\x88`aaa\xc8f\x13gaaa\xbbi>>\x02\x00\r\r>>\xbbe\r\x08\x12\x15\xbbg\x17\x00\r\x14\x04\x12\xbbh\x04\x0f\x14\x0c\x04\x13\x00\x15\x04\xbbg\x12\x0e\x13\x15\x04\x05\xbbe\n\x04\x18\x12\xc8f\x13haaa\xbbe\x00\x13\x06\x12\xbbg\n\x16\x00\x13\x06\x12\xbbi\x08\x0f\x12\x15\x00\x0f\x02\x04\xbbe\x17\x00\r\x12\xbb`\x08\xbb`\n\x13laaa\x13naaa\x13qaaa\x13paaa_aaa\x92maaas`m`}`y`o`e`\x9b\x7f\x14\x0f\r\x14\x02\n\x18>UO]\r\x0e\x02\x00\r\x12_O,,O>>\x02\x00\r\r>>\xc8g\xbbi>>\x0f\x00\x0c\x04>>\xbbk>>\x0c\x0e\x05\x14\r\x04>>\xbbm>>\x10\x14\x00\r\x0f\x00\x0c\x04>>\x13faaa\x13yaaa\xbbl>>\x02\r\x00\x12\x12\x02\x04\r\r>>\x13naaa\x13naaa\x13laaa\x13qaaa\x13paaa[aaa\x92gaaaiam`ub\xbbc,,\x02aaaaaaaaaaaaaaaa`aaa!aaa\x92maaa\x04a;`\x05a;c\x05`2aHc\x9bt\x14\x0f\r\x14\x02\n\x18>UO]\r\x0e\x02\x00\r\x12_O,%/\xc8b\x13Iaaa\x13Haaa\x13Kaaa\x13naaa\x13naaa\x13naaa\x13qaaa\x13paaa\'aaa\x92eaaaiae`\xbbc,%\xc8`\xbbh\x0c\x04\x15\x00\x02\r\x00\x12\x12\x9b@\x06\r\x0e\x03\x00\r\x12IH:F\x0f\x14\x02\r\x04\x0e\x15\x08\x05\x04>\x0c\x00\x11F<A\\A,%I\x9b`H\xc8e\xbbe\x15\x18\x11\x04\xbbe\x05\x08\x02\x15\xbbe\x04\x19\x04\x02\xbbc\x0f\x0c\xc8c\x13Laaa\x13Saaa\x13naaa\x13naaa\x13qaaa\x13paaaVaaa\x92gaaaqbumyb']
trans = lambda s: sum((nm[c] << 2 * i for i, c in enumerate(s)))
if len(sys.argv)!= 2:
    print(f'Usage: {sys.argv[0]} <dna_file>')
    sys.exit(1)
code = open(sys.argv[1]).read()
flag = input('> ').encode()
if len(flag)!= 56:
    exit('WRONG!')
if flag[:6]!= b'.;,;.{':
    exit('WRONG!')
if flag[(-1)]!= 125:
    exit('WRONG!')
flag = flag[6:(-1)]
for i in range(len(flag)):
    m[640 + i] = flag[i]
pc = 0
while pc < len(code):
    pri, pro = map(trans, [code[pc:pc + 2], code[pc + 2:pc + 12]])

    @pri
    case 0:
        s.append(pro)
        pc += 12
    else:  # inserted
        case 1:
            if not s:
                raise Exception('Stack underflow')
            s.pop()
            pc += 2
        else:  # inserted
            case 2:
                if pro not in m:
                    raise Exception(f'Uninitialized memory access at {pro}')
                s.append(m[pro])
                pc += 12
            else:  # inserted
                case 3:
                    if not s:
                        raise Exception('Stack underflow')
                    m[pro] = s.pop()
                    pc += 12
                else:  # inserted
                    case 4:
                        if len(s) < 2:
                            raise Exception('Stack underflow')
                        a, b = (s.pop(), s.pop())
                        s.append(a + b)
                        pc += 2
                    else:  # inserted
                        case 5:
                            if len(s) < 2:
                                raise Exception('Stack underflow')
                            a, b = (s.pop(), s.pop())
                            s.append(b - a)
                            pc += 2
                        else:  # inserted
                            case 6:
                                if len(s) < 2:
                                    raise Exception('Stack underflow')
                                a, b = (s.pop(), s.pop())
                                s.append(a * b)
                                pc += 2
                            else:  # inserted
                                case 7:
                                    if len(s) < 2:
                                        raise Exception('Stack underflow')
                                    a, b = (s.pop(), s.pop())
                                    if a == 0:
                                        raise Exception('Division by zero')
                                    s.append(b % a)
                                    pc += 2
                                else:  # inserted
                                    case 8:
                                        if len(s) < 2:
                                            raise Exception('Stack underflow')
                                        a, b = (s.pop(), s.pop())
                                        s.append(1 if a == b else 0)
                                        pc += 2
                                    else:  # inserted
                                        case 9:
                                            pc = pro
                                        else:  # inserted
                                            case 10:
                                                if not s:
                                                    raise Exception('Stack underflow')
                                                if s.pop() == 1:
                                                    pc = pro
                                                else:  # inserted
                                                    pc += 12
                                            else:  # inserted
                                                case 11:
                                                    if not s:
                                                        raise Exception('Stack underflow')
                                                    if s.pop()!= 1:
                                                        pc = pro
                                                    else:  # inserted
                                                        pc += 12
                                                else:  # inserted
                                                    case 12:
                                                        if not s:
                                                            raise Exception('Stack underflow')
                                                        print(chr(s.pop()), end='')
                                                        pc += 2
                                                    else:  # inserted
                                                        case 13:
                                                            if not s:
                                                                raise Exception('Stack underflow')
                                                            key = s.pop()

                                                            def f():
                                                                return
                                                            f.__code__ = marshal.loads(bytes([b ^ key for b in unlucky.pop(0)]))
                                                            f()
                                                            pc += 2
                                                        else:  # inserted
                                                            case 14:
                                                                if len(s) < 2:
                                                                    raise Exception('Stack underflow')
                                                                a, b = (s.pop(), s.pop())
                                                                if a not in nm or b not in nm:
                                                                    raise Exception('Invalid')
                                                                nm[a], nm[b] = (nm[b], nm[a])
                                                                pc += 2
                                                            else:  # inserted
                                                                case 15:
                                                                    break

we can see that it’s a simple stack based vm, the flag is loaded at 0x280, next thing i did was to make a disasembler for the vm, you can check it in here

it gave me the following diassembly (truncated):

0x0 PUSH [0x280]
0xc PUSH 0x6a
0x18 MULT
0x1a PUSH [0x281]
0x26 PUSH 0x1b
0x32 MULT
...
0x4e0 PUSH [0x2b0]
0x4ec PUSH 0xa1
0x4f8 MULT
0x4fa ADD
0x4fc ADD
...
0x558 ADD
0x55a [0x1000] = POP
0x566 PUSH [0x280]
0x572 PUSH 0x38
0x57e MULT
...
0x3b62 PUSH [0x29a]
0x3b6e CALL MARSHAL
0x3b70 CALL MARSHAL
0x3b72 HALT
0x3b74 MOD
0x3b76 CALL MARSHAL
0x3b78 HALT
0x3b7a HALT
0x3b7c HALT
0x3b7e JMP 0x1fffe IF POP != 1
0x3b8a CALL MARSHAL
0x3b8c PUTC(POP)
0x3b8e MOD
0x3b90 CALL MARSHAL

the first thing i noticed is that it is taking chars of the flag and multiplying them with constants then adds them, this is a dot product (which made me pretty convinced it a linear system, i didn’t use this in my first solve tho), after that it is invoking instruction 14, which calls marshal code using a key, in this case the key is a flag char, i didn’t reverse the python bytecode when solving the chall, afrer that it does some stuff, at this point i got tired of reading the disassembly and decided to make my own emulator so i can debug it, you can find it here

after running the vm with an example flag i got this

$ py vm.py ./dna/vm.dna 
> .;,;.{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} 
MARSHAL:  65
Traceback (most recent call last):
  File "/home/player/ctfs/smiley/dna/vm.py", line 117, in <module>
    f.__code__ = marshal.loads(bytes([b ^ key for b in unlucky.pop(0)]))
ValueError: bad marshal data (unknown type code)

this is because the key (which is a character from the flag) is wrong, after bruteforcing the keys until i got no error i was left with

.;,;.{AAAAAAAAAAAAAAAAAAAAAAiAAAouAAAAAaAAAAAAAAAAAAAAA}

now running the vm again we get:

MARSHAL:  111
MARSHAL:  117
MARSHAL:  105
MARSHAL:  97
CMPEQ 0xa8f2c 0x6b0ce
CMPEQ 0x952d6 0x60833
CMPEQ 0xa0cf4 0x67b2e
CMPEQ 0x87e87 0x576cb
CMPEQ 0x8fbb8 0x5d2be
CMPEQ 0x996f6 0x62e12
CMPEQ 0xa0eba 0x65f0d
CMPEQ 0x981a4 0x61bd6
CMPEQ 0x90214 0x5ec71
CMPEQ 0xa3c6f 0x67bc8
CMPEQ 0xb36f0 0x7396d
CMPEQ 0xa9750 0x6e79d
CMPEQ 0xa4697 0x6d0f2
CMPEQ 0xa52a5 0x682f8
CMPEQ 0x9bd69 0x65dc8
CMPEQ 0xb24c1 0x71555
CMPEQ 0x9f053 0x64bbf
CMPEQ 0x95a53 0x646a9
CMPEQ 0x95e95 0x5fc1a
CMPEQ 0xb0750 0x6f3f5
CMPEQ 0xa1ad8 0x64911
CMPEQ 0x949d0 0x5f763
CMPEQ 0x922ab 0x5e00f
CMPEQ 0x990f4 0x6058c
CMPEQ 0xa0e4a 0x674f6
CMPEQ 0x8749a 0x57963
CMPEQ 0x9931c 0x613df
CMPEQ 0x97981 0x5f156
CMPEQ 0xa01c2 0x67f51
CMPEQ 0x92057 0x5c18d
CMPEQ 0xa24ad 0x67e15
CMPEQ 0xbcae1 0x7a2d5
CMPEQ 0xad88c 0x711d2
CMPEQ 0xa3833 0x6742d
CMPEQ 0x8cce6 0x5ba3b
CMPEQ 0xaecb6 0x6e958
CMPEQ 0xa6859 0x6bab8
CMPEQ 0xa7cac 0x6cd1e
CMPEQ 0xc4c8d 0x7ecc5
CMPEQ 0xa1427 0x66c4d
CMPEQ 0xb1df7 0x72a1f
CMPEQ 0xa8600 0x6bd4d
CMPEQ 0xb652c 0x7820c
CMPEQ 0x91f28 0x5f805
CMPEQ 0x99b79 0x61bf0
CMPEQ 0x8edb6 0x5d3e6
CMPEQ 0xa5c30 0x68bde
CMPEQ 0xa26ea 0x68231
CMPEQ 0xad889 0x70f95
CMPEQ 0x31 0x0
JMP 0x10ebc IF POP != 1
PUTC(POP)
WPUTC(POP)
RPUTC(POP)
OPUTC(POP)
NPUTC(POP)
GPUTC(POP)
!PUTC(POP)

it printed wrong, it did 49 comparisions (here i was sure that it is comparing the resulting vectors from the matrix multiplication of the linear system), it prints “WRONG!” if at least one fails.

The First solution (unintended)

after looking at the comparisions i decided to use z3, by supplying a symbolic flag then adding constraints when the comparisions happen the script is here the script defines the flag as a list of symbolic 8 bitvecs (leaving the keys for the marshal code) using

flag= b".;,;.{AAAAAAAAAAAAAAAAAAAAAAiAAAouAAAAAaAAAAAAAAAAAAAAA}"
flag = list(flag[6:(-1)])
for i in range(len(flag)):
    if flag[i] == ord('A'):
        flag[i]= BitVec(f'bv_{i}', 8)

and the cmp instruction is now like this:

elif pri == 8:
    if len(s) < 2:
        raise Exception('Stack underflow')
    a, b = (s.pop(), s.pop())
    s.append(1 if a == b else 0)
    # this check is for the last comparision, in here i run the z3 check
    if type(a) == int and type(b) == int and a == 49:
        if sol.check() == sat:
            model = sol.model()
            print('.;,;,{', end='')
            for i in range(49):
                try:
                    print(chr(model[flag[i]].as_long()),end='')
                except (KeyError,IndexError):
                    print(chr(flag[i]), end='')
            print('}')
        else:
            print('unsat')
        exit(0)
    # otherwise just add the constraint
    sol.add(a == b)
    pc+=2

i run it and get the flag: .;,;,{we_ought_to_start_storing_our_data_as_dna_instead}

The Second Solution (intended) (after the ctf ended)

i talked before about it being a linear system, in this solve i tried to extract the values of the matrix and the result vector to solve this system, the reason i used z3 in the first solution is because i couldn’t extract all the matrix values from the disassembly (the first part of it has only 11 rows), after the ctf ended i thought of an idea, by multiplying a matrix with a vector/input flag (1, 0, 0 …) the resulting vector will be the first column of the matrix

matrix equation

and since i can extract the resulting vector, i can extract each column of the matrix, i just needed a bit of scripting.

since i am setting all the flag bytes to 0, i had to hard code the keys in instruction 14 like this

elif pri == 13:
    if not s:
        raise Exception('Stack underflow')
    key = s.pop()
    
    if len(unlucky) == 4:
        key = 111
    elif len(unlucky) == 3:
        key = 117
    elif len(unlucky) == 2:
        key = 105
    elif len(unlucky) == 1:
        key = 97
    print("MARSHAL: ", key)
    def f():
        return
    f.__code__ = marshal.loads(bytes([b ^ key for b in unlucky.pop(0)]))
    f()
    pc+=2

to extract the columns and the result vector i used this:

matrix = np.zeros((49, 49), dtype=int) # matrix of values
rslt = np.zeros((49), dtype=int) # the compared vector
...
flag = list(b'\x00'*49)
flag[index] = 1
...

elif pri == 8:
    if len(s) < 2:
        raise Exception('Stack underflow')
    a, b = (s.pop(), s.pop())
    s.append(1 if a == b else 0)
    if a == 0x31 and b == 0:
        if index == 0:
            rslt[:] = e
        # this is the last comparision, break at it
        matrix[:, index] = column
        print(index, column, e)
        break
    else:
        if index == 0:
            e.append(a)
        column.append(b)
    print(f"CMPEQ {hex(a)} {hex(b)}")
    pc+=2

after extracting the values we can solve and print the flag using a simple linalg.solve from numpy:

flag = np.linalg.solve(matrix, rslt)
print(''.join(chr(round(val)) for val in flag))

full script here

after running it i get the flag: .;,;,{we_ought_to_start_storing_our_data_as_dna_instead}