Yuri’s Simple Keygen challenge

Adriik

2026-02-02

8~ minutes

Índice

Note: I am not a English speaker, and I used DeepL Write to “fix” my bad english. Expect maybe weird english. Also, I am noob at reverse engineering. This is my second challenge.

About the challenge

We have a program called SimpleKeyGen that validates serial keys passed as argument. Therefore, we need to analyze the software in order to generate valid keys for it.

Static analysis

Information about the binary

First, we need to understand the software we are dealing with:

> file SimpleKeyGen

SimpleKeyGen: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4b657ffd4f43a704861b200c9f2235f8fe395e27, not stripped

This is an ELF (Executable and Linkable Format) binary for Linux x86-64, compiled with PIE (Position Independent Executable). This means that the program’s base memory position is random. Additionally, it is dynamically linked, meaning functions from libraries are not included in the program.

We can also see that the binary is not stripped, allowing us to view the symbol table:

> objdump -t SimpleKeyGen | grep " F "

00000000000010a0 l     F .text  0000000000000000    deregister_tm_clones
00000000000010d0 l     F .text  0000000000000000    register_tm_clones
0000000000001110 l     F .text  0000000000000000    __do_global_dtors_aux
0000000000001160 l     F .text  0000000000000000    frame_dummy
0000000000001300 g     F .text  0000000000000005    __libc_csu_fini
0000000000000000       F *UND*  0000000000000000    puts@@GLIBC_2.2.5
0000000000001308 g     F .fini  0000000000000000    .hidden _fini
0000000000000000       F *UND*  0000000000000000    strlen@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000    printf@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000    __libc_start_main@@GLIBC_2.2.5
0000000000001197 g     F .text  000000000000008f    checkSerial
0000000000001290 g     F .text  0000000000000065    __libc_csu_init
0000000000001070 g     F .text  000000000000002f    _start
0000000000001226 g     F .text  0000000000000061    main
0000000000001169 g     F .text  000000000000002e    usage
0000000000000000       F *UND*  0000000000000000    exit@@GLIBC_2.2.5
0000000000000000  w    F *UND*  0000000000000000    __cxa_finalize@@GLIBC_2.2.5
0000000000001000 g     F .init  0000000000000000    .hidden _init

We can see the use of some glibc functions, such as printf and puts to print strings and strlen to determine the length of a string. There is no function for getting input from the user because the key is passed as a parameter to the program.

Then, we have the main function, the usage function that shows how to use the program, and one interesting function: checkSerial.

Analysis of the assembly code

We start by analyzing the main function to understand the program’s flow. We can see the disassembly with objdump --disassemble=main -M intel SimpleKeyGen:

0000000000001226 <main>:
    1226:   55                      push   rbp
    1227:   48 89 e5                mov    rbp,rsp
    122a:   48 83 ec 10             sub    rsp,0x10
    122e:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
    1231:   48 89 75 f0             mov    QWORD PTR [rbp-0x10],rsi
    1235:   83 7d fc 02             cmp    DWORD PTR [rbp-0x4],0x2
    1239:   74 0f                   je     124a <main+0x24>
    123b:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
    123f:   48 8b 00                mov    rax,QWORD PTR [rax]
    1242:   48 89 c7                mov    rdi,rax
    1245:   e8 1f ff ff ff          call   1169 <usage>
    124a:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
    124e:   48 83 c0 08             add    rax,0x8
    1252:   48 8b 00                mov    rax,QWORD PTR [rax]
    1255:   48 89 c7                mov    rdi,rax
    1258:   e8 3a ff ff ff          call   1197 <checkSerial>
    125d:   85 c0                   test   eax,eax
    125f:   75 13                   jne    1274 <main+0x4e>
    1261:   48 8d 3d a9 0d 00 00    lea    rdi,[rip+0xda9]        # 2011 <_IO_stdin_used+0x11>
    1268:   e8 c3 fd ff ff          call   1030 <puts@plt>
    126d:   b8 00 00 00 00          mov    eax,0x0
    1272:   eb 11                   jmp    1285 <main+0x5f>
    1274:   48 8d 3d a2 0d 00 00    lea    rdi,[rip+0xda2]        # 201d <_IO_stdin_used+0x1d>
    127b:   e8 b0 fd ff ff          call   1030 <puts@plt>
    1280:   b8 ff ff ff ff          mov    eax,0xffffffff
    1285:   c9                      leave
    1286:   c3                      ret

First, we see the creation of the stack frame and the main arguments being moved to the stack. Then, we see the check to ensure that the program has exactly two arguments. The check is for two because a program always has its own name as the first argument.

The relevant part of the main function is as follows:

124a:   48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
124e:   48 83 c0 08             add    rax,0x8
1252:   48 8b 00                mov    rax,QWORD PTR [rax]
1255:   48 89 c7                mov    rdi,rax
1258:   e8 3a ff ff ff          call   1197 <checkSerial>

We can see that the second argument of the program is passed to the checkSerial function. We know is the second one because rbp-0x10 contains the pointer to the beginning of the vector argument, and then 8 bytes is added to the register (64 bits = 8 bytes).

So, we know now checkSerial checks the parameter we passed to the program. Knowing that, we can focus in the checkSerial function.

We can use again objdump to see the assembly code:

0000000000001197 <checkSerial>:
    1197:   55                      push   rbp
    1198:   48 89 e5                mov    rbp,rsp
    119b:   53                      push   rbx
    119c:   48 83 ec 28             sub    rsp,0x28
    11a0:   48 89 7d d8             mov    QWORD PTR [rbp-0x28],rdi
    11a4:   48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
    11a8:   48 89 c7                mov    rdi,rax
    11ab:   e8 90 fe ff ff          call   1040 <strlen@plt>
    11b0:   48 83 f8 10             cmp    rax,0x10
    11b4:   74 07                   je     11bd <checkSerial+0x26>
    11b6:   b8 ff ff ff ff          mov    eax,0xffffffff
    11bb:   eb 62                   jmp    121f <checkSerial+0x88>
    11bd:   c7 45 ec 00 00 00 00    mov    DWORD PTR [rbp-0x14],0x0
    11c4:   eb 3d                   jmp    1203 <checkSerial+0x6c>
    11c6:   8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
    11c9:   48 63 d0                movsxd rdx,eax
    11cc:   48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
    11d0:   48 01 d0                add    rax,rdx
    11d3:   0f b6 00                movzx  eax,BYTE PTR [rax]
    11d6:   0f be d0                movsx  edx,al
    11d9:   8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
    11dc:   48 98                   cdqe
    11de:   48 8d 48 01             lea    rcx,[rax+0x1]
    11e2:   48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
    11e6:   48 01 c8                add    rax,rcx
    11e9:   0f b6 00                movzx  eax,BYTE PTR [rax]
    11ec:   0f be c0                movsx  eax,al
    11ef:   29 c2                   sub    edx,eax
    11f1:   89 d0                   mov    eax,edx
    11f3:   83 f8 ff                cmp    eax,0xffffffff
    11f6:   74 07                   je     11ff <checkSerial+0x68>
    11f8:   b8 ff ff ff ff          mov    eax,0xffffffff
    11fd:   eb 20                   jmp    121f <checkSerial+0x88>
    11ff:   83 45 ec 02             add    DWORD PTR [rbp-0x14],0x2
    1203:   8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
    1206:   48 63 d8                movsxd rbx,eax
    1209:   48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
    120d:   48 89 c7                mov    rdi,rax
    1210:   e8 2b fe ff ff          call   1040 <strlen@plt>
    1215:   48 39 c3                cmp    rbx,rax
    1218:   72 ac                   jb     11c6 <checkSerial+0x2f>
    121a:   b8 00 00 00 00          mov    eax,0x0
    121f:   48 83 c4 28             add    rsp,0x28
    1223:   5b                      pop    rbx
    1224:   5d                      pop    rbp
    1225:   c3                      ret

Okay, we could analyze the assembly code for this function, but with all the jumps and register movement, it would be easier to open this in Ghidra and decompile it.

Too much assembly code, time for Ghidra

undefined8 checkSerial(char *param_1)
{
    size_t sVar1;
    undefined8 uVar2;
    int local_1c;

    sVar1 = strlen(param_1);
    if (sVar1 == 0x10) {
        for (local_1c = 0;
             sVar1 = strlen(param_1), (ulong)(long)local_1c < sVar1; local_1c = local_1c + 2) {
            if ((int)param_1[local_1c] - (int)param_1[(long)local_1c + 1] != -1) {
                return 0xffffffff;
            }
        }
        uVar2 = 0;
    } else {
        uVar2 = 0xffffffff;
    }
    return uVar2;
}

It’s time to rename some variables.

The code now looks like this:

undefined8 checkSerial(char *string)
{
    size_t string_len;
    undefined8 return_code;
    int i;

    string_len = strlen(string);
    if (string_len == 16) {
        for (i = 0; string_len = strlen(string), (ulong)(long)i < string_len; i = i + 2) {
            if ((int)string[i] - (int)string[(long)i + 1] != -1) {
                return 0xffffffff;
            }
        }
        return_code = 0;
    }
    else {
        return_code = 0xffffffff;
    }
    return return_code;
}

First, if the string’s length isn’t 16, the function returns -1 (although it shows 0xffffffff, we need to think of this as a two-complement value).

The function traverses the string by two-step increments and checks if the subtraction of the current character and the next character is different from -1; if so, the function returns -1. Otherwise, the function continues.

For example, if we have the two characters ‘A’ (0x41) and ‘B’ (0x42), subtracting them in that order yields -1. Therefore, a valid key is a string of 16 ascending characters, such as “abcdefghijklmnop”.

Time to test it!

> ./SimpleKeyGen abcdefghijklmnop
Good Serial

We now need to create a small program that generates all possible valid keys.

#include <stdio.h>

#define KEY_LEN 17

int main(void)
{
        unsigned char key[KEY_LEN];
        unsigned char last_char = 0x7e;
        unsigned char start = 0x20;
        unsigned char end = start + 0x10;

        while (end != last_char) {
                for (int i = 0; i < KEY_LEN - 1; i++)
                        key[i] = start + i;
                key[KEY_LEN - 1] = '\0';
                printf("Key: %s\n", key);
                start++;
                end++;
        }

        return 0;
}

Now, we can generate all the valid keys that use only printable characters. Here are the first five generated keys:

Key:  !"#$%&'()*+,-./
Key: !"#$%&'()*+,-./0
Key: "#$%&'()*+,-./01
Key: #$%&'()*+,-./012
Key: $%&'()*+,-./0123

We can test to see if the keys are valid. (We need to escape certain characters to avoid having the shell interpret them).

> ./SimpleKeyGen " \!\"#$%&'()*+,-./"
Good Serial

> ./SimpleKeyGen "#$%&'()*+,-./012"
Good Serial

Conclusion

Although it may be a simple program with a simple algorithm, we learned how to analyze a binary to understand how it works in detail. We focus on the important parts of the program and write a program that generates valid keys using the knowledge we gain from analyzing how the software validates keys. We can apply this knowledge to any software to analyze it and see how it works and what it does. Perhaps we will discover that our favorite software is performing suspicious activities.