Reverse Engineering
EzPyracy (380 pts) π₯
PyArmor, Python DLL, License Validation
N3RV3_GL17CH (500 pts) - upsolve
Packed, Polynomial, Input Validation
EzPyracy (380 pts)
Description
-
Solution
Given PE32+ executable, decompile it using IDA. Through strings we can see that this executable compiled with PyInstaller.

Extract using this code , then we will see there is a .pyc file of the challenge. Using pylingual we can see that the decompiled result shows that the code is protected using pyarmor.

At first i tried well known method using https://github.com/Svenskithesource/PyArmor-Unpacker/tree/main but it failed, so i decided to reopening some notes I made in the past regarding pyarmor, because i've been solving several pyarmor challenges in past and also created a pyarmor challenge in past.
I decided to use the same approach like the first link i show above, so using pyinjector to spawn python shell. Finding the main frame manually then we can do some enumeration and actually we can do disassembly for the running function. E.g

But we can't extract all function because most of functions are stay encrypted. If you look at code above, we can see that there is a call to a function named C_ENTER_CO_OBJECT_INDEX, we can utilize that to decrypt the encrypted functions and the constant passed to that function can be obtained also from co_consts.

Okay now we just need to create a recursive function to decrypt all pyarmor packed modules and then dump the pyc file.

Next, we have several options with the pyc file
pylingualto decompile itpycdasordis.disto disassemble it
Through both output we can reconstruct the important part of the program
So basically the first part of defeating pyarmor protection is quite useless, since the validation is in DLL and the data passed to the DLL function from the python is actually "unprocessed" (also there is log lol). The next step is reverse engineering the DLL part, we can utilize IDA to decompile and debug it.
Set breakpoint on validate function
Set process options as following
Applications:
<python13_path\python.exe>Parameters:
test.py
After we send input, breakpoint will hit and we can start debugging the DLL.

This program validate our license in format BLOCK1-BLOCK2-BLOCK3-BLOCK4. Validation is done by checking each block and relation for each block which is tied with username. Given username is SecuriNets, so our objective is to generate a valid license for username SecuriNets.
Block3
Calculate hash for given username (
SecuriNets)Block3 = hash(username) ^ 0x5a5a
Block1 and Block4
Block1 and Block4 converted to base36
Do comparation as following
(to_base36(Block1) + to_base36(Block4)) & 0xFFFFF == (4 * hash(username) & 0xFFFFF)Looks like bruteforceable
Block2
Compare derived 12 lower bits of Block2 with derived value from Block3
((171 * username_length) ^ to_base36(Block2)) & 0xFFF == (Block3[0]<<6) | (2*Block3[1]) | (Block3[2]&1)Looks like bruteforceable
Block Relation Check
Block4 first character
It must same as Sum of Block1 characters and username length
Block4 and Block1 Block2
Block4[1] + Block4[2] == (Block1[0] ^ Block2[3]) & 0x3F(Block4[3] * Block2[0] + Block1[1]) % 37 == Sum of digits Block3
Vowel and digit counts comparation
The
count of digitsin the serial (mod 7) must match thecount of specific vowelsin the username (mod 7)
Last, input serial to the program (GUI or reconstructed python script) to validate it.

Flag: Securinets{00E7-I4IX-7353-0W11}
N3RV3_GL17CH (500 pts)
Description
-
Solution
Given PE32 executable, open it using IDA.

Through main function we can see that the program is packed, i don't know what kind of packer but I have seen similar packers several times so breakpoint at following jmp eax and the next instruction will be the real instruction.
During breakpoint on jmp eax, we can force IDA to reanalyze to make us easier to check the unpacked codes (or you can dump the the unpacked PE using scylla on x32dbg). If we continue the the process we'll see a GUI application and an input form as shown below

Input Conversion
So the first idea that came to my mind is setting up breakpoint at function that parse user input and i found that it is user32_GetDlgItemTextA . From microsoft documentation we know that the third argument will be the variable that store the value. So looking at the third argument in the end of GetDlgItemTextA function will show the value parsed.


There are two value returned from GetDlgItemTextA , first one is static value which is mov[edem+20h+var_9]eax and the second one is our input. Let's setup hardware breakpoint on our input.

At first i tried to follow process above that process our input but in the end it does nothing to our input, then in the next hardware breakpoint we will see the actual code.

sub_408680 basically do conversion as following
To validate the result, breakpoint at following address and check ecx value
Return from function sub_408680 (sub_4087D0) is the core process of this program, it does calculation and validation. To validate this assumption, i tried to modify the return for random input to 1 and now the program popup a message.

Okay now we can narrowing our focus to this function.
MD5 Function
In the end of function we can see some constant, through searching and validating through input output we know that this function is MD5 hash.
input (72 bytes) -> output (16 bytes)



Big Integer Conversion
Now we know the plaintext passed to MD5 function, which is v506. That variable are filled in following block
Following is sub_407A20 which is just another conversion right before MD5 hash.
Breakpoint at following address
eax is an array, so let's create a script to dump eax returned from sub_407A20.
Validated it through second argument after sub_407A20 called
So basically this function do conversion from array that stores piece of big integer values (base 2^30) to single integer value.
Polynomial Functions
With my limited knowledge of cryptography i tried to understand this stripped, no string library reference, and no constants reference functions.
At first, i tried to ask GPT to determine each function in this block but it failed and it can only show generic information about it. At least now i know that it is polynomial things (although some functions are misinterpreted).

Until this step we know that the process as following
Input
("AB")-> convert to integer(0x263)-> polynomial things([1061301508, 1], [1012031152]...)-> convert to 72 bytes(042D427FB05E523C1...)-> MD5 hash(9792731806F56...)
The only left is the polynomial things which seems to be the one that plays the biggest role. If we take a look on the code, we can see that there is a pattern where in fact the entire code is several blocks of code repeated. Following is one block
In my analysis i renamed some function that do free memory to narrowing our focus only to poly related function, back to sub_408680 that we already have the returned values, the output is array 2 dimension with length 6 as following, so we assume that this is polynomial with degree = 6.
During debugging, i found there are many data passed as argument that has same structure. E.g in following code
If we take a look on above function call, we'll see several arguments passed and the values returned are written in 2nd argument, we can know this because at initial the 2nd argument will be all zero and after function call it will be filled with different values based on arguments. Following is script to dump the values.
Output
Arg3 (input)
0x19eee0 [[408265793], [591766954, 2], [368106728, 3], [54215304, 2], [744138553, 3], [90226521, 1]]
Arg2 (output)
0x19eeb8 [[816531586], [109792089, 1], [736213461, 2], [108430613], [414535287, 3], [180453042, 2]]
Through converting it to single integer we can see that sub_418170 looks like doubling the input. But there is something wrong
Some of values are showing False output
Setting up breakpoint at Arg3 (input) we can see that there is big integer calculation as following
Preparation
If we set breakpoint on
[esi+4],[esi+8], or[esi+0xc]we can see how the prepared values processed


We can see that our input processed with value [0x3FFFFFFB, 3] which is same as 0xfffffffb. If we check the operation (subtraction) and the function result we can see that the subtraction is part of modulo operation and this whole function do modulo to our doubled values. Let's rewrite our code.
So in context of this challenge, this operation is polynomial doubling or P(x) + P(x) andt he polynomial are over the finite field within P = 0xfffffffb (2^32 - 5). Now we can continue to next function.
Modify dump_arg.py as following to dump sub_41A850 argument and returned values
Output
Arg1
0x272159c [[401877261], [319151251], [448897956, 1], [373083325], [135070006], [998919543, 1]]
Arg4
0x19ef50 [[816531586], [109792089, 1], [736213461, 2], [108430613], [414535287, 3], [180453042, 2]]
Arg5
0x19eedc [[780246396], [323573734, 2], [414923207], [565161876], [438396663], [753613874, 2]]
Arg3 (result)
0x19eeb4 [[854080791, 2], [475215032], [872537324, 1], [820548349, 1], [298070272, 2], [62262485, 2]]
In sub_41A850, you'll see that Arg1 is static and the value passed is not from stack (not started with 0x19...) while the others are from stack. Because we already know how the polynomial in this executable works, we can do blackbox through input output and some debugging to determine what kind of operation of each polynomial functions. First, let's ask AI to create several function that do basic polynomial operation and combine it with our previous functions, then try it with arguments and result from sub_41A850 .
After some trial and debugging we found that sub_41A850 do polynomial multiplication with Arg1 as modulus polynomial.

Do the same approach for rest function and following is my final check function.
Reconstructing Check Function
After renaming each function now we can see it clearly how this function works
static_value (probably a key)
MD5 -> simple calculation that convert hash to exponent -> compute polynomial (1 time using X as base value) -> polynomial multiplication inverse -> conv2 function -> MD5
user_input
conv function -> use as exponent -> compute polynomial (3 times using Y as base value) -> compute polynomial from static value -> compute result from both polynomial -> polynomial multiplication inverse -> conv2 function -> MD5
Next, create a python script that reconstruct the whole check function.

Until this step we've successfully recovered the whole check function.
Solving Crypto Part
Basically we just need to find a correct value that match value before last MD5, thus we can skip last MD5 process. We also can see that our input is used as an exponent, so maybe this challenge lead to discrete log problem. We can try to leverage AI to solve this challenge with a DLP approach, because this part is too crypto for me.

Trying the input with the executable we can ensure that it shows the same hash value which means it is correct.


Talking with the author and he confirms that the input is intended to be varies as long as it is valid input then it will be one of valid flag. Besides that we also can generate bunch of valid flag, because we already know the order and n value.


Flag: Securinets{<valid_input>}
Last updated