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 endsLAYERING
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
.rodatahence we needed to make it local
In next post, we will be adding more layers to this, see you soon pwners : D