Hacker Challenge III Phase 1 - Gynvael Coldwind - vexillium

25 downloads 269 Views 181KB Size Report
Oct 20, 2008 ... Hacker Challenge III Phase 1 by Gynvael Coldwind [email protected] team Vexillium. 1. Background. The long awaited Hacked ...
Hacker Challenge III Phase 1 by Gynvael Coldwind [email protected] team Vexillium

1. Background The long awaited Hacked Challenge III has finally started on Monday, the 20 th of October 2008. The task was similar to the last years – there is a PE file that does some math using data from an external source (or to simply put it – from a file). However, it is missing a password, it has some not welcomed limits, and, finally, it has a very interesting mathematical formula, that is to be taken out of the application by using pure reverse engineering techniques. The formal objectives were only reversing the formula, and patching the limits, however to test the patch, recovering or bypassing the password is required anyway. This report it shared into a few sections, in which I describe how the attack was performed, how much time did it take, and what tools were used. At the end of the report, a short conclusion, containing some comments about the protection, is present. One last remark before I start – I have separated Attack Narrative and Time to Break sections, just like the previous years.

2. Attack Narrative The original package with the application contained an executable file final.exe, a data source file data.in, and two validation files with results expected to be output when the password is correct – password.results, and when the limits are removed – final.results. Additionally, a PDF with instructions was also in the package (see Background section).

Reconnaissance My first step was to get to see what am I dealing with. I started by putting the final.exe to an entropy test (see Tools section), and have learned the following few things: 1. No protector is used – if a protector was used, almost all the entropy would be in the red area.

2. There are probably 2 or 3 (A and B) encrypted functions in the code are – the red entropy level. 3. There probably are two encrypted ares in the data area (C and D). 4. There probably is some constant padding at the end of the file (E). That would be all as far as entropy goes. Now it was time to feed the file to IDA, and to execute it to see what happens.

Password attack After execution, the application displays an out of scale chart (the blue line at the top of the window).

I've created a dump (dumped.exe) of the process at this point (by attaching OllyDbg and using the OllyDump plugin). A good idea would be to rebuild the imports here, however I have expected some complications related to checksum-type-protections, so I decided to stick to the original binary, in addition to using only non-INT3 breakpoints (memory rights, trap flag, hardware breakpoints). At this time the analysis of final.exe has finished, and it appeared to be a standard Microsoft Visual C++ product, and additionally, the app seems to be really straight forward. At first sight that is :)

IDA pinpoints the location of WinMain, which in this case is located at VA 403F50. Looking through the function, I've noticed some MessageBox calls with error strings like “File "data.in" not found”. Knowing where the error handling is allows back tracing the code to reach the handling of data.in file. The error is directly related to the results of call at VA 40404F to the function at VA 4039A0. Entering the function shows that it's either ciphered or forged later (probably ciphered though). Now a little switch to the dumped.exe analysis in IDA, to see if this function is deciphered there, and... it is. So the analysis continues from here. The first thing, even before opening the file, is setting some variables with straaange values: mov mov mov mov mov mov mov mov

[ebp+var_28], 6D890A09h [ebp+var_24], 3D02712h [ebp+var_20], 46A20F75h [ebp+var_1C], 1E2CF0EFh [ebp+var_18], 5C2C3392h [ebp+var_14], 0D836FF6Fh [ebp+var_10], 0B9792E74h [ebp+var_C], 0A9A0EBE0h

This probably is a hash or checksum of something (TBD later). After this there is a call to fopen, and some data reading functions. Some lines later, a string “salt” appears (VA 403AAD), which almost pinpoints the location of password validating function. lea push push call ... push push call mov xor mov push mov push mov push lea push lea push mov call mov push push mov lea push call mov

ecx, [ebp+String] 100h ; MaxCount ecx ; Buf ebx ; fgets edi 8 ; ds:malloc edi, eax eax, eax [edi], eax 4 ; [edi+4], ax offset aSalt ; [edi+6], al 8 ; eax, [ebp+Src] eax ; ecx, [ebp+var_4] ecx ; byte ptr [edi+7], copy_or_sth edx, [ebp+var_4] edx eax [ebp+var_8], eax eax, [ebp+var_4] eax hash_or_sth ecx, eax

Size

int "salt" Size Src int 40h

The “salt” seems to be in some way appended to the entered password, and then a hash_or_sth function is called. Let's take a look at that function: ... mov mov mov mov mov mov mov

dword dword dword dword dword dword dword

ptr ptr ptr ptr ptr ptr ptr

[eax], 6A09E667h [eax+4], 0BB67AE85h [eax+8], 3C6EF372h [eax+0Ch], 0A54FF53Ah [eax+10h], 510E527Fh [eax+14h], 9B05688Ch [eax+18h], 1F83D9ABh

mov mov ...

dword ptr [eax+1Ch], 5BE0CD19h [esp+224h+var_200], 428A2F98h

Entering the DWORDs in Google shows that it's the SHA-256 hash with non or little modifications. Since I didn't want to waste time to crack the SHA-256 hash (the straaange values at the beginning of the functions are the hash) and risk the hashing function being modified in any way, I decided to check if the password if just used in a CMP/JE manner, or are it's values used for anything else. And, a short analysis of the usage of the password and hash buffers showed that it's not really used for anything else. So the object was to find a proper CMP/JE, and patch it out in some way that doesn't trigger the checksums. The CMP/JE instruction (or CMP/JNE in this case) responsible for the final password validation is the following: .text:00403B05 .text:00403B08

cmp jnz

eax, 8 short loc_403B3D

The “8” there, is the number of hash DWORDs that have to match, for the validation to be passed. Since I didn't have any part of the password, I decide to change that “8” to “0”, in that case any incorrect password would work (however the correct password would not, but since I don't have it, it's OK anyway). But how to patch it? This area is encrypted, and probably checksumed in some way too.

Patch mechanism I figured to create a virus-like patch. Mainly, the patch would extend the last (.rsrc) section, extend the ImageSize, and change the AddressOfEntryPoint to the extended .rsrc section. The code in the .rsrc section would place an IAT hook on fopen, which is called in the program AFTER the code is decrypted. And then the code would do a standard PUSH/RET sequence to return to the OEP. After the fopen hook would hit, it would remove the hook, patch the password CMP/JNE sequence, and jmp to the original fopen function. This way every checksum mechanism that would potentially be called before fopen, and that would NOT checksum the PE header, would be useless. See the illustration.

For the source of the patcher and patch see the Tools section. OK, so currently, I have the password patched out, a patching mechanism working, and an open way to do the challenge objectives. The application of course displayed the correct chart:

Break the limits The first (in chronological terms) objective I completed, was the limit removal. It appeared to be very easy, since the limits weren't concealed, and at this point, weren't “checksumed” anyway. I'll just paste here one of several limit checks, since they are very similar. lea eax, [ebp+String] push eax ; String call ds:atof fcomp qword ptr [edi] add esp, 4 fnstsw ax test ah, 5 jp short loc_403B27 fld qword ptr [edi] jmp short loc_403B37 loc_403B27: lea ecx, [ebp+String] push ecx ; String call ds:atof

add esp, 4 loc_403B37: fstp dbl_409060

The schema is simple – atof changes a string to a float, then the comparison is made (fcomp), and then some jump takes place. If the jump takes place, then another atof is called for the same string, and the returned value is stored in a global variable (RED-GREEN-VIOLET path). However, if that jump would not be taken, then the limit is enforced, and stored in the variable (RED-BLUE-VIOLET path). The following Virtual Addresses have been patched by me to get rid of the limits: LOCATION

BYTES

TO WHAT

403c35

1

jmp (EB)

403c89

1

jmp (EB)

403bf8

3

nops (909090)

403b93

1

jmp (EB)

403b21

1

jmp (EB)

The chart displayed after the limits are taken of is the following:

Let's move the the second and final objective.

The final objective The final objective was to reverse engineer a formula, that was located in function H that was in function G that was in function F. Since the location was described (imho) pretty much accurately (see the instructions in the original package), it wasn't as hard to find as I had predicted. Functions F, G and H have the following offsets: LOCATION

FUNCTION

4017F0

F

4014F0

G

4013D0

H

The H function contained was build of two things: –



an import-breakpoint detection routine (which, since I used only hardware breakpoints wasn't effective at all) the formula written as floating point operations and calls do methods responsible for operations on complex numbers

The formula was a little mixed up, but no anti-RE tricks were used in it anyway. The most interesting (imho) part of the formula was the inlined pow(float,int) function: VA 401492: mov eax, esi fld ds jge short loc_40149E neg eax loc_40149E: fld1 fld st loc_4014A2: test al, 1 jz short loc_4014A8 fmul st, st(2) loc_4014A8: shr eax, 1 jz short loc_4014B2 fld st(2) fmulp st(3), st jmp short loc_4014A2 loc_4014B2: test esi, esi fstp st(2) jge short loc_4014BC fdivrp st(1), st jmp short loc_4014BE loc_4014BC: fstp st loc_4014BE: lea eax, [ebp+arg_4] fstp qword ptr [ebp+arg_4]

The above function translated to a higher level language: double pow(double what, int power) { double result = 1; int worker_power = (power >= 0) ? power : - power; while(worker_power != 0) { // Power if(worker_power & 1) result *= worker_power; // Iterate worker_power >>= 1; what *= what; } if(power < 0) result = 1.0 / result; return result; }

Of course, it's just a power function after all, so I've substituted it in the formula with a “call” to the function pow. The final reversed formula looks like this: result = exp(LN2 * log(p_C * p_C) * (double)p_14 - d_408838h[p_14] log(f_401180((double)p_14 + p_4 + g_405238))) * pow(g_405248, p_14);

The formula uses a global array of doubles (d_408838), two global variables (g_405238, and g_405248), and an internal function (f_401180). Additionally, there are a few parameters used in the functon. The mathematical formula looks like this:

results=e

2

 LN2∗log pC  ∗ p 14 −d 408838 [ p14 ]−log f 401180  p 14  p4 g 405238 

 p 14 

∗ g 405248 



And that completed the second objective.

A look after After finishing and sending the task, I decided to check what security mechanisms were used – hence I choose such and non other vector of attack, I haven't really stumbled upon much security mechanisms, so this would be my only chance to see what's inside. A few things that have caught my attention: 1. The time measuring of the exception handling (VA 402280): push 0FFFFFFFFh push offset unk_4053F0 push offset _except_handler3 mov eax, large fs:0 push eax mov large fs:0, esp ... call ds:GetTickCount mov [ebp+var_38], eax push 0FAh ; dwMilliseconds call ds:Sleep mov [ebp+var_4], 0 int 3 ; Trap to Debugger ... loc_4023A6: call ds:GetTickCount sub eax, [ebp+var_38] add eax, 0FFFFFF06h cmp eax, 6D6h ja loc_40256F ... loc_40256F: ; Code push 0FFFFFFFFh call ds:exit

So basically, the function sets the a SEH-based exception handling, then acquires the time, and issues a software breakpoint exception. If there is a debugger present which doesn't forward INT3 to the application, it will go into the interactive mode. When a user will continue the debugging, the second time is acquired, and the time delta is calculated and compared to 1750ms. If it's above, then it terminated the process (exit(-1)). To break this mechanism, the attacker only has to turn on INT3 forwarding to the application in his debugger (many people has this turned on by default). Also the 1750ms limit is kinda large, since an attacker on a fast machine if a medium reflex can click “continue” before the 1750ms time limit exceeds (well, there is a 250ms Sleep earlier, so there is only 1500ms for reaction). 2. Checksum mechanisms I wondered if the checksum mechanisms I've expected to exist, do really exist. Yes, they do. And they use the “SHA-256” to do the checksums. So just checking the references to the “hash_or_sth” (as I've marked it) functions shows two checksumed places:

LOCATION

CHECKSUM START

CHECKSUM BYTE COUNT

4010CD

403F50

280h

40400C

403930

620h

There are several ways to bypass this mechanism. The one that I had chosen is described in the attack narrative. 3. Parts of code encrypted Well I'm not really going to write much here. Some parts were encrypted, however they were decrypted at the beginning of the code, so a simple PE dump is sufficient to bypass this security mechanism.

Appendix: a comment on the password Another thing I've tried afterwards, was to break the password hash. I've created a brute-force tool based on a dumped version of final.exe (see the tool section for description and code). I've been running the brute for four different charsets (a-z, A-Za-z0-9, 0-9._-, a-z0-9) on a Quad Core machine for the last few days, and I also tried dictionary attacks, but I couldn't get the password cracked. This means could mean one of three things: 1. I screw the brute force script somewhere and it slipped my tests 2. The password uses a different charset than I expected or there was not enough time to check the proper combination (since the brute force is still running as I write this report). 3. The hash or the function in final.exe were changed in some way after generating the initial hash Well, that's that.

3. Time to break Disclaimer: All approximation.

the

numbers

I

give

here

are

pretty

much

only

an

WHAT

TIME TO BREAK

Total time

5h

Reconnaissance

10m

COMMENT +1h trying to get my debugger running on x86-64 Vista

Getting to know the 50m application

Running around the code, making dump, looking for tha password

Breaking the password 1h

Spent on writing and testing the patch (I've got the patcher

Breaking the limits

1h

They were pretty straight forward, however it took a little time to do them anyway.

Finding the formula

20m

Looking through lots of code to find the function

Reversing the formula

1h 40m

FPU reverse engineering took some time, more than I've expected

Defeating encryption

the 5m

Just a dump

Defeating checksums

the 0m

Ignored due to the chosen attack path

Defeating the import- 0m breakpoint detection

Ignored due breakpoints

Defeating exception time detection

Ignored due to having INT3 forwarded by default

the 0m handling debugger

Time spent debugging

to

using

only

a

hardware

on 30%

Time spent in IDA

50%

Mainly reversing the formula and looking for limits.

Time spent searching 0% for information on the web Time tools

spent

making 0%

Time spent on drinking 20% tea and thinking

Since all my tools I've used were coded earlier, I've just coded the patchers body (7 lines?) and the patch, and both are not really tools.

4. Tools used IDA Pro Simply because it is the best interactive disassembler created up to date. OllyDbg A great graphical debugger for Windows family. Additionally I've used OllyDump plug-in for dumping the image from memory to disk. Hexplorer My favorite lite hex editor. I like its “Matrix” theme :) MinGW GCC, NASM C, C++ and Assembler compilers. GVIM My favorite code/text editor. Windows Calc A calculator with shiny buttons. Ent A tool made by me and a friend of mine. It calculates the entropy of a file and draws a chart (TGA format). See the report from the previous HackerChallenge for source code. The Patch To remove the limits, and the password protection, the following patching tool has been created. See “Attack Narrative” for patch and tool description. Patcher source code (C – MinGW GCC): #include void copy(FILE *out, char *what) { FILE *f; char buf[1024]; int i; f = fopen(what, "rb"); while( ( i = fread(buf, 1, 1024, f) ) != 0 ) fwrite(buf, 1, i, out);

fclose(f); } void patch_from_file(FILE *out, int offset, char *what) { FILE *f; char buf[1024]; int i; fseek(out, offset, SEEK_SET); f = fopen(what, "rb"); while( ( i = fread(buf, 1, 1024, f) ) != 0 ) fwrite(buf, 1, i, out); fclose(f); } void patch(FILE *out, int offset, int count, char *what) { fseek(out, offset, SEEK_SET); fwrite(what, 1, count, out); } int main(void) { FILE *out; int i; out = fopen("finalbreak.exe", "wb"); copy(out, "final.exe"); copy(out, "code.bin"); patch(out, patch(out, patch(out, patch(out, patch(out,

0x100, 0x128, 0x250, 0x258, 0x26c,

4, 4, 4, 4, 4,

"\x00\xB0\x00\x00"); "\x00\xC0\x00\x00"); "\x00\x20\x00\x00"); "\x00\x20\x00\x00"); "\x60\x00\x00\xe0");

// // // // //

fclose(out); return 0; }

Patch source code (Assembler – NASM): [bits 32] [org 0x40b000] %define VirtualProtect [0x00405058] %define imp_fopen 0x0040510C start: ; Patch push org_rights push 0x0040 ; rwx push 1 push imp_fopen call VirtualProtect ; Save Org mov eax, [imp_fopen] mov [org_fopen], eax ; Hook mov eax, fopen mov [imp_fopen], eax ; Go back to oep push 0x404620 ret org_rights dd 0 org_fopen dd 0 times (0x400-($-start)) db 0 fopen: ; Remove hook mov eax, [org_fopen] mov [imp_fopen], eax

EP img size .rsrc vsize .rsrc raw size .rsrc flags

; Patch SOB push org_rights push 0x0040 ; rwx push 1 push 0x00403B07 call VirtualProtect mov mov mov mov mov mov mov mov

byte byte byte byte byte byte byte byte

[0x00403B07], [0x00403c35], [0x00403c89], [0x00403bf8], [0x00403bf9], [0x00403bfa], [0x00403b93], [0x00403b21],

0 0xeb 0xeb 0x90 0x90 0x90 0xeb 0xeb

; Password check ; \ ; \ ; \ ; } Limits ; / ; / ; /

; Ret to org push dword [org_fopen] ret times 0x1000 db 0

Password cracker The password was not required to be acquired, however it won't hurt to find it anyway. Hence I created a password cracking utility. The tool uses brute force method (I've created a dictionary attack variant of it too, but since the source is very similar, I decided not to include it here) to find a collision with a given hash. To calculate the hash this tool uses the functions directly from a dumped executable of the final.exe (let's call it dumped.exe). It loads the dumped.exe into memory (as a binary data file, without paying attention to the PE format stuff) and patches 5 calls in two functions (in function at VA 4030E0 calls to memset, memcpy (2x) and operand new are patched, and in function at VA 4031B0 a call to the operand new is patched) with addresses of the memset, memcpy and 2 my fake-new functions. After this, both functions become usable (I have DEP turned off, so no page right changes are needed). The first function prepares the data (with salt), and the second one calculates the hash. Using this approach I don't have to worry about the hash function not being compatible with any other implementation of the same hash (SHA-256), and additionally, using this method the code is easily move to another x86-base platform (Linux/Unix). However, this method has no place for optimization of the hash function. // Code for GCC (Linux/MinGW) #include #include #include #include // Some #define #define #define #define #define #define #define

offsets HASH_OR_STH HASH_OR_STH_CALL_NEW_OFFSET COPY_OR_STH COPY_OR_STH_CALL_NEW_OFFSET COPY_OR_STH_CALL_MEMSET_OFFSET COPY_OR_STH_CALL_MEMCPY1_OFFSET COPY_OR_STH_CALL_MEMCPY2_OFFSET

// Function pointers

0x31B0 0x31D9 0x30E0 0x314F 0x315A 0x316A 0x317C

int* (*hash_or_sth)(int *a1, void* a2, int a3); void* (*copy_or_sth)(int *a1, void *Src, size_t Size, int a4, int a5); // Fake alloc functions void* my_fake_alloc1(unsigned int len) { (void)len; // UNUSED // Static array + unrolled zeroing static int array[32]; array[0] = 0; array[1] = 0; array[2] = 0; array[3] = 0; array[4] = 0; 0; array[6] = 0; array[7] = 0; array[8] = 0; array[9] = 0; array[10] = 0; array[11] = 0; array[12] = 0; array[13] = 0; array[14] array[15] = 0; array[16] = 0; array[17] = 0; array[18] = 0; array[19] = array[20] = 0; array[21] = 0; array[22] = 0; array[23] = 0; array[24] array[25] = 0; array[26] = 0; array[27] = 0; array[28] = 0; array[29] = array[30] = 0; array[31] = 0;

array[5] = = 0; 0; = 0; 0;

return array; } void* my_fake_alloc2(unsigned int len) { (void)len; // UNUSED // Static array + unrolled zeroing static int array[32]; array[0] = 0; array[1] = 0; array[2] = 0; array[3] = 0; array[4] = 0; 0; array[6] = 0; array[7] = 0; array[8] = 0; array[9] = 0; array[10] = 0; array[11] = 0; array[12] = 0; array[13] = 0; array[14] array[15] = 0; array[16] = 0; array[17] = 0; array[18] = 0; array[19] = array[20] = 0; array[21] = 0; array[22] = 0; array[23] = 0; array[24] array[25] = 0; array[26] = 0; array[27] = 0; array[28] = 0; array[29] = array[30] = 0; array[31] = 0;

array[5] = = 0; 0; = 0; 0;

return array; } // Redirect a call function void fix_call(uint8_t *data, uint32_t call_offset, void* where_to) { uint32_t post_call = (uint32_t)&data[call_offset+5]; uint32_t addr_where = (uint32_t)where_to; uint32_t what = addr_where - post_call; *(uint32_t*)&data[call_offset+1] = what; } // Calculate a hash void *hcsha(const char *what) { // Static array + unlooped zeroing static int array[32]; array[0] = 0; array[1] = 0; array[2] = 0; array[3] = 0; array[4] = 0; 0; array[6] = 0; array[7] = 0; array[8] = 0; array[9] = 0; array[10] = 0; array[11] = 0; array[12] = 0; array[13] = 0; array[14] array[15] = 0; array[16] = 0; array[17] = 0; array[18] = 0; array[19] = array[20] = 0; array[21] = 0; array[22] = 0; array[23] = 0; array[24] array[25] = 0; array[26] = 0; array[27] = 0; array[28] = 0; array[29] = array[30] = 0; array[31] = 0; // Copy char *buf = (char*)array; strcpy(buf, what); // Exec external functions from DUMP.exe int len = 0; void *sth = copy_or_sth(&len, buf, 8, (int)"salt", 4); int* x = hash_or_sth(&len, sth, 1); // Return return x; } // Seeked hash uint32_t good_hash[] = { #ifndef TESTHASH 0x0013FE80 , 0x6D890A09, 0x0013FE90, 0x5C2C3392 , #else

0x03D02712 , 0xD836FF6F,

0x46A20F75, 0xB9792E74 ,

0x1E2CF0EF, 0xA9A0EBE0

array[5] = = 0; 0; = 0; 0;

0x41bd7753,0x199d6a14,0x70f7176a,0xd73b27cc,0x92c6cc51,0x36357ef1,0x73b52568,0x6cf5d42a // abcde #endif }; // Brute function from some old project of mine int brute( int len, const char *charset, int charset_len, int *start_point, int *end_point ) { int point[ 64 ]; int i; /* ur daily iterator */ char variant[ 64 ]; int end_it_sirth_spare_me_the_discrace = 0; static uint32_t no; /* start_point -> point */ memcpy( point, start_point, len * sizeof( int ) ); /* go go go */ while( !end_it_sirth_spare_me_the_discrace ) { /* end? */ if( memcmp( point, end_point, len * sizeof( int ) ) == 0 ) end_it_sirth_spare_me_the_discrace = 1; /* fill the variant */ for( i = len-1; i >= 0; i-- ) variant[ (len-1)-i ] = charset[ point[ i ] ]; variant[ len ] = '\0'; no++; if(no % 1000000 == 0) { printf("%s\r", variant); // For looking fancy fflush(stdout); } // Check if(memcmp(hcsha(variant), good_hash, 8*4) == 0) { printf("\nhaslo: [%s]\n", variant); fflush(stdout); return 1; } // Go next point[ 0 ]++; for( i = 0; i < len; i++ ) { point[ i+1 ] += point[ i ] / charset_len; point[ i ] %= charset_len; } } // Done return 0; } unsigned char * FileGetContent(const char *FileName, size_t *Size) { FILE *f; size_t FileSize; unsigned char *Data; // Open the file f = fopen(FileName, "rb"); if(!f) return NULL; // Get file size fseek(f, 0, SEEK_END); FileSize = ftell(f); fseek(f, 0, SEEK_SET); // Allocate memory Data = (unsigned char*)malloc(FileSize + 1); if(!Data) { fclose(f);

return NULL; } // Read file content FileSize = fread(Data, 1, FileSize, f); Data[FileSize] = 0; // Close the file fclose(f); // Return *Size = FileSize; return Data; } int main(void) { size_t sss; uint8_t *data = FileGetContent("DUMP.exe", &sss); // Get function pointers hash_or_sth = (typeof(hash_or_sth))&data[HASH_OR_STH]; copy_or_sth = (typeof(copy_or_sth))&data[COPY_OR_STH]; // Fix NEW fix_call(data, fix_call(data, fix_call(data, fix_call(data, fix_call(data,

HASH_OR_STH_CALL_NEW_OFFSET, (void*)my_fake_alloc1); COPY_OR_STH_CALL_NEW_OFFSET, (void*)my_fake_alloc2); COPY_OR_STH_CALL_MEMSET_OFFSET, (void*)memset); COPY_OR_STH_CALL_MEMCPY1_OFFSET, (void*)memcpy); COPY_OR_STH_CALL_MEMCPY2_OFFSET, (void*)memcpy);

#define SZ (sizeof(charset)-1) #define LEN (sizeof(start)/sizeof(int)) // Charset char charset[] = "abcdefghijkmlnopqrstuvwxyz" #ifdef WITH_DIGIT "0123456789" #endif #ifdef WITH_LARGE "ABCDEFGHIJKLMNOPQRSTUVWXYZ" #endif ; // State of the brute function int start[ ] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, end[ ] = {SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1,SZ -1,SZ-1,SZ-1,SZ-1,SZ-1,SZ-1}; // Brute it, length from 1 to 8 int i; for(i = 1; i