Wing IDE Security Study
- Tutorial

Good health! I will not be surprised that you have not even heard of this program before. Like me, until the day when Python Debugger came in handy . Yes, I know, there is pdb , but its functionality and how it is presented, I did not like at all. After a short search, I came across this wonderful product. There is everything that can come in handy in debugging your Python applications (I’ll say right away: I haven’t studied this language, so if any inaccuracies come up, please do not swear).
Caution: repeating the steps from the article, you act at your own peril and risk!
So, we are starting ...
The patient, I must say, is unusual. Firstly: it comes with source (!!!), albeit in bytecode; secondly, as it sometimes happens ... in general, you will 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 on the absence of Python , so install it too . I need version 2 (currently it is version 2.7.9 ). Run the program again. This time offers to install patches, and restart. So let's do it.
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 key length (20, not including hyphens), and the characters with which it should begin. In principle, from this it is already possible to start exploring protection - we will find this line in the program files.
More interesting. The search result was found in the file ./bin/2.7/src.zip !
Yes Yes. Everything is really so: the program comes with the source. We will have to delve into them.
Stage two: we rummage in source codes
Turn on search in archives in Total Commander and find that line again. The line is in the file: ./bin/2.7/src.zip/process/wingctl.pyo . PYO files are binaries 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 have come in handy to me:
- Easy Python Decompiler ( EPD ) - a shell in which two decompilers are protected ( Uncompyle2 and Decompyle ++ );
- Fork Uncompyle2 - sometimes unpacks what others can not unpack.
So unpack the entire archive src.zip a folder the src (next to already have a folder the src , albeit there is unpacked and everything else) and set on it the EPD :

We are waiting for the end of the process, and go to inspect what happened. And the output was decompiled files with the end of _dis . We will rename them .py . Everything would be fine, but it turns out that there are also files with the ending _dis_failed , which means that the decompiler did not master these files. Fortunately, there is only one file: edit / editor.pyo_dis_failed
Let's try to set 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 delete all the pyo / pyc files from the src folder, and rename .py * _dis to .py .
Next, we repeat all of the above for the opensource.zip archive , unpacking it in a neighboring folder of the same name. I decided not to touch the external.zip archive , because, having examined it, you can see that there are libraries that can be installed separately for our Python. So let's do it:
pip install docutils- py2pdf - put it in the external folder;
- Imaging-1.1.7 - run and install. Youcan deletefrom the external folder;
- pygtk is the same as with the previous files.
The remaining libraries ( pyscintilla2 and pysqlite ) are simply extracted from the external.zip archive and decompiled, as before.
Stages three and four: the source code itself. Debugging
After searching the 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 at startup the source files from the src , external , opensource folders of the root directory with the Wing IDE (and not with the script) will be used .
Looking at the root folder, I found another src folder , and .py files in it. Throw them into our src folder , with overwriting (here all the same originals, not decompiled files).
Now, all three folders (indicated slightly above), copy to the root directory of the program. Let's try to win ...
Run the Wing IDE , and open the wing.py file from the directory in itbin directory . Next, in the menu Debug -> Debug Environment ... in the parameter field specify --use-src . Now start the debugger ( F5 key ). If all the fraud with copying folders was 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 about the bad license id ( wingctl.py ) earlier , and put the crack before this message:

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

We press Continue and we get on
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 that the License ID characters belong to the textutils.BASE30 set :
BASE30 = '123456789ABCDEFGHJKLMNPQRTVWXY'
Like other checks in __ValidateAndNormalize (id) no. We correct the identifier we entered and repeat again. We have already passed the test for 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]
Because we have a Professional version, then the second character must be N - correct, and return. abstract.ValidateAndNormalizeLicenseID (id) walked without errors. Perfectly. Oops:
if len(errs) == 0 and id[0] == 'T':
errs.append(_('You cannot enter a trial license id here'))
Fixim (I chose E ), and continue. Having run my eyes below the code, I did not find anything in addition to the previous checks, so I boldly let go of debugging further on F5 . New window:

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

First check function: abstract.ValidateAndNormalizeActivation (act) . It again checks for BASE30 affiliation . Check for the prefix that we have already passed:
if id2[:3] != kActivationPrefix:
errs.append(_("Invalid prefix: Should be '%s'") % kActivationPrefix)
The next interesting place:
err, info = self.fLicMgr._ValidateLicenseDict(lic2, None)
if err == abstract.kLicenseOK:
Go to self.fLicMgr._ValidateLicenseDict . Here the hash from the license is formed:
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 input box, although it differs a few digits. Okay, let's think that there are some random parts that do not affect activation (which, incidentally, will be further confirmed!).
Next, the first three characters are cut off from the activation code, remove hyphens, convert to BASE16 , and add 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 lichash ( request code ), the name of the operating system for which the key is made, the version of the program, and the converted activation code. Why did I pay attention to this place? The fact is that this control is a pyd- file (which can be seen by adding the name of the object 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 knows how to do. Well then, let's look at it from the Hex Rays decompiler...
Step Five: This Is Not Python
Pull our control ( ctlutil.pyd ) into IDA Pro 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 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 sub_10001410 procedure, this one looks most interesting:
if ( sub_10001020(v6, &v9) || strcmp(&v9, v7) )
{
result = PyInt_FromLong(0);
}
We’ll go into sub_10001020 too. It would be interesting not to name the variables by sight, but to give a name and name them properly. So let's do it. Set up the IDA Pro debugger :

I think everything is clear from the screenshot: we specified an application that will eventually load our pyd- file.
Now put the breaker at the beginning of sub_10001020 , and begin to look into the variables and input parameters. After a short debugging process, we come to such a listing of the function:
Convert_reqest_key function code
int __usercall convert_reqest_key@(char *version@, const char *platform@, 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 of calling 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 convert_reqest_key function and then compared with that converted activation code. Remember that conversion?
Next, the first three characters are cut off from the activation code, remove hyphens, convert to BASE16 , and padded with zeros, if necessary
So, to get the correct activation code, we can now do the following:
- Let the conversion function convert_reqest_key be executed ;
- At the place of strcmp execution , look for the contents of out_key ;
- Remove extra zeros at the beginning of out_key ;
- Convert out_key back to BASE30 ;
- Add to the beginning of the resulting line the removed three characters ( AXX );
- Optionally stick hyphens every five characters.
I won’t philosophize slyly, but squeeze print right into the python program code:
print("AXX" + textutils.BaseConvert("FCBCFEFD2FF684FA6A4F", textutils.BASE16, textutils.BASE30))
I got a key at the output:
wingide - 2015/05/24 04:03:47 - AXX3Q6BQHKQ773D24P58
Entering it into the activation key input field, I received the cherished:

RESULTS
As you can see, the hacking process is not so much complicated as interesting! Exploring your own source code in a compiled version of it ... this, of course, is funny.
I don’t know why the authors attached their sources to their program (albeit for the most part, in the form of bytecode). But I think you understand that this is not worth it!
Thanks to all.