Skip to content

Commit

Permalink
Implement poc nfs
Browse files Browse the repository at this point in the history
  • Loading branch information
twiggler committed Jan 17, 2025
1 parent 3a3a0c4 commit 3e96e96
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 34 deletions.
26 changes: 24 additions & 2 deletions dissect/target/helpers/nfs/client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from typing import Generic, NamedTuple, TypeVar
from typing import Generic, Iterator, NamedTuple, TypeVar

from dissect.target.helpers.nfs.nfs import (
CookieVerf3,
EntryPlus3,
FileAttributes3,
FileHandle3,
NfsStat,
Read3args,
ReadDirPlusParams,
)
from dissect.target.helpers.nfs.serializer import (
Read3ArgsSerializer,
Read3ResultDeserializer,
ReadDirPlusParamsSerializer,
ReadDirPlusResultDeserializer,
)
Expand All @@ -19,6 +22,10 @@
Verifier = TypeVar("Verifier")


class ReadFileError(Exception):
pass


class ReadDirResult(NamedTuple):
dir_attributes: FileAttributes3 | None
entries: list[EntryPlus3]
Expand All @@ -30,6 +37,7 @@ class ReadDirResult(NamedTuple):
class Client(Generic[Credentials, Verifier]):
DIR_COUNT = 4096 # See https://datatracker.ietf.org/doc/html/rfc1813#section-3.3.17
MAX_COUNT = 32768
READ_CHUNK_SIZE = 1024 * 1024

def __init__(self, rpc_client: SunRpcClient[Credentials, Verifier]):
self._rpc_client = rpc_client
Expand All @@ -39,7 +47,7 @@ def connect(cls, hostname: str, port: int, auth: AuthScheme[Credentials, Verifie
rpc_client = SunRpcClient.connect(hostname, port, auth, local_port)
return Client(rpc_client)

def readdirplus(self, dir: FileHandle3) -> list[EntryPlus3] | NfsStat:
def readdirplus(self, dir: FileHandle3) -> ReadDirResult | NfsStat:
"""Read the contents of a directory, including file attributes"""

entries = list[EntryPlus3]()
Expand All @@ -61,3 +69,17 @@ def readdirplus(self, dir: FileHandle3) -> list[EntryPlus3] | NfsStat:

cookie = result.entries[-1].cookie
cookieverf = result.cookieverf

def readfile_by_handle(self, handle: FileHandle3) -> Iterator[bytes]:
"""Read a file by its file handle"""
offset = 0
count = self.READ_CHUNK_SIZE
while True:
params = Read3args(handle, offset, count)
result = self._rpc_client.call(100003, 3, 6, params, Read3ArgsSerializer(), Read3ResultDeserializer())
if isinstance(result, NfsStat):
raise ReadFileError(f"Failed to read file: {result}")
yield result.data
if result.eof:
return
offset += result.count
25 changes: 17 additions & 8 deletions dissect/target/helpers/nfs/demo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import argparse
from dissect.target.helpers.sunrpc.client import Client, auth_null, auth_unix
from dissect.target.helpers.nfs.serializer import MountResultDeserializer

from dissect.target.helpers.nfs.client import Client as NfsClient
from dissect.target.helpers.nfs.nfs import EntryPlus3
from dissect.target.helpers.nfs.serializer import MountResultDeserializer
from dissect.target.helpers.sunrpc.client import Client, auth_null, auth_unix
from dissect.target.helpers.sunrpc.serializer import (
PortMappingSerializer,
StringSerializer,
Expand All @@ -16,9 +18,6 @@
NFS_PROGRAM = 100003
NFS_V3 = 3

hostname = "localhost"
root = "/home/roel"


# NFS client demo, showing how to connect to an NFS server and list the contents of a directory
# Note: some nfs servers require connecting using a low port number (use --port)
Expand All @@ -29,6 +28,7 @@ def main():
parser.add_argument("--port", type=int, default=0, help="The local port to bind to (default: 0)")
parser.add_argument("--uid", type=int, default=1000, help="The user id to use for authentication")
parser.add_argument("--gid", type=int, default=1000, help="The group id to use for authentication")
parser.add_argument("--index", type=int, default=0, help="The index of the file to read (starting at 1)")
args = parser.parse_args()

# RdJ: Perhaps move portmapper to nfs client and cache the mapping
Expand All @@ -38,16 +38,25 @@ def main():
params_nfs = PortMapping(program=NFS_PROGRAM, version=NFS_V3, protocol=Protocol.TCP)
nfs_port = port_mapper_client.call(100000, 2, 3, params_nfs, PortMappingSerializer(), UInt32Serializer())

mount_client = Client.connect(hostname, mount_port, auth_null(), args.port)
mount_client = Client.connect(args.hostname, mount_port, auth_null(), args.port)
mount_result = mount_client.call(
MOUNT_PROGRAM, MOUNT_V3, MOUNT, args.root, StringSerializer(), MountResultDeserializer()
)
mount_client.close()

auth = auth_unix("twigtop", args.uid, args.gid, [])
nfs_client = NfsClient.connect(hostname, nfs_port, auth, args.port)
nfs_client = NfsClient.connect(args.hostname, nfs_port, auth, args.port)
readdir_result = nfs_client.readdirplus(mount_result.filehandle)
print(readdir_result)
for index, entry in enumerate(readdir_result.entries, start=1):
if entry.attributes:
print(f"{index:<5} {entry.name:<30} {entry.attributes.size:<10}")

file_entry: EntryPlus3 = readdir_result.entries[args.index - 1]
if file_entry.attributes:
file_contents = nfs_client.readfile_by_handle(file_entry.handle)
with open(file_entry.name, "wb") as f:
for chunk in file_contents:
f.write(chunk)


if __name__ == "__main__":
Expand Down
20 changes: 19 additions & 1 deletion dissect/target/helpers/nfs/nfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from typing import ClassVar


# See https://datatracker.ietf.org/doc/html/rfc1057


class NfsStat(Enum):
NFS3_OK = 0
NFS3ERR_PERM = 1
Expand Down Expand Up @@ -125,7 +128,7 @@ class EntryPlus3:
fileid: int
name: str
cookie: int
attrs: FileAttributes3 | None
attributes: FileAttributes3 | None
handle: FileHandle3 | None


Expand All @@ -135,3 +138,18 @@ class ReadDirPlusResult3:
cookieverf: CookieVerf3
entries: list[EntryPlus3]
eof: bool


@dataclass
class Read3args:
file: FileHandle3
offset: int
count: int


@dataclass
class Read3resok:
file_attributes: FileAttributes3 | None
count: int
eof: bool
data: bytes
25 changes: 24 additions & 1 deletion dissect/target/helpers/nfs/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
MountStat,
NfsStat,
NfsTime3,
Read3args,
Read3resok,
ReadDirPlusParams,
ReadDirPlusResult3,
SpecData3,
Expand Down Expand Up @@ -110,6 +112,27 @@ def deserialize(self, payload: io.BytesIO) -> ReadDirPlusResult3:

entries.append(entry)

eof = self._read_enum(payload, Bool)
eof = self._read_enum(payload, Bool) == Bool.TRUE

return ReadDirPlusResult3(dir_attributes, CookieVerf3(cookieverf), entries, eof)


class Read3ArgsSerializer(Serializer[ReadDirPlusParams]):
def serialize(self, args: Read3args) -> bytes:
result = self._write_var_length_opaque(args.file.opaque)
result += self._write_uint64(args.offset)
result += self._write_uint32(args.count)
return result


class Read3ResultDeserializer(Deserializer[Read3resok]):
def deserialize(self, payload: io.BytesIO) -> Read3resok:
stat = self._read_enum(payload, NfsStat)
if stat != NfsStat.NFS3_OK:
return stat

file_attributes = self._read_optional(payload, FileAttributesSerializer())
count = self._read_uint32(payload)
eof = self._read_enum(payload, Bool) == Bool.TRUE
data = self._read_var_length_opaque(payload)
return Read3resok(file_attributes, count, eof, data)
9 changes: 6 additions & 3 deletions dissect/target/helpers/sunrpc/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import random
import socket
from typing import Generic, NamedTuple, TypeVar

from dissect.target.helpers.sunrpc import sunrpc
import random
from dissect.target.helpers.sunrpc.serializer import (
AuthNullSerializer,
AuthSerializer,
Expand Down Expand Up @@ -43,6 +43,7 @@ def auth_unix(machine: str | None, uid: int, gid: int, gids: list[int]) -> AuthS
)


# RdJ: Error handing is a bit minimalistic. Expand later on.
class MismatchXidError(Exception):
pass

Expand Down Expand Up @@ -139,8 +140,10 @@ def _receive(self) -> bytes:

fragment_header = int.from_bytes(header, "big")
fragment_size = fragment_header & 0x7FFFFFFF
fragment = self._sock.recv(fragment_size)
fragments.append(fragment)
while fragment_size > 0:
fragment = self._sock.recv(fragment_size)
fragments.append(fragment)
fragment_size -= len(fragment)

# Check for last fragment or underflow
if (fragment_header & 0x80000000) > 0 or len(fragment) < fragment_size:
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/helpers/sunrpc/serializer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations


import io
from abc import ABC, abstractmethod
from enum import Enum
import io
from typing import Generic, TypeVar

from dissect.target.helpers.sunrpc import sunrpc

ProcedureParams = TypeVar("ProcedureParams")
Expand Down
Loading

0 comments on commit 3e96e96

Please sign in to comment.