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
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!}')