Log in

View Full Version : I'm reversing a compiled TCL+Tk under Linux, can't find where it connects to the net


seriousman
May 14th, 2010, 12:15
Hi,

So, I've been trying to crack a program under Linux using IDA.
Interestingly, the program seems to be written in TCL/TK, which switched my interests from cracking the program to understanding how it works. I tried searching in the Internet information about reversing/cracking Tcl executable but found none.
So here is the result of what I found, that can be helpful for interested in the future (You'll find my question in the end):

After being called, the program calls _Tcl_findexecutable to find the tcl interpreter then start a new interpreter using _Tcl_CreateInterp.
Now using the call _Tcl_SetVar it passes the arguments (argv0..) to the TCL interpreter, calls the Tk interepreter through the call _Tk_MainLoop.

Now through the two commands _Tcl_CreateCommand and _Tcl_CreateObjCommand the program creates the tcl functions using their "real" name and links to their address in the program, a little example:

Code:

.text:08054EE6 mov [esp+638h+var_628], 0
.text:08054EEE mov [esp+638h+var_62C], 0Fh
.text:08054EF6 mov [esp+638h+var_630], offset sub_805FB00
.text:08054EFE mov [esp+638h+var_634], offset aStoreactivatio ; "storeActivationKey"
.text:08054F06 mov [esp+638h+var_638], edi
.text:08054F09 call _Tcl_CreateCommand


and
Code:

.text:08055026 mov [esp+638h+var_628], 0
.text:0805502E mov [esp+638h+var_62C], 16h
.text:08055036 mov [esp+638h+var_630], offset sub_804E020
.text:0805503E mov [esp+638h+var_634], offset aShowhelp ; "showHelp"
.text:08055046 mov [esp+638h+var_638], edi
.text:08055049 call _Tcl_CreateObjCommand


I would say that _Tcl_CreateObjCommand creates classes where _Tcl_CreateCommand create simple command, but I'm not sure. Remark, that the commentaries are added by IDA and are the values of the used offsets.
So you probably maybe think, cracking a executable Tcl program is simple, as you have the original name of all procedures.
Unfortunately it is not.

For example in my case, I was trying to crack a software protection that required a serial that will be checked, the serial then is stored in a file and a connection to the internet is made to activate it. Now I cracked the part were the program checks if the serial is valid and write to a file, but I can't find how and were the program connects to the internet, there is no calls, no function nothing.

So, if you have cracked before a compiled Tcl/Tk program and have some ideas, tell me. I will keep you updated.

p.s. This is my first post, so I'm not sure if it is ok to say the name of the software I'm cracking. Please tell me if it is. Anyone interested in the software please msg me and I will give you the link for the demo and the name.

FrankRizzo
May 14th, 2010, 20:42
Sure, PM me a link.

FrankRizzo
May 15th, 2010, 12:56
OK, I think I can tell you what's going on here. The main exe houses 2 things, it houses the code that instantiates the TCL interpreter, and it houses some "native methods". These are the things that you can see, things like checkLicenseType, and storeActivationKey. I think these methods were written in C, and just call the TCL Interpreter's methods to pass the values back and forth to the script. Now, the chunks that do things like access the web to validate the entered key are written in straight TCL, and compiled to bytecode, and stored in one of the .dat files in the Data directory. (It's also possible that they're stored encrypted, and decrypted before being passed to the parser, but that's just conjecture at this point). I believe that the reason you are unable to locate the actual URL is that it's built "on the fly" by the TCL code. So, that code doesn't exist in the .exe.

So, to complete this target, you have a couple of options. You can either spend some time learning TCL bytecode, so that you can "manually disassemble" the functions stored in the .dat files, or you can use the hosts file to map their host to localhost, and you try to return them what they're looking for. (Granted you'd be flying blind, as that code is for all intents and purposes "missing", but it could be something simple. I did an app once that returned "YES" or "NO" from the server.) So, since you have a log of what was sent to the server, and what it got back, that might be something that you can tackle.

FrankRizzo
May 15th, 2010, 13:31
A couple of quick notes:

The .dat files in question on Linux are in the usr/lib/<PRODUCT NAME> directory.
On Windows, they're in the <INSTALL LOCATION>/Data directory.

deluxeT.dat file is the same on both windows, and Linux.
zrad.dat and dw.dat don't appear to do anything, as I 00'd them out, and it still asks for the key.
hw.dat is different between windows and Linux though, but replacing the windows one with the Linux one made no obvious difference. But 00'ing it out caused me to get the "Enter your key" popup box, but it was blank. hw.dat could contain the "resources", and my blanking erased the background image, the text, and the "dialog box template" (for lack of a better name).

seriousman
May 16th, 2010, 06:20
Yeah, I have remarked the dat file, they contain surely some resources, but I don't think that they are bytecodes.
Plus, I researched Tcl bytecode, and it seems that it executable simply using the command
%tclsh code.tbs
I've tried and the command didn't even recognize the headers.

FrankRizzo
May 16th, 2010, 09:59
OK, I've found some stuff. (I'm working on both the windows & Linux versions at the same time, since they're basically the same, I figured one might give a little more insight than the other. In this case, it certainly did!)

sub_8054B70 calls sub_805FD90. This function gets the file size, and passes it to the all important function sub_805F380. This function reads the data in, and does a simple descrambling on it, and stores it in a supplied return buffer. This buffer is then executed with TCL_Eval.

To make things simpler to understand, I'll paste in the code from the windows version, as it's easier to understand.

Here's the top level function:

Code:
signed int __cdecl sub_412570(int a1, int a2)
{
int retVal; // edi@2
int v3; // ST14_4@3
FILE *v4; // eax@3
void *Memory; // [sp+8h] [bp-F8h]@1
char Filename; // [sp+Ch] [bp-F4h]@1
unsigned int v8; // [sp+FCh] [bp-4h]@1

v8 = (unsigned int)&Memory ^ SecurityCookie;
sprintf(&Filename, "%s/%s", &dataDir, a2);
if ( PreprocessData(&Filename, (int *)&Memory) <= 0 )
return 1;
retVal = Tcl_Eval(a1, Memory);
free(Memory);
if ( retVal == 1 )
{
v3 = *(_DWORD *)(a1 + 8);
v4 = _iob_func();
fprintf(v4 + 2, "Error file '%s' line %d\n", a2, v3);
return 1;
}
return 0;
}


Which calls PreprocessData:

Code:
signed int __cdecl PreprocessData(const char *Filename, int *Memory)
{
size_t retVal; // eax@1
char v4; // [sp+4h] [bp-30h]@1
size_t fileSize; // [sp+18h] [bp-1Ch]@3

retVal = stat64i32(Filename, &v4);
if ( retVal != -1 )
retVal = fileSize;
return readAndDecodeData(Filename, 0, retVal, Memory);
}


And finally, readAndDecodeData:

Code:
signed int __cdecl readAndDecodeData(const char *Filename, __int32 Offset, size_t DstBufSize, int *Memory)
{
signed int result; // eax@2
int *tempBuf; // ebx@3
unsigned int BufferSize; // esi@3
int Buffer; // eax@3
FILE *fileHandle; // edi@5

if ( DstBufSize == -1 )
{
result = -1;
}
else
{
BufferSize = DstBufSize - 1;
Buffer = (int)malloc(DstBufSize);
tempBuf = Memory;
*Memory = Buffer;
if ( Buffer )
{
fileHandle = fopen(Filename, "rb";
if ( fileHandle )
{
fseek(fileHandle, Offset, 0);
if ( fread(&DstBufSize, 1u, 1u, fileHandle) == 1 ) // Variable reuse here, DstBufSize at this point becomes "cryptbyte"
{
if ( fread((void *)*tempBuf, 1u, BufferSize, fileHandle) == BufferSize )
{
fclose(fileHandle);
decodeBuffer(*tempBuf, BufferSize, DstBufSize); // Same thing here, DstBufSize is really "cryptbyte".
result = BufferSize;
}
else
{
fclose(fileHandle);
free((void *)*tempBuf);
result = -1;
}
}
else
{
fclose(fileHandle);
free((void *)*tempBuf);
result = -1;
}
}
else
{
free((void *)*tempBuf);
result = -1;
}
}
else
{
result = -1;
}
}
return result;
}


This should be enough to get you going. If not, let me know.

FrankRizzo
May 16th, 2010, 11:31
OR, you know, if you got REALLY bored, you could write some code that looked like this:

Code:
#include <errno.h>
#include <fcntl.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>


int main(int argc, char *argv[])
{
unsigned char cryptByte;
unsigned char result;
unsigned char *buffer;
int counter;
int inFile, outFile;
int fileSize;

if(argc != 3)
{
printf("USAGE: %s InputFile OutputFile\n", argv[0]);
exit(EXIT_FAILURE);
}

inFile = open(argv[1], O_RDONLY);
if(inFile == -1)
{
printf("Unable to open %s for reading [%s]\n", argv[1], strerror(errno));
exit(EXIT_FAILURE);
}

outFile = open(argv[2], O_WRONLY | O_CREAT);
if(outFile == -1)
{
printf("Unable to open %s for writing [%s]\n", argv[2], strerror(errno));
exit(EXIT_FAILURE);
}

fileSize = lseek(inFile, 0, SEEK_END);
lseek(inFile, 0, SEEK_SET);

// The 1st byte in the file is the crypto byte
if(read(inFile, &cryptByte, 1) == -1)
{
printf("Unable to read cryptobyte from %s [%s]\n", argv[2], strerror(errno));
close(inFile);
close(outFile);
unlink(argv[3]);
exit(EXIT_FAILURE);
}

buffer = (unsigned char *)malloc(fileSize);
if(!buffer)
{
printf("Unable to malloc %d bytes for buffer\n", fileSize);
close(inFile);
close(outFile);
unlink(argv[3]);
exit(EXIT_FAILURE);
}

memset(buffer, 0, fileSize);

if(read(inFile, buffer, (fileSize - 1)) == -1)
{
printf("Unable to read buffer full from %s [%s]\n", argv[2], strerror(errno));
close(inFile);
close(outFile);
unlink(argv[3]);
free(buffer);
exit(EXIT_FAILURE);
}

close(inFile);

for(counter = 0; counter < fileSize; counter++)
{
result = buffer[counter];
result -= (counter & 0xFF);
result -= cryptByte;
result &= 0xFF;
buffer[counter] = result;
}

buffer[counter] = 0x00;

if(write(outFile, buffer, fileSize) == -1)
{
printf("Unable to write buffer full to %s [%s]\n", argv[3], strerror(errno));
close(outFile);
unlink(argv[3]);
free(buffer);
exit(EXIT_FAILURE);
}

close(outFile);
free(buffer);
printf("All Done!\n";
}


And run the .dat files through it, and what comes out the other end could be useful to you.

FrankRizzo
May 16th, 2010, 15:17
After a little deobfuscation you might even come up with something that looks like this:

Code:
proc ActivateLicense { Parent }
{
global $TARGET_LicenseG $TARGET_EditionG macAddressG URL $TARGET_ActReasonG $TARGET_StatusG

set msg "To use, your license needs to be activated. You must be connected to the internet so the key can be sent to the server for activation. Would you like to proceed?"
set res [MessageBox $msg question okcancel $Parent ""]
if { $res == "cancel" }
{
destroy $Parent
return "true"
}
set url "${URL}/activate.php?license=${$TARGET_LicenseG}&ma=${macAddressG}&os=[GetOS]&edition=${$TARGET_EditionG}&reason=${$TARGET_ActReasonG}"
set mbox [DoTasksWhileWaiting "$TARGET_: license activation" "Please wait while server is contacted..." $Parent]
set cval [catch { set res [http::geturl $url -timeout 15000 ]} err]
wm transient $mbox ""
destroy $mbox

if { $cval || ([string first "200" [http::code $res]] == -1) }
{
MessageBox "Error" error ok $Parent "Server may be down, please try later."
return "true"
}

set ServerResponse [string trim [http::data $res]]

if { [string first "Error (exceeded)" $ServerResponse] != -1 }
{
MessageBox "Error" error ok $Parent "License already activated. Please contact us at\n${URL}/support.php with your license key if this seems in error."
return "true"
}
elseif {[string first "Error (not found)" $ServerResponse] != -1}
{
MessageBox "Error" error ok $Parent "License not found. Please contact us at\n${URL}/support.php with your license key."
return "true"
}
elseif {[string first "Error" $ServerResponse] != -1 || [string length $ServerResponse] != 205}
{
MessageBox "Error" error ok $Parent "Server error, please try again later."
return "true"
}
else
{
if { [storeActivationKey $ServerResponse] == "error" }
{
MessageBox "Error" error ok $Parent "Activation error. Please contact us at ${URL}/support.php ."
return "true"
}
set $TARGET_StatusG "active"
MessageBox "License activated" info ok $Parent "Success. Your license has been activated"
return "false"
}
}


Where $TARGET_ is the target name.

Once you got done snooping, and modifying the raw TCL code, you could write a little app like the one above that does the opposite (adds the byte count, and "cryptbyte" to the data, and recreate the .dat files with your fixes in place). If it were me, I'd use $00 for the cryptbyte just to be fancy.

seriousman
May 17th, 2010, 10:35
I'm really amazed, just a question how did you get back the C code from the routine sub_805F380 .
Did you do it by hand, or did you use the tool.

Thanks for all the help. I will try to finish the rest tomorrow or the day after and post the results here.

p.s. By the way, I've just found out what the second number in the activate.php is: the MAC address Hopefully it's impossible to trace persons from MAC addresses.

EDIT: Okay, a bit of research showed me it was Hex-Ray. Man, many things changed since last time I cracked back in 2003. Everything is simpler

FrankRizzo
May 17th, 2010, 15:27
The tools are much better I'll give you that, but the best tool is still above the shoulders.

In theory, you can just patch the TCL code, whip up a quick "re-encryptor", and be done.

If you dig through the TCL more, you can find the place where they display the dialog to ask for the serial, and the code that calls checkLicenseType, etc.

Let me know if you still have any issues.

FrankRizzo
May 19th, 2010, 22:18
Yeah, sorry, I can't leave it hanging. Here's the tool that does both encryption/decryption.

Code:
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <memory.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAXFILELEN 512

int main(int argc, char *argv[])
{
unsigned char cryptByte;
unsigned char result;
unsigned char *buffer;
int counter;
int inFile, outFile;
int fileSize;
int opt;
char input[MAXFILELEN], output[MAXFILELEN];
bool bDecrypt;

if(argc < 3)
{
printf("USAGE: %s -d (for decrytpt) -e (for encrypt) -i <input file> -o <output file> -k <Key (in decimal, for output only)>\n", argv[0]);
exit(EXIT_FAILURE);
}

while((opt = getopt(argc, argv, "dei:k:") != -1)
{
switch(opt)
{
case 'i':
memset(input, 0x00, MAXFILELEN);
strncpy(input, optarg, (MAXFILELEN - 1));
break;

case 'o':
memset(output, 0x00, MAXFILELEN);
strncpy(output, optarg, (MAXFILELEN - 1));
break;

case 'k':
cryptByte = atoi(optarg);
break;

case 'e':
bDecrypt = false;
break;

case 'd':
bDecrypt = true;
break;

default:
printf("Unknown option '%c'\n", opt);
printf("USAGE: %s -d (for decrytpt) -e (for encrypt) -i <input file> -o <output file> -k <Key (in decimal, for output only)>\n", argv[0]);
exit(EXIT_FAILURE);
}
}

inFile = open(input, O_RDONLY);
if(inFile == -1)
{
printf("Unable to open %s for reading [%s]\n", input, strerror(errno));
exit(EXIT_FAILURE);
}

outFile = open(output, O_WRONLY | O_CREAT);
if(outFile == -1)
{
printf("Unable to open %s for writing [%s]\n", output, strerror(errno));
exit(EXIT_FAILURE);
}

fileSize = lseek(inFile, 0, SEEK_END);
lseek(inFile, 0, SEEK_SET);

if(bDecrypt == true)
{
// The 1st byte in the file is the crypto byte
if(read(inFile, &cryptByte, 1) == -1)
{
printf("Unable to read cryptobyte from %s [%s]\n", argv[2], strerror(errno));
close(inFile);
close(outFile);
unlink(output);
exit(EXIT_FAILURE);
}
}
else
{
// The 1st byte in the file is the crypto byte
if(write(outFile, &cryptByte, 1) == -1)
{
printf("Unable to write cryptobyte to %s [%s]\n", argv[2], strerror(errno));
close(inFile);
close(outFile);
unlink(output);
exit(EXIT_FAILURE);
}
}

buffer = (unsigned char *)malloc(fileSize);
if(!buffer)
{
printf("Unable to malloc %d bytes for buffer\n", fileSize);
close(inFile);
close(outFile);
unlink(output);
exit(EXIT_FAILURE);
}

memset(buffer, 0, fileSize);

if(read(inFile, buffer, (fileSize - 1)) == -1)
{
printf("Unable to read buffer full from %s [%s]\n", argv[2], strerror(errno));
close(inFile);
close(outFile);
unlink(output);
free(buffer);
exit(EXIT_FAILURE);
}

close(inFile);

for(counter = 0; counter < fileSize; counter++)
{
result = buffer[counter];
if(bDecrypt == true)
{
result -= (counter & 0xFF);
result -= cryptByte;
}
else
{
result += (counter & 0xFF);
result += cryptByte;
}
result &= 0xFF;
buffer[counter] = result;
}

buffer[counter] = 0x00;

if(write(outFile, buffer, fileSize) == -1)
{
printf("Unable to write buffer full to %s [%s]\n", output, strerror(errno));
close(outFile);
unlink(output);
free(buffer);
exit(EXIT_FAILURE);
}

close(outFile);
free(buffer);
printf("All Done!\n";

exit(EXIT_SUCCESS);
}


Not perfect, but functional.

FrankRizzo
May 20th, 2010, 17:05
And last, but not least. You could write some TCL code that looked like this:

Code:
source $fontPropsFileG
array set defaultChineseFont [array get chineseFontG]
# crack starts here
set TARGET_StatusG "active"
# crack ends here
if { $TARGET_StatusG == "none" } {if { [OOOOO00OO00OO00O "normal"] == "true" } {exit}


I show the surrounding code so that you know where to put the "patch". Also, substitute TARGET_ with the target name.

Now, for those of you NOT involved in this task, thanks for your patience. I promise to drop it now, as the only thing left to do would be to rewrite it in a better language.

seriousman
May 25th, 2010, 05:59
You're a perfectionist :P
Sorry, I was planning to write the tool, but found no time because of my studies.
Anyway, thanks man for everything, your help was very useful, I've learned much.