
Table Of Contents
I had just started learning C and after completing few basics, I was looking for my first project to make. After thinking around, I landed on making a CrackMe challenge in C. Goal would be to start small, compile the binary, view the disassembly, view the pseudo-C code in IDA-Free and co-relate everything and move to adding more complexities.
GGs, that sounds fun! Lets code, shall we?
MAIN.C
First I wrote a simple main.c
program—
#include <stdint.h>#include <sys/mman.h>#include <string.h>
int main(){ uint8_t code [] = {0xB8, 0x42, 0x00, 0x00, 0x00, 0xC3}; // mov eax, 0x42; ret
void *mem = mmap(NULL, 1024, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); // create a protected executable anonymous private memory region
memcpy(mem, code, sizeof(code)); // copy the code to that region
int (*func)() = mem; // cast a function pointer and point it to mem
int result = func(); // execute the func() function and store the result in result variable
return result; // return the result which should be 66}
Lets compile the program and view its pseudo-C code
gcc -m32 -fno-stack-protector -z execstack -no-pie -fno-pic main.c -o main
-
-m32 : Produces a 32-bit binary
-
-fno-stack-protector : Disables canary-based stack protection
-
-z execstack : Marks the stack as executable
-
-no-pie : Generates a binary with a fixed base address instead of randomized addresses (ASLR for PIE)
-
-fno-pic : Disables generation of position-independent code. Relevant mostly for shared libraries or PIEs.
-
main.c : Source file to compile.
-
-o main : Names the output binary
main
.
PSEUDO-C
int __cdecl main(int argc, const char **argv, const char **envp){ int (*v3)(void); // function pointer that takes no argument
v3 = (int (*)(void))mmap(0, 1024u, 7, 0x22, -1, 0); // calls mmap to allocate 1024 bytes of memory with read, write, and execute permissions *(_DWORD *)v3 = 0x42B8; // store 0x42B8 in memory *((_WORD *)v3 + 2) = 0xC300; // store next bytes (0xC300) with offset of 4 (2 WORDs) return v3(); // calls the function executing the code}
DISASSEMBLY
and now its disassembly
; int __cdecl main(int argc, const char **argv, const char **envp)public mainmain proc near
; variablesvar_1A= dword ptr -1Ahvar_16= word ptr -16hvar_14= dword ptr -14hvar_10= dword ptr -10hvar_C= dword ptr -0Chvar_4= dword ptr -4argc= dword ptr 8argv= dword ptr 0Chenvp= dword ptr 10h
; __unwind {; stack frame setuplea ecx, [esp+4]and esp, 0FFFFFFF0hpush dword ptr [ecx-4]push ebpmov ebp, esppush ecxsub esp, 24h
; call mmap to allocate executable memory and storing return pointer in var_Cmov [ebp+var_1A], 42B8hmov [ebp+var_16], 0C300hsub esp, 8push 0 ; offsetpush 0FFFFFFFFh ; fdpush 22h ; '"' ; flagspush 7 ; protpush 400h ; lenpush 0 ; addrcall _mmapadd esp, 20hmov [ebp+var_C], eax
; copy machine code to allocated memorymov eax, [ebp+var_C]mov edx, [ebp+var_1A]mov [eax], edxmovzx edx, [ebp+var_16]mov [eax+4], dx
; prepare and call the functionmov eax, [ebp+var_C]mov [ebp+var_10], eaxmov eax, [ebp+var_10]call eax
; return the result and clean the stackmov [ebp+var_14], eaxmov eax, [ebp+var_14]mov ecx, [ebp+var_4]leavelea esp, [ecx-4]retn; } // starts at 8049166main endp
_text ends
LAYERING
Now that we have seen code in all three forms, let’s add some layers. I wrote another file validate.c
—
#include <stdio.h>#include <string.h>int validate(const char *input);int main(){ char input[64]; scanf("%63s", input); if (validate(input)) { printf("Correct!\n"); } else { printf("Wrong!\n"); } return 0;}
int validate(const char *input){ const char *flag = "pwning-since-1337"; int i = 0; for ( ; ; i++) { unsigned char a = (unsigned char)input[i]; unsigned char b = (unsigned char)flag[i]; if (a != b) { return 0; } if (a == 0) { return 1; } }}
I compiled it again and this time we extract just the raw instruction bytes of validate part of the code
objdump -M intel -d validate | awk '/<validate>:/,/^$/' | awk '/^[[:space:]]*[0-9a-f]+:/ {for(i=2;i<=10;i++) if($i ~ /^[0-9a-f][0-9a-f]$/) printf "0x%s, ", $i} END {print ""}'
(thanks chatGPT).
which now we will be XORing with a key (0x1337
) using python—
def xor_encrypt(buf: bytearray, key: int) -> None: key_bytes = [key & 0xFF, (key >> 8) & 0xFF] for i in range(len(buf)): buf[i] ^= key_bytes[i % 2]
validate_bytes = [0x55, 0x89, 0xe5, 0x83, 0xec, 0x10, 0xc7, 0x45, 0xf8, 0x1d, 0xa0, 0x04,0x08, 0xc7, 0x45, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x55, 0xfc, 0x8b,0x45, 0x08, 0x01, 0xd0, 0x0f, 0xb6, 0x00, 0x88, 0x45, 0xf7, 0x8b, 0x55,0xfc, 0x8b, 0x45, 0xf8, 0x01, 0xd0, 0x0f, 0xb6, 0x00, 0x88, 0x45, 0xf6,0x0f, 0xb6, 0x45, 0xf7, 0x3a, 0x45, 0xf6, 0x74, 0x07, 0xb8, 0x00, 0x00,0x00, 0x00, 0xeb, 0x13, 0x80, 0x7d, 0xf7, 0x00, 0x75, 0x07, 0xb8, 0x01,0x00, 0x00, 0x00, 0xeb, 0x06, 0x83, 0x45, 0xfc, 0x01, 0xeb, 0xc1, 0xc9,0xc3
]
data = bytearray(validate_bytes)
xor_encrypt(data, 0x1337)
print("Encrypted:", ', '.join(f'0x{b:02x}' for b in data))
Now we update our C code to look something like this—
#include <stdint.h>#include <sys/mman.h>#include <string.h>#include <stdio.h>
void xor_decrypt(uint8_t *buf, size_t len, uint16_t key);
int main(){ char input[64]; printf("WELCOME TRAVELLER, SPEAK THY SHAN'T BE STONED: "); fflush(stdout); if (scanf("%63s", input) != 1) { return 1; }
uint8_t code[] = {0x62, 0x9a, 0xd2, 0x90, 0xdb, 0x03, 0xf0, 0x56, 0xcf, 0x0e, 0x97, 0x17, 0x3f, 0xd4, 0x72, 0xef, 0x37, 0x13, 0x37, 0x13, 0xbc, 0x46, 0xcb, 0x98, 0x72, 0x1b, 0x36, 0xc3, 0x38, 0xa5, 0x37, 0x9b, 0x72, 0xe4, 0xbc, 0x46, 0xcb, 0x98, 0x72, 0xeb, 0x36, 0xc3, 0x38, 0xa5, 0x37, 0x9b, 0x72, 0xe5, 0x38, 0xa5, 0x72, 0xe4, 0x0d, 0x56, 0xc1, 0x67, 0x30, 0xab, 0x37, 0x13, 0x37, 0x13, 0xdc, 0x00, 0xb7, 0x6e, 0xc0, 0x13, 0x42, 0x14, 0x8f, 0x12, 0x37, 0x13, 0x37, 0xf8, 0x31, 0x90, 0x72, 0xef, 0x36, 0xf8, 0xf6, 0xda, 0xf4}; // encrypted xor
void *mem = mmap(NULL, 1024, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); // create a protected executable anonymous private memory region
memcpy(mem, code, sizeof(code)); // copy the code to that region
xor_decrypt((uint8_t *)mem, sizeof(code), 0x1337); // decrypt mem in runtime (use sizeof code as we only need to decrypt that many bytes) int (*validate_func)(const char *) = mem; // cast a function pointer and point it to mem
int ok = validate_func(input);
puts(ok ? "YOU ARE SAVED TRAVELLER, YOU MAY PROCEED!" : "YOU GOT STONED BY THE MEDUSA!");}
void xor_decrypt(uint8_t *buf, size_t len, uint16_t key){ uint8_t key_bytes[2]; key_bytes[0] = key & 0xFF; // lower byte key_bytes[1] = (key >> 8) & 0xFF; // upper byte for (size_t i = 0; i < len; i++) { buf[i] ^= key_bytes[i % 2]; // alternate between lower and upper byte }}
PROBLEMO
But there comes a problem, no matter what I entered, wrong flag or right flag, It would always give me "YOU GOT STONED BY THE MEDUSA!"
What went wrong, after pondering and tinkering I realised that the flag pwning-since-1337
would be stored in .rodata
and there would be no way to access it in validate
’s function.
We need to write self contained function which has the pwning-since-1337
itself.
So we just make an local array, easy-peasy-lemon-squeezy!
Here is our updated validate-self-contained.c
—
#include <stdio.h>#include <string.h>
int validate(const char *input);
int main(){ char input[64]; scanf("%63s", input);
if (validate(input)) { printf("Correct!\n"); } else { printf("Wrong!\n"); }
return 0;}
int validate(const char *input){// Store the flag as a local array, not as a pointer to a string literalconst unsigned char flag[] = {'p', 'w', 'n', 'i', 'n', 'g', '-', 's', 'i', 'n', 'c', 'e', '-', '1', '3', '3', '7', 0};
int i = 0;while (1){ unsigned char a = (unsigned char)input[i]; unsigned char b = flag[i]; if (a != b) { return 0; }
if (a == 0) { return 1; } i++;}}
Compile. Extract. XOR. Same yadda yadda process
and lets hit run!
yipeee, it is working!
We now have a simple working CrackMe.
WHAT WE LEARNT
- we can create a executable region in memory from which we can execute code
- how to alternatively use key’s both bytes to XOR
- the data or our flag was stored in
.rodata
hence we needed to make it local
In next post, we will be adding more layers to this, see you soon pwners : D