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.
Yet another crackme. To begin, download the attachment and install it on your android device.
Solution
Given APK file, decopmile it using JADX-GUI.
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 https://github.com/jakev/pyxamstore.
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.
privateboolcheckFlag(string f) {int[] array =newint[] {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 =newint[] {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 =newulong[] { 16684662107559623091UL,13659980421084405632UL,11938144112493055466UL,17764897102866017993UL,11375978084890832581UL,14699674141193569951UL };ulong num =14627333968358193854UL;int num2 =8;Dictionary<int,int> dictionary =newDictionary<int,int>();for (int i =0; i <array.Length; i++) {dictionary[array[i]] =array2[i]; }StringBuilder stringBuilder =newStringBuilder();foreach (char c in f) {stringBuilder.Append((char)dictionary[(int)c]); }int num3 = num2 -f.Length% num2;string text =stringBuilder.ToString() +newstring('\u0001', num3);List<ulong> list =newList<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 =newList<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]) {returnfalse; } }returntrue; }
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 =14627333968358193854dictionary ={}for i inrange(len(array)): dictionary[array2[i]]= array[i]dictionary[1]=0tmp = []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 stringdefall_smt(s,initial_terms):defblock_term(s,m,t): s.add(t != m.eval(t, model_completion=True))deffix_term(s,m,t): s.add(t == m.eval(t, model_completion=True))defall_smt_rec(terms):if sat == s.check(): m = s.model()yield mfor i inrange(len(terms)): s.push()block_term(s, m, terms[i])for j inrange(i):fix_term(s, m, terms[j])yield fromall_smt_rec(terms[i:]) s.pop()yield fromall_smt_rec(list(initial_terms))a = bytes.fromhex("CE21DB64D150E01B0D3EFB0A522F949DAFB1586B8AEEC1F0FC190AE3E91ED04A62F247A8200BD36C1C1C565B9BB34D3DCE8380C0E67EE809BD14C447A1F62FD1315C1E10D12AE05345FE8558A6AD03CC104BD394FE62854E4A2735E2940F499186D780354C67C3AA3C67E83FE46723DE8E2D462536E1F3907D0FB9148CE7B6A61A9080798535FD518C10E93F32CC4BB542DEF55713C8099B4D1984915F9D773031C7288D1DF471E4D1A30C0759AA0DAD163540B9286AB54C245A8DA6A6C456DDC09BBFCCDE0C5BC175DD77BBF62B431D1303AD73A3AC4DEAA52FC23E4A1AF56572E54A10108CFB100A4D7971F6C7805464B002AAD87C3953ECADB44E2FEBE04700")
b = bytes.fromhex("BD10B650BD35BF787F0A98611F1CCBA9F0D96C05EEACB8989D773CBC812EA0793D8B77DD7F6FE3022B433868A8D7120AA1DCF5F38321DC67DA669B77D3A955E26E3A2E628E5E886236A1E72D91F23293677BBDF0CD10DA7F2C78568AA0382EE1B188B0471304F7C46314DB0CBB134BEFBB722414598390A40969CC7AE29E8C8F69A1ED4DE950A232FE248A547FFF14811DB6C139778A70F32C77B2CE37AD07036EBE18F84290418AE6FC62346ACE529A796A358A4D3581224328D296D49B2CEE9FFD8FBE817833F0068215CEC17472426433C31790DE12DBC370A1567E2D921545BA7A624FEFCF7E553E4A42A9B3E86551EF609BB71E5A6798CBC1204192DA6E00")
flag =b"hkcert24{"for i inrange(28): flag +=bytes([a[i] ^ b[i]])haystack = [BitVec("x{}".format(i), 8)for i inrange(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 =5s.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 inall_smt(s, haystack): tmp_flag =b""for i in haystack:try: tmp_flag +=bytes([model[i].as_long()])exceptExceptionas 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
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
defpost(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
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.
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 the step by step guide 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!
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 xora = [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 xora = [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.
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
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.
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.
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/shpython/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
1 i LOAD_LOCALS 182 n LOAD_LOCALS 43 p LOAD_LOCALS 454 u UNARY_NOT 25 t LOAD_LOCALS 916 h LOAD_LOCALS 2787 k LOAD_LOCALS 488 c LOAD_LOCALS 309 e UNARY_NOT 310 r UNARY_NOT 111 t LOAD_LOCALS 91122 (<<)134 (<<)14{ LOAD_LOCALS 015 u UNARY_NOT 216 _ LOAD_LOCALS 217 r UNARY_NOT 118 _ LOAD_LOCALS 219 r UNARY_NOT 1203 (+)214 (<<)22 l UNARY_NOT_NEG 223 _ LOAD_LOCALS 224 p LOAD_LOCALS 4525 y LOAD_LOCALS 267267 (+)27 h LOAD_LOCALS 278280 (??)29 n LOAD_LOCALS 4301 (?)31 c LOAD_LOCALS 3032 _ LOAD_LOCALS 233 b LOAD_LOCALS 9434 y LOAD_LOCALS 267354 (<<)363 (+)37 c LOAD_LOCALS 30380 (??)39 d LOAD_LOCALS 28403 (+)41 _ LOAD_LOCALS 242 m LOAD_LOCALS 6434 (<<)44 s UNARY_NOT_NEG 345 t LOAD_LOCALS 91463 (+)47 r UNARY_NOT 148} LOAD_LOCALS 19149 y LOAD_LOCALS 26750 o LOAD_LOCALS 2951 u UNARY_NOT 252 LOAD_LOCALS 1253 p LOAD_LOCALS 4554 a UNARY_NOT_NEG 155 s UNARY_NOT_NEG 356 s UNARY_NOT_NEG 357 e UNARY_NOT 358 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.
"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
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.
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