Sickle
16 minutos de lectura
Se nos proporciona un código fuente en Python que verifica si la flag es correcta:
import pickle, io
payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
f = io.BytesIO(payload)
res = pickle.load(f)
if isinstance(res, bool) and res:
print("Congratulations!!")
else:
print("Nope")
Lo que hace es cargar un payload serializado en pickle
y verifica si el resultado es verdadero:
$ python3 problem.py
FLAG> SECCON{asdf}
Nope
Por lo tanto, debemos analizar el contenido serializado en pickle
para determinar cómo se está verificando la flag.
Análisis de pickle
Podemos usar pickletools.dis
para mostrar los opcodes que se ejecutan en la VM de pickle
:
$ python3 -q
>>> payload = b'\x8c\x08builtins\x8c\x07getattr\x93\x942\x8c\x08builtins\x8c\x05input\x93\x8c\x06FLAG> \x85R\x8c\x06encode\x86R)R\x940g0\n\x8c\x08builtins\x8c\x04dict\x93\x8c\x03get\x86R\x8c\x08builtins\x8c\x07globals\x93)R\x8c\x01f\x86R\x8c\x04seek\x86R\x94g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__add__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__mul__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x06__eq__\x86R\x940g3\ng5\n\x8c\x08builtins\x8c\x03len\x93g1\n\x85RM@\x00\x86RM\x05\x01\x86R\x85R.0g0\ng1\n\x8c\x0b__getitem__\x86R\x940M\x00\x00\x940g2\ng3\ng0\ng6\ng7\n\x85R\x8c\x06__le__\x86RM\x7f\x00\x85RMJ\x01\x86R\x85R.0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM@\x00\x86RMU\x00\x86RM"\x01\x86R\x85R0g0\ng0\n]\x94\x8c\x06append\x86R\x940g8\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\nfrom_bytes\x86R\x940M\x00\x00p7\n0g9\ng11\ng6\n\x8c\x08builtins\x8c\x05slice\x93g4\ng7\nM\x08\x00\x86Rg4\ng3\ng7\nM\x01\x00\x86RM\x08\x00\x86R\x86R\x85R\x8c\x06little\x86R\x85R0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RMw\x00\x86RM\xc9\x01\x86R\x85R0g0\n]\x94\x8c\x06append\x86R\x940g0\ng12\n\x8c\x0b__getitem__\x86R\x940g0\n\x8c\x08builtins\x8c\x03int\x93\x8c\x07__xor__\x86R\x940I1244422970072434993\n\x940M\x00\x00p7\n0g13\n\x8c\x08builtins\x8c\x03pow\x93g15\ng10\ng7\n\x85Rg16\n\x86RI65537\nI18446744073709551557\n\x87R\x85R0g14\ng7\n\x85Rp16\n0g2\ng3\ng4\ng5\ng3\ng7\nM\x01\x00\x86Rp7\nM\x08\x00\x86RM\x83\x00\x86RM\xa7\x02\x86R\x85R0g0\ng12\n\x8c\x06__eq__\x86R(I8215359690687096682\nI1862662588367509514\nI8350772864914849965\nI11616510986494699232\nI3711648467207374797\nI9722127090168848805\nI16780197523811627561\nI18138828537077112905\nl\x85R.'
>>>
>>> from pickletools import dis
>>> dis(payload)
0: \x8c SHORT_BINUNICODE 'builtins'
10: \x8c SHORT_BINUNICODE 'getattr'
19: \x93 STACK_GLOBAL
20: \x94 MEMOIZE (as 0)
21: 2 DUP
22: \x8c SHORT_BINUNICODE 'builtins'
32: \x8c SHORT_BINUNICODE 'input'
39: \x93 STACK_GLOBAL
40: \x8c SHORT_BINUNICODE 'FLAG> '
48: \x85 TUPLE1
49: R REDUCE
50: \x8c SHORT_BINUNICODE 'encode'
58: \x86 TUPLE2
59: R REDUCE
60: ) EMPTY_TUPLE
61: R REDUCE
62: \x94 MEMOIZE (as 1)
63: 0 POP
64: g GET 0
67: \x8c SHORT_BINUNICODE 'builtins'
77: \x8c SHORT_BINUNICODE 'dict'
83: \x93 STACK_GLOBAL
84: \x8c SHORT_BINUNICODE 'get'
89: \x86 TUPLE2
90: R REDUCE
91: \x8c SHORT_BINUNICODE 'builtins'
101: \x8c SHORT_BINUNICODE 'globals'
110: \x93 STACK_GLOBAL
111: ) EMPTY_TUPLE
112: R REDUCE
113: \x8c SHORT_BINUNICODE 'f'
116: \x86 TUPLE2
117: R REDUCE
118: \x8c SHORT_BINUNICODE 'seek'
124: \x86 TUPLE2
125: R REDUCE
126: \x94 MEMOIZE (as 2)
127: g GET 0
130: \x8c SHORT_BINUNICODE 'builtins'
140: \x8c SHORT_BINUNICODE 'int'
145: \x93 STACK_GLOBAL
146: \x8c SHORT_BINUNICODE '__add__'
155: \x86 TUPLE2
156: R REDUCE
157: \x94 MEMOIZE (as 3)
158: 0 POP
159: g GET 0
162: \x8c SHORT_BINUNICODE 'builtins'
172: \x8c SHORT_BINUNICODE 'int'
177: \x93 STACK_GLOBAL
178: \x8c SHORT_BINUNICODE '__mul__'
187: \x86 TUPLE2
188: R REDUCE
189: \x94 MEMOIZE (as 4)
190: 0 POP
191: g GET 0
194: \x8c SHORT_BINUNICODE 'builtins'
204: \x8c SHORT_BINUNICODE 'int'
209: \x93 STACK_GLOBAL
210: \x8c SHORT_BINUNICODE '__eq__'
218: \x86 TUPLE2
219: R REDUCE
220: \x94 MEMOIZE (as 5)
221: 0 POP
222: g GET 3
225: g GET 5
228: \x8c SHORT_BINUNICODE 'builtins'
238: \x8c SHORT_BINUNICODE 'len'
243: \x93 STACK_GLOBAL
244: g GET 1
247: \x85 TUPLE1
248: R REDUCE
249: M BININT2 64
252: \x86 TUPLE2
253: R REDUCE
254: M BININT2 261
257: \x86 TUPLE2
258: R REDUCE
259: \x85 TUPLE1
260: R REDUCE
261: . STOP
highest protocol among opcodes = 4
Hay muchos opcodes, podemos usar este recurso para ver una descripción de los diferentes opcodes.
Primera etapa
Al principio, podría ser difícil de entender los opcodes de pickle
, pero una vez que estamos dentro, todo va bien.
Este fragmento cargará la función getattr
en memo 0:
0: \x8c SHORT_BINUNICODE 'builtins'
10: \x8c SHORT_BINUNICODE 'getattr'
19: \x93 STACK_GLOBAL
20: \x94 MEMOIZE (as 0)
Este carga input
y luego la llama con 'FLAG> '
como primer argumento y usa .encode()
para transformar de str
a bytes
. El resultado se almacena en memo 1:
22: \x8c SHORT_BINUNICODE 'builtins'
32: \x8c SHORT_BINUNICODE 'input'
39: \x93 STACK_GLOBAL
40: \x8c SHORT_BINUNICODE 'FLAG> '
48: \x85 TUPLE1
49: R REDUCE
50: \x8c SHORT_BINUNICODE 'encode'
58: \x86 TUPLE2
59: R REDUCE
60: ) EMPTY_TUPLE
61: R REDUCE
62: \x94 MEMOIZE (as 1)
63: 0 POP
Aquí, la VM toma memo 0 (getattr
) y coge la función dict.get
para obtener variable f
del diccionario globals
. Al final, almacena la función f.seek
en memo 2:
64: g GET 0
67: \x8c SHORT_BINUNICODE 'builtins'
77: \x8c SHORT_BINUNICODE 'dict'
83: \x93 STACK_GLOBAL
84: \x8c SHORT_BINUNICODE 'get'
89: \x86 TUPLE2
90: R REDUCE
91: \x8c SHORT_BINUNICODE 'builtins'
101: \x8c SHORT_BINUNICODE 'globals'
110: \x93 STACK_GLOBAL
111: ) EMPTY_TUPLE
112: R REDUCE
113: \x8c SHORT_BINUNICODE 'f'
116: \x86 TUPLE2
117: R REDUCE
118: \x8c SHORT_BINUNICODE 'seek'
124: \x86 TUPLE2
125: R REDUCE
126: \x94 MEMOIZE (as 2)
Esta variable f
se usa para leer el payload de pickle
(f = io.BytesIO(payload)
).
Ahora, las funciones int.__add__
, int.__mul__
y int.__eq__
se almacenan en memo 3, 4, y 5, respectivamente:
127: g GET 0
130: \x8c SHORT_BINUNICODE 'builtins'
140: \x8c SHORT_BINUNICODE 'int'
145: \x93 STACK_GLOBAL
146: \x8c SHORT_BINUNICODE '__add__'
155: \x86 TUPLE2
156: R REDUCE
157: \x94 MEMOIZE (as 3)
158: 0 POP
162: \x8c SHORT_BINUNICODE 'builtins'
172: \x8c SHORT_BINUNICODE 'int'
177: \x93 STACK_GLOBAL
178: \x8c SHORT_BINUNICODE '__mul__'
187: \x86 TUPLE2
188: R REDUCE
189: \x94 MEMOIZE (as 4)
190: 0 POP
191: g GET 0
194: \x8c SHORT_BINUNICODE 'builtins'
204: \x8c SHORT_BINUNICODE 'int'
209: \x93 STACK_GLOBAL
210: \x8c SHORT_BINUNICODE '__eq__'
218: \x86 TUPLE2
219: R REDUCE
220: \x94 MEMOIZE (as 5)
221: 0 POP
Y este es el final del desensamblado:
222: g GET 3
225: g GET 5
228: \x8c SHORT_BINUNICODE 'builtins'
238: \x8c SHORT_BINUNICODE 'len'
243: \x93 STACK_GLOBAL
244: g GET 1
247: \x85 TUPLE1
248: R REDUCE
249: M BININT2 64
252: \x86 TUPLE2
253: R REDUCE
254: M BININT2 261
257: \x86 TUPLE2
258: R REDUCE
259: \x85 TUPLE1
260: R REDUCE
261: . STOP
Lo que hace es tomar int.__add__
(memo 3), int.__eq__
(memo 5), luego carga len
y la llama con nuestros datos de entrada (memo 1). El resultado de len
se pasa como primer argumento a int.__eq__
, con segundo argumento 64
. El resultado (0
ó 1
) se suma a 261
con int.__add__
. El último opcode REDUCE
(línea 260) llamará a f.seek
(permanece en la pila de fragmentos anteriores).
Entonces, si la longitud de nuestra entrada tiene longitud 64
, el resultado de int.__eq__
será 1
, y luego el resultado de int.__add__
será 262
. Este valor se pasa a f.seek
. Como resultado, la VM de pickle
se saltará el opcode STOP
y continuará (sí, el payload de pickle
tiene más de 261
bytes).
Segunda etapa
Para analizar esto con pickletools
, quité el opcode STOP
y llamé a dis
otra vez. Luego, necesitamos modificar algunos offsets para mantener todo como estaba:
262: 0 POP
263: g GET 0
266: g GET 1
269: \x8c SHORT_BINUNICODE '__getitem__'
282: \x86 TUPLE2
283: R REDUCE
284: \x94 MEMOIZE (as 6)
285: 0 POP
286: M BININT2 0
289: \x94 MEMOIZE (as 7)
290: 0 POP
291: g GET 2
294: g GET 3
297: g GET 0
300: g GET 6
303: g GET 7
306: \x85 TUPLE1
307: R REDUCE
308: \x8c SHORT_BINUNICODE '__le__'
316: \x86 TUPLE2
317: R REDUCE
318: M BININT2 127
321: \x85 TUPLE1
322: R REDUCE
323: M BININT2 330
326: \x86 TUPLE2
327: R REDUCE
328: \x85 TUPLE1
329: R REDUCE
330: . STOP
Este fragmento toma getattr
, y nuestra entrada (vamos a llamarla our_input
) y almacena our_input.__getitem__
en memo 6. Luego, 0
se almacena en memo 7.
Entonces, el programa carga f.seek
, int.__add__
, getattr
, our_input.__getitem__
y our_input[0]
. Básicamente, la función __le__
se llama sobre our_input[0]
con argumento 127
(esto es lo mismo que our_input[0] < 0x7f
). Después de eso, el programa llama a int.__add__
con el resultado anterior y 330
. Del mismo modo que en la etapa anterior, si la condición es verdadera, el programa ejecutará f.seek(331)
y se saltará el opcode STOP
de la línea 330.
Tercera etapa
Si eliminamos este opcode STOP
, dis
se quejará de que memo 7 ya está definido:
>>> dis(payload.replace(b'.', b'') + b'.')
...
322: M BININT2 330
325: \x86 TUPLE2
326: R REDUCE
327: \x85 TUPLE1
328: R REDUCE
329: 0 POP
330: g GET 2
333: g GET 3
336: g GET 4
339: g GET 5
342: g GET 3
345: g GET 7
348: M BININT2 1
351: \x86 TUPLE2
352: R REDUCE
353: p PUT 7
Traceback (most recent call last):
...
ValueError: memo key 7 already defined
Esto sucede por el opcode PUT 7
. Para superar este problema, podemos usar un valor ficticio y luego modificar todos los opcodes PUT
y MEMOIZE
(el opcode MEMOIZE
es siempre incremental). Además, necesitamos arreglar nuevamente los offsets como si los opcodes STOP
estuvieran presentes.
Ya no hay más opcodes STOP
. Entonces, después de arreglar todos los opcodes PUT
y corregir el payload de pickle
, nos queda este código:
331: 0 POP
332: g GET 2
335: g GET 3
338: g GET 4
341: g GET 5
344: g GET 3
347: g GET 7
350: M BININT2 1
353: \x86 TUPLE2
354: R REDUCE
355: p PUT 7
358: M BININT2 64
361: \x86 TUPLE2
362: R REDUCE
363: M BININT2 85
366: \x86 TUPLE2
367: R REDUCE
368: M BININT2 290
371: \x86 TUPLE2
372: R REDUCE
373: \x85 TUPLE1
374: R REDUCE
375: 0 POP
376: g GET 0
379: g GET 0
382: ] EMPTY_LIST
383: \x94 MEMOIZE (as 8)
384: \x8c SHORT_BINUNICODE 'append'
392: \x86 TUPLE2
393: R REDUCE
394: \x94 MEMOIZE (as 9)
395: 0 POP
396: g GET 8
399: \x8c SHORT_BINUNICODE '__getitem__'
412: \x86 TUPLE2
413: R REDUCE
414: \x94 MEMOIZE (as 10)
415: 0 POP
416: g GET 0
419: \x8c SHORT_BINUNICODE 'builtins'
429: \x8c SHORT_BINUNICODE 'int'
434: \x93 STACK_GLOBAL
435: \x8c SHORT_BINUNICODE 'from_bytes'
447: \x86 TUPLE2
448: R REDUCE
449: \x94 MEMOIZE (as 11)
450: 0 POP
451: M BININT2 0
454: p PUT 7
457: 0 POP
458: g GET 9
461: g GET 11
465: g GET 6
468: \x8c SHORT_BINUNICODE 'builtins'
478: \x8c SHORT_BINUNICODE 'slice'
485: \x93 STACK_GLOBAL
486: g GET 4
489: g GET 7
492: M BININT2 8
495: \x86 TUPLE2
496: R REDUCE
497: g GET 4
500: g GET 3
503: g GET 7
506: M BININT2 1
509: \x86 TUPLE2
510: R REDUCE
511: M BININT2 8
514: \x86 TUPLE2
515: R REDUCE
516: \x86 TUPLE2
517: R REDUCE
518: \x85 TUPLE1
519: R REDUCE
520: \x8c SHORT_BINUNICODE 'little'
528: \x86 TUPLE2
529: R REDUCE
530: \x85 TUPLE1
531: R REDUCE
532: 0 POP
533: g GET 2
536: g GET 3
539: g GET 4
542: g GET 5
545: g GET 3
548: g GET 7
551: M BININT2 1
554: \x86 TUPLE2
555: R REDUCE
556: p PUT 7
559: M BININT2 8
562: \x86 TUPLE2
563: R REDUCE
564: M BININT2 119
567: \x86 TUPLE2
568: R REDUCE
569: M BININT2 457
572: \x86 TUPLE2
573: R REDUCE
574: \x85 TUPLE1
575: R REDUCE
576: 0 POP
577: g GET 0
580: ] EMPTY_LIST
581: \x94 MEMOIZE (as 12)
582: \x8c SHORT_BINUNICODE 'append'
590: \x86 TUPLE2
591: R REDUCE
592: \x94 MEMOIZE (as 13)
593: 0 POP
594: g GET 0
597: g GET 12
601: \x8c SHORT_BINUNICODE '__getitem__'
614: \x86 TUPLE2
615: R REDUCE
616: \x94 MEMOIZE (as 14)
617: 0 POP
618: g GET 0
621: \x8c SHORT_BINUNICODE 'builtins'
631: \x8c SHORT_BINUNICODE 'int'
636: \x93 STACK_GLOBAL
637: \x8c SHORT_BINUNICODE '__xor__'
646: \x86 TUPLE2
647: R REDUCE
648: \x94 MEMOIZE (as 15)
649: 0 POP
650: I INT 1244422970072434993
671: \x94 MEMOIZE (as 16)
672: 0 POP
673: M BININT2 0
676: p PUT 7
679: 0 POP
680: g GET 13
684: \x8c SHORT_BINUNICODE 'builtins'
694: \x8c SHORT_BINUNICODE 'pow'
699: \x93 STACK_GLOBAL
700: g GET 15
704: g GET 10
708: g GET 7
711: \x85 TUPLE1
712: R REDUCE
713: g GET 16
717: \x86 TUPLE2
718: R REDUCE
719: I INT 65537
726: I INT 18446744073709551557
748: \x87 TUPLE3
749: R REDUCE
750: \x85 TUPLE1
751: R REDUCE
752: 0 POP
753: g GET 14
757: g GET 7
760: \x85 TUPLE1
761: R REDUCE
762: p PUT 16
766: 0 POP
767: g GET 2
770: g GET 3
773: g GET 4
776: g GET 5
779: g GET 3
782: g GET 7
785: M BININT2 1
788: \x86 TUPLE2
789: R REDUCE
790: p PUT 7
793: M BININT2 8
796: \x86 TUPLE2
797: R REDUCE
798: M BININT2 131
801: \x86 TUPLE2
802: R REDUCE
803: M BININT2 679
806: \x86 TUPLE2
807: R REDUCE
808: \x85 TUPLE1
809: R REDUCE
810: 0 POP
811: g GET 0
814: g GET 12
818: \x8c SHORT_BINUNICODE '__eq__'
826: \x86 TUPLE2
827: R REDUCE
828: ( MARK
829: I INT 8215359690687096682
850: I INT 1862662588367509514
871: I INT 8350772864914849965
892: I INT 11616510986494699232
916: I INT 3711648467207374797
935: I INT 9722127090168848805
956: I INT 16780197523811627561
978: I INT 18138828537077112905
1000: l LIST (MARK at 831)
1001: \x85 TUPLE1
1002: R REDUCE
1003: . STOP
Analicemos este fragmento:
331: 0 POP
332: g GET 2
335: g GET 3
338: g GET 4
341: g GET 5
344: g GET 3
347: g GET 7
350: M BININT2 1
353: \x86 TUPLE2
354: R REDUCE
355: p PUT 7
358: M BININT2 64
361: \x86 TUPLE2
362: R REDUCE
363: M BININT2 85
366: \x86 TUPLE2
367: R REDUCE
368: M BININT2 290
371: \x86 TUPLE2
372: R REDUCE
373: \x85 TUPLE1
374: R REDUCE
Carga estos objetos en la pila:
f.seek
int.__add__
int.__mul__
int.__eq__
int.__add__
0
Primero, ejecuta int.__add__(0, 1)
y almacena el resultado en memo 7. Si este resultado es igual a 64
, Entonces el programa multiplicará el resultado (0
ó 1
) por 85
y luego suma 290
. Finalmente, se llama a f.seek
. Como resultado, el programa está iterando a través de todos los 64
índices y saltando a la línea 290 (recordemos que verifica si el carácter es < 0x7f
). Cuando se alcanza el último índice, el programa salta a la línea 375 (85 + 290
):
375: 0 POP
376: g GET 0
379: g GET 0
382: ] EMPTY_LIST
383: \x94 MEMOIZE (as 8)
384: \x8c SHORT_BINUNICODE 'append'
392: \x86 TUPLE2
393: R REDUCE
394: \x94 MEMOIZE (as 9)
395: 0 POP
396: g GET 8
399: \x8c SHORT_BINUNICODE '__getitem__'
412: \x86 TUPLE2
413: R REDUCE
414: \x94 MEMOIZE (as 10)
415: 0 POP
416: g GET 0
419: \x8c SHORT_BINUNICODE 'builtins'
429: \x8c SHORT_BINUNICODE 'int'
434: \x93 STACK_GLOBAL
435: \x8c SHORT_BINUNICODE 'from_bytes'
447: \x86 TUPLE2
448: R REDUCE
449: \x94 MEMOIZE (as 11)
450: 0 POP
Aquí, el programa crea una lista vacía (llamémosla blocks
), almacenada en memo 8, y luego guarda blocks.append
en memo 9. También guarda blocks.__getitem__
en memo 10. Finalmente, almacena int.from_bytes
en memo 11.
Echemos un vistazo a este código:
451: M BININT2 0
454: p PUT 7
457: 0 POP
458: g GET 9
461: g GET 11
465: g GET 6
468: \x8c SHORT_BINUNICODE 'builtins'
478: \x8c SHORT_BINUNICODE 'slice'
485: \x93 STACK_GLOBAL
486: g GET 4
489: g GET 7
492: M BININT2 8
495: \x86 TUPLE2
496: R REDUCE
Este es el comienzo de otro bucle. Almacena 0
en memo 7 (se utilizará como índice). Luego, carga estos objetos en la pila:
blocks.append
int.from_bytes
our_input.__getitem__
slice
int.__mul__
0
8
La primera función a llamar es int.__mul__(0, 8)
. Entonces, tenemos:
497: g GET 4
500: g GET 3
503: g GET 7
506: M BININT2 1
509: \x86 TUPLE2
510: R REDUCE
511: M BININT2 8
514: \x86 TUPLE2
515: R REDUCE
516: \x86 TUPLE2
517: R REDUCE
518: \x85 TUPLE1
519: R REDUCE
520: \x8c SHORT_BINUNICODE 'little'
528: \x86 TUPLE2
529: R REDUCE
530: \x85 TUPLE1
531: R REDUCE
532: 0 POP
Por lo que se cargan:
int.__mul__
int.__add__
0
1
Y el resultado es que int.__add__(0, 1)
(da 1
) se multiplica como int.__mul__(1, 8)
. Al final, tenemos 0
y 8
en la pila. La siguiente función a llamar es slice
, y luego our_input.__getitem__
. Este procedimiento es lo mismo que ejecutar our_input[0:8]
. Después, int.from_bytes
se ejecuta indicando 'little'
como segundo argumento. Finalmente, el resultado se almacena en blocks
con blocks.append
.
Y este es el código que maneja el bucle:
533: g GET 2
536: g GET 3
539: g GET 4
542: g GET 5
545: g GET 3
548: g GET 7
551: M BININT2 1
554: \x86 TUPLE2
555: R REDUCE
556: p PUT 7
559: M BININT2 8
562: \x86 TUPLE2
563: R REDUCE
564: M BININT2 119
567: \x86 TUPLE2
568: R REDUCE
569: M BININT2 457
572: \x86 TUPLE2 bytes en total)
573: R REDUCE
574: \x85 TUPLE1
575: R REDUCE
576: 0 POP
Básicamente, se aumentará el índice en 8
y se almacenará en memo 7. Luego saltará a la línea 457 hasta que los bloques de 8 bytes se transformen en int
y se guarden en blocks
. Una vez hecho esto, la VM salta a la línea 576 (119 + 457
):
576: 0 POP
577: g GET 0
580: ] EMPTY_LIST
581: \x94 MEMOIZE (as 12)
582: \x8c SHORT_BINUNICODE 'append'
590: \x86 TUPLE2
591: R REDUCE
592: \x94 MEMOIZE (as 13)
593: 0 POP
594: g GET 0
597: g GET 12
601: \x8c SHORT_BINUNICODE '__getitem__'
614: \x86 TUPLE2
615: R REDUCE
616: \x94 MEMOIZE (as 14)
617: 0 POP
618: g GET 0
621: \x8c SHORT_BINUNICODE 'builtins'
631: \x8c SHORT_BINUNICODE 'int'
636: \x93 STACK_GLOBAL
637: \x8c SHORT_BINUNICODE '__xor__'
646: \x86 TUPLE2
647: R REDUCE
648: \x94 MEMOIZE (as 15)
649: 0 POP
650: I INT 1244422970072434993
671: \x94 MEMOIZE (as 16)
672: 0 POP
En estas líneas, se crea una nueva lista (llamémosla ciphertexts
) y se almacena en memo 12. Luego, ciphertexts.append
se guarda en memo 13 y ciphertexts.__getitem__
en memo 14. Después de eso, int.__xor__
estará en memo 15 y el número 1244422970072434993
en memo 16.
Aquí comenzamos otro bucle:
673: M BININT2 0
676: p PUT 7
679: 0 POP
680: g GET 13
684: \x8c SHORT_BINUNICODE 'builtins'
694: \x8c SHORT_BINUNICODE 'pow'
699: \x93 STACK_GLOBAL
700: g GET 15
704: g GET 10
708: g GET 7
711: \x85 TUPLE1
712: R REDUCE
713: g GET 16
717: \x86 TUPLE2
718: R REDUCE
719: I INT 65537
726: I INT 18446744073709551557
748: \x87 TUPLE3
749: R REDUCE
750: \x85 TUPLE1
751: R REDUCE
752: 0 POP
753: g GET 14
757: g GET 7
760: \x85 TUPLE1
761: R REDUCE
762: p PUT 16
766: 0 POP
Se almacenará 0
en memo 7 (la variable índice) y calculará pow(blocks[0], 65537, 18446744073709551557)
. El resultado de estos cálculos se pasa a int.__xor__
con el valor en memo 16 (1244422970072434993
). El resultado de la función pow
se guarda en memo 16 (sobrescribiendo el valor anterior).
Obsérvese que tenemos un cifrado RSA aquí…
Luego, tenemos otro conjunto de condiciones para continuar con el bucle:
767: g GET 2
770: g GET 3
773: g GET 4
776: g GET 5
779: g GET 3
782: g GET 7
785: M BININT2 1
788: \x86 TUPLE2
789: R REDUCE
790: p PUT 7
793: M BININT2 8
796: \x86 TUPLE2
797: R REDUCE
798: M BININT2 131
801: \x86 TUPLE2
802: R REDUCE
803: M BININT2 679
806: \x86 TUPLE2
807: R REDUCE
808: \x85 TUPLE1
809: R REDUCE
Básicamente, saltará a la línea 679 hasta que todos los bloques estén cifrados. Después de eso, el programa saltará a la línea 810 (131 + 679
):
810: 0 POP
811: g GET 0
814: g GET 12
818: \x8c SHORT_BINUNICODE '__eq__'
826: \x86 TUPLE2
827: R REDUCE
828: ( MARK
829: I INT 8215359690687096682
850: I INT 1862662588367509514
871: I INT 8350772864914849965
892: I INT 11616510986494699232
916: I INT 3711648467207374797
935: I INT 9722127090168848805
956: I INT 16780197523811627561
978: I INT 18138828537077112905
1000: l LIST (MARK at 831)
1001: \x85 TUPLE1
1002: R REDUCE
1003: . STOP
Y hemos llegado al final del payload de pickle
. Ahora, el programa simplemente verifica que todos los bloques cifrados (que se calculan de our_input
) coinciden con los que aparecen anteriormente.
Solución
¡Advertencia! ¡Conceptos criptográficos!
Lo que debemos hacer ahora es revertir el cifrado de las salidas esperadas. Sabemos que está usando RSA, donde $e = 65537$ y $n = 18446744073709551557$. En realidad, $n$ es un número primo, por lo que no es una configuración de RSA segura.
Para descifrar RSA necesitaremos $d = e^{-1} \mod{\phi(n)}$. Como $n$ es primo, entonces $\phi(n) = n - 1$, y tenemos todo para calcular $d$. Después de eso, podemos descifrar mensajes como $m = c^d \mod{n}$.
Además, hay que tener en cuenta que también hay una operación XOR aplicada en cada bloque, y el valor cambia en cada bloque (como en modo CBC).
Al final, podemos construir este algoritmo inverso:
#!/usr/bin/env python3
x = 1244422970072434993
e = 65537
n = 18446744073709551557
d = pow(e, -1, n - 1)
ct = [
8215359690687096682,
1862662588367509514,
8350772864914849965,
11616510986494699232,
3711648467207374797,
9722127090168848805,
16780197523811627561,
18138828537077112905
]
flag = b''
for c in ct:
flag += (pow(c, d, n) ^ x).to_bytes(8, 'little')
x = c
print(flag.decode())
Si ejecutamos este script, capturaremos la flag:
$ python3 solve.py
SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
$ python3 problem.py
FLAG> SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
Congratulations!!