> For the complete documentation index, see [llms.txt](https://www.impacket.wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.impacket.wiki/tutorials/intro-to-structure.md).

# Introduction To Impackets Structure

## What you'll build

A custom binary command protocol from scratch using Impacket's `Structure` class. You will define the message format, pack commands into raw bytes, parse raw bytes back into readable fields, and handle variable-length payloads. By the end, you will have a working encoder and decoder for your own protocol.

This is the same class that Impacket uses internally to define every SMB header, every NTLM message, and every RPC packet in the library. Learning it here means you can read and extend any Impacket source file.

***

## Prerequisites

* A Python shell or scratch file
* Create a scratch file:

```bash
touch structure_basics.py
```

***

## Step 1: Define a structure with fixed-size fields

Start with the simplest possible structure: three fixed-size integer fields.

```python
from impacket.structure import Structure

class SimpleHeader(Structure):
    structure = (
        ('Version', '<B'),
        ('Command', '<H'),
        ('SeqNumber', '<L'),
    )
```

The `structure` tuple defines three fields, in order:

* `Version`: `<B` means little-endian unsigned byte (1 byte)
* `Command`: `<H` means little-endian unsigned short (2 bytes)
* `SeqNumber`: `<L` means little-endian unsigned long (4 bytes)

The format specifiers come from Python's `struct` module. The `<` prefix means little-endian. You will use these constantly: `B` (1 byte), `H` (2 bytes), `L` (4 bytes), `Q` (8 bytes).

***

## Step 2: Pack a structure into bytes

Set the fields and convert to raw bytes with `getData()`:

```python
pkt = SimpleHeader()
pkt['Version'] = 1
pkt['Command'] = 0x0005
pkt['SeqNumber'] = 42

raw = pkt.getData()
print(f"Packed ({len(raw)} bytes): {raw.hex()}")
```

Run it:

```
Packed (7 bytes): 01050042000000
```

Seven bytes: 1 for `Version`, 2 for `Command`, 4 for `SeqNumber`. The fields are packed in the exact order you defined them.

***

## Step 3: Unpack bytes back into fields

Pass raw bytes to the constructor, and `Structure` parses them automatically:

```python
raw_bytes = b'\x01\x05\x00\x2a\x00\x00\x00'

parsed = SimpleHeader(raw_bytes)
print(f"Version:   {parsed['Version']}")
print(f"Command:   {hex(parsed['Command'])}")
print(f"SeqNumber: {parsed['SeqNumber']}")
```

```
Version:   1
Command:   0x5
SeqNumber: 42
```

You access fields with dictionary syntax. This is the same `['field']` access pattern you've been using throughout the other tutorials when reading RPC responses or LDAP attributes.

***

## Step 4: Add default values

You can set a default value for any field by appending `=value` to the format specifier:

```python
class VersionedHeader(Structure):
    structure = (
        ('Version', '<B=1'),
        ('Command', '<H=0'),
        ('SeqNumber', '<L=0'),
    )
```

Now create one without setting anything:

```python
pkt = VersionedHeader()
print(f"Version: {pkt['Version']}")

raw = pkt.getData()
print(f"Packed: {raw.hex()}")
```

```
Version: 1
Packed: 01000000000000
```

`Version` defaults to 1 without you touching it. This is how Impacket's SMB structures set things like `StructureSize` to 64 automatically.

***

## Step 5: Add fixed-length byte strings

Use `Ns` (where N is the number of bytes) for fixed-length byte fields:

```python
class AuthHeader(Structure):
    structure = (
        ('Signature', '8s=b""'),
        ('Version', '<H=1'),
        ('Flags', '<L=0'),
    )
```

`8s` means an 8-byte string field, padded with null bytes if you assign fewer than 8 bytes.

```python
pkt = AuthHeader()
pkt['Signature'] = b'MYPROTO\x00'
pkt['Flags'] = 0x03

raw = pkt.getData()
print(f"Packed ({len(raw)} bytes): {raw.hex()}")

parsed = AuthHeader(raw)
print(f"Signature: {parsed['Signature']}")
print(f"Flags:     {hex(parsed['Flags'])}")
```

```
Packed (14 bytes): 4d5950524f544f00010003000000
Signature: b'MYPROTO\x00'
Flags:     0x3
```

***

## Step 6: Add literal (magic) bytes

Some fields should always contain the same value, like a protocol signature. Use the `"` prefix to define a literal:

```python
class MagicHeader(Structure):
    structure = (
        ('Magic', '"\\x89PNG'),
        ('Width', '<L'),
        ('Height', '<L'),
    )
```

The `Magic` field always outputs `\x89PNG` regardless of what you set:

```python
pkt = MagicHeader()
pkt['Width'] = 1920
pkt['Height'] = 1080

raw = pkt.getData()
print(f"First 4 bytes: {raw[:4]}")
```

```
First 4 bytes: b'\x89PNG'
```

When unpacking, `Structure` reads (and verifies) those literal bytes before moving to the next field. This is how Impacket ensures SMB packets start with `\xfeSMB`.

***

## Step 7: Add a "take the rest" field

The `:` format specifier means "raw bytes, take everything that remains":

```python
class MessageWithBody(Structure):
    structure = (
        ('Magic', '"\\xAA\\xBB'),
        ('Type', '<B'),
        ('Body', ':'),
    )
```

```python
msg = MessageWithBody()
msg['Type'] = 1
msg['Body'] = b'Hello from the Structure module'

raw = msg.getData()
print(f"Total size: {len(raw)} bytes")

parsed = MessageWithBody(raw)
print(f"Type: {parsed['Type']}")
print(f"Body: {parsed['Body']}")
```

```
Total size: 33 bytes
Type: 1
Body: b'Hello from the Structure module'
```

The `:` field consumes all remaining bytes during unpacking. This works well when the variable field is the *last* field. If you need a variable field in the middle, you need a length prefix, which is the next step.

***

## Step 8: Add a length-prefixed variable field

When the variable-length data isn't at the end of the structure, you need to tell `Structure` how many bytes to read. This uses a two-field pattern:

```python
class LengthPrefixed(Structure):
    structure = (
        ('Type', '<B'),
        ('DataLen', '<H'),
        ('_Data', '_-Data', 'self["DataLen"]'),
        ('Data', ':'),
        ('Checksum', '<L'),
    )
```

The field `_Data` is special. The format `'_-Data'` means "this field defines the length of the `Data` field." The third element, `'self["DataLen"]'`, is a Python expression that Structure evaluates at parse time to determine how many bytes `Data` should consume. The `_` prefix means this field is not packed into the output.

```python
msg = LengthPrefixed()
msg['Type'] = 3
msg['Data'] = b'PAYLOAD'
msg['DataLen'] = len(msg['Data'])
msg['Checksum'] = 0xDEADBEEF

raw = msg.getData()
print(f"Packed ({len(raw)} bytes): {raw.hex()}")

parsed = LengthPrefixed(raw)
print(f"Type:     {parsed['Type']}")
print(f"DataLen:  {parsed['DataLen']}")
print(f"Data:     {parsed['Data']}")
print(f"Checksum: {hex(parsed['Checksum'])}")
```

```
Type:     3
DataLen:  7
Data:     b'PAYLOAD'
Checksum: 0xdeadbeef
```

Without the `_Data` length field, `Structure` wouldn't know where `Data` ends and `Checksum` begins. This is the pattern Impacket uses everywhere a protocol has a variable-length body followed by more fields.

***

## Step 9: Use auto-calculated lengths

Instead of setting `DataLen` manually, you can make it calculate automatically using the `=` expression syntax:

```python
class AutoLength(Structure):
    structure = (
        ('Type', '<B'),
        ('DataLen', '<H=len(Data)'),
        ('_Data', '_-Data', 'self["DataLen"]'),
        ('Data', ':'),
    )
```

The format `'<H=len(Data)'` tells `Structure` to evaluate `len(Data)` at pack time and use the result as the field value.

```python
msg = AutoLength()
msg['Type'] = 1
msg['Data'] = b'auto-calculated length'

raw = msg.getData()
parsed = AutoLength(raw)

print(f"DataLen: {parsed['DataLen']}")
print(f"Data:    {parsed['Data']}")
```

```
DataLen: 22
Data:    b'auto-calculated length'
```

You set `Data` and `DataLen` populated itself. This eliminates a class of bugs where you forget to update the length field after changing the payload.

***

## Step 10: Nest structures inside structures

A structure can contain another structure by using `:` with a third tuple element specifying the class:

```python
class InnerPayload(Structure):
    structure = (
        ('Command', '<H'),
        ('Value', '<L'),
    )

class OuterPacket(Structure):
    structure = (
        ('Magic', '"\\xCC\\xDD'),
        ('Version', '<B=1'),
        ('Payload', ':', InnerPayload),
    )
```

```python
inner = InnerPayload()
inner['Command'] = 0x0010
inner['Value'] = 9999

outer = OuterPacket()
outer['Payload'] = inner

raw = outer.getData()
print(f"Packed ({len(raw)} bytes): {raw.hex()}")

parsed = OuterPacket(raw)
print(f"Version: {parsed['Version']}")
print(f"Command: {hex(parsed['Payload']['Command'])}")
print(f"Value:   {parsed['Payload']['Value']}")
```

```
Version: 1
Command: 0x10
Value:   9999
```

You access nested fields by chaining dictionary access: `parsed['Payload']['Command']`. This is how Impacket composes complex protocol messages out of smaller reusable pieces.

***

## Mini project: custom command protocol

Combine everything into a protocol definition and a script that encodes and decodes command messages.

The protocol format:

```
[2 bytes] Magic: 0xC0DE
[1 byte]  Version: 1
[1 byte]  Command type
[4 bytes] Session ID
[2 bytes] Payload length
[N bytes] Payload
```

```python
#!/usr/bin/env python3
"""
cmd_protocol.py

Defines a custom binary command protocol using Impacket's Structure class.
Encodes commands into bytes and decodes bytes back into readable fields.

Usage:
    python3 cmd_protocol.py
"""

from impacket.structure import Structure

# Protocol definition

CMD_PING      = 0x01
CMD_EXEC      = 0x02
CMD_UPLOAD    = 0x03
CMD_DOWNLOAD  = 0x04

CMD_NAMES = {
    CMD_PING: "PING",
    CMD_EXEC: "EXEC",
    CMD_UPLOAD: "UPLOAD",
    CMD_DOWNLOAD: "DOWNLOAD",
}


class CommandMessage(Structure):
    structure = (
        ('Magic', '"\\xC0\\xDE'),
        ('Version', '<B=1'),
        ('Command', '<B'),
        ('SessionID', '<L'),
        ('PayloadLen', '<H=len(Payload)'),
        ('_Payload', '_-Payload', 'self["PayloadLen"]'),
        ('Payload', ':'),
    )


# Encoder

def encode_command(command_type, session_id, payload=b''):
    msg = CommandMessage()
    msg['Command'] = command_type
    msg['SessionID'] = session_id
    msg['Payload'] = payload
    return msg.getData()


# Decoder

def decode_command(raw_bytes):
    msg = CommandMessage(raw_bytes)
    return {
        'command': CMD_NAMES.get(msg['Command'], f"UNKNOWN({msg['Command']})"),
        'version': msg['Version'],
        'session_id': msg['SessionID'],
        'payload': msg['Payload'],
        'payload_len': msg['PayloadLen'],
    }


# Test it

def main():
    # Encode a few commands
    messages = [
        encode_command(CMD_PING, 0x0001),
        encode_command(CMD_EXEC, 0x0001, b'whoami'),
        encode_command(CMD_UPLOAD, 0x0002, b'/tmp/payload.bin'),
        encode_command(CMD_DOWNLOAD, 0x0002, b'C:\\Windows\\System32\\config\\SAM'),
    ]

    # Decode and print each one
    for i, raw in enumerate(messages):
        print(f"Message {i + 1} ({len(raw)} bytes): {raw.hex()}")

        decoded = decode_command(raw)
        print(f"  Command:    {decoded['command']}")
        print(f"  Session:    {hex(decoded['session_id'])}")
        print(f"  PayloadLen: {decoded['payload_len']}")
        if decoded['payload']:
            print(f"  Payload:    {decoded['payload']}")
        print()

    # Round-trip: encode, decode, re-encode, verify
    original = encode_command(CMD_EXEC, 0xDEAD, b'ipconfig /all')
    decoded = decode_command(original)
    re_encoded = encode_command(CMD_EXEC, 0xDEAD, decoded['payload'])

    assert original == re_encoded, "Round-trip failed"
    print("[+] Round-trip encode/decode verified")


if __name__ == "__main__":
    main()
```

### Test it

```bash
python3 cmd_protocol.py
```

```
Message 1 (10 bytes): c0de01010100000000
  Command:    PING
  Session:    0x1
  PayloadLen: 0

Message 2 (16 bytes): c0de0102010000000600776f616d69
  Command:    EXEC
  Session:    0x1
  PayloadLen: 6
  Payload:    b'whoami'

Message 3 (26 bytes): c0de010302000000100...
  Command:    UPLOAD
  Session:    0x2
  PayloadLen: 16
  Payload:    b'/tmp/payload.bin'

Message 4 (39 bytes): c0de010402000000...
  Command:    DOWNLOAD
  Session:    0x2
  PayloadLen: 29
  Payload:    b'C:\Windows\System32\config\SAM'

[+] Round-trip encode/decode verified
```

***

## Review: what you learned

1. **`Structure`** defines binary formats as a tuple of `(field_name, format_specifier)` pairs.
2. **`getData()`** packs fields into bytes. **Passing bytes to the constructor** unpacks them.
3. **Format specifiers** follow Python's `struct` module: `<B` (1 byte), `<H` (2 bytes), `<L` (4 bytes), `<Q` (8 bytes).
4. **Default values** use `=` in the format: `'<H=64'` sets the default to 64.
5. **`Ns`** defines fixed-length byte strings. `'8s'` is always 8 bytes.
6. **Literal fields** (`'"\\xfeSMB'`) output fixed bytes, used for protocol magic signatures.
7. **`':'`** means "raw bytes, take everything remaining."
8. **Length-prefixed fields** use the `'_-FieldName'` pattern with a calculation expression to parse variable-length data in the middle of a structure.
9. **Auto-calculated fields** like `'<H=len(Data)'` evaluate Python expressions at pack time.
10. **Nested structures** use `':'` with a class as the third tuple element.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://www.impacket.wiki/tutorials/intro-to-structure.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
