📜 ⬆️ ⬇️

Wing IDE Protection Study



Good health! I would not be surprised that you have not even heard about this program before. Like me, until the day when the Python Debugger came in handy. Yes, I know, there is pdb , but its functionality and the way it is presented, I didn’t like at all. After a brief search, I stumbled upon this wonderful product. There is everything that can be useful in debugging your Python applications (I’ll say right away that I haven’t studied this language, so if any inaccuracies come up, please don't swear).

Caution: repeating actions from the article, you act at your own risk!

So, we start ...


The patient, I must say, is unusual. First of all: it comes with source code (!!!), even in bytecode; secondly, as sometimes happens ... well, see.

First of all, download the program ( Wing IDE Professional v 5.1.4 ). Install, inspect the folder. The main executable file is located at ./bin/wing.exe . Run it. Swears at the lack of Python , so install it. Version 2 is required (at the moment it is version 2.7.9 ). Run the program again. This time offers to install patches, and restart. So do.
')
Now a window pops up asking for a license (because we have a pro version). Let's introduce some nonsense:



We get the following answer:



What is funny: the program itself tells us the length of the key (20, not including the hyphens), and the characters with which it should start. In principle, it is already possible to start exploring protection from this - we will find this line in the program files.
Further - more interesting. The search result was found in the file ./bin/2.7/src.zip !

Yes Yes. Everything is really like this: the program comes with source. In them we also should dig.

Stage Two: Digging the Source


Let's include in Archive search in Total Commander , and we will find that line again. The line is in the file: ./bin/2.7/src.zip/process/wingctl.pyo . PYO files are binary with optimized Python bytecode.

Fortunately for Python, there are a couple of bytecode decompilers. In order not to bother you with searches, I will give links to those that came in handy to me:

  1. Easy Python Decompiler ( EPD ) - a shell in which two decompilers are wired ( Uncompyle2 and Decompyle ++ );
  2. Fork Uncompyle2 - sometimes unpacks what others can not unpack.

So, we will unpack the entire src.zip archive into the src folder (next to it there is the src folder, let them unpack and everything else) and add EPD to it:



We wait until the end of the process, and we go to inspect what happened. And it turned out decompiled files with the end of _dis . We will rename them to .py . Everything would be fine, but it turns out that there are also files with the end of _dis_failed , which means that the decompiler has not mastered these files. Fortunately, there is only one file: edit / editor.pyo_dis_failed

Let's try to incite Decompyle ++ on it ... The same trouble. No wonder I gave a link to a spare decompiler, because it was he who did what others failed. Now, remove all pyo / pyc files from the src folder, and rename the .py * _dis file to .py .

Next, repeat all of the above for the archive opensource.zip , unpacking it in the next folder of the same name. Archive external.zip I decided not to touch, because, having examined it, you can see that there are libraries that can be installed separately for our Python. So do:

  1. pip install docutils 
  2. py2pdf - put it in the external folder;
  3. Imaging-1.1.7 - run and install. From the external folder can be deleted;
  4. pygtk is the same as previous files.

The remaining libraries ( pyscintilla2 and pysqlite ) are simply extracted from external.zip , and decompiled as before.

Stages three and four: the source code itself. Debugging


Having rummaged through Python scripts, I came across the wing.py file in the root of the program folder. And the first comment tells us:
 # Top level script for invoking Wing IDE. If --use-src is specified # as an arg, then the files in WINGHOME/src, WINGHOME/external, # WINGHOME/opensource will be used; otherwise, the files in the version # specific bin directory will be used if it exists. 


In a nutshell: if the script is given the --use-src parameter, then the source will be used from the src , external , opensource directories of the root directory with the Wing IDE (and not with the script).

Looking into the root folder, I found another src folder, and .py files in it. Throw them in our src folder, with overwriting (here all the same originals, not decompiled files).

Now all three folders (indicated just above), copy to the root directory of the program. Let's try it ...

Run the Wing IDE , and open the wing.py file from the bin directory in it. Next in the menu Debug -> Debug Environment ... in the parameter field specify - use-src . Now start the debugger ( F5 key). If all frauds with copying folders were successful, we will get a second copy of the running Wing IDE . Perfectly!

Next: open the file in the parent Wing IDE , in which we found the line on the bad license id ( wingctl.py ) earlier , and set the bryak to this message:



In the debugging Wing IDE, go to the menu Help -> Enter License ... , and enter the key according to the rules (remember ?: 20 characters, with the first one from the set ['T', 'N', 'E', 'C' , '1', '3', '6'] ):



We press Continue and we get on grandma bryaku. The first interesting function: abstract.ValidateAndNormalizeLicenseID (id) . Go to it for F7 . There's one more: __ValidateAndNormalize (id) . Head over to her.

First validation check:
 for c in code: if c in ('-', ' ', '\t'): pass elif c not in textutils.BASE30: code2 += c badchars.add(c) else: code2 += c 

We see that we are required to have the characters of the License ID belong to the textutils.BASE30 set:
 BASE30 = '123456789ABCDEFGHJKLMNPQRTVWXY' 

There seems to be no other checks in __ValidateAndNormalize (id) . We correct the identifier entered by us and we repeat again. We have already checked the first character:
 if len(id2) > 0 and id2[0] not in kLicenseUseCodes: errs.append(_('Invalid first character: Should be one of %s') % str(kLicenseUseCodes)) 

And here is the second character:
 if len(id2) > 1 and id2[1] != kLicenseProdCode: 

 kLicenseProdCodes = {config.kProd101: '1', config.kProdPersonal: 'L', config.kProdProfessional: 'N', config.kProdEnterprise: 'E'} kLicenseProdCode = kLicenseProdCodes[config.kProductCode] 

Since we have the Professional version, then the second character must be N - correct, and come back. abstract.ValidateAndNormalizeLicenseID (id) passed without errors. Perfectly. Oops:
 if len(errs) == 0 and id[0] == 'T': errs.append(_('You cannot enter a trial license id here')) 

Fix (I chose E ), and continue. Having run through the eyes below the code, I did not find anything in addition to the previous checks, so I boldly released debugging further along F5 . New window:



We enter a random text, we get an error message (again 20 characters, and the activation code must start with AXX ), we find it in the files, set the breakpoint:



The first verification function is abstract.ValidateAndNormalizeActivation (act) . It again checks on the ownership of BASE30 . Check for the prefix, which we have already passed:
 if id2[:3] != kActivationPrefix: errs.append(_("Invalid prefix: Should be '%s'") % kActivationPrefix) 

The following interesting place:
 err, info = self.fLicMgr._ValidateLicenseDict(lic2, None) if err == abstract.kLicenseOK: 

Go to self.fLicMgr._ValidateLicenseDict . A license hash is formed here:
 lichash = CreateActivationRequest(lic) act30 = lic['activation'] if lichash[2] not in 'X34': hasher = sha.new() hasher.update(lichash) hasher.update(lic['license']) digest = hasher.hexdigest().upper() lichash = lichash[:3] + textutils.SHAToBase30(digest) errs, lichash = ValidateAndNormalizeRequest(lichash) 

If you look at the contents of lichash after executing this block, you will notice that its text is similar to the request code displayed in the activation code entry box, although a few numbers are different. Okay, we will think that there are some random parts that do not affect activation (which, by the way, will be further confirmed!).

Then, the first three characters are cut off from the activation code, hyphens are removed, converted to BASE16 , and padded with zeros if necessary:
 act = act30.replace('-', '')[3:] hexact = textutils.BaseConvert(act, textutils.BASE30, textutils.BASE16) while len(hexact) < 20: hexact = '0' + hexact 

And here it is, the most interesting:
 valid = control.validate(lichash, lic['os'], lic['version'][:lic['version'].find('.')], hexact) 

Some control calls the validate function, passing it the lichash ( request code ), the name of the operating system for which the key is being made, the version of the program, and the converted activation code. Why did I stop attention at this place? The fact is that this control is a pyd file (as can be seen by adding the object name to watch , and looking at the __file__ field), which are ordinary DLLs with one exported function (not validate ), which gives Python information about what she can do. Well, let's look at it from the Hex Rays decompiler ...

Stage Five: This Is Not Python


Drag in our control in IDA Pro ( ctlutil.pyd ) and look at the exported initctlutil function:
 int initctlutil() { return Py_InitModule4(aCtlutil, &off_10003094, 0, 0, 1013); } 

off_10003094 is a structure in which the names and the address of the exported methods are indicated. Here is our validate :
 .data:100030A4 dd offset aValidate ; "validate" .data:100030A8 dd offset sub_10001410 

Of all the code that contains the procedure sub_10001410, the most interesting is this:
 if ( sub_10001020(v6, &v9) || strcmp(&v9, v7) ) { result = PyInt_FromLong(0); } 

Head over to sub_10001020 too. It would be interesting not to give the names of variables by eye, but to give them a bad name and name them. So do. Configure the IDA Pro debugger:



I think everything is clear from the screenshot: we have specified an application that will eventually load our pyd file.

Now set the breakpoint at the beginning of sub_10001020 , and start looking into the variables and input parameters. After a short debugging process, we come to the following listing function:
Convert_reqest_key function code
 int __usercall convert_reqest_key@<eax>(char *version@<eax>, const char *platform@<ecx>, const char *activation_key, char *out_key) { unsigned int len_1; // edi@1 const char *platform_; // esi@1 char *version_; // ebx@1 int ver_; // eax@2 signed int mul1; // ecx@3 signed int mul2; // esi@3 signed int mul3; // ebp@3 bool v11; // zf@15 const char *act_key_ptr; // eax@31 char v13; // dl@32 const char *act_key_ptr_1; // eax@35 unsigned int len_2; // ecx@35 char v16; // dl@36 const char *act_key_ptr_2; // eax@39 unsigned int len_3; // ecx@39 char v19; // dl@40 int P3_; // ebx@42 const char *act_key_ptr_3; // eax@45 unsigned int len_4; // ecx@45 char v23; // dl@46 unsigned int P4; // ebp@47 signed int mul4; // [sp+10h] [bp-18h]@0 unsigned int P3; // [sp+14h] [bp-14h]@1 unsigned int P2; // [sp+18h] [bp-10h]@1 unsigned int P1; // [sp+1Ch] [bp-Ch]@1 len_1 = 0; platform_ = platform; version_ = version; P1 = 0; P2 = 0; P3 = 0; if ( !strcmp(platform, aWindows) ) { ver_ = (unsigned __int8)*version_; if ( *version_ == '2' ) { mul1 = 142; mul2 = 43; mul3 = 201; mul4 = 38; goto LABEL_31; } if ( (_BYTE)ver_ == '3' ) { mul1 = 23; mul2 = 163; mul3 = 2; mul4 = 115; goto LABEL_31; } if ( (_BYTE)ver_ == '4' ) { mul1 = 17; mul2 = 87; mul3 = 120; mul4 = 34; goto LABEL_31; } } else if ( !strcmp(platform_, aMacosx) ) { ver_ = (unsigned __int8)*version_; if ( *version_ == '2' ) { mul1 = 41; mul2 = 207; mul3 = 104; mul4 = 77; goto LABEL_31; } if ( (_BYTE)ver_ == '3' ) { mul1 = 128; mul2 = 178; mul3 = 104; mul4 = 95; goto LABEL_31; } if ( (_BYTE)ver_ == '4' ) { mul1 = 67; mul2 = 167; mul3 = 74; mul4 = 13; goto LABEL_31; } } else { v11 = strcmp(platform_, aLinux) == 0; LOBYTE(ver_) = *version_; if ( v11 ) { if ( (_BYTE)ver_ == '2' ) { mul1 = 48; mul2 = 104; mul3 = 234; mul4 = 247; goto LABEL_31; } if ( (_BYTE)ver_ == '3' ) { mul2 = 52; mul1 = 254; mul3 = 98; mul4 = 235; goto LABEL_31; } if ( (_BYTE)ver_ == '4' ) { mul1 = 207; mul2 = 45; mul3 = 198; mul4 = 189; goto LABEL_31; } } else { if ( (_BYTE)ver_ == '2' ) { mul1 = 123; mul2 = 202; mul3 = 97; mul4 = 211; goto LABEL_31; } if ( (_BYTE)ver_ == '3' ) { mul1 = 127; mul2 = 45; mul3 = 209; mul4 = 198; goto LABEL_31; } if ( (_BYTE)ver_ == '4' ) { mul2 = 4; mul1 = 240; mul3 = 47; mul4 = 98; goto LABEL_31; } } } if ( (_BYTE)ver_ == '5' ) { mul1 = 7; mul2 = 123; mul3 = 23; mul4 = 87; } else { mul1 = 0; mul2 = 0; mul3 = 0; } LABEL_31: act_key_ptr = activation_key; do v13 = *act_key_ptr++; while ( v13 ); if ( act_key_ptr != activation_key + 1 ) { do P1 = (P1 * mul1 + activation_key[len_1++]) & 0xFFFFF; while ( len_1 < strlen(activation_key) ); } act_key_ptr_1 = activation_key; len_2 = 0; do v16 = *act_key_ptr_1++; while ( v16 ); if ( act_key_ptr_1 != activation_key + 1 ) { do P2 = (P2 * mul2 + activation_key[len_2++]) & 0xFFFFF; while ( len_2 < strlen(activation_key) ); } act_key_ptr_2 = activation_key; len_3 = 0; do v19 = *act_key_ptr_2++; while ( v19 ); if ( act_key_ptr_2 != activation_key + 1 ) { P3_ = 0; do P3_ = (P3_ * mul3 + activation_key[len_3++]) & 0xFFFFF; while ( len_3 < strlen(activation_key) ); P3 = P3_; } act_key_ptr_3 = activation_key; len_4 = 0; do v23 = *act_key_ptr_3++; while ( v23 ); P4 = 0; if ( act_key_ptr_3 != activation_key + 1 ) { do P4 = (P4 * mul4 + activation_key[len_4++]) & 0xFFFFF; while ( len_4 < strlen(activation_key) ); } sprintf(out_key, a_5x_5x_5x_5x, P1, P2, P3, P4); return 0; } 


And the place to call this function takes the following form:
 if ( convert_reqest_key(version, platform, request_key, out_key) || strcmp(out_key, act_key_hash) ) { result = PyInt_FromLong(0); } 

From all this we can conclude that the request code is converted using the function convert_reqest_key and then compared with the converted activation code. Remember that conversion?
Then, the first three characters are cut off from the activation code, hyphens are removed, converted to BASE16 , and padded with zeros if necessary.

So, in order to get the correct activation code, we can now proceed as follows:
  1. Let the convert_reqest_key conversion function execute ;
  2. Look at the contents of out_key at the place of execution of strcmp ;
  3. Remove extra zeros at the beginning of out_key ;
  4. Convert out_key back to BASE30 ;
  5. Append three characters to the beginning of the resulting string ( AXX );
  6. Navigate hyphens every five characters if desired.

I will not philosophize slyly, but I will squeeze print directly into python , the program code:
 print("AXX" + textutils.BaseConvert("FCBCFEFD2FF684FA6A4F", textutils.BASE16, textutils.BASE30)) 

At the output I received a key:
wingide - 2015/05/24 04:03:47 - AXX3Q6BQHKQ773D24P58


Entering it in the input field of the activation key, I received the cherished



RESULTS


As you can see, the hacking process is not so difficult as it turned out to be interesting! It’s fun to explore your sources in the compiled version ...

I do not know why the authors attached to its program its source code (albeit for the most part, in the form of byte-code). But I think you understand that you should not do this!

Thanks to all.

Source: https://habr.com/ru/post/258613/


All Articles