⏪
CTFs
TwitterGithub
  • 👋Introduction
  • 📚Write Up
    • 2024
      • 📖1337UP LIVE CTF
        • Reverse Engineering
        • Mobile
        • Forensic
        • Misc
      • 📖HKCERT CTF Quals
        • Reverse Engineering
        • Binary Exploitation
      • 📖Flare-On 11
        • Challenge #1 - frog
      • 📖Intechfest
        • Reverse Engineering
        • Forensic
        • Cryptography
        • Mobile
      • 📖Cyber Breaker Competition (1v1)
        • Reverse Engineering
        • Web Exploitation
        • Cryptography
        • Binary Exploitation
      • 📖Cyber Breaker Competition Quals
        • Reverse Engineering
        • Web Exploitation
        • Cryptography
      • 📖BlackHat MEA Quals
        • Reverse Engineering
        • Forensic
      • 📖TFC CTF
        • Reverse Engineering
        • Forensic
        • Misc
      • 📖DeadSec CTF
        • Reverse Engineering
        • Web Exploitation
      • 📖Aptos - Code Collision CTF
        • Reverse Engineering
        • Misc
      • 📖DownUnder CTF
        • Reverse Engineering
      • 📖JustCTF
        • Reverse Engineering
        • Forensic
        • Misc
      • 📖Akasec CTF
        • Reverse Engineering
        • Forensic
      • 📖Codegate CTF Preliminary
        • Reverse Engineering
      • 📖NahamCon CTF
        • Cryptography
        • Reverse Engineering
        • Malware
        • Misc
        • Mobile
        • Scripting
        • Web Exploitation
        • Forensic
      • 📖SAS CTF Quals
        • Reverse Engineering
      • 📖SwampCTF
        • Reverse Engineering
        • Misc
        • Cryptography
      • 📖UNbreakable International
        • Reverse Engineering
        • Network
        • Cryptography
      • 📖ACSC
        • Reverse Engineering
        • Hardware
        • Web Exploitation
      • 📖0xL4ugh
        • Mobile
    • 2023
      • 📖BlackHat MEA Final
        • Reverse Engineering
        • Web Exploitation
      • 📖Flare-On 10
        • Challenge #1 - X
        • Challenge #2 - ItsOnFire
        • Challenge #3 - mypassion
        • Challenge #4 - aimbot
        • Challenge #5 - where_am_i
        • Challenge #6 - FlareSay
        • Challenge #7 - flake
        • Challenge #8 - AmongRust
        • Challenge #9 - mbransom
        • Challenge #10 - kupo
        • Challenge #11 - over_the_rainbow
        • Challenge #12 - HVM
        • Challenge #13 - y0da
      • 📖LakeCTF Quals
        • Reverse Engineering
        • Cryptography
      • 📖TSG CTF
        • Reverse Engineering
        • Cryptography
      • 📖ISITDTU Quals
        • Web Exploitation
        • Misc
        • Reverse Engineering
      • 📖BlackHat MEA Quals
        • Reverse Engineering
      • 📖ASCIS Final
        • Reverse Engineering
        • Web Exploitation
        • Cryptography
      • 📖ASCIS Quals
        • Reverse Engineering
        • Forensic
        • Cryptography
      • 📖IFest
        • Reverse Engineering
        • Cryptography
        • Misc
      • 📖Cyber Jawara International
        • Reverse Engineering
        • Forensic
        • Cryptography
        • Web Exploitation
      • 📖Intechfest
        • Reverse Engineering
        • Forensic
        • Cryptography
        • Mobile
      • 📖CSAW Quals
        • Reverse Engineering
      • 📖SECCON Quals
        • Reverse Engineering
      • 📖CTFZone Quals
        • Reverse Engineering
      • 📖Securinets Quals
        • Reverse Engineering
      • 📖Compfest Final (Attack Defense)
        • Web Exploitation
        • Cryptography
      • 📖Compfest Quals
        • Reverse Engineering
        • Cryptography
        • Forensic
        • Misc
      • 📖Tenable
        • Reverse Engineering
        • Cryptography
        • Steganography
      • 📖ASCWG Quals
        • Reverse Engineering
        • Cryptography
      • 📖Gemastik Quals
        • Reverse Engineering
      • 📖BSides Indore
        • Reverse Engineering
        • Cryptography
      • 📖NahamCon CTF
        • Cryptography
      • 📖HSCTF
        • Reverse Engineering
        • Cryptography
        • Web Exploitation
        • Misc
      • 📖ACSC
        • Reverse Engineering
      • 📖HackTM Quals
        • Reverse Engineering
    • 2022
      • 📖Intechfest
        • Reverse Engineering
        • Mobile
        • Cryptography
      • 📖NCW Final
        • Reverse Engineering
      • 📖NCW Quals
        • Reverse Engineering
        • Misc
        • Cryptography
      • 📖Compfest Final
        • Reverse Engineering
        • Forensic
      • 📖Compfest Quals
        • Reverse Engineering
        • Cryptography
      • 📖IFest
        • Reverse Engineering
        • Cryptography
        • Forensic
    • 2021
      • 📖Cyber Jawara Final
        • Reverse Engineering
      • 📖Cyber Jawara Quals
        • Reverse Engineering
        • Cryptography
      • 📖DarkCon CTF
        • Reverse Engineering
      • 📖Wreck IT Quals
        • Mobile
      • 📖MDT4.0 Final
        • Reverse Engineering
        • Cryptography
        • Forensic
      • 📖MDT4.0 Quals
        • Reverse Engineering
        • Cryptography
      • 📖IFest
        • Reverse Engineering
        • Cryptography
      • 📖Compfest Final
        • Reverse Engineering
      • 📖Compfest Quals
        • Reverse Engineering
        • Cryptography
    • 2020
      • 📖Deep CTF
        • Reverse Engineering
  • 🚩Lifetime CTF
    • 📖Hack The Box
      • Reverse Engineering
        • TBU
Powered by GitBook
On this page
  • Void (100 pts)
  • Description
  • Solution
  • Yet another crackme (100 pts)
  • Description
  • Solution
  • Baby Cracker (100 pts)
  • Description
  • Solution
  • Cyp.ress (200 pts)
  • Description
  • Solution
  • Solution
  • Flag Checker (200 pts)
  • Description
  • Solution
  • ISA 101 (200 pts)
  • Description
  • Solution
  • Morph (300 pts)
  • Description
  • Solution
  • Black Magic (400 pts)
  • Description
  • Solution
  • Bashed! (500 pts)
  • Description
  • Solution
  1. Write Up
  2. 2024
  3. HKCERT CTF Quals

Reverse Engineering

PreviousHKCERT CTF QualsNextBinary Exploitation

Last updated 5 months ago

Challenge
Link

Void (100 pts)

Yet another crackme (100 pts)

Baby Cracker (100 pts)

Cyp.ress (200 pts)

Flag Checker (200 pts)

ISA 101 (200 pts)

Morph (300 pts)

Black Magic (400 pts) 🥇

Bashed! (500 pts)

Void (100 pts)

Description

I made a simple webpage that checks whether the flag is correct... Wait, where are the flag-checking functions?

Solution

Given access to URL , take a look on the page source.

Looks like there are a bunch of whitespace and we see some readable code in the end of the HTML

We can see that there is eval(f) in the end, so my assumption is it will exec the javascript code. To get the executed code we can try to change eval to console.log then take a look on console browser.

Flag: hkcert24{j4v4scr1p7_1s_n0w_alm0s7_y3t_4n0th3r_wh173sp4c3_pr09r4mm1n9_l4ngu4g3}

Yet another crackme (100 pts)

Description

Yet another crackme. To begin, download the attachment and install it on your android device.

Solution

Given APK file, decopmile it using JADX-GUI.

Through the out directory we found that there is CrackMe.dll, open it using dnspy.

In MainPage class there is function onCounterClicked that will call checkFlag function. If the return True it will shows correct flag, so lets take a look on checkFlag function.

		private bool checkFlag(string f)
		{
			int[] array = new int[]
			{
				9, 10, 11, 12, 13, 32, 33, 34, 35, 36,
				37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
				47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
				57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
				67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
				77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
				87, 88, 89, 90, 91, 92, 93, 94, 95, 96,
				97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
				107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
				117, 118, 119, 120, 121, 122, 123, 124, 125, 126
			};
			int[] array2 = new int[]
			{
				58, 38, 66, 88, 78, 39, 80, 125, 64, 106,
				48, 49, 98, 32, 42, 59, 126, 93, 33, 56,
				112, 120, 60, 117, 111, 45, 87, 35, 10, 68,
				61, 77, 11, 55, 121, 74, 107, 104, 65, 63,
				46, 110, 34, 41, 102, 97, 81, 12, 47, 51,
				103, 89, 115, 75, 54, 92, 90, 76, 113, 122,
				114, 52, 72, 70, 50, 94, 91, 73, 84, 95,
				36, 82, 124, 53, 108, 101, 9, 13, 44, 96,
				67, 85, 116, 123, 100, 37, 43, 119, 71, 105,
				118, 69, 99, 79, 86, 109, 62, 83, 40, 57
			};
			ulong[] array3 = new ulong[] { 16684662107559623091UL, 13659980421084405632UL, 11938144112493055466UL, 17764897102866017993UL, 11375978084890832581UL, 14699674141193569951UL };
			ulong num = 14627333968358193854UL;
			int num2 = 8;
			Dictionary<int, int> dictionary = new Dictionary<int, int>();
			for (int i = 0; i < array.Length; i++)
			{
				dictionary[array[i]] = array2[i];
			}
			StringBuilder stringBuilder = new StringBuilder();
			foreach (char c in f)
			{
				stringBuilder.Append((char)dictionary[(int)c]);
			}
			int num3 = num2 - f.Length % num2;
			string text = stringBuilder.ToString() + new string('\u0001', num3);
			List<ulong> list = new List<ulong>();
			for (int k = 0; k < text.Length - 1; k += num2)
			{
				ulong num4 = BitConverter.ToUInt64(Encoding.ASCII.GetBytes(text.Substring(k, num2)), 0);
				list.Add(num4);
			}
			List<ulong> list2 = new List<ulong>();
			foreach (ulong num5 in list)
			{
				ulong num6 = num ^ num5;
				list2.Add(num6);
			}
			for (int l = 0; l < array3.Length; l++)
			{
				if (array3[l] != list2[l])
				{
					return false;
				}
			}
			return true;
		}

Above code do the following steps

  • Create dictionary using static values

  • Add padding to the input

  • Map the input (char) and append it to a string builder.

  • Convert the input string to integer 8 bytes

  • Xor each 8 bytes and compare it

All the process above can be reversed, so the solution is by reversing the algorithm. Below is the script we use to solve the challenge

from Crypto.Util.number import *

array = [9, 10, 11, 12, 13, 32, 33, 34, 35, 36,
				37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
				47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
				57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
				67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
				77, 78, 79, 80, 81, 82, 83, 84, 85, 86,
				87, 88, 89, 90, 91, 92, 93, 94, 95, 96,
				97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
				107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
				117, 118, 119, 120, 121, 122, 123, 124, 125, 126
]

array2 = [58, 38, 66, 88, 78, 39, 80, 125, 64, 106,
				48, 49, 98, 32, 42, 59, 126, 93, 33, 56,
				112, 120, 60, 117, 111, 45, 87, 35, 10, 68,
				61, 77, 11, 55, 121, 74, 107, 104, 65, 63,
				46, 110, 34, 41, 102, 97, 81, 12, 47, 51,
				103, 89, 115, 75, 54, 92, 90, 76, 113, 122,
				114, 52, 72, 70, 50, 94, 91, 73, 84, 95,
				36, 82, 124, 53, 108, 101, 9, 13, 44, 96,
				67, 85, 116, 123, 100, 37, 43, 119, 71, 105,
				118, 69, 99, 79, 86, 109, 62, 83, 40, 57
]

array3 = [16684662107559623091, 13659980421084405632, 11938144112493055466, 17764897102866017993, 11375978084890832581, 14699674141193569951 ]
num = 14627333968358193854

dictionary = {}

for i in range(len(array)):
	dictionary[array2[i]] = array[i]

dictionary[1] = 0


tmp = []
for i in array3:
	tmp.append(long_to_bytes(i ^ num)[::-1])

flag = b""
for i in tmp:
	for j in i:
		flag += bytes([(dictionary[j])])

print(flag)

Flag: hkcert24{f0r3v3r_r3m3mb3r_x4m4r1n_2024-5-1}

Baby Cracker (100 pts)

Description

For those who never tried even the simplest reverse, this is for you! Check the guide to experiment how simple reverse works!

For experience player, this is just a simple crackme, not worth your attention, maybe go to solve some 5-stars :)

Solution

Given ELF file, open it using IDA.

We can see that there is two validation. First validation is by using arithmetic operator (addition and multiplication) and the second validation is using logic operator (xor). The details of each validation are as follows

  • First validation

    • 4 bytes validation, can be solved using z3 or bruteforce

  • Second validation

    • 28 bytes validation (flag value after hkcert24{, we know it from the strstr function and haystack[i+9])

    • Can be solved by reversing the flow (just do the xor)

from z3 import *
import string

def all_smt(s, initial_terms):
    def block_term(s, m, t):
        s.add(t != m.eval(t, model_completion=True))
    def fix_term(s, m, t):
        s.add(t == m.eval(t, model_completion=True))
    def all_smt_rec(terms):
        if sat == s.check():
           m = s.model()
           yield m
           for i in range(len(terms)):
               s.push()
               block_term(s, m, terms[i])
               for j in range(i):
                   fix_term(s, m, terms[j])
               yield from all_smt_rec(terms[i:])
               s.pop()   
    yield from all_smt_rec(list(initial_terms))

a = bytes.fromhex("CE21DB64D150E01B0D3EFB0A522F949DAFB1586B8AEEC1F0FC190AE3E91ED04A62F247A8200BD36C1C1C565B9BB34D3DCE8380C0E67EE809BD14C447A1F62FD1315C1E10D12AE05345FE8558A6AD03CC104BD394FE62854E4A2735E2940F499186D780354C67C3AA3C67E83FE46723DE8E2D462536E1F3907D0FB9148CE7B6A61A9080798535FD518C10E93F32CC4BB542DEF55713C8099B4D1984915F9D773031C7288D1DF471E4D1A30C0759AA0DAD163540B9286AB54C245A8DA6A6C456DDC09BBFCCDE0C5BC175DD77BBF62B431D1303AD73A3AC4DEAA52FC23E4A1AF56572E54A10108CFB100A4D7971F6C7805464B002AAD87C3953ECADB44E2FEBE04700")
b = bytes.fromhex("BD10B650BD35BF787F0A98611F1CCBA9F0D96C05EEACB8989D773CBC812EA0793D8B77DD7F6FE3022B433868A8D7120AA1DCF5F38321DC67DA669B77D3A955E26E3A2E628E5E886236A1E72D91F23293677BBDF0CD10DA7F2C78568AA0382EE1B188B0471304F7C46314DB0CBB134BEFBB722414598390A40969CC7AE29E8C8F69A1ED4DE950A232FE248A547FFF14811DB6C139778A70F32C77B2CE37AD07036EBE18F84290418AE6FC62346ACE529A796A358A4D3581224328D296D49B2CEE9FFD8FBE817833F0068215CEC17472426433C31790DE12DBC370A1567E2D921545BA7A624FEFCF7E553E4A42A9B3E86551EF609BB71E5A6798CBC1204192DA6E00")

flag = b"hkcert24{"
for i in range(28):
    flag += bytes([a[i] ^ b[i]])

haystack = [BitVec("x{}".format(i), 8) for i in range(5)]

s = Solver()

list_char = string.printable[:-6]
for i in haystack:
    s.add(z3.Or(*[ord(j) == i for j in list_char]))

v6 = 5
s.add(haystack[v6 - 1] == ord('}'))
s.add(haystack[v6 - 2] == ord('1'))
s.add(haystack[v6 - 5] + haystack[v6 - 4] + haystack[v6 - 3] == 300)
s.add(2 * haystack[v6 - 5] + haystack[v6 - 4] + 2 * haystack[v6 - 3] == 496)
s.add(haystack[v6 - 5] + 3 * haystack[v6 - 4] + haystack[v6 - 3] == 508 )

for model in all_smt(s, haystack):
    tmp_flag = b""
    for i in haystack:
        try:
            tmp_flag += bytes([model[i].as_long()])
        except Exception as e:
            tmp_flag += b"?"
    print(flag + tmp_flag)

There are so many valid solution but we still able to guess the actual one.

Flag: hkcert24{s1m4le_cr4ckM3_4_h4ndByhan6_cha1}

Cyp.ress (200 pts)

Description

You will get sser.pyc when you reverse the title. Now reverse it back for me.

Solution

Solution

Given pyc file, tried to decompile using pycdc but it failed. Lets use pycdas instead

sser.cpython-312.pyc (Python 3.12)
[Code]
    File Name: sser.py
    Object Name: <module>
    Qualified Name: <module>
    Arg Count: 0
    Pos Only Arg Count: 0
    KW Only Arg Count: 0
    Stack Size: 6
    Flags: 0x00000000
    [Names]
        'os'
        'requests'
        'Crypto.Cipher'
        'AES'
        'hashlib'
        'get_nonce'
        'input'
        'encode'
        'flag'
        'nonce'
        'post'
        'hex'
        'r'
        'bytes'
        'fromhex'
        'text'
        'c0'
        'sha256'
        'digest'
        'key'
        'iv'
        'new'
        'MODE_CFB'
        'cipher'
        'encrypt'
        'c1'
        'print'
    [Locals+Names]
    [Constants]
        0
        None
        (
            'AES'
        )
        [Code]
            File Name: sser.py
            Object Name: get_nonce
            Qualified Name: get_nonce
            Arg Count: 0
            Pos Only Arg Count: 0
            KW Only Arg Count: 0
            Stack Size: 4
            Flags: 0x00000003 (CO_OPTIMIZED | CO_NEWLOCALS)
            [Names]
                'os'
                'urandom'
                'hashlib'
                'sha256'
                'digest'
            [Locals+Names]
                'nonce'
            [Constants]
                None
                16
                b'pow/'
                3
                b'\x00\x00\x00'
            [Disassembly]
                0       RESUME                          0
                2       NOP
                4       LOAD_GLOBAL                     1: NULL + os
                14      LOAD_ATTR                       2: urandom
                34      LOAD_CONST                      1: 16
                36      CALL                            1
                44      STORE_FAST                      0: nonce
                46      LOAD_GLOBAL                     5: NULL + hashlib
                56      LOAD_ATTR                       6: sha256
                76      LOAD_CONST                      2: b'pow/'
                78      LOAD_FAST                       0: nonce
                80      BINARY_OP                       0 (+)
                84      CALL                            1
                92      LOAD_ATTR                       9: digest
                112     CALL                            0
                120     LOAD_CONST                      0: None
                122     LOAD_CONST                      3: 3
                124     BINARY_SLICE
                126     LOAD_CONST                      4: b'\x00\x00\x00'
                128     COMPARE_OP                      40 (==)
                132     POP_JUMP_IF_FALSE               2 (to 138)
                134     LOAD_FAST                       0: nonce
                136     RETURN_VALUE
                138     JUMP_BACKWARD                   68 (to 4)
        'What is the flag?> '
        'https://c12-cypress.hkcert24.pwnable.hk/'
        'nonce'
        (
            'json'
        )
        b'key/'
        16
        b'iv/'
        '🙆🙅'
    [Disassembly]
        0       RESUME                          0
        2       LOAD_CONST                      0: 0
        4       LOAD_CONST                      1: None
        6       IMPORT_NAME                     0: os
        8       STORE_NAME                      0: os
        10      LOAD_CONST                      0: 0
        12      LOAD_CONST                      1: None
        14      IMPORT_NAME                     1: requests
        16      STORE_NAME                      1: requests
        18      LOAD_CONST                      0: 0
        20      LOAD_CONST                      2: ('AES',)
        22      IMPORT_NAME                     2: Crypto.Cipher
        24      IMPORT_FROM                     3: AES
        26      STORE_NAME                      3: AES
        28      POP_TOP
        30      LOAD_CONST                      0: 0
        32      LOAD_CONST                      1: None
        34      IMPORT_NAME                     4: hashlib
        36      STORE_NAME                      4: hashlib
        38      LOAD_CONST                      4: 'What is the flag?> '
        40      CALL_INTRINSIC_1                1 (INTRINSIC_PRINT)
        42      POP_TOP
        44      LOAD_CONST                      3: <CODE> get_nonce
        46      MAKE_FUNCTION                   0
        48      STORE_NAME                      5: get_nonce
        50      PUSH_NULL
        52      LOAD_NAME                       6: input
        54      BUILD_STRING                    0
        56      CALL                            1
        64      LOAD_ATTR                       15: encode
        84      CALL                            0
        92      STORE_NAME                      8: flag
        94      PUSH_NULL
        96      LOAD_NAME                       5: get_nonce
        98      CALL                            0
        106     STORE_NAME                      9: nonce
        108     PUSH_NULL
        110     LOAD_NAME                       1: requests
        112     LOAD_ATTR                       20: post
        132     LOAD_CONST                      5: 'https://c12-cypress.hkcert24.pwnable.hk/'
        134     LOAD_CONST                      6: 'nonce'
        136     LOAD_NAME                       9: nonce
        138     LOAD_ATTR                       23: hex
        158     CALL                            0
        166     BUILD_MAP                       1
        168     KW_NAMES                        7: ('json',)
        170     CALL                            2
        178     STORE_NAME                      12: r
        180     LOAD_NAME                       13: bytes
        182     LOAD_ATTR                       29: fromhex
        202     LOAD_NAME                       12: r
        204     LOAD_ATTR                       30: text
        224     CALL                            1
        232     STORE_NAME                      16: c0
        234     PUSH_NULL
        236     LOAD_NAME                       4: hashlib
        238     LOAD_ATTR                       34: sha256
        258     LOAD_CONST                      8: b'key/'
        260     LOAD_NAME                       9: nonce
        262     BINARY_OP                       0 (+)
        266     CALL                            1
        274     LOAD_ATTR                       37: digest
        294     CALL                            0
        302     LOAD_CONST                      1: None
        304     LOAD_CONST                      9: 16
        306     BINARY_SLICE
        308     STORE_NAME                      19: key
        310     PUSH_NULL
        312     LOAD_NAME                       4: hashlib
        314     LOAD_ATTR                       34: sha256
        334     LOAD_CONST                      10: b'iv/'
        336     LOAD_NAME                       9: nonce
        338     BINARY_OP                       0 (+)
        342     CALL                            1
        350     LOAD_ATTR                       37: digest
        370     CALL                            0
        378     LOAD_CONST                      1: None
        380     LOAD_CONST                      9: 16
        382     BINARY_SLICE
        384     STORE_NAME                      20: iv
        386     PUSH_NULL
        388     LOAD_NAME                       3: AES
        390     LOAD_ATTR                       42: new
        410     LOAD_NAME                       19: key
        412     LOAD_NAME                       3: AES
        414     LOAD_ATTR                       44: MODE_CFB
        434     LOAD_NAME                       20: iv
        436     CALL                            3
        444     STORE_NAME                      23: cipher
        446     LOAD_NAME                       23: cipher
        448     LOAD_ATTR                       49: encrypt
        468     LOAD_NAME                       8: flag
        470     CALL                            1
        478     STORE_NAME                      25: c1
        480     PUSH_NULL
        482     LOAD_NAME                       26: print
        484     LOAD_CONST                      11: '🙆🙅'
        486     LOAD_NAME                       16: c0
        488     LOAD_NAME                       25: c1
        490     COMPARE_OP                      55 (!=)
        494     BINARY_SUBSCR
        498     CALL                            1
        506     POP_TOP
        508     RETURN_CONST                    1: None

The algorithm is not complex, so we can easility understand it. The program tried to send nonce to the server then the nonce will be used as the base value for generating key and iv same as in client side. The generated key and iv will be used to do encryption with AES MODE_CFB and in the end the ciphertext will be validated with the value from server. So the easy way we can hook the data in request function and AES function.

First, modify code in requests library to printout the ciphertext (flag)

python3.12/site-packages/requests/api.py
def post(url, data=None, json=None, **kwargs):
    r"""Sends a POST request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """
    res = request("post", url, data=data, json=json, **kwargs)
    print(res.text)
    return res

Second, modufy code in AES to dump the key and iv

python3.12/site-packages/Crypto/Cipher/AES.py
def new(key, mode, *args, **kwargs):
    print("key",key)
    print("mode",mode)
    print("args",*args)
    kwargs["add_aes_modes"] = True
    return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)
from Crypto.Cipher import AES

ct = bytes.fromhex("9517ef893189dd9eb5bc6d3e83f6fd40bc7f098ac152cb0d313cd174fa937bc70bbe707bf37fc2d4956d7add77a49cf2704c246e072b7a5f5d6331ce2a73146b18")
key = b'f0\nN\x18\xf4\xc7g\x1e\xfe\x9d6%\xf0Zj'
iv = b'\x8e{\x10\xcdl\x987Q\x03S\xc7:\x1b\xe0\xda\xc0'
cipher = AES.new(key, AES.MODE_CFB, iv)
print(cipher.decrypt(ct))

Flag: hkcert24{y0u_c4n_h00k_func710ns_t0_35c4p3_fr0m_r3v3r5e_3n9e3r1n9}

Flag Checker (200 pts)

Description

Wonton noodles are called 'small minced', rice is called 'handsome', so what was the flag called? Don't worry, I've written a program to help you check if it's right.

Solution

Given ELF file, open it using IDA.

Another flag checker, the algorithm should be in sub_1392.

The important function is on sub_12EF, because it will process our input then the return value will be processed by modulo then comparation. Lets take a look on sub_12EF

It looks like multiplication function, we can validate it by changing the argument during debugging.

Now we know the logic, the next step just recreate the algorithm then brute it. Because it is only 4 bytes so it will not take long times to do bruteforce.

import string
from itertools import product
from Crypto.Util.number import *
import tqdm

unk_4420 = [0xee0f9df9, 0xc4e73ead, 0xd09b50ef, 0x9f758e2b, 0xc7d7db0b, 0x9dbe2a69, 0xe38a352b, 0xca1fffff, 0xf125338f, 0xf5fee2fb, 0xbb5851d7, 0x999b4abd, 0xa1c55209, 0xf4ca133b, 0xe8e26fa5, 0xc32dda41, 0xdd6982fb, 0xfc5f4d91, 0xa0037abb, 0xa113b9dd, 0xc93308f3, 0x9448ea39, 0xb2ff73ad, 0x86c0a263, 0x8b0c8427, 0xe1c1771d, 0xce477441, 0xe86f3df9, 0xec65e2bd, 0xeb800149, 0xa37f8b1d, 0xc7639133]
unk_4020 = [0x89e216c7, 0x8bba1f53, 0xeedac203, 0xa3a18665, 0x94ed1363, 0xf962506f, 0xd9ce8aaf, 0xb1375ea5, 0xb5f3a527, 0x864090e5, 0x8b69129d, 0xe93765cb, 0xe69d6f8f, 0xef102543, 0xbf72b95b, 0xb92fc919, 0xbb5f04b1, 0xd6db2593, 0xaf09d917, 0xe6cacc41, 0xf6e60f6f, 0xc11f4cb5, 0xf7617ab7, 0xfa6a6b91, 0xb45d2387, 0x83e969c3, 0xb21505b3, 0xc7455743, 0xcbbb1795, 0xc5b265ad, 0xd65d3205, 0xef73c749]
unk_4220 = [0x34455a43, 0x1490eb26, 0x72151d59, 0x39ad5153, 0x49f794e0, 0x2cb40abd, 0x80bed156, 0x855cc82f, 0x900b909a, 0x6372d3fd, 0x352d899e, 0x2981bcb2, 0x27b68bfd, 0x64a2dd3a, 0x3db5729d, 0x805016d6, 0x97acee3, 0x17c89862, 0x9f4002a9, 0x9964c05, 0xdb22b70c, 0xba1db04a, 0xc0066755, 0xb3f68f9, 0x899942f8, 0xa10d428, 0x235acde2, 0x679d88d, 0x17e5e57, 0xaab78154, 0x5f922062, 0xa72b4fd]


flag = [b'?' for _ in range(100)]

for i in tqdm.tqdm(product(string.printable[:-6], repeat=4)):
	val = bytes_to_long(''.join(i).encode())
	for j in range(0, len(unk_4420), 2):
		tmp_val = (val * unk_4420[j]) % unk_4020[j]
		if tmp_val in unk_4220:
			index_val = unk_4220.index(tmp_val)
			tmp_val_2 = (val * unk_4420[j+1]) % unk_4020[j+1]
			if tmp_val_2 == unk_4220[index_val + 1]:
				flag[j//2] = ''.join(i).encode()

print(b''.join(flag))

Flag: hkcert24{51mpl3_l1n34r_c0n6ru3nc35_50lv3d_w17h_cr7_65894851}

ISA 101 (200 pts)

Description

If you haven't tried our last year's ISA challenges, they are back this year with some changes here and there!

This is a challenge to help you get along with the web interface debug environment, which combines as an debugger, an interpreter and challenge connections!

Check out the ISH fullchain series after this!

Solution

Given custom asm file, open it using text editor. We can use the playground to debug the program

The objective of this challenge is executing flag binary. From the syscall table, we know which one the instruction that receive our input.

Set breakpoint on 87 (after syscall input), then analyze until XOR instruction that processed our input.

From the debugger we can see that our input (stored at R6) will be xored with R7. R7 values originated from R5, lets take a look on value on R5

We can also see those values in push instruction on the assembly.

So our input will be xored with those static keys and then will be executed as a command. To generate valid command we just need to do the xor with those static keys, below is our script to generate the valid commands.

from Crypto.Util.number import *
from pwn import xor

a = [0xb146f66e,0x2fd8b7c1,0x95e11585,0xcf39fb28,0xb3accf4c,0xdb22a8cb,0xe21f60cd,0xb660d0fe,0x8be89ec9,0x241bd185,0x161d7e99,0xbf3a7f64,0xea7454ee,0x2e04ce47,0x18b25e16,0x2295643e,0x49f8d91f,0x3f541ea6,0x113d8a6f,0x38726ccc,0x2e27be68,0xd4e398ea,0x7fcba040,0xeec775f5,0x478ff266,0x718a3507,0x536edeba,0xf0efb119,0x9efdd1c2,0x977b4203,0x2ceeda0d,0xfdc086ff,0x2303c15a,0x3c9d30a1,0x193f231b,0x1a06a63f,0x5c829f5,0x49c872b8,0x92bcbdad,0xa9a5a84e,0xb16969c,0xb58b3659,0x642069c9,0x9c37ba69,0x623277a4,0x17b6f65c,0xa6a21506,0x15881c76,0x96ed9c50,0x21226b56,0xd8890218,0xca6eddde,0x9a18e395,0x936f6277,0xaf23d230,0x88d9666a,0xff591d2f,0xce454872,0xf3391e9f,0x4ddd147f,0x404bcc99,0x5becacfd,0x1d9f2f1,0xc833a241]
a = a[::-1]
key = b""
for i in a:
	key += long_to_bytes(i)[::-1]

command = b"ls\x00\x00"
print(xor(command, key[:len(command)]).hex())
from Crypto.Util.number import *
from pwn import xor

a = [0xb146f66e,0x2fd8b7c1,0x95e11585,0xcf39fb28,0xb3accf4c,0xdb22a8cb,0xe21f60cd,0xb660d0fe,0x8be89ec9,0x241bd185,0x161d7e99,0xbf3a7f64,0xea7454ee,0x2e04ce47,0x18b25e16,0x2295643e,0x49f8d91f,0x3f541ea6,0x113d8a6f,0x38726ccc,0x2e27be68,0xd4e398ea,0x7fcba040,0xeec775f5,0x478ff266,0x718a3507,0x536edeba,0xf0efb119,0x9efdd1c2,0x977b4203,0x2ceeda0d,0xfdc086ff,0x2303c15a,0x3c9d30a1,0x193f231b,0x1a06a63f,0x5c829f5,0x49c872b8,0x92bcbdad,0xa9a5a84e,0xb16969c,0xb58b3659,0x642069c9,0x9c37ba69,0x623277a4,0x17b6f65c,0xa6a21506,0x15881c76,0x96ed9c50,0x21226b56,0xd8890218,0xca6eddde,0x9a18e395,0x936f6277,0xaf23d230,0x88d9666a,0xff591d2f,0xce454872,0xf3391e9f,0x4ddd147f,0x404bcc99,0x5becacfd,0x1d9f2f1,0xc833a241]
a = a[::-1]
key = b""
for i in a:
	key += long_to_bytes(i)[::-1]

command = b"exec printflag_19876bc2\x00"
print(xor(command, key[:len(command)]).hex())

Flag: hkcert24{x0r_1n_isa_r04d_t0_fullch41n!!!}

Morph (300 pts)

Description

Binary sometimes mixed together, just like fried rice or bibimbap.

Solution

Given ELF file, open it using IDA.

From code above we can see that there is decompress function and verify_* function. Lets take a look on decompress function

Then take a look on verify_0 function

So decompress function will do xor with key and size based on the argument. After the function has been decompressed it will be called to verify our input. Lets try to apply decryption to verify_0 to take a look on the logic of input validation.

from idaapi import *
import idc

def patch(addr, length, key):
	for i in range(length):
		val = get_bytes(addr+i, 1)
		new_val = val[0] ^ key
		patch_byte(addr+i, new_val)
	ida_bytes.del_items(addr, 0, length)
	create_insn(addr)
	add_func(addr, addr + length)

def get_val(prev_addr):
	print(prev_addr)
	insn = insn_t()
	k = ""
	size = ""
	verif_addr = ""
	for addr in prev_addr[::-1]:
		tmp = idc.print_operand(addr, 0)
		if tmp == "edx":
			k = int(idc.print_operand(addr, 1).replace("h", ""),16)
			break
		elif tmp == "esi":
			size = int(idc.print_operand(addr, 1).replace("h", ""),16)
		elif tmp == "rax":
			verif_addr = get_name_ea(0, idc.print_operand(addr, 1))
	return verif_addr, size, k



dict = {}
addr = 0x25B07B 
insn = insn_t()
prev_addr = []
final_arr = []
while addr < 0x000000000025BC4B:
	prev_addr.append(addr)
	size = decode_insn(insn, addr)
	tmp = idc.print_operand(addr, 0)
	if "_Z10decompressPcic" ==  tmp:
		print("nice", addr)
		verif_addr, length, k = get_val(prev_addr)
		patch(verif_addr, length, k)
	addr += size
print(final_arr)

The verify function validate several index of our input and looks like there is a pattern that we can use to dump all the constraints. Below is the flow to dump all constraints

  • Decrypt the function

  • Find constraints format (index and correct value)

from idaapi import *
import idc

def patch(addr, length, key):
	for i in range(length):
		val = get_bytes(addr+i, 1)
		new_val = val[0] ^ key
		patch_byte(addr+i, new_val)
	ida_bytes.del_items(addr, 0, length)
	create_insn(addr)
	add_func(addr, addr + length)

def get_constraints(addr, length):
	arr = []
	start_address = addr
	while start_address < (addr + length):
		print(hex(start_address))
		create_insn(start_address)
		insn = insn_t()
		size = decode_insn(insn, start_address)
		inst = idc.print_insn_mnem(start_address)
		if inst == "mov":
			tmp = idc.print_operand(start_address, 0)
			if tmp == "esi":
				val = idc.print_operand(start_address, 1)
				val = int(val.replace("h", ""),16)
				arr.append(val)
		elif inst == "cmp":
			val = idc.print_operand(start_address, 1)
			val = int(val.replace("h", ""),16)
			arr.append(val)
			break
		start_address += size
	return arr



def get_val(prev_addr):
	print(prev_addr)
	insn = insn_t()
	k = ""
	size = ""
	verif_addr = ""
	for addr in prev_addr[::-1]:
		tmp = idc.print_operand(addr, 0)
		if tmp == "edx":
			k = int(idc.print_operand(addr, 1).replace("h", ""),16)
			break
		elif tmp == "esi":
			size = int(idc.print_operand(addr, 1).replace("h", ""),16)
		elif tmp == "rax":
			verif_addr = get_name_ea(0, idc.print_operand(addr, 1))
	return verif_addr, size, k

dict = {}
addr = 0x25B07B 
insn = insn_t()
prev_addr = []
final_arr = []
while addr < 0x000000000025BC4B:
	prev_addr.append(addr)
	size = decode_insn(insn, addr)
	tmp = idc.print_operand(addr, 0)
	if "_Z10decompressPcic" ==  tmp:
		print("nice", addr)
		verif_addr, length, k = get_val(prev_addr)
		patch(verif_addr, length, k)
		final_arr.append(get_constraints(verif_addr, length))
		prev_addr = []
	addr += size
print(final_arr)

After that put the output to z3 format

from z3 import *
import string

list_char = string.printable[:-6]

inp = [BitVec("x{}".format(i), 8) for i in range(55)]

data = [[48, 49, 88], [14, 15, 93], [11, 12, 10], [51, 52, 70], [42, 43, 100], [12, 13, 57], [37, 38, 54], [21, 22, 9], [26, 27, 87], [1, 2, 8], [7, 8, 79], [22, 23, 56], [49, 50, 27], [45, 46, 30], [40, 41, 49], [29, 30, 28], [44, 45, 30], [19, 20, 72], [53, 54, 78], [39, 40, 106], [10, 11, 95], [46, 47, 50], [6, 7, 6], [38, 39, 92], [3, 4, 23], [23, 24, 60], [35, 36, 4], [13, 14, 50], [8, 9, 8], [41, 42, 94], [2, 3, 6], [20, 21, 95], [18, 19, 31], [47, 48, 89], [52, 53, 65], [5, 6, 70], [25, 26, 84], [50, 51, 67], [15, 16, 84], [30, 31, 88], [17, 18, 87], [28, 29, 43], [4, 5, 6], [0, 1, 3], [9, 10, 64], [31, 32, 111], [33, 34, 28], [27, 28, 108], [43, 44, 11], [24, 25, 83], [36, 37, 106], [32, 33, 43], [16, 17, 85], [34, 35, 89]]

s = Solver()
for i in data:
	s.add(inp[i[0]] ^ inp[i[1]] == i[2])

for i in inp:
    s.add(z3.Or(*[ord(j) == i for j in list_char]))

known = b"hkcert24{"
for i in range(len(known)):
	s.add(known[i] == inp[i])

s.add(inp[54] == ord('}'))

print(s.check())

model = s.model()

flag = b""
for i in inp:
    try:
    	flag += bytes([model[i].as_long()])
    except Exception as e:
    	flag += b"?"

print(flag)

Flag: hkcert24{s3lf_m0d1fy1ng_c0d3_th0_th15_i5_n0T_A_m4lw4r3}

Black Magic (400 pts)

Description

It seems university students often plays Black Magic. It should be the first time tuning (meaning: guessing, telepath, shamanism, mind-reading) for many people.

Many language contains some black magic within. To those python expert: do you really know Python deep within? Time to tune what Python is thinking!

Solution

Given pyc file with the environment (docker). At first i tried to decompile using pycdc and it failed.

Next, i tried to do disasm using pycdas and it works!

We can see that it has around 10k lines of code and it would be painful if we just do it statically. During the competition i've an idea to do dump information from several opcode that will be executed. During the competition i did several modification until i found the opcode that give much information.

We can see that there is STORE_DEREF and BINARY_SUBSCR, STORE_DEREF basically storing value to variable and BINARY_SUBSCR is getting i-th value from a variable. I decided to dump the pyobject on those opcodes.

Python/generated_cases.c.h
------SNIPPET------
TARGET(BINARY_SUBSCR) {
            PREDICTED(BINARY_SUBSCR);
            static_assert(INLINE_CACHE_ENTRIES_BINARY_SUBSCR == 1, "incorrect cache size");
            PyObject *sub = stack_pointer[-1];
            PyObject *container = stack_pointer[-2];
            PyObject *res;
            
            PyObject* repr = PyObject_Repr(sub);
            PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
            const char *bytes = PyBytes_AS_STRING(str);
            printf("\nSUBSCR: %s\n", bytes);
            Py_XDECREF(repr);
            Py_XDECREF(str);

            PyObject* repr2 = PyObject_Repr(container);
            PyObject* str2 = PyUnicode_AsEncodedString(repr2, "utf-8", "~E~");
            const char *bytes2 = PyBytes_AS_STRING(str2);
            printf("CONTAINER: %s\n", bytes2);
            Py_XDECREF(repr2);
            Py_XDECREF(str2);

            #line 403 "Python/bytecodes.c"
            #if ENABLE_SPECIALIZATION
            _PyBinarySubscrCache *cache = (_PyBin
------SNIPPET------        
TARGET(STORE_DEREF) {
            PyObject *v = stack_pointer[-1];

            PyObject* repr = PyObject_Repr(v);
            PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
            const char *bytes = PyBytes_AS_STRING(str);
            printf("\nSTORE_DEREF: %s\n", bytes);
            Py_XDECREF(repr);
            Py_XDECREF(str);

            #line 1471 "Python/bytecodes.c"
            PyObject *cell = GETLOCAL(oparg);
            PyObject *oldobj = PyCell_GET(cell);


            PyCell_SET(cell, v);
            Py_XDECREF(oldobj);
            #line 2001 "Python/generated_cases.c.h"
            STACK_SHRINK(1);
            DISPATCH();
        }
------SNIPPET------

We see some pattern, string 'input' generated from locals that has been converted to a string such as index 18 == i, index 4 == n, and etc.

Now, we need to find out how those 18,4,45,2,91 values generated. Take a look on the opcode

        0       LOAD_LOCALS
        2       FORMAT_VALUE                    1 (FVC_STR)
        4       BUILD_TUPLE                     0
        6       GET_LEN
        8       SWAP                            2
        10      POP_TOP
        12      UNARY_INVERT
        14      UNARY_NEGATIVE
        16      BUILD_TUPLE                     0
        18      GET_LEN
        20      SWAP                            2
        22      POP_TOP
        24      UNARY_INVERT
        26      UNARY_NEGATIVE
        28      BINARY_OP                       3 (<<)
        32      BUILD_TUPLE                     0
        34      GET_LEN
        36      SWAP                            2
        38      POP_TOP
        40      UNARY_INVERT
        42      UNARY_NEGATIVE
        44      BINARY_OP                       3 (<<)
        48      BUILD_TUPLE                     0
        50      GET_LEN
        52      SWAP                            2
        54      POP_TOP
        56      UNARY_INVERT
        58      UNARY_NEGATIVE
        60      BINARY_OP                       3 (<<)
        64      BUILD_TUPLE                     0
        66      GET_LEN
        68      SWAP                            2
        70      POP_TOP
        72      UNARY_INVERT
        74      UNARY_NEGATIVE
        76      BINARY_OP                       0 (+)
        80      BUILD_TUPLE                     0
        82      GET_LEN
        84      SWAP                            2
        86      POP_TOP
        88      UNARY_INVERT
        90      UNARY_NEGATIVE
        92      BINARY_OP                       3 (<<)
        96      BINARY_SUBSCR
        100     STORE_DEREF                     7 <INVALID>

There are several operation such as + and <<. So lets dump the pyobject again.

Python/generated_cases.c.h
TARGET(BINARY_OP) {
            PREDICTED(BINARY_OP);
            static_assert(INLINE_CACHE_ENTRIES_BINARY_OP == 1, "incorrect cache size");
            PyObject *rhs = stack_pointer[-1];
            PyObject *lhs = stack_pointer[-2];

            if(strcmp(Py_TYPE(lhs)->tp_name, "int") == 0 ){
                PyObject* repr = PyObject_Repr(lhs);
                PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
                const char *bytes = PyBytes_AS_STRING(str);
                printf("\nLHS: %s\n", bytes);
                Py_XDECREF(repr);
                Py_XDECREF(str);
            }

            if(strcmp(Py_TYPE(rhs)->tp_name, "int") == 0 ){
                PyObject* repr2 = PyObject_Repr(rhs);
                PyObject* str2 = PyUnicode_AsEncodedString(repr2, "utf-8", "~E~");
                const char *bytes2 = PyBytes_AS_STRING(str2);
                printf("RHS: %s\n", bytes2);
                Py_XDECREF(repr2);
                Py_XDECREF(str2);
            }


            PyObject *res;
            #line 3377 "Python/bytecodes.c"
            #if ENABLE_SPECIALIZATION
            _PyBinaryOpCache *cache = (_PyBinaryOpCache *)next_instr;
            if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) {
                next_instr--;
                _Py_Specialize_BinaryOp(lhs, rhs, next_instr, oparg, &GETLOCAL(0));
                DISPATCH_SAME_OPARG();
            }
            STAT_INC(BINARY_OP, deferred);
            DECREMENT_ADAPTIVE_COUNTER(cache->counter);
            #endif  /* ENABLE_SPECIALIZATION */
            assert(0 <= oparg);
            assert((unsigned)oparg < Py_ARRAY_LENGTH(binary_ops));
            assert(binary_ops[oparg]);
            res = binary_ops[oparg](lhs, rhs);
            #line 4671 "Python/generated_cases.c.h"
            Py_DECREF(lhs);
            Py_DECREF(rhs);
            #line 3392 "Python/bytecodes.c"
            if (res == NULL) goto pop_2_error;
            #line 4676 "Python/generated_cases.c.h"
            STACK_SHRINK(1);
            printf("OP: %x\n", oparg);
            stack_pointer[-1] = res;
            next_instr += 1;
            DISPATCH();
        }
-------------------
 TARGET(BINARY_SUBSCR) {
            PREDICTED(BINARY_SUBSCR);
            static_assert(INLINE_CACHE_ENTRIES_BINARY_SUBSCR == 1, "incorrect cache size");
            PyObject *sub = stack_pointer[-1];
            PyObject *container = stack_pointer[-2];
            PyObject *res;
            
            #line 403 "Python/bytecodes.c"
            #if ENABLE_SPECIALIZATION
            _PyBinarySubscrCache *cache = (_PyBinarySubscrCache *)next_instr;
            if (ADAPTIVE_COUNTER_IS_ZERO(cache->counter)) {
                next_instr--;
                _Py_Specialize_BinarySubscr(container, sub, next_instr);
                DISPATCH_SAME_OPARG();
            }
            STAT_INC(BINARY_SUBSCR, deferred);
            DECREMENT_ADAPTIVE_COUNTER(cache->counter);
            #endif  /* ENABLE_SPECIALIZATION */
            res = PyObject_GetItem(container, sub);
            #line 570 "Python/generated_cases.c.h"
            Py_DECREF(container);
            Py_DECREF(sub);
            #line 415 "Python/bytecodes.c"
            if (res == NULL) goto pop_2_error;
            #line 575 "Python/generated_cases.c.h"
            STACK_SHRINK(1);

            PyObject* repr = PyObject_Repr(res);
            PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
            const char *bytes = PyBytes_AS_STRING(str);
            printf("\nSUBSCR_RESULT: %s\n", bytes);
            Py_XDECREF(repr);
            Py_XDECREF(str);

            stack_pointer[-1] = res;
            next_instr += 1;
            DISPATCH();
        }
----------------------

The first BINARY_SUBSCR result is 'i' and there are 5 operations before the BINARY_SUBSCR. 4 operations shift left and one operation addition. So we can conclude that 3 is shiftleft and 0 is addition. Now lets examine how value 1 generated.

        32      BUILD_TUPLE                     0
        34      GET_LEN
        36      SWAP                            2
        38      POP_TOP
        40      UNARY_INVERT
        42      UNARY_NEGATIVE

Above opcode create value 1 and we can reconstruct it like below code in python

>>> -~(len(()))
1

And then for each operation we know that it will process value on top of stack, so basically for the first generated index we can reconstruct like below

>>> (((((1<<1)<<1)<<1)+1)<<1)
18

Now we can parse the opcode with this information, but there are another two problem left as below

  • my locals() return slightly different value

    • we can use the docker to printout the same locals() like the flag server

  • I tried to parse based on BINARY_OP, FORMAT_VALUE, and COMPARE_OP instruction the there are several different pattern

    • there is pattern to generate value 0 like below

      • So i put identifier/new line as BINARY_OP (??) in the dumped opcode

    • there is pattern to generate value 1 like below

      • So i put identifier/new line as BINARY_OP (?) in the dumped opcode

    • Not all values derived from locals()

      • Some of the opcode contains UNARY_NOT instruction before FORMAT_VALUE which means that the value will be derived from "True"

      • If there is no UNARY_NOT so the value will be derived from "False"

Lets solve the first issue which is printout the locals, change the run.sh in the src directory to below code

#!/bin/sh

python /app/src/lol.py

Create lol.py and put below code (somehow the first

input()
print(locals())

Now we have the locals(), change the __file__ value to /app/src/a.pyc and change SourceFileLoader to SourcelessFileLoader. Then create a parser and got the flag


def conv(arr):
	init = 1
	for j in arr:
		if j == "<<":
			init <<= 1
		elif j == "+":
			init += 1
		elif j == "?":
			init += 0
		elif j == "??":
			init -= 1
	return init



f = open("dump2.asm", "r").read().split("\n")


LOAD_LOCALS = "{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourcelessFileLoader object at 0x7fffff0ff650>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/app/src/a.pyc', '__cached__': None}"
UNARY_NOT = "True"
UNARY_NOT_NEG = "False"

# print(len(LOAD_LOCALS))

tmp_opr = []
arr_const = []
var_arr = []
comp_index = []
for i in range(len(f)):
	if "BINARY_OP" in f[i]:
		tmp_opr.append(f[i].split(" ")[-1][1:-1])
	elif "STORE_DEREF" in f[i]:
		if tmp_opr == []:
			continue
		# print(tmp_opr, conv(tmp_opr))
		arr_const.append(conv(tmp_opr))
		tmp_opr = []
	elif "FORMAT_VALUE" in f[i]:
		var_check = (f[i-1].split(" ")[-1])
		if var_check == "UNARY_NOT":
			# print(var_check)
			if "UNARY_NEGATIVE" in f[i-2]:
				var_arr.append("UNARY_NOT_NEG")
			else:
				var_arr.append(var_check)
		else:
			var_arr.append(var_check)
	elif "COMPARE_OP" in f[i]:
		comp_index.append(conv(tmp_opr))
		tmp_opr = []

for i in range(len(var_arr)):
	# print(var_arr[i], arr_const[i])
	if var_arr[i] == "LOAD_LOCALS":
		print(i+1, LOAD_LOCALS[arr_const[i]], var_arr[i] , arr_const[i])
	elif var_arr[i] == "UNARY_NOT":
		print(i+1, UNARY_NOT[arr_const[i]], var_arr[i],  arr_const[i])
	elif var_arr[i] == "UNARY_NOT_NEG":
		print(i+1, UNARY_NOT_NEG[arr_const[i]], var_arr[i],  arr_const[i])
	else:
		# print("UNKNOWN", arr_const[i])
		print(i+1, arr_const[i], var_arr[i])
		# exit()
1 i LOAD_LOCALS 18
2 n LOAD_LOCALS 4
3 p LOAD_LOCALS 45
4 u UNARY_NOT 2
5 t LOAD_LOCALS 91
6 h LOAD_LOCALS 278
7 k LOAD_LOCALS 48
8 c LOAD_LOCALS 30
9 e UNARY_NOT 3
10 r UNARY_NOT 1
11 t LOAD_LOCALS 91
12 2 (<<)
13 4 (<<)
14 { LOAD_LOCALS 0
15 u UNARY_NOT 2
16 _ LOAD_LOCALS 2
17 r UNARY_NOT 1
18 _ LOAD_LOCALS 2
19 r UNARY_NOT 1
20 3 (+)
21 4 (<<)
22 l UNARY_NOT_NEG 2
23 _ LOAD_LOCALS 2
24 p LOAD_LOCALS 45
25 y LOAD_LOCALS 267
26 7 (+)
27 h LOAD_LOCALS 278
28 0 (??)
29 n LOAD_LOCALS 4
30 1 (?)
31 c LOAD_LOCALS 30
32 _ LOAD_LOCALS 2
33 b LOAD_LOCALS 94
34 y LOAD_LOCALS 267
35 4 (<<)
36 3 (+)
37 c LOAD_LOCALS 30
38 0 (??)
39 d LOAD_LOCALS 28
40 3 (+)
41 _ LOAD_LOCALS 2
42 m LOAD_LOCALS 6
43 4 (<<)
44 s UNARY_NOT_NEG 3
45 t LOAD_LOCALS 91
46 3 (+)
47 r UNARY_NOT 1
48 } LOAD_LOCALS 191
49 y LOAD_LOCALS 267
50 o LOAD_LOCALS 29
51 u UNARY_NOT 2
52   LOAD_LOCALS 12
53 p LOAD_LOCALS 45
54 a UNARY_NOT_NEG 1
55 s UNARY_NOT_NEG 3
56 s UNARY_NOT_NEG 3
57 e UNARY_NOT 3
58 d LOAD_LOCALS 28

Last, just construct the flag

Flag: hkcert24{u_r_r34l_py7h0n1c_by43c0d3_m4st3r}

Bashed! (500 pts)

Description

The program has only one meaningful function, and the function is less than 500 bytes long. What's hard understanding it?

Solution

Given .sh file, it is obfuscated and use some emojy as the identifier.

#!/bin/bash

FLAG=$1
GALF=1

if ! [[ "$FLAG" =~ ^[0-9A-Za-z_{}]{87}$ ]]; then echo 💔; exit 0; fi
function 🌚() { echo $(printf "%d" "'$1"); }
function 🌝() { echo $(printf "%x" "$1"); }
function 🍋() {
    u=$(echo -n ${FLAG:$(($(🌚 $1)-$(🌚 👂))):$(($(🌚 $2)-$(🌚 👂)))} | sha1sum);
    if [[ ${u:1:1} == $(🌝 $(($(🌚 $3)-$(🌚 👂)))) ]];
        then wget https://c22-bashed.hkcert24.pwnable.hk/$4.sh -O $(basename $0) >/dev/null 2>&1; GALF=$((GALF*$(🌚 $6)));
        else wget https://c22-bashed.hkcert24.pwnable.hk/$5.sh -O $(basename $0) >/dev/null 2>&1; GALF=$((GALF*$(🌚 $7)));
    fi;
}
function 👂() { 🍋 $1 $5 $2 $3 $7 $6 $4; }
function 👃() { 🍋 $5 $6 $4 $7 $3 $2 $1; }
function 👄() { 🍋 $5 $6 $2 $4 $3 $1 $7; }
function 👅() { 🍋 $7 $5 $3 $6 $2 $1 $4; }
function 👆() { 🍋 $5 $3 $7 $1 $4 $2 $6; }
function 👇() { 🍋 $6 $3 $2 $1 $7 $5 $4; }
function 👈() { 🍋 $3 $6 $5 $1 $7 $2 $4; }
----------
👜 👧 👌 👂 💕 👅 👶 💆  
👯 👂 👄 👴 👂 👅 👉 👳 \
👾 👑 👃 💈 👠 👮 👼 👳  
👬 👗 👴 👆 👭 👏 👅 👸  
👖 👽 👮 👓 👤 👋 👄 👯 \
👩 👃 💍 👸 👇 👘 👿 👂  
👍 👅 👙 👣 👄 💖 👓 👂  


if [[ "$GALF" -ne 0 ]]; then echo ❤️; else echo 💔; fi

To trace the actual code, we can utilize -x argument in bash.

So the code actually did the process like below

  • sha1(input[x:y]).hexdigest()[1] == z, return a if true else return b

We can validate it also through sending multiple different flags.

*above dump is output from the solver script (so ignore the wget because it has been patched to always return true"

>>> import hashlib
>>> hashlib.sha1(b"Gd").hexdigest()[1]
'd'
>>> hashlib.sha1(b"ut").hexdigest()[1]
'7'

"Gd" and "ut" is on the same index, which is 47,48. Through analysis by trying to send the first valid comparation, i also found that if the input is correct it will use fifth emoji and if it is wrong it will use the sixth emoji as the downloaded file. For example

+ 👚 👣 👺 👅 👳 👱 👄 👂
+ 🍋 👱 👄 👅 👺 👣 👳 👂

if [[ 3 == 3 ]] -> wget https://c22-bashed.hkcert24.pwnable.hk/👺.sh -O tmp.sh
if [[ 4 == 3 ]] -> wget https://c22-bashed.hkcert24.pwnable.hk/👣.sh -O tmp.sh

The code downloaded from valid input and invalid input is different, so we need to download the valid file (valid input). To make the program always return a valid code i patch the .sh file to always download the correct file.

if ! [[ "$FLAG" =~ ^[0-9A-Za-z_{}]{87}$ ]]; then echo 💔; exit 0; fi
function 🌚() { echo $(printf "%d" "'$1"); }
function 🌝() { echo $(printf "%x" "$1"); }
function 🍋() {
    u=$(echo -n ${FLAG:$(($(🌚 $1)-$(🌚 👂))):$(($(🌚 $2)-$(🌚 👂)))} | sha1sum);
    if [[ ${u:1:1} == $(🌝 $(($(🌚 $3)-$(🌚 👂)))) ]];
        then wget https://c22-bashed.hkcert24.pwnable.hk/$4.sh -O $(basename $0) >/dev/null 2>&1; GALF=$((GALF*$(🌚 $6)));
        else wget https://c22-bashed.hkcert24.pwnable.hk/$4.sh -O $(basename $0) >/dev/null 2>&1; GALF=$((GALF*$(🌚 $6)));
    fi;exit;
}
  • Modify line 8 to get the file same as the correct comparation

  • add exit, because we know the process is self replace so we need to do a modification to make the program run continously when we do a patch on each validation process

Now our solver will be looks like below

  • patch the program so it will download the correct code

  • if the comparation contain more than 1 byte value it will always false, so skip the patch process

  • run for 3 different input and create a last script to parse the debug information generated

from pwn import *
import os

context.log_level = 'error'

def write_true_exit(prev_data, check_var):
	f = open("tmp.sh", "r").read()
	start = f.index("fi;")
	out_data = f[:start+3] + "exit\n" + f[start+4:]
	target = "wget https://c22-bashed.hkcert24.pwnable.hk/$5.sh -O $(basename $0) >/dev/null 2>&1; GALF=$((GALF*$(🌚 $7)));"

	assert "${u:1:1}" in out_data
	assert target in out_data
	
	print(f"{check_var=}")
	if check_var:
		out_data = out_data.replace(target, "wget https://c22-bashed.hkcert24.pwnable.hk/$4.sh -O $(basename $0) >/dev/null 2>&1; GALF=$((GALF*$(🌚 $6)));")
	
	if prev_data != "":
		tmp_data = out_data.split("\n")
		index_val = ""
		for i in range(103, len(out_data)):
			# print(prev_data)
			if prev_data[:15] in tmp_data[i]:
				length = len(prev_data)
				length += 1
				index_val = i + (length // 16)
				print(length, index_val)
				break
		new_data = '\n'.join(tmp_data[:103])
		new_data += "\n"
		new_data += '\n'.join(tmp_data[index_val:])
		out_data = new_data

	outf = open("tmp.sh", "w")
	outf.write(out_data)
	outf.close()

def check_cmp(prev_data):
	f = open("tmp.sh", "r").read()
	start = f.index("fi;")
	out_data = f[:start+3] + "exit\n" + f[start+4:]
	out_data = out_data.replace("$(basename $0)", "$(basename $0)x")

	if prev_data != "":
		tmp_data = out_data.split("\n")
		index_val = ""
		for i in range(103, len(out_data)):
			# print(prev_data)
			if prev_data[:15] in tmp_data[i]:
				length = len(prev_data)
				length += 1
				index_val = i + (length // 16)
				print(length, index_val)
				break
		new_data = '\n'.join(tmp_data[:103])
		new_data += "\n"
		new_data += '\n'.join(tmp_data[index_val:])
		out_data = new_data

	outf = open("tmp.shx", "w")
	outf.write(out_data)
	outf.close()

def count_cmp(data):
	tmp = data.split(" == ")
	a1 = tmp[0].split("[ ")[-1]
	a2 = tmp[1].split(" ]")[0]
	print("count_cmp",a1, a2)
	if len(a2) > 1:
		return False
	return True


def get_prev(data):
	tmp_data = data.split("\n")
	for j in range(len(tmp_data)):
		if "GALF=" in tmp_data[j]:
			return tmp_data[j+2][2:]

# inp = "hkcert24{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZQWERTYUIOPASDFG}"
# inp = "}GFDSAPOIUYTREWQZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210{42treckh"
inp = "C5NnSxm32TPznxXOtS185Pr5sue2VKvNSMEdw{RO7eWYO{nGdHEmE5Klpg81G9IAZVuQkPh3njceH{MrnJryu{w"

debug_data = open("dump", "a")

prev_data = ""
target = 214
for _ in range(target):
	print(_, prev_data)
	check_cmp(prev_data)
	r = process(["bash", "-x", "tmp.shx", inp])
	tmp_debug = r.recvall().decode()
	r.close()

	check_var = count_cmp(tmp_debug)
	
	write_true_exit(prev_data, check_var)
	
	r = process(["bash", "-x", "tmp.sh", inp])
	tmp_debug = r.recvall().decode()
	r.close()
	
	prev_data = get_prev(tmp_debug)
	debug_data.write(tmp_debug)
	print()

debug_data.close()

For the last script, it does the following flow

  • Parse the data to get the actual index and value compared

  • Brute to get the list of possible value (too much possibility)

  • Reduce the possibility by check it recursively with the value next to it

import hashlib
from itertools import product
import string


def parse_index_cmp(f, inp):
	cmp_val = []
	inp_val = []

	for i in range(len(f)):
		if "echo -n" in f[i]:
			inp_val.append(f[i].split(" ")[-1].replace("'", ""))
		elif  "==" in f[i]:
			cmp_val.append(f[i].split("==")[-1].split(" ")[1])

	list_index = []
	for i in inp_val:
		index_val = inp.index(i)
		length = len(i)
		list_index.append([index_val, length])
	return list_index, cmp_val

def find_all(target, data):
	arr_index = []
	for i in range(len(data)):
		if target == data[i:i+len(target)]:
			arr_index.append(i)
	return arr_index

def find_intersection(arr1, arr2, arr3):
    set1 = set(arr1)
    set2 = set(arr2)
    set3 = set(arr3)
    result = list(set1.intersection(set2, set3))
    
    return result

def brute_char(tmp_found, length, check_val):
	arr_found = []
	if tmp_found == []:
		for i in product(string.printable[:-6], repeat=length):
			p = ''.join(i).encode()
			tmp_val = hashlib.sha1(p).hexdigest()
			if tmp_val[1] == check_val:
				arr_found.append(p)
	else:
		for p in tmp_found:
			tmp_val = hashlib.sha1(p[:length]).hexdigest()
			if tmp_val[1] == check_val:
				arr_found.append(p)
	return arr_found

def get_flag(arr, target_range):
	for i in range(target_range):
		tmp1 = arr[i]
		tmp2 = arr[i+1]
		new_arr = []
		new_arr_2 = []
		if len(tmp1[0]) != 1:
			for j in tmp1:
				for k in tmp2:
					if check_char(k.decode()):
						if len(tmp1[0]) == 2:
							if j[1] == k[0]:
								if k not in new_arr:
									new_arr.append(k)
								if j not in new_arr_2:
									new_arr_2.append(j)
						elif len(tmp1[0]) == 3:
							if len(tmp2[0]) == 1:
								if j[1] == k[0]:
									if k not in new_arr:
										new_arr.append(k)
									if j not in new_arr_2:
										new_arr_2.append(j)
							else:
								if j[1:3] == k[:2]:
									if k not in new_arr:
										new_arr.append(k)
									if j not in new_arr_2:
										new_arr_2.append(j)
			arr[i+1] = new_arr
			arr[i] = new_arr_2
	return arr

def check_char(target):
	list_char = string.ascii_uppercase + string.digits + string.ascii_lowercase + "_{}"
	return all(c in list_char for c in target)


f = open("dump4", "r").read().split("\n")
inp = "hkcert24{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZQWERTYUIOPASDFG}"
f2 = open("dump5", "r").read().split("\n")
inp2 = inp[::-1]
f3 = open("dump6", "r").read().split("\n")
inp3 = "C5NnSxm32TPznxXOtS185Pr5sue2VKvNSMEdw{RO7eWYO{nGdHEmE5Klpg81G9IAZVuQkPh3njceH{MrnJryu{w"

list_index_1, cmp_val_1 = parse_index_cmp(f, inp)
list_index_2, cmp_val_2 = parse_index_cmp(f2, inp2)
list_index_3, cmp_val_3 = parse_index_cmp(f3, inp3)

assert cmp_val_1 == cmp_val_2
assert cmp_val_1 == cmp_val_3

fix_list_index = []
for i in range(len(list_index_1)):
	if (list_index_1[i] != list_index_2[i]) or (list_index_2[i] != list_index_3[i]) or (list_index_3[i] != list_index_1[i]):

		val = inp[list_index_1[i][0]:list_index_1[i][0] + list_index_1[i][1]]
		arr_index_1 = find_all(val, inp)

		val = inp2[list_index_2[i][0]:list_index_2[i][0] + list_index_2[i][1]]
		arr_index_2 = find_all(val, inp2)

		val = inp3[list_index_3[i][0]:list_index_3[i][0] + list_index_3[i][1]]
		arr_index_3 = find_all(val, inp3)

		inter = find_intersection(arr_index_1, arr_index_2, arr_index_3)
		if len(inter) != 1:
			print(arr_index_1)
			print(arr_index_2)
			print(arr_index_3)
			exit()
		fix_list_index.append([inter[0], list_index_1[i][1]])
	else:
		fix_list_index.append(list_index_1[i])

d = {}

for i in range(len(fix_list_index)):
	if fix_list_index[i][0] not in d:
		d[fix_list_index[i][0]] = [[fix_list_index[i][1], i]]
	else:
		d[fix_list_index[i][0]].append([fix_list_index[i][1], i])

sorted_d = dict(sorted(d.items()))

all_found = []
for i in range(87):
	tmp_found = []
	for tmp_val in sorted_d[i]:
		if tmp_val[0] == 3:
			if len(cmp_val_1[tmp_val[1]]) == 1:
				tmp_found = brute_char(tmp_found, 3, cmp_val_1[tmp_val[1]])

	for tmp_val in sorted_d[i]:
		if tmp_val[0] == 2:
			if len(cmp_val_1[tmp_val[1]]) == 1:
				tmp_found = brute_char(tmp_found, 2, cmp_val_1[tmp_val[1]])

	for tmp_val in sorted_d[i]:
		if tmp_val[0] == 1:
			if len(cmp_val_1[tmp_val[1]]) == 1:
				tmp_found = brute_char(tmp_found, 1, cmp_val_1[tmp_val[1]])
	all_found.append(tmp_found)

target_range = 86
for _ in range(10):
	all_found = get_flag(all_found, target_range)

for i in all_found:
	print(i)
  • dump4, dump5, and dump6 is the output from debug file from previous script

[b'hkc']
[b'kce']
[b'cer']
[b'er']
[b'rt2']
[b't2']
[b'24']
[b'4{s']
[b'{s3']
[b's3']
[b'33m']
[b'3m']
[b'm1c', b'm1n', b'm1o', b'm1J', b'mya', b'mNd', b'mNi']
[b'1', b'y', b'N']
[b'n9l']
[b'9ly']
[b'ly']
[b'y_b']
[b'_b3']
[b'b3g']
[b'3g1']
[b'g19']
[b'19n']
[b'9n_']
[b'n_b']
[b'_b4']
[b'b4s']
[b'4sh']
[b'sh_']
[b'h_s']
[b'_sc']
[b'scj', b'scr', b'scE', b'scV']
[b'c']
[b'r1p']
[b'1p']
[b'p7s']
[b'7s_']
[b's_']
[b'_c0']
[b'c0']
[b'0u1']
[b'u1d']
[b'1d_']
[b'd_b']
[b'_b3']
[b'b3_']
[b'3_']
[b'_d4']
[b'd4n']
[b'4n9']
[b'n93']
[b'93r']
[b'3r0', b'3rw', b'3ry', b'3rC']
[b'r']
[b'0us', b'0u{']
[b'us4', b'us_', b'u{d', b'u{D', b'u{T']
[b's', b'{']
[b'_wh']
[b'wh3']
[b'h3']
[b'3n_']
[b'n_7']
[b'_7']
[b'7h3']
[b'h3']
[b'3y_']
[b'y_4']
[b'_4r']
[b'4r3']
[b'r3_']
[b'3_s']
[b'_s3']
[b's3']
[b'3l']
[b'lf_']
[b'f_m']
[b'_m0']
[b'm0d']
[b'0d1']
[b'd1']
[b'1f']
[b'fy1']
[b'y1n']
[b'1n9']
[b'n9d', b'n9e', b'n9i', b'n9n', b'n9o', b'n9p', b'n9H', b'n9X', b'n9}']
[b'9']
[b'q', b'C', b'S', b'T', b'U', b'W', b'/', b'}']

Now just reconstruct the flag, it is not hard because the flag is readable .

Flag: hkcert24{s33m1n9ly_b3g19n_b4sh_scr1p7s_c0u1d_b3_d4n93r0us_wh3n_7h3y_4r3_s3lf_m0d1fy1n9}

Looking at the directory structure we found "xamarin", so i assume that it use xamarin as the tech stack. In directory resources from JADX-GUI export, i found that there is assemblies directory that we can utilize to get the Xamarin DLL. Tool that i used to do the unpack is .

Check the to see how to use the frontend and explanation of what this really is! You will try out one of the hardcore reversing method: dealing with assembly and understand them directly!

Here is the if you need more references.

Challenge:

Playground:

(dump2.asm)

📚
📖
https://github.com/jakev/pyxamstore
step by step guide
full documentation
https://c58a-ish-1.hkcert24.pwnable.hk?id=3
https://c58b-ish-2.hkcert24.pwnable.hk?id=1
https://gist.github.com/kos0ng/910b3e8d78534b1329a78f1330c28119
Here
Here
Here
Here
Here
Here
Here
Here
Here
https://c09-void.hkcert24.pwnable.hk/