Crackme 13: buga0205's Code Linux

Apr 17, 2018

Link: (binary)

$ ./CodeLinux
*****This is Code Linux Agent!*****
- Enter The Code : 1234

Alert! Unidentified User!

The first thing to notice is the size of this executable, which contains a large amount of code since it was statically linked. It also seems like most symbols were stripped, so Hopper only lists procedures as sub_XXXXXX with hex addresses. By looking at the parameters, we can deduce the names of some of them. For example, sub_8050120 seems to be printf:

08049716         mov        dword [esp+0x20+var_20], aThisIsCodeLinu
0804971d         call       sub_8050120
0804978a         mov        dword [esp+0x20+var_20], aNalertUnidenti
08049791         call       sub_8050120

We can rename them as we go along in Hopper. This program also has some basic anti-debugging protection, visible here:

# strace ./CodeLinux
execve("./CodeLinux", ["./CodeLinux"], [/* 8 vars */]) = 0
ptrace(PTRACE_TRACEME)                  = -1 EPERM (Operation not permitted)
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7730000
write(1, "Alert! I hate debugging stuff,,,"..., 34Alert! I hate debugging stuff,,,\n) = 34

This string is easy to find in the binary:

080496fc         call       sub_8075510
08049701         test       eax, eax
08049703         jns        loc_8049716 ; this is the block below

08049705         mov        dword [esp+0x20+var_20], aAlertIHateDebu
0804970c         call       sub_804fab0
08049711         jmp        loc_8049832

08049716         mov        dword [esp+0x20+var_20], aThisIsCodeLinu  

In the past, we’ve replaced the conditional jump at 08049703 with a JMP to bypass the alert; let’s use a different approach here, just for the novelty of it: we’ll replace test eax, eax with xor eax, eax and it will never print the error message. We can find the code for it at 08049862 and notice that it also takes 2 bytes, 0x31 0xC0 when the test was 0x85 0xC0. We can therefore replace a single byte at 08049701 to create a new binary. While we’re at it, let’s remove the call to nanosleep that comes before “Alert! Unidentified User”. It seems to sleep for 1 second, and this value matches:

08049742         mov        dword [esp], 0xf4240 ; 1 million
08049749         call       sub_80754c0   

We can replace 0xf4240 with zeros, which gives us a new binary. Tracing it with an incorrect input shows the fixed nanosleep call:

write(1, "- Enter The Code : ", 19- Enter The Code : )     = 19
read(0, 1234
"1234\n", 1024)                 = 5
nanosleep({tv_sec=0, tv_nsec=0}, NULL)  = 0
write(1, "\n", 1
)                       = 1
write(1, "Alert! Unidentified User! \n", 27Alert! Unidentified User!

With these two out of the way, let’s start looking at the way our input is processed. We enter sub_805c060 with our password on the stack, and make eax point to it. It processes the input 4 bytes at a time, successively masking it with interesting constants like 0xfefefeff or 0x1010100. This is a fast implementation of strlen; see the glibc’s take on it on StackOverflow.

We test the parity of the length and fail if it’s not odd:

0804977e         and        edx, 0x1
08049781         sub        edx, eax
08049783         mov        eax, edx
08049785         cmp        eax, 0x1        ; check for odd length
08049788         jne        sub_80496cc+214 ; jump to 080497a2, over the printf call
0804978a         mov        dword [esp], aNalertUnidenti ; argument #1 for method printf, "\\nAlert! Unidentified User! "
08049791         call       printf                       ; printf
08049796         mov        dword [esp], 0x1
0804979d         call       sub_804f0d0
080497a2         mov        eax, dword [dword_80f3f9c]

We then add 1 to the lenght, subtract 9, multiply it by 2, compare it to 0x29 (41), and only ontinue if it’s under. So if (length + 1) * 2 < 41, the length can’t be more than 19:

080497a7         sub        eax, 0x9
080497aa         add        eax, eax
080497ac         cmp        eax, 0x29
080497af         jle        sub_80496cc+253

At 080497fc, we call sub_080495a9, which encodes our input. At 0x080495b5, we observe that 0x80f50a0 contains our input string. After the call to sub_08049535, the same address contains a base-64 encoded version of it.

sub_8049058 then transforms our input. Hopper decompiles it as such:

int sub_8049058(int arg0, int arg1) {
    var_8 = arg1;
    var_4 = arg0;
    var_C = strlen(var_4);
    var_10 = 0x0;
    do {
            eax = var_10;
            if (eax >= var_C) {
            if (((*(int8_t *)(var_4 + var_10) & 0xff) > 0x60) && ((*(int8_t *)(var_4 + var_10) & 0xff) <= 0x7a)) {
                    *(int8_t *)(var_10 + var_4) = (*(int8_t *)(var_4 + var_10) & 0xff) - 0x61;
                    if (var_8 + sign_extend_32(*(int8_t *)(var_4 + var_10) & 0xff) < 0x0) {
                            *(int8_t *)(var_10 + var_4) = (*(int8_t *)(var_4 + var_10) & 0xff) + 0x1a;
                    ecx = sign_extend_32(*(int8_t *)(var_4 + var_10) & 0xff) + var_8;
                    *(int8_t *)(var_10 + var_4) = ecx - ((SAR(HIDWORD(ecx * 0x4ec4ec4f), 0x3)) - (SAR(ecx, 0x1f))) * 0x1a;
                    *(int8_t *)(var_10 + var_4) = (*(int8_t *)(var_4 + var_10) & 0xff) + 0x61;
            if (((*(int8_t *)(var_4 + var_10) & 0xff) > 0x40) && ((*(int8_t *)(var_4 + var_10) & 0xff) <= 0x5a)) {
                    *(int8_t *)(var_10 + var_4) = (*(int8_t *)(var_4 + var_10) & 0xff) - 0x41;
                    if (var_8 + sign_extend_32(*(int8_t *)(var_4 + var_10) & 0xff) < 0x0) {
                            *(int8_t *)(var_10 + var_4) = (*(int8_t *)(var_4 + var_10) & 0xff) + 0x1a;
                    ecx = sign_extend_32(*(int8_t *)(var_4 + var_10) & 0xff) + var_8;
                    *(int8_t *)(var_10 + var_4) = ecx - ((SAR(HIDWORD(ecx * 0x4ec4ec4f), 0x3)) - (SAR(ecx, 0x1f))) * 0x1a;
                    *(int8_t *)(var_10 + var_4) = (*(int8_t *)(var_4 + var_10) & 0xff) + 0x41;
            var_10 = var_10 + 0x1;
    } while (true);
    return eax;

We can recognize some values in the alphabet range: 0x60 is right before 0x61, which is ‘a’. Similarly, 0x7a is ‘z’. var_10 is incremented at each loop, so it’s a counter. var_4 is added to var_10 and dereferenced, so it’s a pointer. Cleaning it up is straightforward:

int sub_8049058(char *arg0, int arg1) {
    length_plus_1 = arg1; // the length of our input, plus one.
    input = arg0; // our input
    length = strlen(input);
    int i = 0;
    do {
            eax = i;
            if (eax >= length) {
            if ((input[i] >= 'a') && (input[i] <= 'z')) {
                    input[i] = input[i] - 'a';
                    if (length_plus_1 + input[i] < 0x0) {
                            input[i] = input[i] + 26;
                    tmp = input[i] + length_plus_1;
                    input[i] = tmp - ((SAR(HIDWORD(tmp * 0x4ec4ec4f), 0x3)) - (SAR(tmp, 0x1f))) * 26;
                    input[i] = input[i] + 'a';
            if ((input[i] >= 'A') && (input[i] <= 'Z')) {
                    input[i] = input[i] - 'A';
                    if (length_plus_1 + input[i] < 0x0) {
                            input[i] = input[i] + 26;
                    tmp = input[i] + length_plus_1;
                    input[i] = tmp - ((SAR(HIDWORD(tmp * 0x4ec4ec4f), 0x3)) - (SAR(tmp, 0x1f))) * 26;
                    input[i] = input[i] + 'A';
    } while (true);
    return eax;

If our input is “12345”, the string we operate on is “MTIzNDU=”. Let’s run through the transformation of the first character, with length_plus_1 = 6:

  1. ’M’ - ‘A’ is 0xC (12)
  2. 12 + length_plus_1 = 18
  3. if 18 < 0, we’d add 26. Not the case here.
  4. We multiply 0x12 by 0x4EC4EC4F, this is 0x589d89d8e which is split between EDX (higher 32 bits) and EAX (lower 32 bits). HIDWORD is EDX here.
  5. EDX is then shift right by 3 bits which will reduce it to zero. Similarly we shift tmp by 0x1f bits which also sets it to zero.
  6. The new input is therefore the old input + length_plus_1, mod 26 and scaled back to its original a-z or A-Z range.
  7. chr(0x41 + (ord(’M’) - ord(‘A’) + 6)) == ’S’

Returning from the encoding procedures, we end up at 0x08049820. Our input was transformed in-place, so 0x80f4fa0 contains our shifted base64-encoded input. We then call 0x080495ed, in which the first instructions seem to refer to characters in the alphabet range:

080495ed         push       ebp
080495ee         mov        ebp, esp
080495f0         push       ebx
080495f1         sub        esp, 0x44
080495f4         mov        eax, dword [ebp+arg_0]
080495f7         mov        dword [ebp+var_3C], eax
080495fa         mov        eax, dword [gs:0x14]
08049600         mov        dword [ebp+var_C], eax
08049603         xor        eax, eax
08049605         mov        dword [ebp+var_29], 0x3654634b
0804960c         mov        dword [ebp+var_25], 0x4a40564c
08049613         mov        dword [ebp+var_21], 0x6c315543
0804961a         mov        dword [ebp+var_1D], 0x4a623656
08049621         mov        dword [ebp+var_19], 0x63503456
08049628         mov        dword [ebp+var_15], 0x66305554
0804962f         mov        dword [ebp+var_11], 0x3939657e

Taken in order but as big-endian values, they encode “KcT6LV@JCU1lV6bJV4PcTU0f~e99”. This doesn’t look like base-64, but let’s continue. A loop follows, performing an XOR of each byte with 0x4:

0804964a         mov        edx, dword [ebp+var_30]     ; our string
0804964d         mov        eax, dword [ebp+var_3C]     ; a counter initialized at zero
08049650         add        edx, eax
08049652         mov        ecx, dword [ebp+var_30]     ; still our string
08049655         mov        eax, dword [ebp+var_3C]
08049658         add        eax, ecx
0804965a         movzx      eax, byte [eax]
0804965d         xor        eax, 0x4
08049660         mov        byte [edx], al               ; store result in place
08049662         add        dword [ebp+var_30], 0x1      ; this is a counter

08049666         mov        ebx, dword [ebp+var_30]
08049669         lea        eax, dword [ebp+var_29]
0804966c         mov        dword [esp+0x48+var_48], eax ; argument #1 for method strlen
0804966f         call       strlen                       ; strlen
08049674         cmp        ebx, eax
08049676         jb         loc_804964a                  ; loop over the full string

“KcT6LV@JCU1lV6bJV4PcTU0f~e99” XOR’d with 0x4, gives “OgP2HRDNGQ5hR2fNR0TgPQ4bza==” which definitely looks like base-64 data.

This string is transformed using the length_plus_1 shift, and then compared to our shifted-encoded input in 08049699:

08049699         test       eax, eax
0804969b         jne        loc_80496b5 ; jumps right over the success message

0804969d         mov        dword [esp+0x48+var_48], aNyouAreACodeLi            ; argument #1 for method printf, "\\nYou are a Code Linux Memeber!!"
080496a4         call       printf                                              ; printf
080496a9         mov        dword [esp+0x48+var_48], 0x1
080496b0         call       sub_804f0d0

Note that at this point we could reverse the JNE into a JE or just NOP it and this would let us reach the success message. Here’s a patched binary with this change:

$ ./CodeLinux.swapped
*****This is Code Linux Agent!*****
- Enter The Code : 12345

You are a Code Linux Memeber!!

For the sake of it, let’s decode what the secret actually was. Since the length is 19, it was shifted by 20 positions so we need to reconstruct the string by shifting it 20 times in the other direction – by subtracting instead of adding:

>>> transform = lambda c: chr(((26 - 20 + c - 0x41) % 26) + 0x41) if c >= 0x41 and c <= 0x5a \
... else chr(((26 - 20 + c - 0x61) % 26) + 0x61) if c >= 0x61 and c <= 0x7a \
... else chr(c)
>>> tmp = ''.join(map(lambda c: transform(ord(c)), "OgP2HRDNGQ5hR2fNR0TgPQ4bza=="))
>>> print tmp
>>> import base64
>>> base64.b64decode(tmp)

In action:

$ ./CodeLinux
*****This is Code Linux Agent!*****
- Enter The Code : Rev5rS1ng_iS_FfUn!~

You are a Code Linux Memeber!!