duksctf

Bunch of sec. enthusiasts who sometimes play CTF

PlaidCTF 2020 - Reee

Execute an unscrambling function to get the compare algorithm. Then figure out what the algorithm does to recover the flag.

Description

Reversing (150 pts)

Story

Tired from all of the craziness in the Inner Sanctum, you decide to venture out to the beach to relax. You doze off in the sand only to be awoken by the loud “reee” of an osprey. A shell falls out of its talons and lands right where your head was a moment ago. No rest for the weary, huh? It looks a little funny, so you pick it up and realize that it’s backwards. I guess you’ll have to reverse it. Problem Details

Download

Hint: Flag format

The flag format is pctf{$FLAG}. This constraint should resolve any ambiguities in solutions.

Details

Points: 150

Category: reverse

Solution

The file is a standard x86-64 binary:

$ file reee
reee: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3ccec76609cd013bea7ee34dffc8441bfa1d7181, stripped

And it’s waiting an argument:

$ ./reee hello
Wrong!

I opened it in radare2:

I remarked that the argument is passed to the fcn.004006e5 function and if the result is 0 then the string “Correct!” is printed. But this function seems scrambled:

[0x004006e5]> s fcn.004006e5; pd 10
       ╎╎   ; CALL XREFS from main @ 0x400699, 0x4006a5, 0x4006db
 60: fcn.004006e5 (int64_t arg2, uint32_t arg4, int64_t arg_58ae7f6ah);
 bp: 1 (vars 0, args 1)
 sp: 0 (vars 0, args 0)
 rg: 2 (vars 0, args 2)
      ╎╎   0x004006e5      f9             stc
      ╎╎   0x004006e6      93             xchg eax, ebx
     ┌───< 0x004006e7      752d           jne 0x400716
     ╎╎   0x004006e9      dbc6           fcmovnb st(0), st(6)
     ╎╎   0x004006eb      ab             stosd dword [rdi], eax
     ╎└─< 0x004006ec      e0a2           loopne 0x400690
         0x004006ee      3b499d         cmp ecx, dword [rcx - 0x63] ; arg4
         0x004006f1      8c5c86dd       mov word [rsi + rax*4 - 0x23], ds ; arg2
          0x004006f5      0e             invalid
      └──< 0x004006f6      73cd           jae 0x4006c5                ; main+0x77

In fact if we break just before the function call we have the function unscrambled. I just checked that the scrambling was the same for different input arguments:

[0x004006d4]> ood hello; dcu 0x4006d4
child received signal 9
Stepping failed!
Process with PID 10921 started...
= attach 10921 10921
File dbg:////reee  hello reopened in read-write mode
Continue until 0x004006d4 using 1 bpsize
[0x7fde560fcfe6]> s fcn.004006e5; pd 10
            ; CALL XREFS from main @ 0x400699, 0x4006a5, 0x4006db
 60: fcn.004006e5 (int64_t arg2, uint32_t arg4, int64_t arg_58ae7f6ah);
 bp: 1 (vars 0, args 1)
 sp: 0 (vars 0, args 0)
 rg: 2 (vars 0, args 2)
           0x004006e5      55             push rbp
           0x004006e6      488bec         mov rbp, rsp
           0x004006e9      e814000000     call 0x400702
           0x004006ee      8ac8           mov cl, al                  ; arg4
           0x004006f0      b832e45fae     mov eax, 0xae5fe432
            0x004006f5      05ce1ba051     add eax, 0x51a01bce
            0x004006fa      ffc0           inc eax
        ┌─< 0x004006fc      7308           jae 0x400706
           0x004006fe      8ac1           mov al, cl
           0x00400700      c9             leave

Then we can decompile the function:

[0x00400702]> pdg
bool fcn.00400702(void)
{
    char cVar1;
    char *in_RAX;
    int32_t iVar2;
    int64_t iVar3;
    char *pcVar4;
    uint8_t uVar5;
    int32_t iVar6;
    int32_t iVar7;
    bool bVar8;
    
    iVar3 = -1;
    pcVar4 = in_RAX;
    do {
        if (iVar3 == 0) break;
        iVar3 = iVar3 + -1;
        cVar1 = *pcVar4;
        pcVar4 = pcVar4 + 1;
    } while (cVar1 != '\0');
    iVar2 = ~(uint32_t)iVar3 - 1;
    uVar5 = 0x50;
    iVar6 = 0;
    while (iVar6 < 0x539) {
        iVar7 = 0;
        while (iVar7 < iVar2) {
            in_RAX[iVar7] = in_RAX[iVar7] ^ uVar5;
            uVar5 = uVar5 ^ in_RAX[iVar7];
            iVar7 = iVar7 + 1;
        }
        iVar6 = iVar6 + 1;
    }
    bVar8 = true;
    iVar6 = 0;
    while (iVar6 < iVar2) {
        if (bVar8 == false) {
            bVar8 = false;
        } else {
            bVar8 = in_RAX[iVar6] == *(char *)((int64_t)iVar6 + 0x4008eb);
        }
        iVar6 = iVar6 + 1;
    }
    return bVar8;
}

So the first byte of the flag is XORed with 0x50 the second byte is XORed with the first byte of the flag and so on 1337 times. The result is compared with the data stored at 0x4008eb. If they match it prints “Correct”. Since the 34th byte is 0 I guessed the flag length was 33. So we can starts from the results:

[0x00400702]> pxj 34@0x4008eb
[72,95,54,53,53,37,20,44,29,1,3,45,12,111,53,97,126,52,10,68,36,44,74,70,25,89,91,14,120,116,41,19,44,0]

And come back to the beginning, i.e. until the value to XORed equals 0x50. Since we do not know the last byte value XORed we have to bruteforce it. It gives in Python:

data = [72,95,54,53,53,37,20,44,29,1,3,45,12,111,53,97,126,52,10,68,36,44,74,70,25,89,91,14,120,116,41,19,44]
length = len(data)-1

for k in range(0,256):
	mask = k
	flag = bytearray(data)
	for i in range(0,1337):
		for j in range(length,-1,-1):
			mask = flag[j] ^ mask
			flag[j] = flag[j] ^ mask
	if mask == 0x50:
		print(flag[:length+1])

And I got the flag:

bytearray(b'pctf{ok_nothing_too_fancy_there!}')
Written on April 19, 2020