How to make a keygen.

ctfs.me – resqua

In this article I will explain how to unpack a packed/mangled binary and how to make a keygen. Using the ctfs.me resqua challenge as a example.

The binary I’ll be working with is not stripped, so the the symbol table is still intact. But there is an other methode used to make reverse engineering a little more difficult.

They have mangled some parts of the code, if we try to disassemble this we get a bunch of instructions that doesn’t make much sense.

When a program gets executed it’s first gets loaded into memory.
After this happens the executable sectors of the programs get a write protected flag set so the instructions can’t be altered.

In this case however some of the programs executable sectors are mangled.
and before it can get executed has to be de-mangeld first.

For this to happen the write flag has to be set on the memory, so instructions can be altered/ or un-mangled.

So first I will have to find out where this happens and what memory sectors are effected.

Below is a disassembly of the “unlock” function that un-mangles the code in memory.

                                    unlock:
00000000004007dd 55                     push       rbp                          ; Begin of unwind block (FDE at 0x400cfc), CODE XREF=main+20
00000000004007de 4889E5                 mov        rbp, rsp
00000000004007e1 4883EC40               sub        rsp, 0x40
00000000004007e5 B809004000             mov        eax, 0x400009
00000000004007ea 8B00                   mov        eax, dword [rax]
00000000004007ec 8945C8                 mov        dword [rbp+var_38], eax
00000000004007ef B80D004000             mov        eax, 0x40000d
00000000004007f4 0FB700                 movzx      eax, word [rax]
00000000004007f7 98                     cwde
00000000004007f8 8945CC                 mov        dword [rbp+var_34], eax
00000000004007fb C745D003000000         mov        dword [rbp+var_30], 0x3
0000000000400802 48C745D801004000       mov        qword [rbp+var_28], 0x400001
000000000040080a 8B45C8                 mov        eax, dword [rbp+var_38]
000000000040080d 4898                   cdqe
000000000040080f 480500004000           add        rax, 0x400000
0000000000400815 488945E0               mov        qword [rbp+var_20], rax
0000000000400819 8B45C8                 mov        eax, dword [rbp+var_38]
000000000040081c 4863D0                 movsxd     rdx, eax
000000000040081f 8B45CC                 mov        eax, dword [rbp+var_34]
0000000000400822 4898                   cdqe
0000000000400824 4801D0                 add        rax, rdx
0000000000400827 480500004000           add        rax, 0x400000
000000000040082d 488945E8               mov        qword [rbp+var_18], rax
0000000000400831 BF1E000000             mov        edi, 0x1e                    ; argument "__name" for method j_sysconf
0000000000400836 E885FEFFFF             call       j_sysconf                    ; sysconf
000000000040083b 488945F0               mov        qword [rbp+var_10], rax
000000000040083f 488B45F0               mov        rax, qword [rbp+var_10]
0000000000400843 48F7D8                 neg        rax
0000000000400846 4889C2                 mov        rdx, rax
0000000000400849 488B45E0               mov        rax, qword [rbp+var_20]
000000000040084d 4821D0                 and        rax, rdx
0000000000400850 488945F8               mov        qword [rbp+var_8], rax
0000000000400854 488B45E8               mov        rax, qword [rbp+var_18]
0000000000400858 89C2                   mov        edx, eax
000000000040085a 488B45F8               mov        rax, qword [rbp+var_8]
000000000040085e 29C2                   sub        edx, eax
0000000000400860 89D0                   mov        eax, edx
0000000000400862 8945D4                 mov        dword [rbp+var_2C], eax
0000000000400865 8B45D4                 mov        eax, dword [rbp+var_2C]
0000000000400868 4863C8                 movsxd     rcx, eax
000000000040086b 488B45F8               mov        rax, qword [rbp+var_8]
000000000040086f BA07000000             mov        edx, 0x7                     ; argument "__prot" for method j_mprotect
0000000000400874 4889CE                 mov        rsi, rcx                     ; argument "__len" for method j_mprotect
0000000000400877 4889C7                 mov        rdi, rax                     ; argument "__addr" for method j_mprotect
000000000040087a E831FEFFFF             call       j_mprotect                   ; mprotect
000000000040087f C745C400000000         mov        dword [rbp+var_3C], 0x0
0000000000400886 EB3E                   jmp        loc_4008c6

                                    loc_400888:
0000000000400888 8B45C4                 mov        eax, dword [rbp+var_3C]      ; CODE XREF=unlock+239
000000000040088b 4863D0                 movsxd     rdx, eax
000000000040088e 488B45E0               mov        rax, qword [rbp+var_20]
0000000000400892 488D0C02               lea        rcx, qword [rdx+rax]
0000000000400896 8B45C4                 mov        eax, dword [rbp+var_3C]
0000000000400899 4863D0                 movsxd     rdx, eax
000000000040089c 488B45E0               mov        rax, qword [rbp+var_20]
00000000004008a0 4801D0                 add        rax, rdx
00000000004008a3 0FB600                 movzx      eax, byte [rax]
00000000004008a6 89C6                   mov        esi, eax
00000000004008a8 8B45C4                 mov        eax, dword [rbp+var_3C]
00000000004008ab 99                     cdq
00000000004008ac F77DD0                 idiv       dword [rbp+var_30]
00000000004008af 89D0                   mov        eax, edx
00000000004008b1 4863D0                 movsxd     rdx, eax
00000000004008b4 488B45D8               mov        rax, qword [rbp+var_28]
00000000004008b8 4801D0                 add        rax, rdx
00000000004008bb 0FB600                 movzx      eax, byte [rax]
00000000004008be 31F0                   xor        eax, esi
00000000004008c0 8801                   mov        byte [rcx], al
00000000004008c2 8345C401               add        dword [rbp+var_3C], 0x1

                                    loc_4008c6:
00000000004008c6 8B45C4                 mov        eax, dword [rbp+var_3C]      ; CODE XREF=unlock+169
00000000004008c9 3B45CC                 cmp        eax, dword [rbp+var_34]
00000000004008cc 7CBA                   jl         loc_400888

00000000004008ce 8B45D4                 mov        eax, dword [rbp+var_2C]
00000000004008d1 4863C8                 movsxd     rcx, eax
00000000004008d4 488B45F8               mov        rax, qword [rbp+var_8]
00000000004008d8 BA05000000             mov        edx, 0x5                     ; argument "__prot" for method j_mprotect
00000000004008dd 4889CE                 mov        rsi, rcx                     ; argument "__len" for method j_mprotect
00000000004008e0 4889C7                 mov        rdi, rax                     ; argument "__addr" for method j_mprotect
00000000004008e3 E8C8FDFFFF             call       j_mprotect                   ; mprotect
00000000004008e8 C9                     leave
00000000004008e9 C3                     ret

I could dive into this code to see how it is changing what data, but that would be a waste of time since I only need the result of wat its doing and I could just run it and dump it from memory.

Dumping

In the disassembly of the “unlock” function you can see two calls to mprotect.
I set 2 breakpoints:
one right before the first call to mprotect.
and one right after the second call to mprotect.

after hitting the first breakpoint I check check the rax and rcx registers to see what arguments are passed to mprotect.

rax 0x0000000000400000    
rcx 0x0000000000000ba2

rax holds the start address of the affected memory and the second and rcx holds the length.

So I dump the memory in that range.

>>> dump binary memory before.bin 0x400000 0x400ba2 

Now i continue to the second breakpoint and i do the same.

>>> dump binary memory after.bin 0x400000 0x400ba2 

I use bindiff to check if the dumpfiles indeed differ.

$ diff before.bin after.bin 
Binary files before.bin and after.bin differ

Patching

To analyze the code it is help full to be able to run it in a debugger.
to do this with the dumped code, I will now make a patched executable.

The dump starts at offset 0x400000 and since this is the beginning of the file I can just append the data from the original file starting at the offset 0xba2 to the dumpfile.

$ cp after.bin patched
$ dd if=resqua bs=1 skip=2978 of=patched oflag=append conv=notruncd

The mangled part of code is now un-mangled in the binary. So the call to “unlock” has to be overwritten by NOP’s. I do this hopper since I’m going to use it for disassembly anyway.

                                    main:
00000000004008ea 55                     push       rbp                          ; End of unwind block (FDE at 0x400cfc), Begin of unwind block (FDE at 0x400d5c), DATA XREF=_start+29
00000000004008eb 4889E5                 mov        rbp, rsp
00000000004008ee 4883EC10               sub        rsp, 0x10
00000000004008f2 897DFC                 mov        dword [rbp+var_4], edi
00000000004008f5 488975F0               mov        qword [rbp+var_10], rsi
00000000004008f9 B800000000             mov        eax, 0x0
00000000004008fe E8DAFEFFFF             call       unlock                       ; unlock
0000000000400903 B800000000             mov        eax, 0x0
0000000000400908 E8C1000000             call       run                          ; run
000000000040090d B800000000             mov        eax, 0x0
0000000000400912 C9                     leave
0000000000400913 C3                     ret
                                    main:
00000000004008ea 55                     push       rbp                          ; End of unwind block (FDE at 0x400cfc), Begin of unwind block (FDE at 0x400d5c), DATA XREF=_start+29
00000000004008eb 4889E5                 mov        rbp, rsp
00000000004008ee 4883EC10               sub        rsp, 0x10
00000000004008f2 897DFC                 mov        dword [rbp+var_4], edi
00000000004008f5 488975F0               mov        qword [rbp+var_10], rsi
00000000004008f9 B800000000             mov        eax, 0x0
00000000004008fe 0F1F440000             nop        dword [rax+rax]                      ; unlock
0000000000400903 B800000000             mov        eax, 0x0
0000000000400908 E8C1000000             call       run                          ; run
000000000040090d B800000000             mov        eax, 0x0
0000000000400912 C9                     leave
0000000000400913 C3                     ret

analyze the code

lets take a look at the “run” function at 0x4009ce that was mangled before.

00000000004009ce 55                     push       rbp                          ; End of unwind block (FDE at 0x400d1c), Begin of unwind block (FDE at 0x400d3c), CODE XREF=main+30
00000000004009cf 4889E5                 mov        rbp, rsp
00000000004009d2 4883C480               add        rsp, 0xffffffffffffff80
00000000004009d6 64488B042528000000     mov        rax, qword [fs:0x28]
00000000004009df 488945F8               mov        qword [rbp+var_8], rax
00000000004009e3 31C0                   xor        eax, eax
00000000004009e5 BFB80B4000             mov        edi, aEnterAValidSer         ; argument "__format" for method j_printf, "Enter a valid serial number: "
00000000004009ea B800000000             mov        eax, 0x0
00000000004009ef E87CFCFFFF             call       j_printf                     ; printf
00000000004009f4 488D45E0               lea        rax, qword [rbp+var_20]
00000000004009f8 4889C7                 mov        rdi, rax                     ; argument "__str" for method j_gets
00000000004009fb E8A0FCFFFF             call       j_gets                       ; gets
0000000000400a00 488D45E0               lea        rax, qword [rbp+var_20]

the first thing that gets checked after getting user input is the length of the input string. It hast to be 19 charcters long otherwise it wil jump to 0x400a2a and print out “Wrong format!”

0000000000400a00 488D45E0               lea        rax, qword [rbp+var_20]
0000000000400a04 4889C7                 mov        rdi, rax                     ; argument "__s" for method j_strlen
0000000000400a07 E844FCFFFF             call       j_strlen                     ; strlen
0000000000400a0c 4883F813               cmp        rax, 0x13
0000000000400a10 7518                   jne        Wrong input

0000000000400a12 0FB645E4               movzx      eax, byte [rbp+var_1C]
0000000000400a16 3C2D                   cmp        al, 0x2d
0000000000400a18 7510                   jne        Wrong input

0000000000400a1a 0FB645E9               movzx      eax, byte [rbp+var_17]
0000000000400a1e 3C2D                   cmp        al, 0x2d
0000000000400a20 7508                   jne        Wrong input

0000000000400a22 0FB645EE               movzx      eax, byte [rbp+var_12]
0000000000400a26 3C2D                   cmp        al, 0x2d
0000000000400a28 740F                   je         loc_400a39

                                    Wrong input:
0000000000400a2a BFD60B4000             mov        edi, 0x400bd6                ; argument "__s" for method j_puts, CODE XREF=run+66, run+74, run+82
0000000000400a2f E80CFCFFFF             call       j_puts                       ; puts
0000000000400a34 E953010000             jmp        loc_400b8c

Then it will check if the 5th, 10th and 15th byte equals to 0x2d which has the ASCII value of “-” otherwise it will jump to 0x400a0c

Now we know that the serial code has to have the following format.
XXXX-XXXX-XXXX-XXXX

Lets check what happens next.

                                   loc_400a39:
0000000000400a39 C7458C00000000         mov        dword [rbp+var_74], 0x0      ; CODE XREF=run+90
0000000000400a40 EB26                   jmp        loc_400a68

                                    loc_400a42:
0000000000400a42 8B458C                 mov        eax, dword [rbp+var_74]      ; CODE XREF=run+158
0000000000400a45 4898                   cdqe
0000000000400a47 0FB64405E0             movzx      eax, byte [rbp+rax+var_20]
0000000000400a4c 3C30                   cmp        al, 0x30
0000000000400a4e 7514                   jne        loc_400a64

0000000000400a50 BFF00B4000             mov        edi, 0x400bf0                ; argument "__s" for method j_puts
0000000000400a55 E8E6FBFFFF             call       j_puts                       ; puts
0000000000400a5a BF01000000             mov        edi, 0x1                     ; argument "__status" for method j_exit
0000000000400a5f E87CFCFFFF             call       j_exit                       ; exit
                        ; endp

                                    loc_400a64:
0000000000400a64 83458C01               add        dword [rbp+var_74], 0x1      ; CODE XREF=run+128

                                    loc_400a68:
0000000000400a68 837D8C13               cmp        dword [rbp+var_74], 0x13     ; CODE XREF=run+114
0000000000400a6c 7ED4                   jle        loc_400a42

Here it will cycle trough the provided serial number and prints “Invalide serial number!” if any of the bytes equals 0x30 (or ASCII “0”)

now we have learned a little more about the requirements of a valid serial code.

0000000000400a6e 488D4DE0               lea        rcx, qword [rbp+var_20]
0000000000400a72 488D45A0               lea        rax, qword [rbp+var_60]
0000000000400a76 BA04000000             mov        edx, 0x4                     ; argument "__n" for method j_strncpy
0000000000400a7b 4889CE                 mov        rsi, rcx                     ; argument "__src" for method j_strncpy
0000000000400a7e 4889C7                 mov        rdi, rax                     ; argument "__dest" for method j_strncpy
0000000000400a81 E8AAFBFFFF             call       j_strncpy                    ; strncpy
0000000000400a86 C645A400               mov        byte [rbp+var_5C], 0x0
0000000000400a8a 488D45E0               lea        rax, qword [rbp+var_20]
0000000000400a8e 488D4805               lea        rcx, qword [rax+5]
0000000000400a92 488D45B0               lea        rax, qword [rbp+var_50]
0000000000400a96 BA04000000             mov        edx, 0x4                     ; argument "__n" for method j_strncpy
0000000000400a9b 4889CE                 mov        rsi, rcx                     ; argument "__src" for method j_strncpy
0000000000400a9e 4889C7                 mov        rdi, rax                     ; argument "__dest" for method j_strncpy
0000000000400aa1 E88AFBFFFF             call       j_strncpy                    ; strncpy
0000000000400aa6 C645B400               mov        byte [rbp+var_4C], 0x0
0000000000400aaa 488D45E0               lea        rax, qword [rbp+var_20]
0000000000400aae 488D480A               lea        rcx, qword [rax+0xa]
0000000000400ab2 488D45C0               lea        rax, qword [rbp+var_40]
0000000000400ab6 BA04000000             mov        edx, 0x4                     ; argument "__n" for method j_strncpy
0000000000400abb 4889CE                 mov        rsi, rcx                     ; argument "__src" for method j_strncpy
0000000000400abe 4889C7                 mov        rdi, rax                     ; argument "__dest" for method j_strncpy
0000000000400ac1 E86AFBFFFF             call       j_strncpy                    ; strncpy
0000000000400ac6 C645C400               mov        byte [rbp+var_3C], 0x0
0000000000400aca 488D45E0               lea        rax, qword [rbp+var_20]
0000000000400ace 488D480F               lea        rcx, qword [rax+0xf]
0000000000400ad2 488D45D0               lea        rax, qword [rbp+var_30]
0000000000400ad6 BA04000000             mov        edx, 0x4                     ; argument "__n" for method j_strncpy
0000000000400adb 4889CE                 mov        rsi, rcx                     ; argument "__src" for method j_strncpy
0000000000400ade 4889C7                 mov        rdi, rax                     ; argument "__dest" for method j_strncpy
0000000000400ae1 E84AFBFFFF             call       j_strncpy                    ; strncpy
0000000000400ae6 C645D400               mov        byte [rbp+var_2C], 0x0
0000000000400aea 488D45A0               lea        rax, qword [rbp+var_60]
0000000000400aee 4889C7                 mov        rdi, rax                     ; argument "__nptr" for method j_atoi
0000000000400af1 E8DAFBFFFF             call       j_atoi                       ; atoi
0000000000400af6 894590                 mov        dword [rbp+var_70], eax
0000000000400af9 488D45B0               lea        rax, qword [rbp+var_50]
0000000000400afd 4889C7                 mov        rdi, rax                     ; argument "__nptr" for method j_atoi
0000000000400b00 E8CBFBFFFF             call       j_atoi                       ; atoi
0000000000400b05 894594                 mov        dword [rbp+var_6C], eax
0000000000400b08 488D45C0               lea        rax, qword [rbp+var_40]
0000000000400b0c 4889C7                 mov        rdi, rax                     ; argument "__nptr" for method j_atoi
0000000000400b0f E8BCFBFFFF             call       j_atoi                       ; atoi
0000000000400b14 894598                 mov        dword [rbp+var_68], eax
0000000000400b17 488D45D0               lea        rax, qword [rbp+var_30]
0000000000400b1b 4889C7                 mov        rdi, rax                     ; argument "__nptr" for method j_atoi
0000000000400b1e E8ADFBFFFF             call       j_atoi                       ; atoi
0000000000400b23 89459C                 mov        dword [rbp+var_64], eax

Over here the chunks* of the serial number gets copied to their own parts in memory using strncopy(). Shown as var_60, var_50, var_40 and var_30 in hoppers disassembly
*(by chunks I mean the parts before, after and in between the ‘-‘ of the serial number)

changes these chucks of serial into integers using atoi(). and stores the results to other parts of memory. Shown as var_70, var_6c, var_68 and var_64.

0000000000400b26 8B4590                 mov        eax, dword [rbp+var_70]
0000000000400b29 89C7                   mov        edi, eax                     ; argument #1 for method c
0000000000400b2b E862FEFFFF             call       c                            ; c
0000000000400b30 85C0                   test       eax, eax
0000000000400b32 744E                   je         loc_400b82

0000000000400b34 8B4594                 mov        eax, dword [rbp+var_6C]
0000000000400b37 89C7                   mov        edi, eax                     ; argument #1 for method c
0000000000400b39 E854FEFFFF             call       c                            ; c
0000000000400b3e 85C0                   test       eax, eax
0000000000400b40 7440                   je         loc_400b82

0000000000400b42 8B4598                 mov        eax, dword [rbp+var_68]
0000000000400b45 89C7                   mov        edi, eax                     ; argument #1 for method c
0000000000400b47 E846FEFFFF             call       c                            ; c
0000000000400b4c 85C0                   test       eax, eax
0000000000400b4e 7432                   je         loc_400b82

0000000000400b50 8B459C                 mov        eax, dword [rbp+var_64]
0000000000400b53 89C7                   mov        edi, eax                     ; argument #1 for method c
0000000000400b55 E838FEFFFF             call       c                            ; c
0000000000400b5a 85C0                   test       eax, eax
0000000000400b5c 7424                   je         loc_400b82

0000000000400b5e 8B4590                 mov        eax, dword [rbp+var_70]
0000000000400b61 3B4594                 cmp        eax, dword [rbp+var_6C]
0000000000400b64 7D1C                   jge        loc_400b82

0000000000400b66 8B4594                 mov        eax, dword [rbp+var_6C]
0000000000400b69 3B4598                 cmp        eax, dword [rbp+var_68]
0000000000400b6c 7D14                   jge        loc_400b82

0000000000400b6e 8B4598                 mov        eax, dword [rbp+var_68]
0000000000400b71 3B459C                 cmp        eax, dword [rbp+var_64]
0000000000400b74 7D0C                   jge        loc_400b82

0000000000400b76 BF100C4000             mov        edi, 0x400c10                ; argument "__s" for method j_puts
0000000000400b7b E8C0FAFFFF             call       j_puts                       ; puts
0000000000400b80 EB0A                   jmp        loc_400b8c

                                    loc_400b82:
0000000000400b82 BFF00B4000             mov        edi, 0x400bf0                ; argument "__s" for method j_puts, CODE XREF=run+356, run+370, run+384, run+398, run+406, run+414, run+422
0000000000400b87 E8B4FAFFFF             call       j_puts                       ; puts

                                    loc_400b8c:
0000000000400b8c 488B45F8               mov        rax, qword [rbp+var_8]       ; CODE XREF=run+102, run+434
0000000000400b90 644833042528000000     xor        rax, qword [fs:0x28]
0000000000400b99 7405                   je         loc_400ba0

0000000000400b9b E8C0FAFFFF             call       j___stack_chk_fail           ; __stack_chk_fail
                        ; endp

                                    loc_400ba0:
0000000000400ba0 C9                     leave                                   ; CODE XREF=run+459
0000000000400ba1 C3                     ret

One by one the numbers get fed to the function c, if the fuction c returns 0x0 “Invalide serial number!” gets printed.

Also the first chunk cannot be greater than or equal to the second, the second not equal or greater than the third and the third not equal or greater than the fourth or “Invalide serial number!” gets printed

lets have a look at the function named c
                                    c:
0000000000400992 55                     push       rbp                          ; Begin of unwind block (FDE at 0x400d1c), End of unwind block (FDE at 0x400dc4), CODE XREF=run+349, run+363, run+377, run+391
0000000000400993 4889E5                 mov        rbp, rsp
0000000000400996 897DEC                 mov        dword [rbp+var_14], edi
0000000000400999 817DEC56040000         cmp        dword [rbp+var_14], 1110
00000000004009a0 7F07                   jg         loc_4009a9

00000000004009a2 B800000000             mov        eax, 0x0
00000000004009a7 EB23                   jmp        loc_4009cc

                                    loc_4009a9:
00000000004009a9 C745FC01000000         mov        dword [rbp+var_4], 0x1       ; CODE XREF=c+14
00000000004009b0 EB0A                   jmp        loc_4009bc

                                    loc_4009b2:
00000000004009b2 8B45FC                 mov        eax, dword [rbp+var_4]       ; CODE XREF=c+46
00000000004009b5 2945EC                 sub        dword [rbp+var_14], eax
00000000004009b8 8345FC02               add        dword [rbp+var_4], 0x2

                                    loc_4009bc:
00000000004009bc 837DEC00               cmp        dword [rbp+var_14], 0x0      ; CODE XREF=c+30
00000000004009c0 7FF0                   jg         loc_4009b2

00000000004009c2 837DEC00               cmp        dword [rbp+var_14], 0x0
00000000004009c6 0F94C0                 sete       al
00000000004009c9 0FB6C0                 movzx      eax, al

                                    loc_4009cc:
00000000004009cc 5D                     pop        rbp                          ; CODE XREF=c+21
00000000004009cd C3                     ret

first the number gets checked if its grater than 0x456.
var_4 is set to 0x1

var_4 is substracted from the number
var_4 is incremented by 2
and the number gets checked if its grater than 0x0
and this loops until the number is 0x0 or less than 0x0

if the number is equal to 0x0 the function c will return 1 else it wil return 0

that last part translated to python it’s:

def c(num):
var_4 = 1
while num > 0:
num = num - var_4
var_4 +=2
if num == 0:
return 1
else:
return 0

Now I could just feed this function random 4 digit number bigger than 0x456 and see wich ones will pass this test.

Or we can flip it around and generate a list of numbers

def m():
num = 0
x = 1
while num < 9999:
num += x
x = x+2
if (num > 0x456):
print num
m()

The Keygen

Now its time to bring it al together,

Frist I generate an array containing numbers that will pass the check by the c function excluding all the numbers containing a zero

Then i will pick 4 random numbers from the array making sure that the second is higher than the first, third is higher than the second and the forth is higher then the third.

The array is generated in a ascending order so picking a higher number than the previous number can easily be done by restricting the rang were I randomly select from. Then I print the 4 numbers separated by dashes

!/usr/bin/python
from random import randint
chunks = []
num = 0
x = 1
while num < 9999:
num += x
x = x+2
if (num > 0x456) & (num < 10000) & ('0' not in str(num)):
chunks.append(num)
c1 = randint(0,len(chunks)-4)
c2 = randint(c1+1,len(chunks)-3)
c3 = randint(c2+1,len(chunks)-2)
c4 = randint(c3+1,len(chunks)-1)
print ("%d-%d-%d-%d" % (chunks[c1], chunks[c2], chunks[c3], chunks[c4]))

Now I can just pipe the output of my keygen to ./resqua and check if it works 100% of the time.

$ while true; do python gen.py| tee /dev/pts/2 |./resqua; done;
5184-7396-8649-8836
Enter a valid serial number: Congrats, valid serial number!
5776-6241-7225-8836
Enter a valid serial number: Congrats, valid serial number!
1225-6241-7744-8281
Enter a valid serial number: Congrats, valid serial number!
Enter a valid serial number: Congrats, valid serial number!
2116-5929-8836-9216
Enter a valid serial number: Congrats, valid serial number!
3721-5625-7921-8464
5476-8649-8836-9216
Enter a valid serial number: Congrats, valid serial number!
1936-3481-8836-9216
Enter a valid serial number: Congrats, valid serial number!
1764-5625-8649-8836
Enter a valid serial number: Congrats, valid serial number!
1521-3249-7569-8649
Enter a valid serial number: Congrats, valid serial number!
1225-3364-6889-8649
Enter a valid serial number: Congrats, valid serial number!
Enter a valid serial number: Congrats, valid serial number!
5184-5929-7569-9216
1156-3364-8649-9216
Enter a valid serial number: Congrats, valid serial number!
7744-8649-8836-9216
etc.... etc.... etc... etc.. etc.