⚠ Spoiler: Đây là write-up cho các challenge của Flare-on 9 tổ chức vào khoảng tháng 11/2022 tại Website.

[11] The challenge that shall not be named.

Protection, Obfuscation, Restrictions... Oh my!!

The good part about this one is that if you fail to solve it I don't need to ship you a prize.

7-zip password: flare

Công cụ sử dụng:

  • pyinstxtractor-ng
  • Detect It Easy v3.06

Đây là trở ngại cuối cùng trước khi nhận giải. Không biết năm nay thì chúng ta sẽ có phần thưởng là gì. Mình thực rất rất mong chờ vì những phần quà của FLARE team (bây giờ thuộc về Mandiant) luôn đặc biệt và độc đáo. Bên cạnh tri thức mà chúng mình nhận được khi vượt qua những thử thách thì phần quà là quả ngọt cuối cùng, luôn là một động lực khích lệ tinh thần cực kì to lớn.

Trước khi đến với challenge này thì trên những người khác trên trang Twitter có trao đổi những tips nhỏ về những challenge trong Flare-On. Trong đó với bài 11 này tồn tại một tip rất hữu ích với mình là thời gian làm xong có thể chỉ dưới 30 phút. Đối với Flare-On các năm trước thì các bài về sau mức độ khó, độ phức tạp đều cao hơn dẫn đến người chơi cũng tốn công tốn sức hơn. Nhưng không cần đến 30 phút, một thời gian ngắn như vậy mà có thể giải quyết được thì hẳn là đó phải là một cách giải rất sáng tạo.

Giải nén file đề bài chúng ta được một file duy nhất 11.exe nặng khoảng 9MB.

Scan thử với Detect It Easy v3.06 cho chúng ta biết đây là binary được viết bằng Python và pack lại thành file exe thông qua PyInstaller:

Với PyInstaller thì đây là một packer đã khá quen thuộc và cũng có khá nhiều công cụ để giải nén (extract) dữ liệu từ file binary. Ở đây mình dùng pyinstxtractor-ng.exe

Chạy và extract file:

[+] Processing 11.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.7
[+] Length of package: 8807847 bytes
[+] Found 80 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: 11.pyc
[+] Found 250 files in PYZ archive
[+] Successfully extracted pyinstaller archive: 11.exe

You can now use a python decompiler on the pyc files within the extracted directory

Kết quả chúng ta có thư mục 11.exe_extracted với khá nhiều file như sau:

Chú ý đến file 11.pyc khả năng cao là entry point của chương trình. Để decompile từ file pyc thành file source code python, bước tiếp theo mình sử dụng uncompyle6:

# uncompyle6 version 3.8.0
# Python bytecode 3.7.0 (3394)
# Decompiled from: Python 3.8.10 (default, Jun 22 2022, 20:18:18) 
# [GCC 9.4.0]
# Embedded file name: dist\obf\11.py
from pytransform import pyarmor
pyarmor(__name__, __file__, b'PYARMOR\x00\x00\x03\x07\x00B\r\r\n\t0\xe0\x02\x01\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00a\x02\x00\x00\x0b\x00\x00x\xa7\xf5\x80\x15\x8c\x1f\x90\xbb\x16Xu\x86\x9d\xbb\xbd\x8d\x00\x00\x00\x00\x00\x00\x00\x0054$\xf1\xeb,\nY\xa9\x9b\xa5\xb3\xba\xdc\xd97\xba\x13\x0b\x89 \xd2\x14\xa7\xccH0\x9b)\xd4\x0f\xfb\xe4`\xbd\xcf\xa28\xfc\xf1\x08\x87w\x1a\xfb%+\xc1\xbe\x8b\xc0]8h\x1f\x88\xa6CB>*\xdd\xf6\xec\xf5\xe30\xf9\x856\xfa\xd9P\xc8C\xc1\xbdm\xca&\x81\xa9\xfb\x07HE\x1b\x00\x9e\x00a\x0c\xf2\xd0\x87\x0c<\xf8\xddZf\xf1,\x84\xce\r\x14*s\x11\x82\x88\x8d\xa7\x00k\xd9s\xae\xd3\xfc\x16v\x0f\xb9\xd1\xd3\xd02\xecQ\x9a\xd7aL\xdf\xc1~u\xca\x8a\xd4xk\xde\x030;\xb2Q\xc8$\xddQ\xd3Jj\xd1U\xccV\xd1\x03\xa9\xbf\x9f\xed\xe68n\xac&\xd67\x0c\xfd\xc6^\x0e\xb40\x07\x97|\xab\xadBc<T\x0b d$\x94\xf9\x90Oq\x027\xe4\xf2\xec\xc9\xbc\xfaL7dN\x83\x96X\xab\xf7\x18\xad\xfc\xf7\x992\x87\x1d\xe8p\x97C\xd4D.\x1b;F_ \x91t\tM\x155\x0c\xb9\x9f\xd0W C\x19oz4.\x998\xe7\xa9\x98\xd4\xd2\x9f\x95H\x91\xf2`\x1c\xfa\xa4,\xa9d?day\xc4\xf3\xcb\xc8r\xf7\x97\xd1u\xfe\xec\x91\xc1\xe6V\xa3j\x0f\xb9\xd5\xa1a\xd5\x17\x8b!\xc4{A\xb2t\x85\xfe\x88\xffaO\x05\xc5\xacg\xed;]\xb9\xdd\x7fS\xef\xe4F\xf9"\x0c\xd9\x1a\xb6\x88-Y \xdd\xea\xc9\xf1>:\xbf][\xdf[\x07\xb9\xe2@\xeeq\xf9Ho\xc3\xc4sD\xcd\xcc\x8a\x11tq\xf6;\xe9\x84\x7fb\xe9\xf4t\x80\xe4l)_\xeaQ\x10\x8f^-\xc5\x11\xe7\x84x\xe7-\xb2\x15[5\xb0\xdck\x1awh\r;\x9by\x14\x1a\xe0:\xbd\x904\xa2\xfap[\xe0\x9fn3\x7fk;3n\xf8\xe3%\xc6t\xbf|\x12\x9a\x1b\xe2\xf1C\x10\xbe\xee\xe7.\x98>k\xb9r\xf9\x9cN8\xae\xc0\x8bA\x0f\xbb\x8d\xf4\x04\xb0\x01,\x05\xaa\xc5\r\xce\x91\'\x98\xc6\xd3Y\x1b\xd1U\xd3\xd7d|{I\x18JG\xa63\xd6\'r\xcf!7\x17qd\xb7|\x1f\x7f\x17\xb4\xa8\xb9\xa8\xdaz\x02g\xc7+]F\x10\x18l\x0c\x91g\xd0e\x1f\xe4\xa67\xb2\xba\x9f\xef\xba\xc7[3_\x12C\xe9\xf4s\x87q\xa3\xec\xa0\xcc\x06\xf4\x9f\xe1\xb3\xe6R\x93\xf2\xd57i\xf8\x96\xb3x\xa7uEw\x12D\x8c\xc6XkdfY\xe0J2N\xbf\x85o\x8e\x81|C\xa91#y\xd9u\xf1\xd1BC\xcc}\xe8;?\x12S\x16', 2)
# okay decompiling 11.pyc

Đến đây chúng ta đụng phải chướng ngại vật tiếp theo, đó chính là PyArmor.

PyArmor is a command line tool used to obfuscate python scripts, bind obfuscated scripts to fixed machine or expire obfuscated scripts.

Về cơ bản, pyarmor sẽ mã hóa source code của chúng ta và đưa vào thư viện native pytransform.pyd (pyd bản chất chính là DLL) để giải mã, kiểm tra xem code có toàn vẹn không, sau đó đưa code cho python interpreter để chạy, rồi ngay lập tức mã hóa lại. Điều này khiến cho việc lấy lại source code trở thành khá khó khăn.

Có đúng như vậy không? Quay trở lại với tips ở đầu bài trên Twitter. Đó là status sau: https://twitter.com/kaanezder/status/1577232102042398720

Và mình cũng đã tham khảo được CTF mà status kia có nhắc đến: https://devilinside.me/blogs/unpacking-pyarmor

Trước hết chúng ta cần dựng lại môi trường để chạy file 11.pyc vì đối với các phiên bản python khác nhau, bytecode cũng sẽ khác nên tốt nhất là dựng lại môi trường với phiên bản python giống hệt. Khi thực hiện decompile ở trên, ta có được thông tin là code bằng Python bytecode 3.7.0 (3394) nhưng khi kiểm tra file python37.dll trong thư mục 11.exe_extracted, chúng ta sẽ thấy version khác đi một chút:

Như vậy phiên bản chúng ta cần dùng phải là 3.7.9. Tải về file python-3.7.9-embed-amd64.zip và giải nén ra một thư mục.

Chúng ta copy file 11.pyc vào thư mục này và chạy thử sẽ thấy báo lỗi thiếu module, chúng ta cần copy thêm file pytransform.pyd vào thư mục này:

Chạy lại lần nữa, lần này thấy thiếu module _crypt. Đến đây chúng ta đã có điều kiện cần để follow theo write-up trong giải CTF kia.

Ý tưởng ở đây là import hijacking: thay vì module _crypt gốc, chúng ta sẽ tạo một file _crypt.py ở cùng thư mục để code chương trình sẽ import module fake này vào. Trong module này chúng ta sẽ thực hiện set debug, để có thể inspect vào bên trong các module trong chương trình (do code chương trình chắc chắn đã được load rồi mới chạy đến bước import _crypt), dựa vào stack trace, frame để lần ra đến đoạn code gọi import.

Tạo một file _crypt.py với nội dung đơn giản như dưới đây rồi để vào chung thư mục và chạy lại lần nữa.

import pdb
print(123)
pdb.set_trace()

Lần này chúng ta đã hit breakpoint:

Lần lượt theo các bước trong writeup: liệt kê các frame đang có trong chương trình.

(Pdb) import inspect
(Pdb) for frameinfo in inspect.stack(): print(frameinfo.frame)
<frame at 0x00000215F5092528, file '<stdin>', line 1, code <module>>
<frame at 0x00000215F4D6A768, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\pdb.py', line 374, code default>
<frame at 0x00000215F50D6428, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\cmd.py', line 216, code onecmd>
<frame at 0x00000215F4D69048, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\pdb.py', line 416, code onecmd>
<frame at 0x00000215F4D680F8, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\cmd.py', line 138, code cmdloop>
<frame at 0x00000215F50CF958, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\pdb.py', line 319, code _cmdloop>
<frame at 0x00000215F50CE3C8, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\pdb.py', line 350, code interaction>
<frame at 0x00000215F50CE048, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\pdb.py', line 291, code user_return>
<frame at 0x00000215F4FB79A8, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\bdb.py', line 151, code dispatch_return>
<frame at 0x00000215F5034CF8, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\bdb.py', line 92, code trace_dispatch>
<frame at 0x00000215F4BD1388, file 'D:\\ctf\\flareon2022\\11_the_challenge_that_shall_not_be_named\\python-3.7.9-embed-amd64\\_crypt.py', line 3, code <module>>
<frame at 0x00000215F2C1C908, file '<frozen importlib._bootstrap>', line 219, code _call_with_frames_removed>
<frame at 0x00000215F4BD0048, file '<frozen importlib._bootstrap_external>', line 728, code exec_module>
<frame at 0x00000215F2E60D68, file '<frozen importlib._bootstrap>', line 677, code _load_unlocked>
<frame at 0x00000215F2C856B8, file '<frozen importlib._bootstrap>', line 967, code _find_and_load_unlocked>
<frame at 0x00000215F331D238, file '<frozen importlib._bootstrap>', line 983, code _find_and_load>
<frame at 0x00000215F2E18CF8, file 'D:\\obj\\windows-release\\37amd64_Release\\msi_python\\zip_amd64\\crypt.py', line 3, code <module>>
<frame at 0x00000215F2E605E8, file '<frozen importlib._bootstrap>', line 638, code _load_backward_compatible>
<frame at 0x00000215F2E60B88, file '<frozen importlib._bootstrap>', line 668, code _load_unlocked>
<frame at 0x00000215F2C83128, file '<frozen importlib._bootstrap>', line 967, code _find_and_load_unlocked>
<frame at 0x00000215F2E1B808, file '<frozen importlib._bootstrap>', line 983, code _find_and_load>
<frame at 0x00000215F331D048, file '<frozen 11>', line 3, code <module>>
<frame at 0x00000215F33178B8, file 'dist\\obf\\11.py', line 2, code <module>>
(Pdb)

Frame chúng ta cần là frame cuối cùng. Mỗi frame có một thuộc tính là f_code là python code object chứa bytecode:

(Pdb) last_frame = inspect.stack()[-1].frame
(Pdb) last_frame
<frame at 0x000001BDB8D978B8, file 'dist\\obf\\11.py', line 2, code <module>>
(Pdb) last_frame.f_code
<code object <module> at 0x000001BDB8CE2AE0, file "dist\obf\11.py", line 1>

Chúng ta có thể sử dụng module dis để disassembly các bytecode này:

(Pdb) import dis
(Pdb) dis.dis(last_frame.f_code)
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('pyarmor',))
              4 IMPORT_NAME              0 (pytransform)
              6 IMPORT_FROM              1 (pyarmor)
              8 STORE_NAME               1 (pyarmor)
             10 POP_TOP

  2          12 LOAD_NAME                1 (pyarmor)
             14 LOAD_NAME                2 (__name__)
             16 LOAD_NAME                3 (__file__)
             18 LOAD_CONST               2 (b'PYARMOR\x00\x00\x03\x07\x00B\r\r\n\t0\xe0\x02\x01\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00a\x02\x00\x00\x0b\x00\x00x\xa7\xf5\x80\x15\x8c\x1f\x90\xbb\x16Xu\x86\x9d\xbb\xbd\x8d\x00\x00\x00\x00\x00\x00\x00\x00\xa9\x02\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00@\x00\x00bs\xcc\x00\x00\x00t\x0e\x83\x00\t\x00S\x00\t\x00\t\x00\x00\x00\x00\x00\xa1u0S\xbb7?\x0f[%\xb5\x7f\xc7\xbc\xc8\x0b\x17\xdf\x12Uz\xf6D\xe9\xa1s0QH3\xb4\nR >} \xbb\x8b\x0e\x03\xda\x12R\xec\xf0c\xea:~<S\x90=\xb8\n\x83,\xb9}4\xb2\x1e\n\xe3\xd8\xbeU\x12\xf8\xd4\xf1\xb5s\x93[\\2\xb4\x07>%\xb5y \xb3\xe7\x0b\x17\xd0\xfdV\xf8\xf0\xfa\xebm?~R\\1\x17\x05\xb8(\x00t&!r\n\x8c\xd3#T\xa3\xf4\xfa\xeb\xa1r0S\xd3;c\x03W%\xf8~\xf8\xdd\r\n\x03\xcc\xa7^\x9b\n\xfe\xeb:y\x01Rb3\x81\x0f["\xb5\x7f\xaf\xb7\x94\x06\x1b\xdf_TE\xf6\xfb\xeb\xa1ruRb\xcc\xe1Y7\xd1\xa4jZ\x8f\xcc\'\xb2\xfcV\xcb)\x01\xe9\x02\x00\x00\x00\xa9\x0fZ\x05crypt\xda\x06base64Z\x08requests\xda\x06configZ\x04ARC4\xda\x06cipher\xda\tb64encodeZ\x07encrypt\xda\x04flagZ\x04post\xda\nexceptionsZ\x10RequestException\xda\x01e\xda\tExceptionz\x0e__armor_wrap__\xa9\x00r\x0c\x00\x00\x00r\x0c\x00\x00\x00\xfa\x0b<frozen 11>\xda\x08<module>\x01\x00\x00\x00s\x1a\x00\x00\x00\x08\x01\x08\x01\x08\x03\x02\x01\x02\x01\x08\x03\x0e\x01\x14\x02\x02\x01\x1a\x01\x14\x01\x10\x01\x10\x01)\n\xe9\x00\x00\x00\x00Nz\x1chttp://www.evil.flare-on.coms.\x00\x00\x00Pyth0n_Prot3ction_tuRn3d_Up_t0_11@flare-on.coms\x19\x00\x00\x00PyArmor_Pr0tecteth_My_K3y)\x03\xda\x03urlr\x08\x00\x00\x00\xda\x03keyr\x11\x00\x00\x00r\x08\x00\x00\x00r\x10\x00\x00\x00)\x01\xda\x04data')
             20 LOAD_CONST               3 (2)
             22 CALL_FUNCTION            4
             24 POP_TOP
             26 LOAD_CONST               4 (None)
             28 RETURN_VALUE
(Pdb)

Và kia rồi, lẫn trong đống string chính là flag chúng ta cần tìm (đi kèm cả secret dùng để mã hóa):

Pyth0n_Prot3ction_tuRn3d_Up_t0_11@flare-on.com

The End?