247CTF - Several Reversing challenges

The Flag Bootloader

I found this challenge linked on the ReverseEngineering subreddit.

HxD => shows some strings:

  • “Unlock code:”
  • “Invalid code!”
  • “247CTF{w!g05 xy.. rpu)+.\!pbce…;;}”

Last one looks like a flag, but probably encrypted/encoded.

Ghidra doesn’t recognize the language => just select x86.
IDA does a significant better job though.

file .\flag.com
.\flag.com: DOS/MBR boot sector

Really a mbr bootloader (duh)

qemu to emulate whatever that is

.\qemu-system-x86_64.exe -drive format=raw,file=.\flag.com

We get this prompt

Booting from Hard Disk...
Unlock code:

Let’s debug this:

Just stole this command from here

qemu-system-x86_64 -s -S -m 512 -fda flag.com

Now attach gdb and break at the beginning

gdb
target remote localhost:1234
break *0x7c00
c

sub_10108 prints stuff

seg000:0115 int     10h             ; - VIDEO - WRITE CHARACTER AND ADVANCE CURSOR (TTY WRITE)
                                    ; AL = character, BH = display page (alpha modes)
                                    ; BL = foreground color (graphics modes)

break *7c1d

sub_10102 reads keystrokes

seg000:0102 sub_10102       proc near               ; CODE XREF: start+3A↓p
seg000:0102                 mov     ax, 1000h
seg000:0105                 int     16h             ; KEYBOARD - GET ENHANCED KEYSTROKE (AT model 339,XT2,XT286,PS)
seg000:0105                                         ; Return: AH = scan code, AL = character
seg000:0107                 retn
seg000:0107 sub_10102       endp

Where al is the ascii char of the keystroke.

Using these two informations:

seg000:0137 loc_10137:
seg000:0137                 mov     si, 7DE6h
seg000:013A                 call    sub_10102
seg000:013D                 mov     ds:7DE6h, al
seg000:0140                 call    sub_10108
seg000:0143                 mov     bx, ds:7DEAh
seg000:0147                 cmp     bx, 7DFDh
seg000:014B                 jz      loc_10284
seg000:014F                 mov     [bx], al
seg000:0151                 add     bx, 1
seg000:0154                 mov     ds:7DEAh, bx
seg000:0158                 cmp     byte ptr ds:7DE6h, 0Dh
seg000:015D                 jnz     short loc_10137

This is the read & write loop. And we can see that the input is saved into 0x7DE6

If we input more than 18 characters we get the message No memory!.
Which we can see here (0x7DEA is the input length).

seg000:0143                 mov     bx, ds:7DEAh
seg000:0147                 cmp     bx, 7DFDh
seg000:014B                 jz      loc_10284
seg000:014F                 mov     [bx], al
seg000:0151                 add     bx, 1
seg000:0154                 mov     ds:7DEAh, bx

We can see the exit jump here:

seg000:0158                 cmp     byte ptr ds:7DE6h, 0Dh
seg000:015D                 jnz     short loc_10137
seg000:015F                 mov     si, 7DD5h
seg000:0162                 call    sub_10108
seg000:0165                 call    sub_1016A

Checks 0x7DE6 for 0x0D (which is the ascii code for enter).

sub_1016A is the check function, which basically consists of this:

seg000:016B                 mov     bx, 7DECh
seg000:016E                 mov     si, 7DAAh
seg000:0171                 add     si, 7
seg000:0174                 mov     al, 4Bh
seg000:0176                 xor     al, 0Ch
seg000:0178                 cmp     [bx], al
seg000:017A                 jnz     loc_1027C

Checks the character with an xor’ed value and quits if it’s not equal.
There is also sometimes a sneaky sub al,X in there.
As the program uses not the user input to xor the key, there is no need for us to input that.
I just added break points at all the checks and changed the z flag there (to skip the jump).

Breakpoint list

break *0x7c7a
break *0x7c8b
break *0x7c9c
break *0x7cad
break *0x7cbe
break *0x7ccf
break *0x7ce0
break *0x7cf1
break *0x7d02
break *0x7d11
break *0x7d20
break *0x7d2f
break *0x7d3e
break *0x7d4d
break *0x7d5c
break *0x7d6b

To set the z flag:

(gdb) set $eflags |= (1 << 6)

If we’re at 0x7D74, we can print the flag with

(gdb) x/s 0x7DAA

Or we just enter c and let the program print it out (can’t copy though).

The more the merrier

file the_more_the_merrier
the_more_the_merrier: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0f750d638337391328fa7432dd362189de908c1e, stripped
./the_more_the_merrier
Nothing to see here..

=> xref “Nothing to see here..” =>

undefined8 NothingHere(void)
{
  puts("Nothing to see here..");
  return 0;
}

Mhh this is the main function as we see in entry:

void entry(undefined8 param_1,undefined8 param_2,undefined8 param_3)
{
  undefined8 in_stack_00000000;
  undefined auStack8 [8];
  
  __libc_start_main(NothingHere,in_stack_00000000,&stack0x00000008,FUN_00100660,&DAT_001006d0,
                    param_3,auStack8);
  do {
  } while( true );
}

The init function is empty too (FUN_00100660).

If we take a look at the assembly in main():

LEA        RAX,[DAT_001006e8]
MOV        qword ptr [RBP + local_10],RAX=>DAT_001006e8
LEA        RDI,[s_Nothing_to_see_here.._0010078c]
CALL       puts
MOV        EAX,0x0
LEAVE
RET

Huh? What’s up with DAT_001006e8?
Let’s look at it:

DAT_001006e8
??         32h    2
??         00h
??         00h
??         00h
??         34h    4
??         00h
??         00h
??         00h
??         37h    7
??         00h
??         00h
??         00h
??         43h    C
??         00h
??         00h
??         00h
??         54h    T
??         00h
??         00h
??         00h
??         46h    F
??         00h
??         00h
??         00h
??         7Bh    {

That looks like the flag…

Let’s just copy it, use some vscode multi cursor shit and get the flag.

Encrypted Password

file encrypted_password
encrypted_password: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e66644a82644c5ac6ab7a507cc5c6e84432ee0ad, stripped

The bytes

local_a8 = 0x3930343965353738;
local_a0 = 0x3861623131383966;
local_98 = 0x3665656562303635;
local_90 = 0x3264373763306266;
local_88 = 0;
local_78 = 0x5a53010106040309;
local_70 = 0x5c585354500a5b00;
local_68 = 0x555157570108520d;
local_60 = 0x5707530453040752;
local_58 = 0;

local_a8 can be converted to the string 875e9409f9811ba8560beee6fb0c77d2.

The xor loop

i = 0;
while( true ) {
  len = strlen((char *)&local_a8);
  if (len <= (ulong)(long)i) break;
  *(byte *)((long)&local_78 + (long)i) =
       *(byte *)((long)&local_78 + (long)i) ^ *(byte *)((long)&local_a8 + (long)i);
  i = i + 1;
}

Finally a strcmp

puts("Enter the secret password:");
fgets(local_48,0x21,stdin);
iVar1 = strcmp(local_48,(char *)&local_78);
if (iVar1 == 0) {
  printf("You found the flag!\n247CTF{s}\n",&local_78);
}

A few options:

  • Xor the bytes ourselves
  • Print the flag after the xor (or get it through debugging)
  • Just patch the strcmp check

Xor’ing ourself

Use 875e9409f9811ba8560beee6fb0c77d2 and xor with the bytes.
Watch out, as you need to reverse the bytes line-by-line (or just use the bytes directly and reverse afterwards).

“Sniffing” the password

While doing this I found out you can use

(gdb) layout asm

to get a really nice overview

gdb ./encrypted_password
(gdb) run
Ctrl + C
(gdb) info files
(gdb) break *0x8000700 # this is the entry point

In the asm we then can see

0x800071d   lea    0xe6(%rip),%rdi        # 0x800080a

Which is the main address.
Let’s break there

break *0x800080a
c

Let’s break right after the xor loop:

001008fe 48 8d 3d        LEA        RDI,[s_Enter_the_secret_password:_00100a48]

So at 0x80008fe

If we want to see the registers, we can disable TUI mode

tui disable
info regs

or enable the register layout

layout regs

At the end of each loop the string is saved

MOV        byte ptr [RBP + RAX*0x1 + -0x70],DL
ADD        dword ptr [RBP + i],0x1

Let’s look at rbp (which is where variables are saved) - 0x70:

(gdb) x/s $rbp - 0x70
0x7ffffffedcd0: "141c85XXXXXXXXXXXXXXXXXXXXXXXXXX"

x displays the address, s shows it as a string, with $ we can access the registers

Patching

Just change

00100935 85 c0           TEST       len,len
00100937 75 18           JNZ        LAB_00100951
00100939 48 8d 45 90     LEA        len=>local_78,[RBP + -0x70]

to

00100937 74 18           JZ        LAB_00100951

Run the program and enter any key that doesn’t match the original key.

Secret Lock

We get a html file?

It’s a fancy lock thingy.

We have no important external js.

onChange() {
    this.code = this.getCode();
    this.flag = this.checkFlag(this.code);
	this.dom.status.textContent = this.flag;
}

Okay getCode:

getCode() {
	let flag = {};
    for (let i = 0, len = this.dom.rows.length; i < len; i++) {
        flag[i] = this.dom.rows[i].querySelector('.is-selected .text').textContent;
    }
    return flag;
}

If we place a console.log(flag) in there, we can see a object (I changed the second row)

Object { 0: "0", 1: "1", 2: "0", 3: "0", 4: "0", 5: "0", 6: "0", 7: "0", 8: "0", 9: "0",  }

Let’s look at checkFlag:

checkFlag(flag) {
    let result = "LOCKED"
	this.dom.lock.classList.remove('verified');
    if (Object.keys(flag).length == 40 && ((flag[37] - flag[37]) * flag[15] == 0) && ...) {
	  result = "";
      for (var idx in flag) {
	    result += (String.fromCharCode(flag[idx]));
	  }
	  this.dom.lock.classList.add('verified');
    }
    return result;
}

The if condition is a long boi (I cut out most of it).
Okay how do we solve this?

We know that

  • the string is 40 chars long => 247CTF{}
  • the numbers are ascii chars => 48 - 57, 65 - 90, 97 - 122

Let’s first get the ascii codes of the ones we know

for (let b of "247CTF{}") { console.log(b.charCodeAt(0)) }
50 52 55 67 84 70 123 125

Okay I tried solving it by hand, but only got like 2 values quickly.
Then I tried bruteforcing. Not my greatest idea as this takes way too long (~60 chars, 40 length).
Then I remembered something about like a solver for that shit.

Introducing: z3

z3? Wasn’t that this old calculator from some Zuse guy?

z3 is a theorem solver by Microsoft and it’s really easy to use and perfect for this kind of work.
To install the python bindings:

pip install z3-solver

Then just create a Solver with

from z3 import *
s = Solver()

Add some conditions with

x = Bool('x')
y = Bool('y')
s.add(x == y)

And let others do the hard work

if s.check == sat:
    print(s.model())

And you get the model/solution.
You need to use those z3 types.
We actually need to use a 8-bit BitVec here, because infinite precision numbers can’t be xored.

flag = []
for i in range(40):
    flag.append(BitVec(f'flag[{i}]', 8))

I just added conditions like this to set the values.

s.add(flag[0] == 50)

Then I could just replace the if’s && with \n and paste it into python.
After that use

lock.checkFlag(JSON.parse(`model json string here`))

in the browser console to get the flag.

Keygen

The challenge is also hosted online, so you can’t just patch the check or something like that.
Let’s start with main:

undefined8 main(void)
{
  puts("Enter a valid product key to gain access to the flag:");
  fgets((char *)input,0x80,stdin);
  len = strcspn((char *)input,"\n");
  *(undefined *)((long)input + len) = 0;
  iVar1 = Verify(input);
  if (iVar1 == 0) {
    puts("Invalid product key!");
  }
  else {
    puts("Valid product key!");
    printFlag();
  }

Okay Verify:

  • Length of 0x20/32
    len = strlen(key);
    if (len == 0x20) {
    i = 0;
    
  • Only chars between @ and Z (including both)
    while (len = strlen(key), (ulong)(long)i < len) {
    if ((key[i] < '@') || ('Z' < key[i])) {
      return 0;
    }
    i = i + 1;
    }
    
  • Adds all chars (except the first one) to total after calling weird func on them
    total = 0xf7;
    j = 1;
    while (len = strlen(key), (ulong)(long)j < len) {
    iVar1 = Add0xB1orB5((ulong)(uint)(int)key[j]);
    total = total + (iVar1 - j) + 0xf7;
    j = j + 1;
    }
    
  • First char through weird function == 0xf7 => ‘B’
    iVar1 = Add0xB1orB5((ulong)(uint)(int)*key);
    if ((total % 0xf8 == iVar1) && (total % 0xf8 == 0xf7)) {
    uVar2 = 1;
    }
    else {
    uVar2 = 0;
    }
    

It wasn’t even necessary to write a keygen script because the check is so simple that you can change the chars by hand…

To debug:

gdb ./flag_keygen
break *0x8001243 (strlen)
break *0x80012b5 (before total calc)

I used gdb, to find out that the total calc starts at j = 1…

After you get a valid product key, use nc to connect and enter the key

nc XXXXXXXXXXXXXX.247ctf.com XXXXX
Enter a valid product key to gain access to the flag:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Valid product key!
247CTF{flag}

Bonus

The z3 solve code:

from z3 import *
import json

s = Solver()
flag = []
for i in range(40):
    flag.append(BitVec(f'flag[{i}]', 8))

s.add(flag[0] == 50)
s.add(flag[1] == 52)
s.add(flag[2] == 55)
s.add(flag[3] == 67)
s.add(flag[4] == 84)
s.add(flag[5] == 70)
s.add(flag[6] == 123)
s.add(flag[39] == 125)

s.add((flag[37] - flag[37]) * flag[15] == 0)
s.add((flag[3] + flag[31]) ^ (flag[29] + flag[8]) == 234)
s.add((flag[32] - flag[12]) * flag[9] == -2332)
s.add((flag[24] - flag[27] + flag[13]) ^ flag[6] == 114)
s.add((flag[38] - flag[15]) * flag[33] == 800)
s.add((flag[34] - flag[21]) * flag[26] == 98)
s.add((flag[29] + flag[0]) ^ (flag[8] + flag[38]) == 248)
s.add((flag[21] * flag[18]) ^ (flag[7] - flag[15]) == 2694)
s.add((flag[28] * flag[23]) ^ (flag[19] - flag[5]) == -9813)
s.add((flag[34] + flag[30]) ^ (flag[37] + flag[6]) == 72)
s.add((flag[23] - flag[22]) * flag[12] == 4950)
s.add((flag[9] * flag[28]) ^ (flag[20] - flag[11]) == 5143)
s.add((flag[2] * flag[22]) ^ (flag[37] - flag[0]) == 2759)
s.add((flag[26] - flag[12]) * flag[3] == -3350)
s.add((flag[35] * flag[0]) ^ (flag[23] - flag[21]) == 2698)
s.add((flag[20] + flag[31]) ^ (flag[5] + flag[10]) == 22)
s.add((flag[31] * flag[19]) ^ (flag[1] - flag[2]) == -2655)
s.add((flag[38] - flag[14]) * flag[18] == 55)
s.add((flag[29] - flag[19] + flag[10]) ^ flag[2] == 93)
s.add((flag[13] - flag[25] + flag[30]) ^ flag[29] == 13)
s.add((flag[35] + flag[33]) ^ (flag[26] + flag[21]) == 249)
s.add((flag[17] + flag[24]) ^ (flag[34] + flag[1]) == 253)
s.add((flag[32] - flag[35] + flag[19]) ^ flag[1] == 0)
s.add((flag[22] - flag[11] + flag[3]) ^ flag[31] == 113)
s.add((flag[19] - flag[0]) * flag[13] == 108)
s.add((flag[19] - flag[17]) * flag[14] == -2475)
s.add((flag[31] - flag[35] + flag[16]) ^ flag[19] == 84)
s.add((flag[24] * flag[27]) ^ (flag[35] - flag[17]) == -5792)
s.add((flag[11] * flag[35]) ^ (flag[15] - flag[28]) == -2845)
s.add((flag[18] - flag[19] + flag[31]) ^ flag[5] == 112)
s.add((flag[20] - flag[6]) * flag[10] == -3933)
s.add((flag[39] - flag[33]) * flag[6] == 3075)
s.add((flag[22] + flag[1]) ^ (flag[39] + flag[14]) == 211)
s.add((flag[37] * flag[24]) ^ (flag[12] - flag[39]) == -5726)
s.add((flag[29] + flag[3]) ^ (flag[8] + flag[11]) == 195)
s.add((flag[26] * flag[7]) ^ (flag[10] - flag[17]) == -2375)
s.add((flag[11] - flag[12]) * flag[12] == -4653)
s.add((flag[13] * flag[5]) ^ (flag[12] - flag[25]) == 3829)
s.add((flag[24] * flag[0]) ^ (flag[13] - flag[23]) == -2829)
s.add((flag[17] + flag[12]) ^ (flag[8] + flag[14]) == 170)
s.add((flag[38] + flag[23]) ^ (flag[11] + flag[1]) == 245)
s.add((flag[22] + flag[5]) ^ (flag[21] + flag[24]) == 19)
s.add((flag[35] - flag[8] + flag[21]) ^ flag[30] == 85)
s.add((flag[18] - flag[31] + flag[28]) ^ flag[29] == 0)
s.add((flag[30] * flag[35]) ^ (flag[27] - flag[29]) == 5501)
s.add((flag[8] - flag[30] + flag[16]) ^ flag[36] == 81)
s.add((flag[13] * flag[18]) ^ (flag[35] - flag[38]) == -2971)
s.add((flag[27] - flag[14]) * flag[39] == 5875)
s.add((flag[34] - flag[33]) * flag[6] == -6027)
s.add((flag[38] * flag[1]) ^ (flag[20] - flag[10]) == -2915)
s.add((flag[1] - flag[1]) * flag[3] == 0)
s.add((flag[36] - flag[20]) * flag[8] == 2640)
s.add((flag[23] - flag[11] + flag[17]) ^ flag[33] == 246)
s.add((flag[13] - flag[38]) * flag[0] == -100)
s.add((flag[28] - flag[14]) * flag[31] == 2142)
s.add((flag[26] + flag[15]) ^ (flag[13] + flag[31]) == 8)
s.add((flag[36] - flag[15]) * flag[17] == 5238)
s.add((flag[16] - flag[30]) * flag[33] == 0)
s.add((flag[2] - flag[20] + flag[13]) ^ flag[6] == 76)
s.add((flag[10] - flag[14] + flag[31]) ^ flag[13] == 3)
s.add((flag[0] * flag[10]) ^ (flag[14] - flag[31]) == 2854)
s.add((flag[28] - flag[34] + flag[14]) ^ flag[14] == 82)
s.add((flag[28] - flag[25]) * flag[1] == 2444)
s.add((flag[34] - flag[12]) * flag[25] == -2400)
s.add((flag[28] * flag[38]) ^ (flag[17] - flag[4]) == 5429)
s.add((flag[21] - flag[21] + flag[26]) ^ flag[23] == 84)
s.add((flag[9] - flag[4] + flag[18]) ^ flag[35] == 47)
s.add((flag[28] - flag[21] + flag[1]) ^ flag[33] == 0)
s.add((flag[24] - flag[25] + flag[22]) ^ flag[0] == 8)
s.add((flag[28] - flag[25]) * flag[12] == 4653)
s.add((flag[1] * flag[15]) ^ (flag[10] - flag[8]) == 2498)
s.add((flag[5] * flag[7]) ^ (flag[15] - flag[34]) == -3429)
s.add((flag[8] * flag[3]) ^ (flag[23] - flag[22]) == 3671)
s.add((flag[25] - flag[33]) * flag[11] == -2600)
s.add((flag[21] + flag[12]) ^ (flag[37] + flag[28]) == 81)
s.add((flag[30] + flag[33]) ^ (flag[34] + flag[14]) == 162)
s.add((flag[6] - flag[25]) * flag[8] == 4015)
s.add((flag[24] - flag[7] + flag[12]) ^ flag[7] == 90)
s.add((flag[18] * flag[12]) ^ (flag[8] - flag[4]) == -5466)
s.add((flag[32] * flag[7]) ^ (flag[32] - flag[27]) == -2730)
s.add((flag[32] * flag[34]) ^ (flag[29] - flag[16]) == 2804)
s.add((flag[25] * flag[22]) ^ (flag[28] - flag[39]) == -2542)
s.add((flag[8] - flag[15]) * flag[6] == 861)
s.add((flag[20] + flag[18]) ^ (flag[25] + flag[36]) == 245)
s.add((flag[5] - flag[28] + flag[14]) ^ flag[39] == 97)
s.add((flag[30] * flag[11]) ^ (flag[16] - flag[11]) == 5216)
s.add((flag[11] + flag[18]) ^ (flag[7] + flag[9]) == 13)
s.add((flag[9] - flag[2]) * flag[30] == -200)
s.add((flag[12] + flag[37]) ^ (flag[9] + flag[4]) == 78)
s.add((flag[10] - flag[37]) * flag[38] == -2408)
s.add((flag[5] * flag[19]) ^ (flag[20] - flag[21]) == 3645)
s.add((flag[27] * flag[29]) ^ (flag[39] - flag[21]) == 10354)
s.add((flag[15] * flag[32]) ^ (flag[7] - flag[22]) == -2642)
s.add((flag[1] - flag[3] + flag[24]) ^ flag[31] == 25)
s.add((flag[13] - flag[0]) * flag[30] == 400)
s.add((flag[18] - flag[15] + flag[36]) ^ flag[28] == 12)
s.add((flag[34] + flag[21]) ^ (flag[12] + flag[37]) == 163)
s.add((flag[36] - flag[33]) * flag[14] == 110)
s.add((flag[2] - flag[3]) * flag[3] == -804)
s.add((flag[35] - flag[27] + flag[22]) ^ flag[4] == 80)
s.add((flag[10] + flag[9]) ^ (flag[17] + flag[2]) == 246)
s.add((flag[25] * flag[4]) ^ (flag[27] - flag[23]) == 4201)
s.add((flag[32] * flag[19]) ^ (flag[3] - flag[25]) == 2877)
s.add((flag[37] - flag[14]) * flag[23] == 4545)
s.add((flag[32] + flag[13]) ^ (flag[31] + flag[32]) == 7)
s.add((flag[11] - flag[25]) * flag[39] == 250)
s.add((flag[17] + flag[31]) ^ (flag[6] + flag[9]) == 36)
s.add((flag[4] + flag[27]) ^ (flag[2] + flag[31]) == 208)
s.add((flag[6] + flag[7]) ^ (flag[26] + flag[21]) == 206)
s.add((flag[19] + flag[25]) ^ (flag[22] + flag[10]) == 10)
s.add((flag[34] + flag[2]) ^ (flag[8] + flag[26]) == 2)
s.add((flag[7] + flag[5]) ^ (flag[12] + flag[14]) == 237)
s.add((flag[1] - flag[13]) * flag[38] == -112)
s.add((flag[0] - flag[19] + flag[16]) ^ flag[0] == 80)
s.add((flag[31] + flag[36]) ^ (flag[3] + flag[2]) == 227)
s.add((flag[32] - flag[3] + flag[26]) ^ flag[4] == 113)
s.add((flag[3] * flag[6]) ^ (flag[16] - flag[27]) == -8241)
s.add((flag[24] + flag[15]) ^ (flag[2] + flag[30]) == 242)
s.add((flag[11] + flag[21]) ^ (flag[31] + flag[20]) == 12)
s.add((flag[9] - flag[26] + flag[23]) ^ flag[30] == 13)
obj = {}
if s.check() == sat:
    model = s.model()
    for m in model:
        obj[int(str(m)[5:-1])] = int(str(model[m]))
print(json.dumps(obj))

Key check script:

# 0x20/32 chars
# between @ and Z
# first char + 0xb5 || char + 0xb1 == f7

def fuck(input):
    if input < 'N':
        return ord(input) + 0xb5
    else:
        return ord(input) + 0xb1

def checkInRange(key):
    for c in key:
        if c < '@' or c > 'Z':
            return False
    return True

def getTotal(key):
    total = 0xf7
    for i in range(1, len(key)):
        total = total + (fuck(key[i]) - i) + 0xf7
    return total


key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

if len(key) != 0x20:
    print("wrong key length")
    exit(0)

if not checkInRange(key):
    print("some char out of range (@ - Z)")
    exit(0)

total = getTotal(key)
print(f"Total: {total}")

if total % 0xf8 != fuck(key[0]) or total % 0xf8 != 0xf7:
    print("not valid")
    print(f"{total} % 0xf8 = {total % 0xf8} != {0xf7}")
    exit(0)

print("valid")