Compare commits

..
Author SHA1 Message Date
moloch-- 6d6df68972 Update Reflektor to v0.0.3
Synchronize the root and implant module graphs and vendor trees with the all-platform Go c-shared loader fixes.
2026-08-17 00:29:26 -07:00
moloch-- 7f863d3172 Fix cross-platform Reflektor integration failures
Close rendered implant files before import renames, use native GCC for Linux/386 Go shared libraries, and preserve actionable integration logs.
2026-08-17 00:28:49 -07:00
moloch-- 6ecb379bce Test Sliver shared libraries with Reflektor 2026-08-16 22:09:22 -07:00
52 changed files with 2673 additions and 1523 deletions

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
name: Reflektor Integration Test Matrix
on:
push:
branches:
- master
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
workflow_dispatch:
permissions:
contents: read
jobs:
darwin:
name: reflektor-darwin-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
goarch: amd64
server_arch: amd64
- runner: macos-15
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: darwin
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: darwin
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e
-repo .
-server ./sliver-server
-target-os darwin
-target-arch ${{ matrix.goarch }}
linux-native:
name: reflektor-linux-${{ matrix.goarch }}-native
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
goarch: amd64
server_arch: amd64
- runner: ubuntu-24.04-arm
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: linux
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: linux
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e
-repo .
-server ./sliver-server
-target-os linux
-target-arch ${{ matrix.goarch }}
linux-386:
name: reflektor-linux-386-docker
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
CONTAINER_NAME: sliver-reflektor-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-reflektor-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build linux/386 test image
run: |
set -euo pipefail
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
docker buildx build \
--load \
--platform linux/386 \
--build-arg "GO_VERSION=${go_version}" \
--file test/reflektor/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
docker buildx prune --all --force
- name: Load Sliver with Reflektor
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
"${IMAGE_NAME}"
- name: Clean up linux/386 Docker resources
if: ${{ always() }}
run: |
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
docker buildx prune --all --force >/dev/null 2>&1 || true
windows:
name: reflektor-windows-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
goarch: amd64
server_arch: amd64
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-11-arm
goarch: arm64
server_arch: amd64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: windows
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: windows
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e.exe
-repo .
-server ./sliver-server.exe
-target-os windows
-target-arch ${{ matrix.goarch }}
+1
View File
@@ -87,6 +87,7 @@ var (
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
ErrNoExternalBuilder = errors.New("no external builders are available")
+2 -71
View File
@@ -3,8 +3,6 @@ import React from "react";
type AsciinemaPlayerProps = {
src: string;
className?: string;
hideControls?: boolean;
cols?: string;
rows?: string;
@@ -20,67 +18,10 @@ type AsciinemaPlayerProps = {
fontSize?: string;
};
function AsciinemaPlayer({
src,
className,
hideControls,
cols,
rows,
autoPlay,
preload,
loop,
startAt,
speed,
idleTimeLimit,
theme,
poster,
fit,
fontSize,
}: AsciinemaPlayerProps) {
function AsciinemaPlayer({ src, ...asciinemaOptions }: AsciinemaPlayerProps) {
const ref = React.useRef<HTMLDivElement>(null);
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
const [player, setPlayer] =
React.useState<typeof import("asciinema-player")>();
const asciinemaOptions = React.useMemo(
() => ({
autoPlay: prefersReducedMotion ? false : autoPlay,
cols,
controls: hideControls ? false : undefined,
fit,
fontSize,
idleTimeLimit,
loop: prefersReducedMotion ? false : loop,
poster,
preload,
rows,
speed,
startAt,
theme,
}),
[
autoPlay,
cols,
fit,
fontSize,
hideControls,
idleTimeLimit,
loop,
poster,
preload,
prefersReducedMotion,
rows,
speed,
startAt,
theme,
],
);
React.useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const syncPreference = () => setPrefersReducedMotion(mediaQuery.matches);
syncPreference();
mediaQuery.addEventListener("change", syncPreference);
return () => mediaQuery.removeEventListener("change", syncPreference);
}, []);
React.useEffect(() => {
import("asciinema-player").then((p) => {
setPlayer(p);
@@ -94,17 +35,7 @@ function AsciinemaPlayer({
};
}, [src, player, asciinemaOptions]);
return (
<div
ref={ref}
className={[
className,
hideControls ? "sliver-asciinema-controls-hidden" : undefined,
]
.filter(Boolean)
.join(" ")}
/>
);
return <div ref={ref} />;
}
export default AsciinemaPlayer;
+5 -2
View File
@@ -60,8 +60,11 @@ const CodeViewer = (props: CodeViewerProps) => {
props.script.source_code
);
const [fontSize, setFontSize] = React.useState(props.fontSize || 13);
const editorContainerClassName =
"col-span-12 mt-4 overflow-hidden rounded-2xl border border-separator bg-surface-secondary";
const editorContainerClassName = React.useMemo(() => {
return theme === Themes.DARK
? "col-span-12 mt-4 rounded-2xl overflow-hidden"
: "col-span-12 mt-4 rounded-2xl overflow-hidden border border-gray-300";
}, [theme]);
return (
<div className={editorContainerClassName}>
@@ -3,9 +3,9 @@ import React from "react";
const LoadingState: React.FC = () => {
return (
<div className="flex min-h-[55vh] w-full items-center justify-center gap-3 text-muted">
<div className="flex min-h-screen w-full items-center justify-center gap-3 text-foreground">
<Spinner aria-label="Loading" />
<span className="text-sm">Loading documentation…</span>
<span>Loading...</span>
</div>
);
};
+43 -53
View File
@@ -27,7 +27,6 @@ import Youtube from "./youtube";
export type MarkdownProps = {
markdown: string;
demoteTopLevelHeading?: boolean;
};
type MarkdownAsciiCast = {
@@ -122,12 +121,12 @@ const slugify = (value: string) => {
};
const headingClassNames: Record<HeadingLevel, string> = {
1: "mt-12 text-3xl font-semibold tracking-tight text-foreground first:mt-0",
2: "mt-12 text-2xl font-semibold tracking-tight text-foreground",
3: "mt-9 text-xl font-semibold tracking-tight text-foreground",
4: "mt-8 text-lg font-semibold tracking-tight text-foreground",
5: "mt-7 text-base font-semibold tracking-tight text-foreground",
6: "mt-7 text-base font-semibold tracking-tight text-foreground",
1: "mt-12 text-3xl font-semibold tracking-tight text-slate-900 first:mt-0 dark:text-slate-100",
2: "mt-10 text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-100",
3: "mt-8 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100",
4: "mt-6 text-lg font-semibold tracking-tight text-slate-900 dark:text-slate-100",
5: "mt-6 text-base font-semibold tracking-tight text-slate-900 dark:text-slate-100",
6: "mt-6 text-base font-semibold uppercase tracking-wide text-slate-700 dark:text-slate-300",
};
const MarkdownViewer = (props: MarkdownProps) => {
@@ -219,9 +218,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
}
const anchor = anchorRef.current;
const renderedLevel =
level === 1 && props.demoteTopLevelHeading ? 2 : level;
const HeadingTag = `h${renderedLevel}`;
const HeadingTag = `h${level}`;
return createElement(
HeadingTag,
@@ -229,7 +226,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
...rest,
id: anchor || undefined,
className: mergeClassNames(
headingClassNames[renderedLevel],
headingClassNames[level],
"scroll-mt-[70px]",
className
),
@@ -239,8 +236,8 @@ const MarkdownViewer = (props: MarkdownProps) => {
{anchor && (
<a
href={`#${anchor}`}
aria-label={`Link to ${textContent}`}
className="-my-2 inline-flex size-10 items-center justify-center rounded-xl text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 hover:bg-surface-secondary hover:text-accent"
aria-label="Copy link to section"
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-400 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 hover:text-primary dark:text-slate-500"
>
<svg
aria-hidden="true"
@@ -286,12 +283,12 @@ const MarkdownViewer = (props: MarkdownProps) => {
h5: createHeadingComponent(5),
h6: createHeadingComponent(6),
};
}, [getAnchor, props.demoteTopLevelHeading]);
}, [getAnchor]);
const proseClassName = mergeClassNames(
"markdown-body prose max-w-none leading-7",
theme === Themes.DARK ? "dark:prose-invert prose-slate" : "prose-slate",
"prose-code:font-mono prose-img:rounded-2xl"
"prose-pre:bg-slate-950/90 prose-pre:text-slate-100 prose-code:font-mono prose-img:rounded-xl"
);
return (
@@ -310,7 +307,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
{...rest}
href={href}
className={mergeClassNames(
"font-medium text-accent underline-offset-4 hover:underline",
"font-medium text-primary transition-colors duration-150 hover:text-primary-dark",
className
)}
onClick={(e) => {
@@ -345,7 +342,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
}
const anchorClassName = mergeClassNames(
"font-medium text-accent underline-offset-4 hover:underline",
"font-medium text-primary transition-colors duration-150 underline-offset-4 hover:text-primary-dark",
className
);
@@ -379,7 +376,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<p
{...rest}
className={mergeClassNames(
"my-6 text-base leading-7 text-foreground/80",
"my-6 text-base leading-7 text-slate-700 dark:text-slate-300",
className
)}
>
@@ -394,7 +391,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<ul
{...rest}
className={mergeClassNames(
"my-6 list-disc space-y-2 pl-6 text-foreground/80 marker:text-accent",
"my-6 list-disc space-y-2 pl-6 text-slate-700 marker:text-primary dark:text-slate-300",
className
)}
>
@@ -409,7 +406,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<ol
{...rest}
className={mergeClassNames(
"my-6 list-decimal space-y-2 pl-6 text-foreground/80 marker:text-accent",
"my-6 list-decimal space-y-2 pl-6 text-slate-700 marker:text-primary dark:text-slate-300",
className
)}
>
@@ -424,7 +421,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<li
{...rest}
className={mergeClassNames(
"leading-6 text-foreground/80",
"leading-6 text-slate-700 dark:text-slate-300",
className
)}
>
@@ -439,7 +436,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<blockquote
{...rest}
className={mergeClassNames(
"my-8 rounded-r-2xl border-l-4 border-accent/40 bg-surface-secondary px-6 py-4 text-base italic text-foreground/80",
"my-8 border-l-4 border-slate-200 bg-slate-50 px-6 py-4 text-base italic text-slate-700 shadow-sm dark:border-slate-700 dark:bg-slate-900/50 dark:text-slate-300",
className
)}
>
@@ -454,7 +451,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<hr
{...rest}
className={mergeClassNames(
"my-12 border-t border-separator",
"my-12 border-t border-slate-200 dark:border-slate-800",
className
)}
/>
@@ -464,11 +461,11 @@ const MarkdownViewer = (props: MarkdownProps) => {
table(tableProps) {
const { className, children, ...rest } = tableProps;
return (
<div className="sliver-scrollbar my-8 overflow-x-auto rounded-2xl border border-separator bg-surface">
<div className="my-8 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950/40">
<table
{...rest}
className={mergeClassNames(
"w-full min-w-max divide-y divide-separator text-left text-sm",
"w-full min-w-max divide-y divide-slate-200 text-left text-sm dark:divide-slate-800",
className
)}
>
@@ -484,7 +481,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<thead
{...rest}
className={mergeClassNames(
"bg-surface-secondary text-sm font-semibold text-muted",
"bg-slate-50 text-sm font-semibold uppercase tracking-wide text-slate-600 dark:bg-slate-900/60 dark:text-slate-300",
className
)}
>
@@ -499,7 +496,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<tbody
{...rest}
className={mergeClassNames(
"divide-y divide-separator",
"divide-y divide-slate-200 dark:divide-slate-800",
className
)}
>
@@ -514,7 +511,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<tr
{...rest}
className={mergeClassNames(
"bg-transparent",
"transition-colors hover:bg-slate-50 dark:hover:bg-slate-900/60",
className
)}
>
@@ -529,7 +526,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<th
{...rest}
className={mergeClassNames(
"px-4 py-3 text-left text-sm font-semibold text-foreground",
"px-4 py-3 text-left text-sm font-semibold text-slate-600 dark:text-slate-200",
className
)}
>
@@ -544,7 +541,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<td
{...rest}
className={mergeClassNames(
"px-4 py-3 align-top text-sm text-foreground/80",
"px-4 py-3 align-top text-sm text-slate-700 dark:text-slate-300",
className
)}
>
@@ -559,7 +556,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<strong
{...rest}
className={mergeClassNames(
"font-semibold text-foreground",
"font-semibold text-slate-900 dark:text-slate-100",
className
)}
>
@@ -574,7 +571,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<em
{...rest}
className={mergeClassNames(
"text-foreground/80",
"text-slate-700 dark:text-slate-300",
className
)}
>
@@ -608,7 +605,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<pre
{...rest}
className={mergeClassNames(
"sliver-scrollbar my-6 overflow-x-auto rounded-2xl border border-separator bg-surface-secondary p-4 text-[13px] leading-6 text-foreground",
"my-6 overflow-x-auto rounded-xl border border-slate-200 bg-slate-50 p-4 text-[13px] leading-6 text-slate-900 shadow-inner dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100",
className
)}
>
@@ -628,7 +625,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
width={1200}
height={720}
className={mergeClassNames(
"my-8 w-full rounded-2xl border border-separator/70 object-contain",
"my-8 w-full rounded-xl border border-slate-200 object-contain shadow-sm dark:border-slate-800",
className
)}
/>
@@ -658,11 +655,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
if (normalizedLang === "youtube") {
const embedId = sourceCode || "";
return (
<div className="not-prose my-8 overflow-hidden rounded-2xl">
<Youtube embedId={embedId.trim()} />
</div>
);
return <Youtube embedId={embedId.trim()} />;
}
if (normalizedLang === "asciinema") {
@@ -675,18 +668,15 @@ const MarkdownViewer = (props: MarkdownProps) => {
return <></>;
}
return (
<div className="sliver-terminal-frame not-prose my-8 w-full max-w-full overflow-x-auto rounded-2xl bg-[#111315]">
<AsciinemaPlayer
className="min-w-[42rem]"
src={srcUrl.toString()}
rows={asciiCast.rows || "18"}
cols={asciiCast.cols || "75"}
idleTimeLimit={asciiCast.idleTimeLimit || 2}
preload={true}
autoPlay={true}
loop={true}
/>
</div>
<AsciinemaPlayer
src={srcUrl.toString()}
rows={asciiCast.rows || "18"}
cols={asciiCast.cols || "75"}
idleTimeLimit={asciiCast.idleTimeLimit || 2}
preload={true}
autoPlay={true}
loop={true}
/>
);
}
@@ -704,7 +694,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
<code
{...rest}
className={mergeClassNames(
"rounded-md bg-surface-secondary px-1.5 py-0.5 font-mono text-[13px] text-foreground",
"rounded-md bg-slate-100 px-1.5 py-0.5 font-mono text-[13px] text-slate-700 dark:bg-slate-800/80 dark:text-slate-200",
className
)}
>
@@ -762,7 +752,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
const formattedSourceCode = sourceCode.replace(/\n$/, "");
const preWrapperClassName = mergeClassNames(
"sliver-scrollbar not-prose mt-4 overflow-x-auto rounded-2xl border border-separator bg-surface-secondary px-4 py-4 text-[13px] leading-6 text-foreground",
"not-prose mt-4 overflow-x-auto rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-[13px] leading-6 text-slate-900 shadow-sm dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100",
className
);
+1 -1
View File
@@ -115,7 +115,7 @@ const Mermaid = ({ diagram, minHeight }: MermaidProps) => {
if (error) {
return (
<div className="not-prose my-6 rounded-2xl bg-danger-soft p-4 text-sm text-danger-soft-foreground">
<div className="not-prose my-6 text-sm text-rose-700 dark:text-rose-300">
Failed to render Mermaid diagram: {error}
</div>
);
+142 -176
View File
@@ -5,28 +5,24 @@ import { Themes } from "@/util/themes";
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
import {
faBars,
faHome,
faMoon,
faSearch,
faSun,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Drawer, SearchField, Tooltip } from "@heroui/react";
import { Button, SearchField, Tooltip } from "@heroui/react";
import { useTheme } from "next-themes";
import NextLink from "next/link";
import { useRouter } from "next/router";
import React from "react";
const routes = [
{ href: "/docs", label: "Docs" },
{ href: "/tutorials", label: "Tutorials" },
{ href: "/talks", label: "Talks" },
{ href: "/docs", label: "Docs" },
];
const discordUrl =
"https://discord.com/channels/791066041198968873/1339996286514106409";
const githubUrl = "https://github.com/BishopFox/sliver";
export default function TopNavbar() {
const router = useRouter();
const { theme, setTheme } = useTheme();
@@ -34,28 +30,9 @@ export default function TopNavbar() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const activeTheme = theme || Themes.DARK;
const isDarkTheme = activeTheme === Themes.DARK;
const lightDarkModeIcon = isDarkTheme ? faSun : faMoon;
const themeToggleLabel = isDarkTheme
? "Switch to light mode"
: "Switch to dark mode";
const toggleTheme = React.useCallback(() => {
setTheme(isDarkTheme ? Themes.LIGHT : Themes.DARK);
}, [isDarkTheme, setTheme]);
React.useEffect(() => {
const desktopBreakpoint = window.matchMedia("(min-width: 768px)");
const closeMobileMenu = (event: MediaQueryListEvent) => {
if (event.matches) {
setIsMobileMenuOpen(false);
}
};
desktopBreakpoint.addEventListener("change", closeMobileMenu);
return () =>
desktopBreakpoint.removeEventListener("change", closeMobileMenu);
}, []);
const lightDarkModeIcon = React.useMemo(() => {
return activeTheme === Themes.DARK ? faSun : faMoon;
}, [activeTheme]);
const handleSearchSubmit = React.useCallback(
(value = query) => {
@@ -64,11 +41,7 @@ export default function TopNavbar() {
if (searchQuery.length === 0) {
return;
}
void router.push({
pathname: "/search/",
query: { search: searchQuery },
});
router.push({ pathname: "/search/", query: { search: searchQuery } });
setQuery("");
setIsMobileMenuOpen(false);
},
@@ -85,13 +58,12 @@ export default function TopNavbar() {
[],
);
const renderSearchInput = (className?: string) => (
const renderSearchInput = (wrapperClassName?: string) => (
<SearchField
aria-label="Search documentation"
className={className}
className={wrapperClassName}
fullWidth
value={query}
variant="secondary"
onChange={setQuery}
onSubmit={handleSearchSubmit}
>
@@ -99,10 +71,8 @@ export default function TopNavbar() {
<SearchField.SearchIcon>
<FontAwesomeIcon icon={faSearch} />
</SearchField.SearchIcon>
<SearchField.Input placeholder="Search documentation…" />
{query.length > 0 ? (
<SearchField.ClearButton aria-label="Clear search" />
) : null}
<SearchField.Input placeholder="Search..." />
{query.length > 0 ? <SearchField.ClearButton aria-label="Clear search" /> : null}
</SearchField.Group>
</SearchField>
);
@@ -112,80 +82,82 @@ export default function TopNavbar() {
return (
<li key={href}>
<NextLink
href={href}
aria-current={isActive ? "page" : undefined}
className={`inline-flex h-10 items-center justify-center rounded-xl px-4 text-sm font-medium no-underline outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
isActive
? "bg-surface-secondary text-foreground shadow-surface"
: "text-muted hover:bg-surface-secondary hover:text-foreground"
}`}
<Button
variant={isActive ? "secondary" : "ghost"}
onPress={() => router.push(href)}
>
{label}
</NextLink>
</Button>
</li>
);
};
const renderMobileRoute = (href: string, label: string) => {
const isActive =
href === "/"
? router.pathname === "/"
: router.pathname.startsWith(href);
const isActive = href === "/" ? router.pathname === "/" : router.pathname.startsWith(href);
return (
<NextLink
<Button
key={href}
href={href}
aria-current={isActive ? "page" : undefined}
className={`inline-flex min-h-10 w-full items-center rounded-xl px-3 py-2 text-sm font-medium no-underline outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
isActive
? "bg-surface-secondary text-foreground shadow-surface"
: "text-muted hover:bg-surface-secondary hover:text-foreground"
}`}
onClick={() => setIsMobileMenuOpen(false)}
fullWidth
variant={isActive ? "primary" : "ghost"}
onPress={() => {
router.push(href);
setIsMobileMenuOpen(false);
}}
>
{href === "/" ? <FontAwesomeIcon fixedWidth icon={faHome} /> : null}
{label}
</NextLink>
</Button>
);
};
return (
<>
<nav
aria-label="Primary navigation"
className="sticky top-0 z-40 h-16 border-b border-separator bg-background/85 backdrop-blur-xl"
>
<div className="mx-auto flex h-16 w-full max-w-[1440px] items-center gap-3 px-4 sm:px-6 lg:px-8">
<NextLink
href="/"
aria-label="Sliver documentation home"
className="-ml-2 inline-flex h-10 shrink-0 items-center gap-2 rounded-xl px-2 text-sm font-semibold text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
<nav className="sliver-navbar-glass sticky top-0 z-40 px-4 shadow-sm md:px-6">
<div className="flex h-16 items-center gap-4">
<button
type="button"
className="flex items-center gap-2 text-foreground"
onClick={() => {
router.push("/");
setIsMobileMenuOpen(false);
}}
aria-label="Sliver C2 home"
>
<span className="flex size-8 items-center justify-center rounded-xl bg-accent text-accent-foreground">
<SliversIcon height={18} />
<SliversIcon />
<span className="hidden font-bold text-inherit sm:block">
Sliver C2
</span>
<span className="hidden items-baseline gap-1.5 sm:flex">
<span>Sliver C2</span>
</span>
</NextLink>
</button>
<ul className="hidden items-center gap-1 md:flex">
{routes.map((route) =>
renderDesktopRoute(route.href, route.label),
)}
<ul className="hidden items-center gap-2 md:flex">
<li>
{renderTooltip(
"Home",
<Button
isIconOnly
aria-label="Home"
variant={router.pathname === "/" ? "secondary" : "ghost"}
onPress={() => router.push("/")}
>
<FontAwesomeIcon fixedWidth icon={faHome} />
</Button>,
)}
</li>
{routes.map((route) => renderDesktopRoute(route.href, route.label))}
</ul>
<div className="ml-auto hidden items-center gap-1 md:flex">
{renderSearchInput("mr-2 w-56 xl:w-72")}
<div className="ml-auto hidden items-center gap-2 md:flex">
{renderSearchInput("w-64")}
{renderTooltip(
themeToggleLabel,
activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode",
<Button
isIconOnly
aria-label={themeToggleLabel}
aria-label={activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
variant="ghost"
onPress={toggleTheme}
onPress={() => {
setTheme(activeTheme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
}}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
</Button>,
@@ -193,55 +165,62 @@ export default function TopNavbar() {
{renderTooltip(
"Join Discord",
<a
href={discordUrl}
target="_blank"
rel="noreferrer"
aria-label="Join Discord"
className="inline-flex size-10 items-center justify-center rounded-xl text-muted no-underline outline-none hover:bg-surface-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40"
>
<FontAwesomeIcon icon={faDiscord} />
</a>,
)}
{renderTooltip(
"View on GitHub",
<a
href={githubUrl}
target="_blank"
rel="noreferrer"
aria-label="View on GitHub"
className="inline-flex size-10 items-center justify-center rounded-xl text-muted no-underline outline-none hover:bg-surface-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40"
>
<FontAwesomeIcon icon={faGithub} />
</a>,
)}
</div>
<div className="ml-auto flex items-center gap-1 md:hidden">
{renderTooltip(
themeToggleLabel,
<Button
isIconOnly
aria-label={themeToggleLabel}
aria-label="Join Discord"
variant="ghost"
onPress={toggleTheme}
onPress={() => {
window.open(
"https://discord.com/channels/791066041198968873/1339996286514106409",
"_blank",
"noopener,noreferrer",
);
}}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
<FontAwesomeIcon icon={faDiscord} />
</Button>,
)}
{renderTooltip(
"View on GitHub",
<Button
isIconOnly
aria-label="View on GitHub"
variant="ghost"
onPress={() => {
window.open(
"https://github.com/BishopFox/sliver",
"_blank",
"noopener,noreferrer",
);
}}
>
<FontAwesomeIcon icon={faGithub} />
</Button>,
)}
</div>
<div className="ml-auto flex items-center gap-2 md:hidden">
{renderTooltip(
activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode",
<Button
isIconOnly
aria-label={activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
variant="ghost"
onPress={() => {
setTheme(activeTheme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
}}
>
<FontAwesomeIcon icon={lightDarkModeIcon} />
</Button>,
)}
{renderTooltip(
isMobileMenuOpen ? "Close menu" : "Open menu",
<Button
isIconOnly
aria-controls="mobile-navigation"
aria-expanded={isMobileMenuOpen}
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
variant="ghost"
onPress={() =>
setIsMobileMenuOpen((current) => !current)
}
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
onPress={() => setIsMobileMenuOpen((current) => !current)}
>
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
</Button>,
@@ -250,56 +229,43 @@ export default function TopNavbar() {
</div>
</nav>
<Drawer.Backdrop
isOpen={isMobileMenuOpen}
variant="blur"
onOpenChange={setIsMobileMenuOpen}
>
<Drawer.Content placement="right">
<Drawer.Dialog
id="mobile-navigation"
className="w-[min(24rem,calc(100vw-1.5rem))]"
>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Heading>Navigate Sliver Docs</Drawer.Heading>
</Drawer.Header>
<Drawer.Body className="flex flex-col gap-5 pt-2">
{renderSearchInput("w-full")}
<nav
aria-label="Mobile navigation"
className="flex flex-col gap-1"
>
{renderMobileRoute("/", "Home")}
{routes.map((route) =>
renderMobileRoute(route.href, route.label),
)}
</nav>
</Drawer.Body>
<Drawer.Footer className="grid grid-cols-2 gap-2">
<a
href={discordUrl}
target="_blank"
rel="noreferrer"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-separator px-4 text-sm font-medium text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
>
<FontAwesomeIcon icon={faDiscord} />
Discord
</a>
<a
href={githubUrl}
target="_blank"
rel="noreferrer"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-separator px-4 text-sm font-medium text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
>
<FontAwesomeIcon icon={faGithub} />
GitHub
</a>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
{isMobileMenuOpen ? (
<div className="sliver-navbar-glass px-4 pb-4 shadow-sm md:hidden">
<div className="mt-3">{renderSearchInput("w-full")}</div>
<div className="mt-4 flex flex-col gap-2">
{renderMobileRoute("/", "Home")}
{routes.map((route) => renderMobileRoute(route.href, route.label))}
<Button
fullWidth
variant="ghost"
onPress={() => {
window.open(
"https://discord.com/channels/791066041198968873/1339996286514106409",
"_blank",
"noopener,noreferrer",
);
}}
>
<FontAwesomeIcon icon={faDiscord} />
Discord
</Button>
<Button
fullWidth
variant="ghost"
onPress={() => {
window.open(
"https://github.com/BishopFox/sliver",
"_blank",
"noopener,noreferrer",
);
}}
>
<FontAwesomeIcon icon={faGithub} />
GitHub
</Button>
</div>
</div>
) : null}
</>
);
}
+40 -33
View File
@@ -3,6 +3,7 @@ import AsciinemaPlayer from "./asciinema";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./TutorialCard.module.css";
export type TutorialCardCardProps = {
name: string;
@@ -12,7 +13,6 @@ export type TutorialCardCardProps = {
rows?: string;
cols?: string;
idleTimeLimit?: number;
hideControls?: boolean;
italicDescription?: boolean;
onPress?: () => void;
@@ -21,48 +21,55 @@ export type TutorialCardCardProps = {
};
export default function TutorialCard(props: TutorialCardCardProps) {
const actionButtonClassName =
"ml-auto h-auto min-h-0 shrink-0 px-0 py-0 text-sm font-semibold [--button-bg-hover:transparent] [--button-bg-pressed:transparent] [--button-bg:transparent] [--button-fg:var(--warning)]";
return (
<Card className="relative h-full min-w-0 gap-0 overflow-hidden bg-black p-0 shadow-surface">
<div className="sliver-terminal-frame relative w-full max-w-full overflow-x-auto bg-[#111315]">
<Card className="relative z-0 gap-0 overflow-hidden p-0">
<Card.Header className="absolute left-3 right-3 top-3 z-10 flex-col items-end p-0 text-right">
<p className="text-sm font-bold uppercase leading-none text-white/70">
{props.name}
</p>
</Card.Header>
<div className={`${styles["hide-control-bar"]} overflow-hidden`}>
<AsciinemaPlayer
className="min-w-[36rem]"
src={props.asciiCast}
hideControls={props.hideControls}
rows={props.rows || "18"}
cols={props.cols || "75"}
idleTimeLimit={props.idleTimeLimit || 2}
rows={props.rows ? props.rows : "18"}
cols={props.cols ? props.cols : "75"}
idleTimeLimit={props.idleTimeLimit ? props.idleTimeLimit : 2}
preload={true}
autoPlay={true}
loop={true}
/>
<div className="pointer-events-none absolute inset-x-3 top-3 z-10 flex justify-end">
<h3 className="max-w-[80%] rounded-xl border border-white/15 bg-black/55 px-3 py-2 text-right text-sm font-semibold text-white/85 shadow-lg backdrop-blur-xl">
{props.name}
</h3>
</div>
</div>
<Card.Footer className="absolute inset-x-3 bottom-10 z-10 flex-row items-center gap-3 rounded-2xl border border-white/15 bg-black/55 px-3 py-2.5 text-white shadow-xl backdrop-blur-xl">
<p
className={`min-w-0 flex-1 text-sm leading-5 text-white/70 ${
props.italicDescription ? "italic" : ""
}`}
>
{props.description}
</p>
{props.showButton ? (
<Button
className="shrink-0 text-white hover:bg-white/10"
size="sm"
variant="ghost"
onPress={props.onPress}
<Card.Footer className="absolute bottom-0 left-0 right-0 z-10 border-t border-white/20 bg-black/40 p-3 backdrop-blur-md">
<div className="flex min-h-10 w-full flex-row items-center gap-4">
<p
className={
props.italicDescription
? "min-w-0 flex-1 text-sm leading-snug text-white/80 italic"
: "min-w-0 flex-1 text-sm leading-snug text-white/80"
}
>
{props.buttonText || "Read tutorial"}
<FontAwesomeIcon icon={faChevronRight} />
</Button>
) : null}
{props.description}
</p>
{props.showButton ? (
<Button
variant="ghost"
size="sm"
onPress={props.onPress}
className={actionButtonClassName}
>
{props.buttonText ? props.buttonText : "Read Tutorial"}
<FontAwesomeIcon icon={faChevronRight} />
</Button>
) : (
<></>
)}
</div>
</Card.Footer>
</Card>
);
+46 -53
View File
@@ -8,7 +8,6 @@ import { fetchDocs as fetchDocsContent, fetchTutorials as fetchTutorialsContent
import { Themes } from "@/util/themes";
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { RouterProvider } from "@heroui/react";
import {
HydrationBoundary,
QueryClient,
@@ -16,19 +15,9 @@ import {
} from "@tanstack/react-query";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import React from "react";
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const navigate = React.useCallback(
(path: string) => {
void router.push(path);
},
[router],
);
// Initialize search
const [search] = React.useState(() => new SearchCtx());
@@ -112,50 +101,54 @@ export default function App({ Component, pageProps }: AppProps) {
>
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<RouterProvider navigate={navigate}>
<SearchContext.Provider value={search}>
<div className="flex min-h-dvh flex-col">
<Navbar />
<main className="sliver-page-content flex-1">
<Component {...pageProps} />
</main>
<footer className="sliver-fixed-footer fixed inset-x-0 bottom-0 z-30 border-t border-separator/70 bg-background/70 backdrop-blur-xl">
<div className="mx-auto flex min-h-[var(--sliver-footer-height)] w-full max-w-7xl flex-col items-center justify-center gap-1.5 px-4 py-3 text-center text-sm text-muted sm:px-6 md:flex-row md:justify-between md:text-left lg:px-8">
<span>
© {new Date().getFullYear()} {" "}
<a
href="https://bishopfox.com/"
className="font-medium text-foreground no-underline hover:text-accent"
rel="noreferrer"
target="_blank"
>
Bishop Fox
</a>
<SearchContext.Provider value={search}>
<Navbar />
<Component {...pageProps} />
<div className="mb-16 md:mb-20"></div>
<footer className="z-20 w-full border-t border-separator bg-surface/80 px-3 py-4 text-center shadow backdrop-blur-md dark:bg-black/75 md:fixed md:bottom-0 md:left-0 md:right-0 md:flex md:items-center md:justify-between md:px-6 md:py-4">
<div className="flex flex-col items-center gap-2 text-xs leading-snug text-muted sm:text-sm md:flex-row md:items-center md:gap-3 md:text-left">
<span>
© {new Date().getFullYear()}&nbsp;
<a
href="https://bishopfox.com/"
className="hover:underline"
rel="noreferrer"
target="_blank"
>
Bishop Fox
</a>
</span>
<span className="hidden md:inline" aria-hidden="true">
·
</span>
<span className="max-w-xs md:max-w-none">
<a
href="https://github.com/BishopFox/sliver/pulls"
rel="noreferrer"
className="hover:underline"
target="_blank"
>
Help improve this documentation
<span className="ml-1 inline-block">
<FontAwesomeIcon icon={faExternalLink} />
</span>
<div className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
<a
href="https://github.com/BishopFox/sliver/pulls"
rel="noreferrer"
className="inline-flex items-center gap-1.5 no-underline hover:text-foreground"
target="_blank"
>
Improve these docs
<FontAwesomeIcon className="text-xs" icon={faExternalLink} />
</a>
<a
href="https://github.com/BishopFox/sliver/blob/master/LICENSE"
target="_blank"
rel="noreferrer"
className="no-underline hover:text-foreground"
>
GPLv3
</a>
</div>
</div>
</footer>
</a>
</span>
</div>
</SearchContext.Provider>
</RouterProvider>
<ul className="mt-3 flex flex-wrap items-center justify-center gap-3 text-xs font-medium text-muted md:mt-0 md:gap-6 md:text-sm">
<li>
<a
href="https://github.com/BishopFox/sliver/blob/master/LICENSE"
target="_blank"
rel="noreferrer"
className="hover:underline"
>
GPLv3 License
</a>
</li>
</ul>
</footer>
</SearchContext.Provider>
</HydrationBoundary>
</QueryClientProvider>
</NextThemesProvider>
+77 -154
View File
@@ -3,18 +3,14 @@ import LoadingState from "@/components/loading-state";
import { Docs } from "@/util/docs";
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
import {
faBookOpen,
faChevronRight,
faSearch,
} from "@fortawesome/free-solid-svg-icons";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
Card,
ListBox,
ScrollShadow,
SearchField,
Separator,
} from "@heroui/react";
import { useQuery } from "@tanstack/react-query";
import Fuse from "fuse.js";
@@ -24,15 +20,6 @@ import { useSearchParams } from "next/navigation";
import { useRouter } from "next/router";
import React from "react";
const featuredDocNames = [
"Getting Started",
"Compile from Source",
"Architecture",
"Armory",
"Stagers",
"Troubleshooting",
];
const DocsIndexPage: NextPage = () => {
const router = useRouter();
@@ -83,20 +70,13 @@ const DocsIndexPage: NextPage = () => {
}, [docs, name, visibleDocs]);
const listboxClasses =
"flex flex-col gap-1 overflow-visible bg-transparent p-0";
const featuredDocs = React.useMemo(() => {
return featuredDocNames
.map((featuredName) => docs?.docs.find((doc) => doc.name === featuredName))
.filter((doc) => doc !== undefined);
}, [docs]);
"p-0 gap-0 divide-y divide-separator bg-surface overflow-visible rounded-lg shadow-sm";
const renderFilterInput = (className?: string) => (
<SearchField
aria-label="Filter documents"
className={className}
fullWidth
variant="secondary"
value={filterValue}
onChange={setFilterValue}
>
@@ -119,144 +99,87 @@ const DocsIndexPage: NextPage = () => {
return (
<>
<Head>
<title>{name ? `${name} · Sliver Docs` : "Sliver Documentation"}</title>
<title>Sliver Docs: {name}</title>
</Head>
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
<div className="mb-8 lg:hidden">
<div className="flex items-center gap-3">
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
<FontAwesomeIcon icon={faBookOpen} />
</span>
<div>
<p className="text-sm font-medium text-foreground">Documentation</p>
<p className="text-xs text-muted">Browse the reference library</p>
</div>
<div className="px-4 pt-4 lg:hidden">
<label
htmlFor="docs-mobile-selector"
className="block text-sm font-medium text-foreground"
>
Select a document
</label>
<div className="mt-2">{renderFilterInput()}</div>
<select
id="docs-mobile-selector"
className="mt-3 w-full rounded-lg border border-separator bg-surface p-2 text-sm"
value={name}
onChange={(event) => {
const selectedName = event.target.value;
router.push({
pathname: "/docs",
query: selectedName ? { name: selectedName } : undefined,
});
}}
>
<option value="">Browse documents…</option>
{mobileDocs.map((doc) => (
<option key={doc.name} value={doc.name}>
{doc.name}
</option>
))}
</select>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
<aside className="hidden lg:block lg:col-span-3">
<div className="sticky top-16 ml-4 flex flex-col gap-3">
{renderFilterInput("mt-2")}
<ScrollShadow className="max-h-[calc(100vh-15rem)] sliver-scrollbar overflow-y-auto pr-1 rounded-lg">
<ListBox
aria-label="Toolbox Menu"
className={listboxClasses}
onAction={(key) => {
router.push({
pathname: "/docs",
query: { name: String(key) },
});
}}
>
{visibleDocs.map((doc) => (
<ListBox.Item
key={doc.name}
id={doc.name}
textValue={doc.name}
className="h-12 rounded-none px-3 first:rounded-t-lg last:rounded-b-lg"
>
{doc.name}
</ListBox.Item>
))}
</ListBox>
</ScrollShadow>
</div>
<div className="mt-5">{renderFilterInput()}</div>
<label
htmlFor="docs-mobile-selector"
className="mt-4 block text-sm font-medium text-foreground"
>
Document
</label>
<select
id="docs-mobile-selector"
className="mt-2 w-full rounded-2xl border border-transparent bg-surface-secondary px-3 py-2.5 text-sm text-foreground outline-none focus:border-accent"
value={name}
onChange={(event) => {
const selectedName = event.target.value;
router.push({
pathname: "/docs",
query: selectedName ? { name: selectedName } : undefined,
});
}}
>
<option value="">Browse documents…</option>
{mobileDocs.map((doc) => (
<option key={doc.name} value={doc.name}>
{doc.name}
</option>
))}
</select>
</div>
<div className="grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-10">
<aside className="sliver-sticky-sidebar hidden border-r border-separator/70 pr-6 lg:block">
<div className="flex h-full min-h-0 flex-col gap-5">
<div className="flex items-center gap-3 px-1">
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
<FontAwesomeIcon icon={faBookOpen} />
</span>
<div>
<p className="text-sm font-medium text-foreground">Documentation</p>
<p className="text-xs text-muted">{docs.docs.length} articles</p>
</div>
</div>
{renderFilterInput()}
<ScrollShadow className="sliver-scrollbar min-h-0 flex-1 overscroll-contain overflow-y-auto pr-2">
<ListBox
aria-label="Documentation"
className={listboxClasses}
selectedKeys={name ? [name] : []}
selectionMode="single"
>
{visibleDocs.map((doc) => (
<ListBox.Item
key={doc.name}
id={doc.name}
href={`/docs/?name=${encodeURIComponent(doc.name)}`}
textValue={doc.name}
className={`min-h-10 rounded-xl px-3 py-2 text-sm ${
doc.name === name
? "bg-surface font-medium text-foreground shadow-surface"
: "text-muted"
}`}
>
{doc.name}
</ListBox.Item>
))}
</ListBox>
</ScrollShadow>
</div>
</aside>
<div
key={name || "documentation-overview"}
className="min-w-0 pb-16 lg:pr-4"
>
{name !== "" ? (
<article className="mx-auto w-full max-w-4xl">
<header className="mb-8 border-b border-separator/70 pb-8">
<p className="text-sm font-medium text-accent">Reference</p>
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
{name}
</h1>
</header>
</aside>
<div className="px-4 pb-8 lg:col-span-9 lg:px-8">
{name !== "" ? (
<Card className="mt-2">
<Card.Header>
<span className="text-3xl">{name}</span>
</Card.Header>
<Separator />
<Card.Content>
<MarkdownViewer
key={name}
markdown={markdown || ""}
demoteTopLevelHeading
/>
</article>
) : (
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
<h1 className="text-sm font-medium text-accent">
Sliver reference
</h1>
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
Browse configuration, transports, extensions, payloads, and
troubleshooting guidance for the Sliver framework.
</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
{featuredDocs.map((doc) => (
<Card key={doc.name} className="h-full">
<Card.Header>
<Card.Title>{doc.name}</Card.Title>
<Card.Description>
Open this topic in the Sliver reference.
</Card.Description>
</Card.Header>
<Card.Footer className="mt-auto">
<Button
variant="ghost"
onPress={() => {
router.push({
pathname: "/docs",
query: { name: doc.name },
});
}}
>
Open article
<FontAwesomeIcon icon={faChevronRight} />
</Button>
</Card.Footer>
</Card>
))}
</div>
</Card.Content>
</Card>
) : (
<div className="mt-8 text-center text-2xl text-foreground/90">
Welcome to the Sliver Wiki!
<div className="mt-2 text-xl text-muted">
Please select a document
</div>
)}
</div>
</div>
)}
</div>
</div>
</>
+130 -198
View File
@@ -1,212 +1,144 @@
import AsciinemaPlayer from "@/components/asciinema";
import { SliversIcon } from "@/components/icons/slivers";
import TutorialCard from "@/components/tutorial-card";
import {
faArrowUpRightFromSquare,
faDownload,
} from "@fortawesome/free-solid-svg-icons";
import { Themes } from "@/util/themes";
import { faDownload, faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Card, buttonVariants } from "@heroui/react";
import Head from "next/head";
import Link from "next/link";
import { Button, Card, Separator } from "@heroui/react";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import React from "react";
export default function Home() {
const router = useRouter();
const { resolvedTheme } = useTheme();
const isDarkTheme = React.useMemo(() => {
return (resolvedTheme || Themes.DARK) !== Themes.LIGHT;
}, [resolvedTheme]);
return (
<>
<Head>
<title>Sliver C2 Documentation</title>
<meta
name="description"
content="Documentation, tutorials, and community resources for the Sliver command and control framework."
/>
</Head>
<div className="relative overflow-hidden">
<div className="sliver-grid-surface pointer-events-none absolute inset-x-0 top-0 h-[34rem] opacity-55" />
<div className="relative mx-auto w-full max-w-7xl px-4 pb-20 pt-8 sm:px-6 sm:pt-12 lg:px-8 lg:pt-16">
<section className="grid min-w-0 grid-cols-1 items-stretch gap-6 lg:grid-cols-12">
<div className="min-w-0 lg:col-span-7">
<Card className="relative h-full min-w-0 gap-0 overflow-hidden rounded-3xl bg-black p-0 shadow-surface">
<div className="absolute right-3 top-3 z-10 max-w-[calc(100%_-_1.5rem)]">
<Link
href="/tutorials"
aria-label="Browse Sliver tutorials"
className="inline-flex min-w-0 cursor-pointer items-center gap-2.5 rounded-xl border border-white/15 bg-black/55 px-3 py-2 text-white no-underline shadow-lg outline-none backdrop-blur-xl hover:bg-black/70 focus-visible:ring-2 focus-visible:ring-white/70"
>
<span className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-white/10 text-white/90">
<SliversIcon height={16} />
</span>
<div className="min-w-0">
<p className="truncate text-xs font-semibold">
Sliver operator console
</p>
<p className="truncate text-[11px] text-white/60">
Interactive command tour
</p>
</div>
</Link>
</div>
<div className="sliver-terminal-frame min-h-[20rem] flex-1 overflow-x-auto bg-[#111315] sm:min-h-[24rem]">
<AsciinemaPlayer
className="sliver-asciinema-bottom-aligned h-full w-full sm:min-w-[35rem]"
src="/asciinema/intro.cast"
fit="width"
hideControls
rows="18"
cols="75"
idleTimeLimit={60}
preload={true}
autoPlay={true}
loop={true}
/>
</div>
</Card>
</div>
<Card className="h-full bg-surface/80 shadow-surface backdrop-blur-2xl lg:col-span-5">
<Card.Header className="pb-3">
<div className="min-w-0">
<p className="text-sm font-medium text-accent">
Operator documentation
</p>
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
Sliver Command &amp; Control
</h1>
</div>
</Card.Header>
<Card.Content className="pt-2">
<p className="text-pretty text-base leading-7 text-muted">
Sliver is a cross-platform command and control framework for
professional red teams. Operate over mTLS, WireGuard, HTTP(S),
and DNS from one console, with broad operating-system and CPU
architecture support.
</p>
<p className="mt-4 text-sm leading-6 text-muted">
Start with the operator guide, download the latest release, or
extend your workflow with community packages from the Armory.
</p>
</Card.Content>
<Card.Footer className="mt-auto flex-col items-stretch gap-3 pt-5">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Button
fullWidth
variant="primary"
onPress={() =>
router.push({
pathname: "/docs",
query: { name: "Getting Started" },
})
}
>
Get started
</Button>
<Button
className="[--button-bg-hover:var(--color-purple-700)] [--button-bg-pressed:var(--color-purple-700)] [--button-bg:var(--color-purple-600)] [--button-fg:var(--color-white)]"
fullWidth
variant="tertiary"
onPress={() => {
window.open(
"https://github.com/BishopFox/sliver/releases/latest",
"_blank",
"noopener,noreferrer",
);
}}
>
<FontAwesomeIcon icon={faDownload} />
Latest release
</Button>
</div>
<a
className={buttonVariants({
fullWidth: true,
variant: "secondary",
})}
href="https://github.com/sliverarmory"
target="_blank"
rel="noreferrer"
>
Browse the Armory
<FontAwesomeIcon
className="text-xs"
icon={faArrowUpRightFromSquare}
/>
</a>
</Card.Footer>
</Card>
</section>
<section
aria-labelledby="quick-guides-heading"
className="mt-12 border-t border-separator/70 pt-10"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2
id="quick-guides-heading"
className="text-sm font-medium text-accent"
>
Quick guides
</h2>
<blockquote className="mt-1 max-w-3xl text-xl font-medium italic leading-snug tracking-tight text-foreground sm:text-2xl">
Bred as living shields, these slivers have proven unruly—they
know they cannot be caught.
</blockquote>
</div>
<Button
variant="ghost"
onPress={() => router.push("/tutorials")}
>
Browse all tutorials
</Button>
</div>
<div className="mt-6 grid min-w-0 grid-cols-1 gap-5 xl:grid-cols-2">
<TutorialCard
name="Getting Started"
description="Install Sliver and open your first operator workflow."
asciiCast="/asciinema/install-1.cast"
cols="133"
rows="32"
idleTimeLimit={1}
hideControls
showButton={true}
buttonText="Read guide"
onPress={() => {
router.push({
pathname: "/docs",
query: { name: "Getting Started" },
});
}}
/>
<TutorialCard
name="Compile from Source"
description="Build a development copy and understand the toolchain."
asciiCast="/asciinema/compile-from-source.cast"
cols="133"
rows="32"
idleTimeLimit={1}
hideControls
showButton={true}
buttonText="Read guide"
onPress={() => {
router.push({
pathname: "/docs",
query: { name: "Compile from Source" },
});
}}
/>
</div>
</section>
<div className="mt-6 flex flex-col gap-6 px-4 sm:px-6 lg:grid lg:grid-cols-12 lg:px-12">
<div className="lg:col-span-6">
<div className="w-full overflow-hidden rounded-xl border border-separator bg-surface shadow-sm">
<div className="w-full overflow-x-auto lg:overflow-visible">
<AsciinemaPlayer
src="/asciinema/intro.cast"
rows="18"
cols="75"
idleTimeLimit={60}
preload={true}
autoPlay={true}
loop={true}
/>
</div>
</div>
</div>
</>
<div className="lg:col-span-6 lg:ml-2">
<Card className="mx-auto max-w-3xl lg:mx-0">
<Card.Header>
<div className="flex items-center">
<SliversIcon className="mr-2" height={28} />
<span className="text-2xl">Sliver Command &amp; Control</span>
</div>
</Card.Header>
<Separator />
<Card.Content>
<p className={isDarkTheme ? "prose dark:prose-invert" : "prose prose-slate"}>
Sliver is a powerful command and control (C2) framework designed
to provide advanced capabilities for covertly managing and
controlling remote systems. With Sliver, security professionals,
red teams, and penetration testers can easily establish a secure
and reliable communication channel over Mutual TLS, HTTP(S), DNS,
or Wireguard with target machines. Enabling them to execute
commands, gather information, and perform various
post-exploitation activities. The framework offers a user-friendly
console interface, extensive functionality, and support for
multiple operating systems as well as multiple CPU architectures,
making it an indispensable tool for conducting comprehensive
offensive security operations.
</p>
<div className="mt-4 flex w-full gap-3">
<Button
className="flex-1"
variant="primary"
onPress={() => {
window.open(
"https://github.com/BishopFox/sliver/releases/latest",
"_blank",
"noopener,noreferrer",
);
}}
>
<FontAwesomeIcon icon={faDownload} />
Download Latest Release
</Button>
<Button
className="sliver-armory-ghost flex-1"
variant="ghost"
onPress={() => {
window.open(
"https://github.com/sliverarmory",
"_blank",
"noopener,noreferrer",
);
}}
>
Visit the Armory
<FontAwesomeIcon icon={faExternalLink} />
</Button>
</div>
</Card.Content>
</Card>
</div>
<div className="col-span-12 mt-8">
<Separator />
</div>
<div className="col-span-12 mt-8">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-9">
<div className="sm:col-span-1 lg:col-span-3">
<TutorialCard
name="Getting Started"
description="A quick start guide to get you up and running"
asciiCast="/asciinema/install-1.cast"
cols="133"
rows="32"
idleTimeLimit={1}
showButton={true}
buttonText="Read Docs"
onPress={() => {
router.push({
pathname: "/docs",
query: { name: "Getting Started" },
});
}}
/>
</div>
<div className="sm:col-span-1 lg:col-span-3">
<TutorialCard
name="Compile From Source"
description="How to compile Sliver from source"
asciiCast="/asciinema/compile-from-source.cast"
cols="133"
rows="32"
idleTimeLimit={1}
showButton={true}
buttonText="Read Docs"
onPress={() => {
router.push({
pathname: "/docs",
query: { name: "Compile from Source" },
});
}}
/>
</div>
</div>
</div>
<div className="col-span-12 mb-8"></div>
</div>
);
}
+37 -136
View File
@@ -1,165 +1,66 @@
import LoadingState from "@/components/loading-state";
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
import { SearchCtx } from "@/util/search-context";
import {
faChevronRight,
faMagnifyingGlass,
} from "@fortawesome/free-solid-svg-icons";
import MarkdownViewer from "@/components/markdown";
import { useSearchContext } from "@/util/search-context";
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Card, SearchField } from "@heroui/react";
import { useQuery } from "@tanstack/react-query";
import { Button, Card, Separator } from "@heroui/react";
import { NextPage } from "next";
import Head from "next/head";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/router";
import React from "react";
export type SearchPageProps = {};
const createExcerpt = (markdown: string) => {
return markdown
.replace(/```[\s\S]*?```/g, " ")
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
.replace(/[#>*_`~|-]/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 240);
};
const SearchPage: NextPage = (props: SearchPageProps) => {
const router = useRouter();
const query = useSearchParams().get("search")?.trim() || "";
const { data: docs, isLoading } = useQuery({
queryKey: ["docs", PREBUILD_VERSION],
queryFn: () => fetchDocsContent(PREBUILD_VERSION),
});
const docsSearch = React.useMemo(() => {
const search = new SearchCtx();
if (docs) {
search.addDocs(docs);
}
return search;
}, [docs]);
const search = useSearchContext();
const query = useSearchParams().get("search");
const searchResults = React.useMemo(() => {
if (query) {
return docsSearch.searchDocs(query);
return search.searchDocs(query);
}
return [];
}, [docsSearch, query]);
if (isLoading || !docs) {
return <LoadingState />;
}
}, [query, search]);
return (
<>
<Head>
<title>{query ? `Search: ${query} · Sliver Docs` : "Search · Sliver Docs"}</title>
</Head>
<div className="mx-auto w-full max-w-5xl px-4 pb-20 pt-12 sm:px-6 lg:px-8 lg:pt-16">
<header className="max-w-3xl">
<p className="text-sm font-medium text-accent">Search the reference</p>
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
Find exactly what you need.
</h1>
<p className="mt-4 text-lg leading-8 text-muted">
Search configuration, workflows, transports, extensions, and
troubleshooting notes across the Sliver documentation.
</p>
</header>
<SearchField
key={query}
aria-label="Search documentation"
className="mt-8 max-w-2xl"
defaultValue={query}
fullWidth
onClear={() => router.push("/search")}
onSubmit={(value) => {
const nextQuery = value.trim();
if (nextQuery) {
router.push({ pathname: "/search", query: { search: nextQuery } });
}
}}
>
<SearchField.Group>
<SearchField.SearchIcon>
<FontAwesomeIcon icon={faMagnifyingGlass} />
</SearchField.SearchIcon>
<SearchField.Input placeholder="Search documentation…" />
<SearchField.ClearButton aria-label="Clear search" />
</SearchField.Group>
</SearchField>
<div className="mt-10 flex items-end justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold text-foreground">
{query ? `Results for “${query.slice(0, 50)}”` : "Search results"}
</h2>
<p className="mt-1 text-sm tabular-nums text-muted">
{searchResults.length} {searchResults.length === 1 ? "result" : "results"}
</p>
<div className="px-4 pb-8 pt-4">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4">
<div className="text-3xl">
Search: &quot;{query?.slice(0, 50)}&quot;
<div className="text-sm text-gray-500">
{searchResults.length} Results
</div>
</div>
{searchResults.length > 0 ? (
<div className="mt-5 grid gap-4">
{searchResults.map((doc) => (
<Card key={doc.name}>
<Card.Header>
<Card.Title>{doc.name}</Card.Title>
<Card.Description className="mt-1 max-w-3xl leading-6">
{createExcerpt(doc.content) || "Open this article in the Sliver reference."}
{doc.content.length > 240 ? "…" : ""}
</Card.Description>
</Card.Header>
<Card.Footer>
<Button
variant="ghost"
onPress={() => {
router.push({ pathname: "/docs", query: { name: doc.name } });
}}
>
Open article
<FontAwesomeIcon icon={faChevronRight} />
</Button>
</Card.Footer>
</Card>
))}
</div>
) : (
<Card className="mt-5">
<Card.Content className="flex flex-col items-start gap-3 p-8">
<span className="flex size-10 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
<FontAwesomeIcon icon={faMagnifyingGlass} />
</span>
<div>
<h2 className="text-lg font-semibold text-foreground">
{query ? "No matching documentation" : "Start with a keyword"}
</h2>
<p className="mt-1 max-w-xl text-sm leading-6 text-muted">
{query
? "Try a command name, transport, feature, or a shorter phrase."
: "Search for topics like staging, WireGuard, profiles, or extensions."}
</p>
</div>
{query ? (
{searchResults.map((doc) => (
<Card key={doc.name}>
<Card.Content className="flex flex-col gap-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<span className="text-xl">{doc.name}</span>
<div className="flex-1"></div>
<Button
variant="outline"
onPress={() => router.push("/docs")}
size="sm"
variant="secondary"
className="w-full sm:w-[150px]"
onPress={() => {
router.push(`/docs?name=${doc.name}`);
}}
>
Browse all documentation
Full Doc
<FontAwesomeIcon icon={faChevronCircleRight} />
</Button>
) : null}
</div>
<Separator />
<div className="max-h-[200px] overflow-hidden">
<MarkdownViewer markdown={doc.content} />
</div>
</Card.Content>
</Card>
)}
))}
</div>
</>
</div>
);
};
+70 -113
View File
@@ -1,7 +1,7 @@
import Youtube from "@/components/youtube";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Accordion, Card } from "@heroui/react";
import { Accordion, Button, Card } from "@heroui/react";
import { NextPage } from "next";
import Head from "next/head";
import React from "react";
@@ -21,7 +21,7 @@ type TalkSection = {
const workshopTalks: Talk[] = [
{
title: "Getting Started with Sliver v1.6",
title:"Getting Started with Sliver v1.6",
description: "Introductory workshop covering basics and new features.",
url: "https://www.youtube.com/watch?v=IOiyXYp1lDc",
},
@@ -47,7 +47,7 @@ const generalTradecraftTalks: Talk[] = [
title: "The Sliver C2 Framework",
description: "General discussion of C2 design.",
url: "https://www.youtube.com/watch?v=tkjMZuZ_8nw",
},
}
];
const communityTalks: Talk[] = [
@@ -100,124 +100,81 @@ const talkSections: TalkSection[] = [
];
const TalksIndexPage: NextPage = () => {
const [overlayHidden, setOverlayHidden] = React.useState<
Record<string, boolean>
>({});
const [footerHidden, setFooterHidden] = React.useState<Record<string, boolean>>(
{}
);
const actionButtonClassName =
"ml-auto h-auto min-h-0 shrink-0 px-0 py-0 text-sm font-semibold [--button-bg-hover:transparent] [--button-bg-pressed:transparent] [--button-bg:transparent] [--button-fg:var(--warning)]";
const hideOverlayFor = React.useCallback((url: string) => {
setOverlayHidden((current) =>
current[url] ? current : { ...current, [url]: true },
);
const hideFooterFor = React.useCallback((url: string) => {
setFooterHidden((prev) => (prev[url] ? prev : { ...prev, [url]: true }));
}, []);
return (
<>
<Head>
<title>Sliver Talks</title>
<meta
name="description"
content="Sliver workshops, technical talks, and community videos."
/>
</Head>
<div className="mt-6 px-4 pb-8 sm:px-6 lg:px-12">
<Accordion
allowsMultipleExpanded
defaultExpandedKeys={["workshops", "general-tradecraft"]}
hideSeparator
className="px-0"
>
{talkSections.map((section) => (
<Accordion.Item
key={section.key}
id={section.key}
>
<Accordion.Heading>
<Accordion.Trigger>
<span className="flex flex-col items-start text-left">
<span className="text-lg font-semibold">{section.title}</span>
<span className="text-sm text-muted">{section.description}</span>
</span>
<Accordion.Indicator className="ml-auto" />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-9">
{section.talks.map((talk) => (
<div key={talk.url} className="sm:col-span-1 lg:col-span-3">
<Card className="relative z-0 gap-0 overflow-hidden p-0">
<Youtube
className="w-full"
url={talk.url}
title={talk.title}
onPlay={() => hideFooterFor(talk.url)}
/>
<div className="relative overflow-hidden">
<div className="sliver-grid-surface pointer-events-none absolute inset-x-0 top-0 h-72 opacity-35" />
<div className="relative mx-auto w-full max-w-7xl px-4 pb-20 pt-8 sm:px-6 sm:pt-10 lg:px-8 lg:pt-12">
<header className="mb-6 max-w-3xl">
<p className="text-sm font-medium text-accent">Talks &amp; workshops</p>
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
Watch Sliver in the field.
</h1>
<p className="mt-3 max-w-2xl text-base leading-7 text-muted">
Practical workshops, technical deep dives, and community sessions
collected in one compact library.
</p>
</header>
<Accordion
allowsMultipleExpanded
defaultExpandedKeys={["workshops", "general-tradecraft"]}
hideSeparator
className="w-full px-0"
>
{talkSections.map((section) => (
<Accordion.Item key={section.key} id={section.key}>
<Accordion.Heading>
<Accordion.Trigger>
<span className="flex flex-col items-start text-left">
<span className="flex items-center gap-2 text-lg font-semibold text-foreground">
{section.title}
<span className="text-xs font-normal tabular-nums text-muted">
{section.talks.length}
</span>
</span>
<span className="mt-1 text-sm text-muted">
{section.description}
</span>
</span>
<Accordion.Indicator className="ml-auto" />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{section.talks.map((talk) => (
<Card
key={talk.url}
className="relative z-0 gap-0 overflow-hidden bg-black p-0 shadow-surface"
>
<Youtube
className="w-full"
url={talk.url}
title={talk.title}
onInteract={() => hideOverlayFor(talk.url)}
onPlay={() => hideOverlayFor(talk.url)}
/>
{overlayHidden[talk.url] ? null : (
<>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-black/85 via-black/35 to-transparent"
/>
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-10">
<div className="flex items-end gap-3 rounded-2xl border border-white/15 bg-black/55 p-3 text-white shadow-xl backdrop-blur-xl">
<div className="min-w-0 flex-1">
<h3 className="text-sm font-semibold leading-snug text-white">
{talk.title}
</h3>
<p className="mt-0.5 truncate text-xs text-white/65">
{talk.description}
</p>
</div>
<a
href={talk.url}
target="_blank"
rel="noreferrer"
aria-label={`Watch ${talk.title} on YouTube`}
className="pointer-events-auto inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-xl px-2.5 text-xs font-semibold text-white no-underline outline-none transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white/70"
>
Watch
<FontAwesomeIcon
className="text-[10px]"
icon={faArrowUpRightFromSquare}
/>
</a>
</div>
</div>
</>
)}
</Card>
))}
</div>
</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
</div>
{footerHidden[talk.url] ? null : (
<Card.Footer className="absolute bottom-0 left-0 right-0 z-10 border-t border-white/20 bg-black/40 p-3 backdrop-blur-md">
<div className="flex min-h-10 w-full items-center gap-4">
<p className="min-w-0 flex-1 text-sm leading-snug text-white/80">{talk.description}</p>
<Button
variant="ghost"
size="sm"
className={actionButtonClassName}
onPress={() => {
window.open(talk.url, "_blank", "noopener,noreferrer");
}}
>
Watch <FontAwesomeIcon icon={faChevronRight} />
</Button>
</div>
</Card.Footer>
)}
</Card>
</div>
))}
</div>
</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
</div>
</>
);
+77 -141
View File
@@ -3,18 +3,14 @@ import LoadingState from "@/components/loading-state";
import { Tutorials } from "@/util/tutorials";
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
import { fetchTutorials as fetchTutorialsContent } from "@/util/content-fetchers";
import {
faChevronRight,
faGraduationCap,
faSearch,
} from "@fortawesome/free-solid-svg-icons";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
Card,
ListBox,
ScrollShadow,
SearchField,
Separator,
} from "@heroui/react";
import { useQuery } from "@tanstack/react-query";
import Fuse from "fuse.js";
@@ -74,14 +70,13 @@ const TutorialsIndexPage: NextPage = () => {
}, [name, tutorials, visibleTutorials]);
const listboxClasses =
"flex flex-col gap-1 overflow-visible bg-transparent p-0";
"p-0 gap-0 divide-y divide-separator bg-surface overflow-visible rounded-lg shadow-sm";
const renderFilterInput = (className?: string) => (
<SearchField
aria-label="Filter tutorials"
className={className}
fullWidth
variant="secondary"
value={filterValue}
onChange={setFilterValue}
>
@@ -104,146 +99,87 @@ const TutorialsIndexPage: NextPage = () => {
return (
<>
<Head>
<title>{name ? `${name} · Sliver Tutorials` : "Sliver Tutorials"}</title>
<title>Sliver Tutorial: {name}</title>
</Head>
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
<div className="mb-8 lg:hidden">
<div className="flex items-center gap-3">
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
<FontAwesomeIcon icon={faGraduationCap} />
</span>
<div>
<p className="text-sm font-medium text-foreground">Tutorials</p>
<p className="text-xs text-muted">Follow a guided workflow</p>
</div>
<div className="px-4 pt-4 lg:hidden">
<label
htmlFor="tutorials-mobile-selector"
className="block text-sm font-medium text-foreground"
>
Select a tutorial
</label>
<div className="mt-2">{renderFilterInput()}</div>
<select
id="tutorials-mobile-selector"
className="mt-3 w-full rounded-lg border border-separator bg-surface p-2 text-sm"
value={name}
onChange={(event) => {
const selectedName = event.target.value;
router.push({
pathname: "/tutorials",
query: selectedName ? { name: selectedName } : undefined,
});
}}
>
<option value="">Browse tutorials…</option>
{mobileTutorials.map((tutorial) => (
<option key={tutorial.name} value={tutorial.name}>
{tutorial.name}
</option>
))}
</select>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
<aside className="hidden lg:block lg:col-span-3">
<div className="sticky top-16 ml-4 flex flex-col gap-3">
{renderFilterInput("mt-2")}
<ScrollShadow className="max-h-[calc(100vh-6rem)] sliver-scrollbar overflow-y-auto pr-1 rounded-lg">
<ListBox
aria-label="Toolbox Menu"
className={listboxClasses}
onAction={(key) => {
router.push({
pathname: "/tutorials",
query: { name: String(key) },
});
}}
>
{visibleTutorials.map((tutorial) => (
<ListBox.Item
key={tutorial.name}
id={tutorial.name}
textValue={tutorial.name}
className="h-12 rounded-none px-3 first:rounded-t-lg last:rounded-b-lg"
>
{tutorial.name}
</ListBox.Item>
))}
</ListBox>
</ScrollShadow>
</div>
<div className="mt-5">{renderFilterInput()}</div>
<label
htmlFor="tutorials-mobile-selector"
className="mt-4 block text-sm font-medium text-foreground"
>
Tutorial
</label>
<select
id="tutorials-mobile-selector"
className="mt-2 w-full rounded-2xl border border-transparent bg-surface-secondary px-3 py-2.5 text-sm text-foreground outline-none focus:border-accent"
value={name}
onChange={(event) => {
const selectedName = event.target.value;
router.push({
pathname: "/tutorials",
query: selectedName ? { name: selectedName } : undefined,
});
}}
>
<option value="">Browse tutorials…</option>
{mobileTutorials.map((tutorial) => (
<option key={tutorial.name} value={tutorial.name}>
{tutorial.name}
</option>
))}
</select>
</div>
<div className="grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-10">
<aside className="sliver-sticky-sidebar hidden border-r border-separator/70 pr-6 lg:block">
<div className="flex h-full min-h-0 flex-col gap-5">
<div className="flex items-center gap-3 px-1">
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
<FontAwesomeIcon icon={faGraduationCap} />
</span>
<div>
<p className="text-sm font-medium text-foreground">Tutorials</p>
<p className="text-xs text-muted">
{tutorials.tutorials.length} chapters
</p>
</div>
</div>
{renderFilterInput()}
<ScrollShadow className="sliver-scrollbar min-h-0 flex-1 overscroll-contain overflow-y-auto pr-2">
<ListBox
aria-label="Tutorials"
className={listboxClasses}
selectedKeys={name ? [name] : []}
selectionMode="single"
>
{visibleTutorials.map((tutorial) => (
<ListBox.Item
key={tutorial.name}
id={tutorial.name}
href={`/tutorials/?name=${encodeURIComponent(tutorial.name)}`}
textValue={tutorial.name}
className={`min-h-10 rounded-xl px-3 py-2 text-sm ${
tutorial.name === name
? "bg-surface font-medium text-foreground shadow-surface"
: "text-muted"
}`}
>
{tutorial.name}
</ListBox.Item>
))}
</ListBox>
</ScrollShadow>
</div>
</aside>
<div
key={name || "tutorials-overview"}
className="min-w-0 pb-16 lg:pr-4"
>
{name !== "" ? (
<article className="mx-auto w-full max-w-4xl">
<header className="mb-8 border-b border-separator/70 pb-8">
<p className="text-sm font-medium text-accent">Tutorial</p>
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
{name}
</h1>
</header>
</aside>
<div className="px-4 pb-8 lg:col-span-9 lg:px-8">
{name !== "" ? (
<Card className="mt-2">
<Card.Header>
<span className="text-3xl">{name}</span>
</Card.Header>
<Separator />
<Card.Content>
<MarkdownViewer
key={name}
markdown={markdown || ""}
demoteTopLevelHeading
/>
</article>
) : (
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
<h1 className="text-sm font-medium text-accent">
Guided learning
</h1>
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
Follow practical walkthroughs covering setup, sessions,
staging, pivots, scripting, and post-exploitation workflows.
</p>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
{tutorials.tutorials.slice(0, 6).map((tutorial) => (
<Card key={tutorial.name} className="h-full">
<Card.Header>
<Card.Title>{tutorial.name}</Card.Title>
<Card.Description>
Continue through this guided Sliver workflow.
</Card.Description>
</Card.Header>
<Card.Footer className="mt-auto">
<Button
variant="ghost"
onPress={() => {
router.push({
pathname: "/tutorials",
query: { name: tutorial.name },
});
}}
>
Start tutorial
<FontAwesomeIcon icon={faChevronRight} />
</Button>
</Card.Footer>
</Card>
))}
</div>
</Card.Content>
</Card>
) : (
<div className="mt-8 text-center text-2xl text-foreground/90">
Welcome to the Sliver Tutorials!
<div className="mt-2 text-xl text-muted">
Please select a chapter
</div>
)}
</div>
</div>
)}
</div>
</div>
</>
+21 -80
View File
@@ -6,39 +6,14 @@
:root {
--font-mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, "Liberation Mono", "Courier New", monospace;
--sliver-navbar-height: 4rem;
--sliver-footer-height: 4.75rem;
}
/* Keep html as the single page scroller so content can pass beneath the shell. */
/*
Keep html as the single scrolling element. Setting overflow on both html
and body breaks sticky sidebar behavior on docs/tutorial pages.
*/
html {
overflow-y: scroll;
scroll-padding-top: calc(var(--sliver-navbar-height) + 1.5rem);
scroll-padding-bottom: calc(
var(--sliver-footer-height) + env(safe-area-inset-bottom)
);
}
body {
min-height: 100dvh;
background:
radial-gradient(circle at 12% -10%, color-mix(in oklab, var(--accent) 12%, transparent), transparent 28rem),
radial-gradient(circle at 88% 4%, color-mix(in oklab, var(--surface-tertiary) 55%, transparent), transparent 26rem),
var(--background);
}
.sliver-page-content {
padding-bottom: calc(
var(--sliver-footer-height) + env(safe-area-inset-bottom)
);
}
.sliver-fixed-footer {
padding-bottom: env(safe-area-inset-bottom);
}
::selection {
background: color-mix(in oklab, var(--accent) 32%, transparent);
}
/* Fallback for engines that do not support scrollbar-gutter. */
@@ -56,22 +31,6 @@ body {
html {
overflow-y: scroll;
}
.sliver-sticky-sidebar {
position: sticky;
top: calc(var(--sliver-navbar-height) + 2.5rem);
align-self: start;
height: calc(
100dvh - var(--sliver-navbar-height) - var(--sliver-footer-height) -
env(safe-area-inset-bottom) - 3.5rem
);
}
}
@media (min-width: 768px) {
:root {
--sliver-footer-height: 3.5rem;
}
}
code,
@@ -85,7 +44,7 @@ pre {
.sliver-scrollbar {
scrollbar-width: thin;
scrollbar-color: color-mix(in oklab, var(--muted) 58%, transparent) transparent;
scrollbar-color: rgba(148, 163, 184, 0.7) transparent;
scrollbar-gutter: stable;
}
@@ -98,35 +57,33 @@ pre {
}
.sliver-scrollbar::-webkit-scrollbar-thumb {
background-color: color-mix(in oklab, var(--muted) 58%, transparent);
background-color: rgba(148, 163, 184, 0.7);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: content-box;
}
.sliver-grid-surface {
background-image:
linear-gradient(color-mix(in oklab, var(--separator) 35%, transparent) 1px, transparent 1px),
linear-gradient(90deg, color-mix(in oklab, var(--separator) 35%, transparent) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: linear-gradient(to bottom, black, transparent 92%);
.sliver-navbar-glass {
width: 100%;
border-bottom: 1px solid var(--separator);
background-color: color-mix(in oklab, var(--surface) 72%, transparent);
-webkit-backdrop-filter: blur(18px) saturate(1.18);
backdrop-filter: blur(18px) saturate(1.18);
}
.sliver-terminal-frame .ap-wrapper {
border-radius: 0 !important;
.sliver-armory-ghost {
--button-bg: transparent;
--button-bg-hover: rgb(109 40 217 / 0.16);
--button-bg-pressed: rgb(109 40 217 / 0.24);
--button-fg: #a78bfa;
}
.sliver-asciinema-bottom-aligned .ap-wrapper {
align-items: flex-end;
justify-content: flex-start;
.dark .sliver-scrollbar {
scrollbar-color: rgba(79, 97, 125, 0.8) transparent;
}
.sliver-terminal-frame .asciinema-player {
background: #111315;
}
.sliver-asciinema-controls-hidden .ap-control-bar {
display: none !important;
.dark .sliver-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(79, 97, 125, 0.8);
}
.asciinema-player {
@@ -137,19 +94,3 @@ pre {
.asciinema-player .asciinema-terminal {
max-width: 100% !important;
}
.markdown-body {
color: var(--foreground);
}
.markdown-body :where(h2, h3, h4) {
scroll-margin-top: calc(var(--sliver-navbar-height) + 1.5rem);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ require (
github.com/sirupsen/logrus v1.9.4
github.com/sliverarmory/beignet v0.0.3
github.com/sliverarmory/malasada v0.0.3
github.com/sliverarmory/reflektor v0.0.2
github.com/sliverarmory/reflektor v0.0.3
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
+2 -2
View File
@@ -678,8 +678,8 @@ github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKC
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
github.com/sliverarmory/malasada v0.0.3/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/reflektor v0.0.2 h1:nrEEFXrYiYFLCqyMpOJpe4RsY0Pu3tc1CvwChsNoAq4=
github.com/sliverarmory/reflektor v0.0.2/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/sliverarmory/reflektor v0.0.3 h1:tCBH9bXRykDa2Z5y8IgtcO+kGxL7OFFetJ4DdjPub8k=
github.com/sliverarmory/reflektor v0.0.3/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/sliverarmory/wasm-donut v0.0.3 h1:sldbfAsz1YziULlhDKpU3DY/+/dAfv5gl26qSqHIpzE=
github.com/sliverarmory/wasm-donut v0.0.3/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f
github.com/lesnuages/go-winio v0.4.19
github.com/miekg/dns v1.1.50
github.com/sliverarmory/reflektor v0.0.2
github.com/sliverarmory/reflektor v0.0.3
github.com/stretchr/testify v1.8.4
github.com/tetratelabs/wazero v1.7.1
github.com/things-go/go-socks5 v0.0.4
+2 -2
View File
@@ -43,8 +43,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sliverarmory/reflektor v0.0.2 h1:nrEEFXrYiYFLCqyMpOJpe4RsY0Pu3tc1CvwChsNoAq4=
github.com/sliverarmory/reflektor v0.0.2/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/sliverarmory/reflektor v0.0.3 h1:tCBH9bXRykDa2Z5y8IgtcO+kGxL7OFFetJ4DdjPub8k=
github.com/sliverarmory/reflektor v0.0.3/go.mod h1:Rw7qTWQQlCM3YTxxeuklFt3Q+kh1Ytlmzg5UVw1Nhkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+33 -7
View File
@@ -36,9 +36,10 @@ var (
)
type Module struct {
mu sync.RWMutex
image []byte
closed bool
mu sync.RWMutex
image []byte
goRuntime bool
closed bool
}
// LoadLibrary loads a Mach-O image into the darwin in-memory loader context.
@@ -51,10 +52,23 @@ func LoadLibrary(data []byte) (*Module, error) {
if err != nil {
return nil, err
}
goRuntime, err := machOHasGoBuildInfo(image)
if err != nil {
return nil, err
}
cloned := make([]byte, len(image))
copy(cloned, image)
return &Module{image: cloned}, nil
return &Module{image: cloned, goRuntime: goRuntime}, nil
}
func machOHasGoBuildInfo(image []byte) (bool, error) {
f, err := macho.NewFile(bytes.NewReader(image))
if err != nil {
return false, fmt.Errorf("inspect Mach-O build info: %w", err)
}
defer f.Close()
return f.Section("__go_buildinfo") != nil, nil
}
// Free releases the in-memory Mach-O bytes.
@@ -94,7 +108,7 @@ func (module *Module) CallExport(name string) error {
image := module.image
module.mu.RUnlock()
rc := memmodLoader(image, symbol)
rc := memmodLoader(image, symbol, module.goRuntime)
runtime.KeepAlive(image)
if rc != 0 {
@@ -395,7 +409,7 @@ func isDarwinSystemInstallName(installName string) bool {
strings.HasPrefix(installName, "/System/Library/")
}
func memmodLoader(bufferRO []byte, entrySymbol string) int {
func memmodLoader(bufferRO []byte, entrySymbol string, isolateGoRuntime bool) int {
if len(bufferRO) == 0 || entrySymbol == "" {
return 1
}
@@ -789,7 +803,14 @@ func memmodLoader(bufferRO []byte, entrySymbol string) int {
// Exported fixture entry points use the C void(void) ABI. Keep that call
// signature exact when cgo supplies the foreign-function bridge.
callVoid0(addrEntry)
if isolateGoRuntime {
if err := callVoid0OnThread(addrEntry); err != nil {
setDarwinLoaderDetail(err.Error())
return 14
}
} else {
callVoid0(addrEntry)
}
// Keep mapped and scratch memory reachable until after entry returns.
runtime.KeepAlive(mapped.mapping)
runtime.KeepAlive(scratch)
@@ -1535,6 +1556,11 @@ func loaderStatusError(code int) error {
return fmt.Errorf("failed to preload Mach-O dependencies: %s", detail)
}
return errors.New("failed to preload Mach-O dependencies")
case 14:
if detail := getDarwinLoaderDetail(); detail != "" {
return fmt.Errorf("failed to call Go export on isolated thread: %s", detail)
}
return errors.New("failed to call Go export on isolated thread")
default:
return fmt.Errorf("in-memory dyld loader failed with status %d", code)
}
@@ -2,7 +2,10 @@
package memmod
import _ "unsafe"
import (
"errors"
_ "unsafe"
)
//go:noescape
func cCall10(fn, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) uintptr
@@ -18,6 +21,11 @@ func callVoid0(fn uintptr) {
_ = call0(fn)
}
func callVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return call2(fn, name, uintptr(flags))
}
@@ -3,7 +3,9 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn10_t)(
uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t,
@@ -33,13 +35,44 @@ static uintptr_t reflektor_call10(
) {
return ((reflektor_fn10_t)fn)(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9);
}
typedef struct {
uintptr_t fn;
} reflektor_void_thread_call;
static void *reflektor_void_thread_entry(void *opaque) {
reflektor_void_thread_call *call = (reflektor_void_thread_call *)opaque;
((reflektor_void_fn0_t)call->fn)();
return NULL;
}
static int reflektor_call_void0_thread(uintptr_t fn) {
pthread_t thread;
reflektor_void_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
int err = pthread_create(&thread, NULL, reflektor_void_thread_entry, &call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
*/
import "C"
import "fmt"
func callVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func callVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return uintptr(C.reflektor_call_dlopen(C.uintptr_t(fn), C.uintptr_t(name), C.int(flags)))
}
+167 -103
View File
@@ -45,17 +45,20 @@ const (
)
type Module struct {
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
closed bool
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
goRuntime bool
closed bool
}
type mappedELF struct {
mapping []byte
loadBias uintptr
progs []*elf.Prog
mapping []byte
loadBias uintptr
progs []*elf.Prog
tlsOffset int64
hasTLS bool
}
type dynamicInitInfo struct {
@@ -80,6 +83,13 @@ type symbolResolver struct {
opened map[string]uintptr
}
var linuxGoTLSSlots = struct {
sync.Mutex
used [linuxGoTLSSlotCount]bool
}{}
const linuxGoTLSSlotCount = 64
func LoadLibrary(data []byte) (*Module, error) {
if len(data) == 0 {
return nil, errors.New("empty ELF image")
@@ -94,11 +104,27 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := validateELFHeaders(f); err != nil {
return nil, err
}
initInfo, err := parseDynamicInitInfo(f)
if err != nil {
return nil, err
}
goRuntime, tlsSlot, tlsOffset, err := prepareLinuxGoRuntimeTLS(f)
if err != nil {
return nil, err
}
tlsSlotReserved := goRuntime
defer func() {
if tlsSlotReserved {
releaseLinuxGoTLSSlot(tlsSlot)
}
}()
mapped, err := mapELFImage(data, f)
if err != nil {
return nil, err
}
mapped.tlsOffset = tlsOffset
mapped.hasTLS = goRuntime
cleanup := true
defer func() {
if cleanup && len(mapped.mapping) != 0 {
@@ -114,19 +140,89 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := applySegmentProtections(mapped); err != nil {
return nil, err
}
if err := runELFInitializers(mapped, f); err != nil {
initializers, err := collectELFInitializers(mapped, f.Class, initInfo)
if err != nil {
return nil, err
}
argc, argv, envp := linuxInitCallArgs(goRuntime)
if goRuntime && argv == 0 {
return nil, errors.New("failed to allocate Go runtime argv/environment vector")
}
// Once a Go constructor starts, its runtime may retain this TLS slot for
// process-lifetime threads even if a later initializer reports an error.
tlsSlotReserved = false
for _, initializer := range initializers {
cCallVoid3(initializer, argc, argv, envp)
}
module := &Module{
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
goRuntime: goRuntime,
}
cleanup = false
return module, nil
}
func prepareLinuxGoRuntimeTLS(f *elf.File) (bool, uintptr, int64, error) {
if f == nil || f.Section(".go.buildinfo") == nil {
return false, 0, 0, nil
}
var tls *elf.Prog
for _, prog := range f.Progs {
if prog.Type != elf.PT_TLS {
continue
}
if tls != nil {
return false, 0, 0, errors.New("Go ELF image has multiple PT_TLS segments")
}
tls = prog
}
if tls == nil || tls.Memsz == 0 {
return false, 0, 0, errors.New("Go ELF image is missing its runtime PT_TLS segment")
}
if tls.Filesz != 0 {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS has unsupported initialized data size %#x", tls.Filesz)
}
wordSize := uint64(8)
if f.Class == elf.ELFCLASS32 {
wordSize = 4
}
if tls.Memsz > 2*wordSize {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS size %#x exceeds the reserved size %#x", tls.Memsz, 2*wordSize)
}
linuxGoTLSSlots.Lock()
defer linuxGoTLSSlots.Unlock()
slot := uintptr(linuxGoTLSSlotCount)
for i := range linuxGoTLSSlots.used {
if !linuxGoTLSSlots.used[i] {
slot = uintptr(i)
break
}
}
if slot == linuxGoTLSSlotCount {
return false, 0, 0, fmt.Errorf("Go ELF TLS slot limit reached (%d)", linuxGoTLSSlotCount)
}
offset, err := linuxGoTLSSlotOffset(slot)
if err != nil {
return false, 0, 0, err
}
linuxGoTLSSlots.used[slot] = true
return true, slot, offset, nil
}
func releaseLinuxGoTLSSlot(slot uintptr) {
if slot >= linuxGoTLSSlotCount {
return
}
linuxGoTLSSlots.Lock()
linuxGoTLSSlots.used[slot] = false
linuxGoTLSSlots.Unlock()
}
func (module *Module) Free() {
module.mu.Lock()
defer module.mu.Unlock()
@@ -135,6 +231,15 @@ func (module *Module) Free() {
return
}
module.closed = true
if module.goRuntime {
// A Go c-shared runtime owns process-lifetime threads and cannot be
// unloaded safely. Close the Reflektor handle while leaving its mapping
// and reserved TLS slot pinned until process exit.
module.mapping = nil
module.symbols = nil
module.loadBias = 0
return
}
if len(module.mapping) != 0 {
_ = unix.Munmap(module.mapping)
@@ -171,7 +276,14 @@ func (module *Module) CallExport(name string) error {
return fmt.Errorf("resolve export %q: %w", name, err)
}
_ = cCall0(addr)
if module.goRuntime {
if err := cCallVoid0OnThread(addr); err != nil {
return fmt.Errorf("call Go export %q on isolated thread: %w", name, err)
}
return nil
}
cCallVoid0(addr)
return nil
}
@@ -437,17 +549,17 @@ func applyOneRelocation(machine elf.Machine, class elf.Class, mapped mappedELF,
switch machine {
case elf.EM_X86_64:
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_386:
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend)
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_AARCH64:
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
default:
return fmt.Errorf("unsupported machine for relocation: %s", machine)
}
}
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_X86_64(relocType) {
case elf.R_X86_64_NONE:
return nil
@@ -455,10 +567,10 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_X86_64_TPOFF64:
// Linux TLS local-exec relocation. The pure-Go loader does not provision
// module TLS blocks, so we apply S+A and rely on payload/runtime behavior
// that does not require a non-zero static TLS offset.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("x86_64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_X86_64_JMP_SLOT, elf.R_X86_64_GLOB_DAT, elf.R_X86_64_64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -489,7 +601,7 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
}
}
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_386(relocType) {
case elf.R_386_NONE:
return nil
@@ -497,8 +609,10 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
writeU32(place, uint32(int64(loadBias)+addend))
return nil
case elf.R_386_TLS_TPOFF:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU32(place, uint32(int64(symValue)+addend))
if !hasTLS {
return errors.New("386 static TLS relocation has no reserved host TLS slot")
}
writeU32(place, uint32(tlsOffset+addend))
return nil
case elf.R_386_JMP_SLOT, elf.R_386_GLOB_DAT:
writeU32(place, uint32(symValue))
@@ -518,7 +632,7 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
}
}
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_AARCH64(relocType) {
case elf.R_AARCH64_NONE:
return nil
@@ -526,8 +640,10 @@ func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symVal
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_AARCH64_TLS_TPREL64:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("arm64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_AARCH64_JUMP_SLOT, elf.R_AARCH64_GLOB_DAT, elf.R_AARCH64_ABS64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -611,24 +727,22 @@ func applySegmentProtections(mapped mappedELF) error {
return nil
}
func runELFInitializers(mapped mappedELF, f *elf.File) error {
info, err := parseDynamicInitInfo(f)
func collectELFInitializers(mapped mappedELF, class elf.Class, info dynamicInitInfo) ([]uintptr, error) {
initializers := make([]uintptr, 0)
var err error
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY")
if err != nil {
return err
return nil, err
}
argc, argv, envp := linuxInitCallArgs()
skip := collectInitSkipAddrs(f, mapped.loadBias)
if err := callDynamicInitArray(mapped, f.Class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitFn(initializers, mapped, uintptr(info.init), "DT_INIT")
if err != nil {
return nil, err
}
if err := callDynamicInitFn(mapped, uintptr(info.init), "DT_INIT", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.initArray, info.initArraySz, "DT_INIT_ARRAY")
if err != nil {
return nil, err
}
if err := callDynamicInitArray(mapped, f.Class, info.initArray, info.initArraySz, "DT_INIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
}
return nil
return initializers, nil
}
func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
@@ -703,24 +817,20 @@ func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
return info, nil
}
func callDynamicInitFn(mapped mappedELF, fn uintptr, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitFn(initializers []uintptr, mapped mappedELF, fn uintptr, source string) ([]uintptr, error) {
if fn == 0 {
return nil
return initializers, nil
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s points outside mapped image: %#x", source, fn)
return nil, fmt.Errorf("%s points outside mapped image: %#x", source, fn)
}
if _, blocked := skip[resolved]; blocked {
return nil
}
_ = cCall3(resolved, argc, argv, envp)
return nil
return append(initializers, resolved), nil
}
func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitArray(initializers []uintptr, mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string) ([]uintptr, error) {
if arrayVAddr == 0 || arraySz == 0 {
return nil
return initializers, nil
}
entrySize := 8
@@ -728,16 +838,16 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
entrySize = 4
}
if arraySz%uint64(entrySize) != 0 {
return fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
return nil, fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
}
arrayLen, err := u64ToInt(arraySz)
if err != nil {
return fmt.Errorf("%s size does not fit in int: %w", source, err)
return nil, fmt.Errorf("%s size does not fit in int: %w", source, err)
}
arrayAddr := mapped.loadBias + uintptr(arrayVAddr)
if !mappedAddressInRange(mapped.mapping, arrayAddr, arrayLen) {
return fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
return nil, fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
}
count := int(arraySz / uint64(entrySize))
@@ -754,57 +864,11 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
return nil, fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
}
if _, blocked := skip[resolved]; blocked {
continue
}
_ = cCall3(resolved, argc, argv, envp)
initializers = append(initializers, resolved)
}
return nil
}
func collectInitSkipAddrs(f *elf.File, loadBias uintptr) map[uintptr]struct{} {
if f == nil || f.Section(".go.buildinfo") == nil {
return nil
}
targets := map[string]struct{}{
"_rt0_386_linux_lib": {},
"_rt0_amd64_linux_lib": {},
"_rt0_arm64_linux_lib": {},
"_rt0_386_lib": {},
"_rt0_amd64_lib": {},
"_rt0_arm64_lib": {},
}
out := make(map[uintptr]struct{})
add := func(symbols []elf.Symbol) {
for _, sym := range symbols {
if sym.Value == 0 || sym.Section == elf.SHN_UNDEF {
continue
}
name := sym.Name
if at := strings.IndexByte(name, '@'); at > 0 {
name = name[:at]
}
if _, ok := targets[name]; !ok {
continue
}
out[loadBias+uintptr(sym.Value)] = struct{}{}
}
}
if dynSyms, err := f.DynamicSymbols(); err == nil {
add(dynSyms)
}
if syms, err := f.Symbols(); err == nil {
add(syms)
}
if len(out) == 0 {
return nil
}
return out
return initializers, nil
}
func normalizeInitFnAddress(mapped mappedELF, fn uintptr) (uintptr, bool) {
@@ -2,6 +2,8 @@
package memmod
import "errors"
//go:noescape
func cCall0(fn uintptr) uintptr
@@ -13,3 +15,21 @@ func cCall2(fn, a0, a1 uintptr) uintptr
//go:noescape
func cCall3(fn, a0, a1, a2 uintptr) uintptr
func cCallVoid0(fn uintptr) {
_ = cCall0(fn)
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
_ = cCall3(fn, a0, a1, a2)
}
func cCallVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
_ = slot
return 0, errors.New("loading a Go c-shared image requires cgo TLS support")
}
@@ -3,13 +3,17 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn0)(void);
typedef uintptr_t (*reflektor_fn1)(uintptr_t);
typedef uintptr_t (*reflektor_fn2)(uintptr_t, uintptr_t);
typedef uintptr_t (*reflektor_fn3)(uintptr_t, uintptr_t, uintptr_t);
typedef void (*reflektor_void_fn0)(void);
typedef void (*reflektor_init_fn)(int, char **, char **);
static uintptr_t reflektor_call0(uintptr_t fn) {
return ((reflektor_fn0)fn)();
@@ -27,31 +31,89 @@ static uintptr_t reflektor_call3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintp
return ((reflektor_fn3)fn)(a0, a1, a2);
}
static uintptr_t reflektor_init_argc = 0;
static uintptr_t reflektor_init_argv = 0;
static uintptr_t reflektor_init_envp = 0;
static void reflektor_call_void0(uintptr_t fn) {
((reflektor_void_fn0)fn)();
}
// Build a tiny synthetic argv/envp/auxv vector for runtimes that expect a
// startup-like layout (argc/argv/envp/auxv). The layout is:
// argv[0] = NULL
// envp[0] = NULL
// auxv[0] = AT_NULL
// auxv[1] = 0
static void reflektor_init_call_args(uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (reflektor_init_argv == 0) {
uintptr_t *vec = (uintptr_t *)calloc(4, sizeof(uintptr_t));
if (vec != NULL) {
reflektor_init_argv = (uintptr_t)vec;
reflektor_init_envp = (uintptr_t)(vec + 1);
static void reflektor_call_void3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintptr_t a2) {
((reflektor_init_fn)fn)((int)a0, (char **)a1, (char **)a2);
}
typedef struct {
uintptr_t fn;
} reflektor_thread_call;
static void *reflektor_thread_entry(void *opaque) {
reflektor_thread_call *call = (reflektor_thread_call *)opaque;
((reflektor_void_fn0)call->fn)();
return NULL;
}
static int reflektor_call_on_thread(reflektor_thread_call *call) {
pthread_t thread;
int err = pthread_create(&thread, NULL, reflektor_thread_entry, call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
static int reflektor_call_void0_thread(uintptr_t fn) {
reflektor_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
return reflektor_call_on_thread(&call);
}
// Go's ELF runtime stores thread state in up to two words of static TLS.
// Reserve independent slots so manually mapped Go runtimes do not alias the
// host Go runtime or each other. Static TLS is instantiated, zeroed, and kept
// at the same thread-pointer-relative offset for every pthread.
#define REFLEKTOR_GO_TLS_SLOTS 64
static __thread uintptr_t reflektor_go_tls_slots[REFLEKTOR_GO_TLS_SLOTS][2]
__attribute__((tls_model("initial-exec")));
static intptr_t reflektor_go_tls_offset(uintptr_t slot) {
if (slot >= REFLEKTOR_GO_TLS_SLOTS) {
return INTPTR_MIN;
}
return (intptr_t)(uintptr_t)&reflektor_go_tls_slots[slot][0] -
(intptr_t)(uintptr_t)__builtin_thread_pointer();
}
extern char **environ;
static uintptr_t reflektor_empty_init_args[4];
// Build a stable argc/argv/envp/auxv vector for runtimes that expect a
// startup-like layout. argc is zero, argv[0] is NULL, the current environment
// follows it, and a terminating AT_NULL pair follows the environment.
static void reflektor_init_call_args(int include_environment, uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (!include_environment) {
*argc = 0;
*argv = (uintptr_t)reflektor_empty_init_args;
*envp = (uintptr_t)(reflektor_empty_init_args + 1);
return;
}
size_t envc = 0;
while (environ != NULL && environ[envc] != NULL) {
envc++;
}
uintptr_t *vec = (uintptr_t *)calloc(envc + 4, sizeof(uintptr_t));
if (vec != NULL) {
for (size_t i = 0; i < envc; i++) {
vec[i + 1] = (uintptr_t)environ[i];
}
}
*argc = reflektor_init_argc;
*argv = reflektor_init_argv;
*envp = reflektor_init_envp;
*argc = 0;
*argv = (uintptr_t)vec;
*envp = vec == NULL ? 0 : (uintptr_t)(vec + 1);
}
*/
import "C"
import "fmt"
func cCall0(fn uintptr) uintptr {
return uintptr(C.reflektor_call0(C.uintptr_t(fn)))
}
@@ -68,10 +130,37 @@ func cCall3(fn, a0, a1, a2 uintptr) uintptr {
return uintptr(C.reflektor_call3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2)))
}
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func cCallVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
C.reflektor_call_void3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2))
}
func cCallVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
if slot >= 64 {
return 0, fmt.Errorf("Go TLS slot %d is out of range", slot)
}
offset := C.reflektor_go_tls_offset(C.uintptr_t(slot))
return int64(offset), nil
}
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
var argc C.uintptr_t
var argv C.uintptr_t
var envp C.uintptr_t
C.reflektor_init_call_args(&argc, &argv, &envp)
include := C.int(0)
if includeEnvironment {
include = 1
}
C.reflektor_init_call_args(include, &argc, &argv, &envp)
return uintptr(argc), uintptr(argv), uintptr(envp)
}
@@ -2,6 +2,7 @@
package memmod
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
_ = includeEnvironment
return 0, 0, 0
}
+59 -18
View File
@@ -6,6 +6,8 @@
package memmod
import (
"bytes"
"debug/buildinfo"
"errors"
"fmt"
"strings"
@@ -28,15 +30,23 @@ func (head *addressList) free() {
}
type Module struct {
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
exceptionTable uintptr
goRuntime bool
runtimeStarted bool
}
func imageHasGoBuildInfo(data []byte) bool {
_, err := buildinfo.Read(bytes.NewReader(data))
return err == nil
}
func (module *Module) headerDirectory(idx int) *IMAGE_DATA_DIRECTORY {
@@ -161,14 +171,22 @@ func (module *Module) finalizeSection(sectionData *sectionFinalizeData) error {
return nil
}
var rtlAddFunctionTable = windows.NewLazySystemDLL("ntdll.dll").NewProc("RtlAddFunctionTable")
func (module *Module) registerExceptionHandlers() {
func (module *Module) registerExceptionHandlers() error {
directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXCEPTION)
if directory.Size == 0 || directory.VirtualAddress == 0 {
return
entrySize := runtimeFunctionEntrySize()
if directory.Size == 0 || directory.VirtualAddress == 0 || entrySize == 0 {
return nil
}
rtlAddFunctionTable.Call(module.codeBase+uintptr(directory.VirtualAddress), uintptr(directory.Size)/unsafe.Sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY{}), module.codeBase)
if uintptr(directory.Size)%entrySize != 0 {
return fmt.Errorf("malformed exception table size %#x for entry size %d", directory.Size, entrySize)
}
module.exceptionTable = module.codeBase + uintptr(directory.VirtualAddress)
count := uint32(uintptr(directory.Size) / entrySize)
if !windows.RtlAddFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)), count, module.codeBase) {
module.exceptionTable = 0
return errors.New("RtlAddFunctionTable returned false")
}
return nil
}
func (module *Module) finalizeSections() error {
@@ -498,7 +516,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return nil, errors.New("Section is not page-aligned")
}
module = &Module{isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0}
module = &Module{
isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0,
goRuntime: imageHasGoBuildInfo(data),
}
defer func() {
if err != nil {
module.Free()
@@ -583,7 +604,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// Register exception tables, if they exist.
module.registerExceptionHandlers()
err = module.registerExceptionHandlers()
if err != nil {
return
}
// Register function PCs.
loadedAddressRangesMu.Lock()
@@ -598,6 +622,12 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// TLS callbacks are executed BEFORE the main loading.
if module.goRuntime {
// Go DLL initialization starts process-lifetime runtime state. From this
// point onward, an error or Close must leave the image and its imports
// pinned rather than detach and unmap live runtime-managed threads.
module.runtimeStarted = true
}
module.executeTLS()
// Get entry point of loaded module.
@@ -619,8 +649,15 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return
}
// Free releases module resources and unloads it.
// Free releases module resources. Started Go runtime images remain pinned.
func (module *Module) Free() {
if module.goRuntime && module.runtimeStarted {
if module.blockedMemory != nil {
module.blockedMemory.free()
module.blockedMemory = nil
}
return
}
if module.initialized {
// Notify library about detaching from process.
syscall.Syscall(module.entry, 3, module.codeBase, uintptr(DLL_PROCESS_DETACH), 0)
@@ -633,6 +670,10 @@ func (module *Module) Free() {
}
module.modules = nil
}
if module.exceptionTable != 0 {
windows.RtlDeleteFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)))
module.exceptionTable = 0
}
if module.codeBase != 0 {
windows.VirtualFree(module.codeBase, 0, windows.MEM_RELEASE)
module.codeBase = 0
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_I386
func runtimeFunctionEntrySize() uintptr { return 0 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
func runtimeFunctionEntrySize() uintptr { return 12 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
func runtimeFunctionEntrySize() uintptr { return 8 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
func runtimeFunctionEntrySize() uintptr { return 8 }
+1 -1
View File
@@ -69,7 +69,7 @@ github.com/miekg/dns
# github.com/pmezard/go-difflib v1.0.0
## explicit
github.com/pmezard/go-difflib/difflib
# github.com/sliverarmory/reflektor v0.0.2
# github.com/sliverarmory/reflektor v0.0.3
## explicit; go 1.26.6
github.com/sliverarmory/reflektor/memmod
# github.com/stretchr/testify v1.8.4
+14 -2
View File
@@ -66,6 +66,7 @@ var (
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
)
@@ -844,11 +845,14 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
return "", nil
}
err = fs.WalkDir(implant.FS, ".", func(fsPath string, f fs.DirEntry, err error) error {
err = fs.WalkDir(implant.FS, ".", func(fsPath string, f fs.DirEntry, walkErr error) (retErr error) {
if walkErr != nil {
return walkErr
}
if f.IsDir() {
return nil
}
buildLog.Debugf("Walking: %s %s %v", fsPath, f.Name(), err)
buildLog.Debugf("Walking: %s %s", fsPath, f.Name())
sliverGoCodeRaw, err := implant.FS.ReadFile(fsPath)
if err != nil {
@@ -882,6 +886,11 @@ func renderSliverGoCode(name string, build *clientpb.ImplantBuild, config *clien
if err != nil {
return err
}
defer func() {
if closeErr := fSliver.Close(); retErr == nil && closeErr != nil {
retErr = closeErr
}
}()
if !util.Contains([]string{".go", ".c", ".h"}, path.Ext(f.Name())) {
buildLog.Debugf("Skipping render for %s, does not appear to be source code file", f.Name())
_, err = fSliver.Write(sliverGoCodeRaw)
@@ -1376,6 +1385,9 @@ func GetCompilerTargets() []*clientpb.CompilerTarget {
platform := strings.SplitN(longPlatform, "/", 2)
switch platform[0] {
case WINDOWS:
if platform[1] != "amd64" && platform[1] != "386" {
continue
}
targets = append(targets, &clientpb.CompilerTarget{
GOOS: platform[0],
GOARCH: platform[1],
+46
View File
@@ -0,0 +1,46 @@
# syntax=docker/dockerfile:1.7
ARG GO_VERSION=1.26.6
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS server-build
WORKDIR /workspace
COPY . .
RUN --mount=type=cache,id=sliver-reflektor-server-go-build,target=/root/.cache/go-build,sharing=locked \
set -eux; \
mkdir -p /out; \
go run -buildvcs=false -mod=vendor ./util/cmd/assets; \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server \
-o /out/sliver-server ./server; \
rm -rf /workspace/server/assets/fs
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS driver-build
WORKDIR /workspace
COPY . .
RUN --mount=type=cache,id=sliver-reflektor-driver-go-build,target=/root/.cache/go-build,sharing=locked \
set -eux; \
mkdir -p /out; \
CGO_ENABLED=0 GOOS=linux GOARCH=386 \
go build -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite \
-o /out/sliver-reflektor-e2e ./test/reflektor
FROM --platform=$TARGETPLATFORM golang:${GO_VERSION}-bookworm AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
# Zig's LLD emits an invalid i386 TLS relocation for Go c-shared binaries.
# Use the native Debian i386 compiler for this same-architecture build instead.
ENV SLIVER_LINUX_CC_32=/usr/bin/i686-linux-gnu-gcc
WORKDIR /workspace
COPY go.mod go.sum ./
COPY --from=server-build /out/sliver-server /workspace/sliver-server
COPY --from=driver-build /out/sliver-reflektor-e2e /usr/local/bin/sliver-reflektor-e2e
CMD ["sliver-reflektor-e2e", "-repo", "/workspace", "-server", "/workspace/sliver-server", "-target-os", "linux", "-target-arch", "386"]
@@ -0,0 +1,31 @@
# Git metadata is not needed by the Reflektor integration image.
/.git/
# go-assets.sh downloads
/server/assets/fs/**.zip
/server/assets/fs/**/garble*
/server/assets/fs/**/zig.tar.xz
/server/assets/fs/**/zig.zip
/server/assets/fs/src.zip
/*.sdk.tar.xz
/sliver-server
/sliver-server.exe
/sliver-client
/sliver-client.exe
/.vscode/
/.vscode/*
/.github/
/.github/*
/build.py
/LICENSE
/SECURITY.md
/README.md
/CONTRIBUTING.md
.downloaded_assets
+739
View File
@@ -0,0 +1,739 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
clientassets "github.com/bishopfox/sliver/client/assets"
consts "github.com/bishopfox/sliver/client/constants"
clienttransport "github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"golang.org/x/mod/modfile"
)
const (
operatorName = "testuser"
reflektorModule = "github.com/sliverarmory/reflektor"
reflektorExport = "StartW"
processLogTailBytes = 1024 * 1024
eventBufferSize = 128
eventSyncTimeout = 30 * time.Second
listenerPollInterval = 250 * time.Millisecond
listenerDialTimeout = 500 * time.Millisecond
cleanupGraceTimeout = 2 * time.Second
cleanupProcessTimeout = 10 * time.Second
)
type options struct {
repoPath string
serverPath string
targetOS string
targetArch string
timeout time.Duration
startupTimeout time.Duration
sessionTimeout time.Duration
}
type managedProcess struct {
cmd *exec.Cmd
done chan struct{}
err error
logPath string
}
type eventPump struct {
events chan *clientpb.Event
err error
}
func main() {
opts := options{}
flag.StringVar(&opts.repoPath, "repo", ".", "path to the Sliver repository")
flag.StringVar(&opts.serverPath, "server", "", "path to the Sliver server executable")
flag.StringVar(&opts.targetOS, "target-os", runtime.GOOS, "shared library target operating system")
flag.StringVar(&opts.targetArch, "target-arch", runtime.GOARCH, "shared library target architecture")
flag.DurationVar(&opts.timeout, "timeout", 60*time.Minute, "overall integration test timeout")
flag.DurationVar(&opts.startupTimeout, "startup-timeout", 10*time.Minute, "Sliver daemon startup timeout")
flag.DurationVar(&opts.sessionTimeout, "session-timeout", 3*time.Minute, "session connection timeout after Reflektor starts")
flag.Parse()
if err := run(opts); err != nil {
fmt.Fprintf(os.Stderr, "reflektor integration test failed: %v\n", err)
os.Exit(1)
}
}
func run(opts options) error {
if err := validateOptions(&opts); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), opts.timeout)
defer cancel()
workDir, err := os.MkdirTemp("", "sliver-reflektor-e2e-")
if err != nil {
return fmt.Errorf("create test directory: %w", err)
}
defer os.RemoveAll(workDir)
serverRoot := filepath.Join(workDir, "server")
clientRoot := filepath.Join(workDir, "client")
homeDir := filepath.Join(workDir, "home")
for _, dir := range []string{serverRoot, clientRoot, homeDir} {
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create isolated test directory %q: %w", dir, err)
}
}
grpcPort, err := unusedTCPPort()
if err != nil {
return fmt.Errorf("select multiplayer port: %w", err)
}
serverEnv := os.Environ()
for name, value := range map[string]string{
"HOME": homeDir,
"SLIVER_CLIENT_ROOT_DIR": clientRoot,
"SLIVER_ROOT_DIR": serverRoot,
"USERPROFILE": homeDir,
} {
serverEnv = envWith(serverEnv, name, value)
}
serverLog := filepath.Join(workDir, "sliver-server.log")
serverProcess, err := startProcess(
opts.serverPath,
[]string{
"daemon",
"--lhost", "127.0.0.1",
"--lport", fmt.Sprintf("%d", grpcPort),
"--force",
},
opts.repoPath,
serverEnv,
serverLog,
)
if err != nil {
return fmt.Errorf("start Sliver daemon: %w", err)
}
defer serverProcess.stop()
fmt.Printf("Started Sliver daemon on 127.0.0.1:%d\n", grpcPort)
startupCtx, startupCancel := context.WithTimeout(ctx, opts.startupTimeout)
err = waitForTCP(startupCtx, fmt.Sprintf("127.0.0.1:%d", grpcPort), serverProcess)
startupCancel()
if err != nil {
return fmt.Errorf("wait for Sliver daemon: %w", err)
}
profilePath := filepath.Join(workDir, "testuser.cfg")
profileCtx, profileCancel := context.WithTimeout(ctx, 2*time.Minute)
profileOutput, profileErr := runCommand(
profileCtx,
opts.repoPath,
serverEnv,
opts.serverPath,
"operator",
"--name", operatorName,
"--lhost", "127.0.0.1",
"--lport", fmt.Sprintf("%d", grpcPort),
"--permissions", "all",
"--save", profilePath,
)
profileCancel()
if profileErr != nil {
return fmt.Errorf("generate operator profile: %w\n%s", profileErr, profileOutput)
}
if _, err := os.Stat(profilePath); err != nil {
return fmt.Errorf("operator command did not create %q: %w\n%s", profilePath, err, profileOutput)
}
clientConfig, err := clientassets.ReadConfig(profilePath)
if err != nil {
return fmt.Errorf("read operator profile: %w", err)
}
if clientConfig.Operator != operatorName || clientConfig.LHost != "127.0.0.1" || clientConfig.LPort != grpcPort {
return fmt.Errorf(
"operator profile mismatch: got operator=%q endpoint=%s:%d",
clientConfig.Operator,
clientConfig.LHost,
clientConfig.LPort,
)
}
fmt.Printf("Saved %s multiplayer profile to disk\n", operatorName)
rpc, conn, err := clienttransport.MTLSConnect(clientConfig)
if err != nil {
return fmt.Errorf("connect to multiplayer gRPC: %w", err)
}
defer clienttransport.CloseGRPCConnection(conn)
version, err := rpc.GetVersion(ctx, &commonpb.Empty{})
if err != nil {
return fmt.Errorf("query Sliver server version: %w", err)
}
fmt.Printf("Connected to Sliver server %s/%s over gRPC\n", version.OS, version.Arch)
eventsCtx, eventsCancel := context.WithCancel(ctx)
defer eventsCancel()
events, err := rpc.Events(eventsCtx, &commonpb.Empty{})
if err != nil {
return fmt.Errorf("subscribe to Sliver events: %w", err)
}
pump := startEventPump(events)
httpPort, err := unusedTCPPort()
if err != nil {
return fmt.Errorf("select HTTP port: %w", err)
}
listener, err := rpc.StartHTTPListener(ctx, &clientpb.HTTPListenerReq{
Domain: "localhost",
Host: "127.0.0.1",
Port: uint32(httpPort),
Secure: false,
EnforceOTP: false,
LongPollTimeout: int64(time.Second),
})
if err != nil {
return fmt.Errorf("start HTTP listener: %w", err)
}
defer func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cleanupCancel()
_, _ = rpc.KillJob(cleanupCtx, &clientpb.KillJobReq{ID: listener.JobID})
}()
listenerCtx, listenerCancel := context.WithTimeout(ctx, eventSyncTimeout)
err = waitForListenerReady(listenerCtx, pump, listener.JobID, fmt.Sprintf("127.0.0.1:%d", httpPort))
listenerCancel()
if err != nil {
return fmt.Errorf("wait for HTTP listener: %w", err)
}
fmt.Printf("Started HTTP listener on 127.0.0.1:%d (job %d)\n", httpPort, listener.JobID)
implantName := fmt.Sprintf("reflektor%s%s", opts.targetOS, opts.targetArch)
generated, err := rpc.Generate(ctx, &clientpb.GenerateReq{
Name: implantName,
Config: &clientpb.ImplantConfig{
GOOS: opts.targetOS,
GOARCH: opts.targetArch,
TemplateName: "sliver",
Debug: false,
ObfuscateSymbols: false,
IsBeacon: false,
Format: clientpb.OutputFormat_SHARED_LIB,
IsSharedLib: true,
RunAtLoad: false,
Exports: []string{reflektorExport},
C2: []*clientpb.ImplantC2{{URL: fmt.Sprintf("http://127.0.0.1:%d?force-http=true&poll-timeout=1s", httpPort)}},
IncludeHTTP: true,
HTTPC2ConfigName: consts.DefaultC2Profile,
ConnectionStrategy: "s",
ReconnectInterval: int64(time.Second),
PollTimeout: int64(time.Second),
MaxConnectionErrors: 10,
NetGoEnabled: true,
},
})
if err != nil {
return fmt.Errorf("generate %s/%s shared library: %w", opts.targetOS, opts.targetArch, err)
}
if generated.File == nil || generated.File.Name == "" || len(generated.File.Data) == 0 {
return errors.New("Sliver returned an empty shared library")
}
buildEventCtx, buildEventCancel := context.WithTimeout(ctx, eventSyncTimeout)
buildEvent, buildEventErr := waitForEvent(buildEventCtx, pump, func(event *clientpb.Event) bool {
return (event.EventType == consts.BuildCompletedEvent && string(event.Data) == generated.File.Name) ||
isListenerStopped(event, listener.JobID)
})
buildEventCancel()
if buildEventErr != nil {
return fmt.Errorf("wait for shared-library build event: %w", buildEventErr)
}
if err := listenerStoppedError(buildEvent, listener.JobID); err != nil {
return err
}
libraryPath := filepath.Join(workDir, filepath.Base(generated.File.Name))
if err := os.WriteFile(libraryPath, generated.File.Data, 0o600); err != nil {
return fmt.Errorf("save generated shared library: %w", err)
}
fmt.Printf("Generated %s/%s session shared library %s\n", opts.targetOS, opts.targetArch, filepath.Base(libraryPath))
reflektorPath, reflektorVersion, err := buildReflektorCLI(ctx, opts.repoPath, workDir, opts.targetOS, opts.targetArch)
if err != nil {
return err
}
fmt.Printf("Built Reflektor CLI %s from Sliver go.mod\n", reflektorVersion)
reflektorLog := filepath.Join(workDir, "reflektor.log")
reflektorProcess, err := startProcess(
reflektorPath,
[]string{"--call-export", reflektorExport, libraryPath},
opts.repoPath,
os.Environ(),
reflektorLog,
)
if err != nil {
return fmt.Errorf("start Reflektor CLI: %w", err)
}
defer reflektorProcess.stop()
sessionCtx, sessionCancel := context.WithTimeout(ctx, opts.sessionTimeout)
defer sessionCancel()
sessionEvent, err := waitForSession(
sessionCtx,
pump,
reflektorProcess,
listener.JobID,
generated.ImplantName,
opts.targetOS,
opts.targetArch,
)
if err != nil {
return fmt.Errorf("%w\nSliver server log:\n%s", err, readLogTail(serverLog))
}
fmt.Printf(
"Reflektor loaded Sliver and connected session %s (%s, %s/%s)\n",
sessionEvent.Session.ID,
sessionEvent.Session.Name,
sessionEvent.Session.OS,
sessionEvent.Session.Arch,
)
return nil
}
func validateOptions(opts *options) error {
if opts.serverPath == "" {
return errors.New("-server is required")
}
repoPath, err := filepath.Abs(opts.repoPath)
if err != nil {
return fmt.Errorf("resolve repository path: %w", err)
}
serverPath, err := filepath.Abs(opts.serverPath)
if err != nil {
return fmt.Errorf("resolve server path: %w", err)
}
if stat, err := os.Stat(serverPath); err != nil {
return fmt.Errorf("stat server executable: %w", err)
} else if stat.IsDir() {
return fmt.Errorf("server executable %q is a directory", serverPath)
}
if _, err := os.Stat(filepath.Join(repoPath, "go.mod")); err != nil {
return fmt.Errorf("repository does not contain go.mod: %w", err)
}
opts.repoPath = repoPath
opts.serverPath = serverPath
opts.targetOS = strings.ToLower(strings.TrimSpace(opts.targetOS))
opts.targetArch = strings.ToLower(strings.TrimSpace(opts.targetArch))
supported := map[string]bool{
"darwin/amd64": true,
"darwin/arm64": true,
"linux/386": true,
"linux/amd64": true,
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
target := opts.targetOS + "/" + opts.targetArch
if !supported[target] {
return fmt.Errorf("target %s is not in Reflektor's supported test matrix", target)
}
if opts.targetOS != runtime.GOOS || opts.targetArch != runtime.GOARCH {
return fmt.Errorf(
"Reflektor must run as the target architecture: driver is %s/%s, target is %s",
runtime.GOOS,
runtime.GOARCH,
target,
)
}
if opts.timeout <= 0 || opts.startupTimeout <= 0 || opts.sessionTimeout <= 0 {
return errors.New("timeouts must be positive")
}
return nil
}
func unusedTCPPort() (int, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, err
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port, nil
}
func waitForTCP(ctx context.Context, address string, process *managedProcess) error {
ticker := time.NewTicker(listenerPollInterval)
defer ticker.Stop()
for {
conn, err := net.DialTimeout("tcp", address, listenerDialTimeout)
if err == nil {
_ = conn.Close()
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-process.done:
return process.failure("Sliver daemon exited before accepting connections")
case <-ticker.C:
}
}
}
func startProcess(path string, args []string, dir string, env []string, logPath string) (*managedProcess, error) {
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return nil, err
}
cmd := exec.Command(path, args...)
cmd.Dir = dir
cmd.Env = env
cmd.Stdout = logFile
cmd.Stderr = logFile
prepareCommand(cmd)
if err := cmd.Start(); err != nil {
_ = logFile.Close()
return nil, err
}
process := &managedProcess{
cmd: cmd,
done: make(chan struct{}),
logPath: logPath,
}
go func() {
process.err = cmd.Wait()
_ = logFile.Close()
close(process.done)
}()
return process, nil
}
func (process *managedProcess) stop() {
select {
case <-process.done:
return
default:
}
terminateProcessTree(process.cmd)
select {
case <-process.done:
return
case <-time.After(cleanupGraceTimeout):
}
killProcessTree(process.cmd)
select {
case <-process.done:
case <-time.After(cleanupProcessTimeout):
fmt.Fprintf(os.Stderr, "timed out cleaning up process tree for %s (pid %d)\n", process.cmd.Path, process.cmd.Process.Pid)
}
}
func (process *managedProcess) failure(message string) error {
processErr := process.err
if processErr == nil {
processErr = errors.New("process exited")
}
return fmt.Errorf("%s: %w\n%s", message, processErr, readLogTail(process.logPath))
}
func readLogTail(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Sprintf("(could not read %s: %v)", path, err)
}
if len(data) > processLogTailBytes {
data = data[len(data)-processLogTailBytes:]
}
return strings.TrimSpace(string(data))
}
func runCommand(ctx context.Context, dir string, env []string, path string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, path, args...)
cmd.Dir = dir
cmd.Env = env
output, err := cmd.CombinedOutput()
return strings.TrimSpace(string(output)), err
}
func envWith(env []string, name string, value string) []string {
prefix := name + "="
updated := make([]string, 0, len(env)+1)
for _, entry := range env {
if !strings.HasPrefix(entry, prefix) {
updated = append(updated, entry)
}
}
return append(updated, prefix+value)
}
func startEventPump(stream rpcpb.SliverRPC_EventsClient) *eventPump {
pump := &eventPump{
events: make(chan *clientpb.Event, eventBufferSize),
}
go func() {
defer close(pump.events)
for {
event, err := stream.Recv()
if err != nil {
pump.err = err
return
}
select {
case pump.events <- event:
case <-stream.Context().Done():
pump.err = stream.Context().Err()
return
}
}
}()
return pump
}
func waitForListenerReady(ctx context.Context, pump *eventPump, jobID uint32, address string) error {
ticker := time.NewTicker(listenerPollInterval)
defer ticker.Stop()
started := false
for {
if started {
conn, err := net.DialTimeout("tcp", address, listenerDialTimeout)
if err == nil {
_ = conn.Close()
return nil
}
}
select {
case <-ctx.Done():
return ctx.Err()
case event, ok := <-pump.events:
if !ok {
return eventStreamError(pump, "event stream ended while starting HTTP listener")
}
if event.Job == nil || event.Job.ID != jobID {
continue
}
switch event.EventType {
case consts.JobStartedEvent:
started = true
case consts.JobStoppedEvent:
if event.Err != "" {
return fmt.Errorf("HTTP listener job %d stopped: %s", jobID, event.Err)
}
return fmt.Errorf("HTTP listener job %d stopped before accepting connections", jobID)
}
case <-ticker.C:
}
}
}
func waitForEvent(ctx context.Context, pump *eventPump, match func(*clientpb.Event) bool) (*clientpb.Event, error) {
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case event, ok := <-pump.events:
if !ok {
return nil, eventStreamError(pump, "event stream ended")
}
if match(event) {
return event, nil
}
}
}
}
func waitForSession(
ctx context.Context,
pump *eventPump,
reflektor *managedProcess,
listenerJobID uint32,
implantName string,
targetOS string,
targetArch string,
) (*clientpb.Event, error) {
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf(
"wait for Reflektor-loaded session: %w\nReflektor log:\n%s",
ctx.Err(),
readLogTail(reflektor.logPath),
)
case <-reflektor.done:
for {
select {
case event, ok := <-pump.events:
if !ok {
return nil, eventStreamError(pump, "event stream ended while waiting for session")
}
if err := listenerStoppedError(event, listenerJobID); err != nil {
return nil, err
}
if matched, err := matchingSession(event, implantName, targetOS, targetArch); matched || err != nil {
return event, err
}
default:
return nil, reflektor.failure("Reflektor exited before Sliver connected")
}
}
case event, ok := <-pump.events:
if !ok {
return nil, eventStreamError(pump, "event stream ended while waiting for session")
}
if err := listenerStoppedError(event, listenerJobID); err != nil {
return nil, err
}
if matched, err := matchingSession(event, implantName, targetOS, targetArch); matched || err != nil {
return event, err
}
}
}
}
func isListenerStopped(event *clientpb.Event, jobID uint32) bool {
return event.EventType == consts.JobStoppedEvent && event.Job != nil && event.Job.ID == jobID
}
func listenerStoppedError(event *clientpb.Event, jobID uint32) error {
if !isListenerStopped(event, jobID) {
return nil
}
if event.Err != "" {
return fmt.Errorf("HTTP listener job %d stopped: %s", jobID, event.Err)
}
return fmt.Errorf("HTTP listener job %d stopped before the session connected", jobID)
}
func matchingSession(event *clientpb.Event, implantName string, targetOS string, targetArch string) (bool, error) {
if event.EventType != consts.SessionOpenedEvent || event.Session == nil {
return false, nil
}
if event.Session.Name != implantName {
return false, nil
}
if event.Session.OS != targetOS || event.Session.Arch != targetArch {
return true, fmt.Errorf(
"session target mismatch: got %s/%s, want %s/%s",
event.Session.OS,
event.Session.Arch,
targetOS,
targetArch,
)
}
return true, nil
}
func eventStreamError(pump *eventPump, message string) error {
if pump.err == nil {
return errors.New(message)
}
return fmt.Errorf("%s: %w", message, pump.err)
}
func buildReflektorCLI(
ctx context.Context,
repoPath string,
workDir string,
targetOS string,
targetArch string,
) (string, string, error) {
goMod, err := os.ReadFile(filepath.Join(repoPath, "go.mod"))
if err != nil {
return "", "", fmt.Errorf("read go.mod: %w", err)
}
parsed, err := modfile.Parse("go.mod", goMod, nil)
if err != nil {
return "", "", fmt.Errorf("parse go.mod: %w", err)
}
version := ""
for _, requirement := range parsed.Require {
if requirement.Mod.Path == reflektorModule {
version = requirement.Mod.Version
break
}
}
if version == "" {
return "", "", fmt.Errorf("%s is not required by go.mod", reflektorModule)
}
binDir := filepath.Join(workDir, "reflektor-bin")
if err := os.MkdirAll(binDir, 0o700); err != nil {
return "", "", fmt.Errorf("create Reflektor output directory: %w", err)
}
name := "reflektor"
if targetOS == "windows" {
name += ".exe"
}
path := filepath.Join(binDir, name)
cgoEnabled := "0"
if targetOS == "darwin" || targetOS == "linux" {
cgoEnabled = "1"
}
buildEnv := os.Environ()
for name, value := range map[string]string{
"CGO_ENABLED": cgoEnabled,
"GOARCH": targetArch,
"GOENV": "off",
"GOFLAGS": "",
"GOOS": targetOS,
"GOWORK": "off",
} {
buildEnv = envWith(buildEnv, name, value)
}
listCommand := exec.CommandContext(ctx, "go", "list", "-mod=readonly", "-m", "-f={{.Version}}", reflektorModule)
listCommand.Dir = repoPath
listCommand.Env = buildEnv
output, err := listCommand.CombinedOutput()
if err != nil {
return "", "", fmt.Errorf("resolve Reflektor CLI version: %w\n%s", err, output)
}
if selected := strings.TrimSpace(string(output)); selected != version {
return "", "", fmt.Errorf("Reflektor version mismatch: go.mod requires %s, module graph selected %s", version, selected)
}
buildCommand := exec.CommandContext(
ctx,
"go",
"build",
"-mod=readonly",
"-trimpath",
"-o", path,
reflektorModule+"/cli",
)
buildCommand.Dir = repoPath
buildCommand.Env = buildEnv
output, err = buildCommand.CombinedOutput()
if err != nil {
return "", "", fmt.Errorf("build Reflektor CLI %s for %s/%s: %w\n%s", version, targetOS, targetArch, err, output)
}
return path, version, nil
}
+20
View File
@@ -0,0 +1,20 @@
//go:build darwin || linux
package main
import (
"os/exec"
"syscall"
)
func prepareCommand(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
func terminateProcessTree(cmd *exec.Cmd) {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
}
func killProcessTree(cmd *exec.Cmd) {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
+18
View File
@@ -0,0 +1,18 @@
//go:build windows
package main
import (
"os/exec"
"strconv"
)
func prepareCommand(_ *exec.Cmd) {}
func terminateProcessTree(cmd *exec.Cmd) {
_ = exec.Command("taskkill.exe", "/PID", strconv.Itoa(cmd.Process.Pid), "/T").Run()
}
func killProcessTree(cmd *exec.Cmd) {
_ = exec.Command("taskkill.exe", "/PID", strconv.Itoa(cmd.Process.Pid), "/T", "/F").Run()
}
+33 -7
View File
@@ -36,9 +36,10 @@ var (
)
type Module struct {
mu sync.RWMutex
image []byte
closed bool
mu sync.RWMutex
image []byte
goRuntime bool
closed bool
}
// LoadLibrary loads a Mach-O image into the darwin in-memory loader context.
@@ -51,10 +52,23 @@ func LoadLibrary(data []byte) (*Module, error) {
if err != nil {
return nil, err
}
goRuntime, err := machOHasGoBuildInfo(image)
if err != nil {
return nil, err
}
cloned := make([]byte, len(image))
copy(cloned, image)
return &Module{image: cloned}, nil
return &Module{image: cloned, goRuntime: goRuntime}, nil
}
func machOHasGoBuildInfo(image []byte) (bool, error) {
f, err := macho.NewFile(bytes.NewReader(image))
if err != nil {
return false, fmt.Errorf("inspect Mach-O build info: %w", err)
}
defer f.Close()
return f.Section("__go_buildinfo") != nil, nil
}
// Free releases the in-memory Mach-O bytes.
@@ -94,7 +108,7 @@ func (module *Module) CallExport(name string) error {
image := module.image
module.mu.RUnlock()
rc := memmodLoader(image, symbol)
rc := memmodLoader(image, symbol, module.goRuntime)
runtime.KeepAlive(image)
if rc != 0 {
@@ -395,7 +409,7 @@ func isDarwinSystemInstallName(installName string) bool {
strings.HasPrefix(installName, "/System/Library/")
}
func memmodLoader(bufferRO []byte, entrySymbol string) int {
func memmodLoader(bufferRO []byte, entrySymbol string, isolateGoRuntime bool) int {
if len(bufferRO) == 0 || entrySymbol == "" {
return 1
}
@@ -789,7 +803,14 @@ func memmodLoader(bufferRO []byte, entrySymbol string) int {
// Exported fixture entry points use the C void(void) ABI. Keep that call
// signature exact when cgo supplies the foreign-function bridge.
callVoid0(addrEntry)
if isolateGoRuntime {
if err := callVoid0OnThread(addrEntry); err != nil {
setDarwinLoaderDetail(err.Error())
return 14
}
} else {
callVoid0(addrEntry)
}
// Keep mapped and scratch memory reachable until after entry returns.
runtime.KeepAlive(mapped.mapping)
runtime.KeepAlive(scratch)
@@ -1535,6 +1556,11 @@ func loaderStatusError(code int) error {
return fmt.Errorf("failed to preload Mach-O dependencies: %s", detail)
}
return errors.New("failed to preload Mach-O dependencies")
case 14:
if detail := getDarwinLoaderDetail(); detail != "" {
return fmt.Errorf("failed to call Go export on isolated thread: %s", detail)
}
return errors.New("failed to call Go export on isolated thread")
default:
return fmt.Errorf("in-memory dyld loader failed with status %d", code)
}
+9 -1
View File
@@ -2,7 +2,10 @@
package memmod
import _ "unsafe"
import (
"errors"
_ "unsafe"
)
//go:noescape
func cCall10(fn, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) uintptr
@@ -18,6 +21,11 @@ func callVoid0(fn uintptr) {
_ = call0(fn)
}
func callVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return call2(fn, name, uintptr(flags))
}
@@ -3,7 +3,9 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn10_t)(
uintptr_t, uintptr_t, uintptr_t, uintptr_t, uintptr_t,
@@ -33,13 +35,44 @@ static uintptr_t reflektor_call10(
) {
return ((reflektor_fn10_t)fn)(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9);
}
typedef struct {
uintptr_t fn;
} reflektor_void_thread_call;
static void *reflektor_void_thread_entry(void *opaque) {
reflektor_void_thread_call *call = (reflektor_void_thread_call *)opaque;
((reflektor_void_fn0_t)call->fn)();
return NULL;
}
static int reflektor_call_void0_thread(uintptr_t fn) {
pthread_t thread;
reflektor_void_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
int err = pthread_create(&thread, NULL, reflektor_void_thread_entry, &call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
*/
import "C"
import "fmt"
func callVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func callVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func callDlopen(fn, name uintptr, flags int) uintptr {
return uintptr(C.reflektor_call_dlopen(C.uintptr_t(fn), C.uintptr_t(name), C.int(flags)))
}
+167 -103
View File
@@ -45,17 +45,20 @@ const (
)
type Module struct {
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
closed bool
mu sync.RWMutex
mapping []byte
loadBias uintptr
symbols map[string]uintptr
goRuntime bool
closed bool
}
type mappedELF struct {
mapping []byte
loadBias uintptr
progs []*elf.Prog
mapping []byte
loadBias uintptr
progs []*elf.Prog
tlsOffset int64
hasTLS bool
}
type dynamicInitInfo struct {
@@ -80,6 +83,13 @@ type symbolResolver struct {
opened map[string]uintptr
}
var linuxGoTLSSlots = struct {
sync.Mutex
used [linuxGoTLSSlotCount]bool
}{}
const linuxGoTLSSlotCount = 64
func LoadLibrary(data []byte) (*Module, error) {
if len(data) == 0 {
return nil, errors.New("empty ELF image")
@@ -94,11 +104,27 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := validateELFHeaders(f); err != nil {
return nil, err
}
initInfo, err := parseDynamicInitInfo(f)
if err != nil {
return nil, err
}
goRuntime, tlsSlot, tlsOffset, err := prepareLinuxGoRuntimeTLS(f)
if err != nil {
return nil, err
}
tlsSlotReserved := goRuntime
defer func() {
if tlsSlotReserved {
releaseLinuxGoTLSSlot(tlsSlot)
}
}()
mapped, err := mapELFImage(data, f)
if err != nil {
return nil, err
}
mapped.tlsOffset = tlsOffset
mapped.hasTLS = goRuntime
cleanup := true
defer func() {
if cleanup && len(mapped.mapping) != 0 {
@@ -114,19 +140,89 @@ func LoadLibrary(data []byte) (*Module, error) {
if err := applySegmentProtections(mapped); err != nil {
return nil, err
}
if err := runELFInitializers(mapped, f); err != nil {
initializers, err := collectELFInitializers(mapped, f.Class, initInfo)
if err != nil {
return nil, err
}
argc, argv, envp := linuxInitCallArgs(goRuntime)
if goRuntime && argv == 0 {
return nil, errors.New("failed to allocate Go runtime argv/environment vector")
}
// Once a Go constructor starts, its runtime may retain this TLS slot for
// process-lifetime threads even if a later initializer reports an error.
tlsSlotReserved = false
for _, initializer := range initializers {
cCallVoid3(initializer, argc, argv, envp)
}
module := &Module{
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
mapping: mapped.mapping,
loadBias: mapped.loadBias,
symbols: buildExportedSymbolTable(f, mapped.loadBias),
goRuntime: goRuntime,
}
cleanup = false
return module, nil
}
func prepareLinuxGoRuntimeTLS(f *elf.File) (bool, uintptr, int64, error) {
if f == nil || f.Section(".go.buildinfo") == nil {
return false, 0, 0, nil
}
var tls *elf.Prog
for _, prog := range f.Progs {
if prog.Type != elf.PT_TLS {
continue
}
if tls != nil {
return false, 0, 0, errors.New("Go ELF image has multiple PT_TLS segments")
}
tls = prog
}
if tls == nil || tls.Memsz == 0 {
return false, 0, 0, errors.New("Go ELF image is missing its runtime PT_TLS segment")
}
if tls.Filesz != 0 {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS has unsupported initialized data size %#x", tls.Filesz)
}
wordSize := uint64(8)
if f.Class == elf.ELFCLASS32 {
wordSize = 4
}
if tls.Memsz > 2*wordSize {
return false, 0, 0, fmt.Errorf("Go ELF PT_TLS size %#x exceeds the reserved size %#x", tls.Memsz, 2*wordSize)
}
linuxGoTLSSlots.Lock()
defer linuxGoTLSSlots.Unlock()
slot := uintptr(linuxGoTLSSlotCount)
for i := range linuxGoTLSSlots.used {
if !linuxGoTLSSlots.used[i] {
slot = uintptr(i)
break
}
}
if slot == linuxGoTLSSlotCount {
return false, 0, 0, fmt.Errorf("Go ELF TLS slot limit reached (%d)", linuxGoTLSSlotCount)
}
offset, err := linuxGoTLSSlotOffset(slot)
if err != nil {
return false, 0, 0, err
}
linuxGoTLSSlots.used[slot] = true
return true, slot, offset, nil
}
func releaseLinuxGoTLSSlot(slot uintptr) {
if slot >= linuxGoTLSSlotCount {
return
}
linuxGoTLSSlots.Lock()
linuxGoTLSSlots.used[slot] = false
linuxGoTLSSlots.Unlock()
}
func (module *Module) Free() {
module.mu.Lock()
defer module.mu.Unlock()
@@ -135,6 +231,15 @@ func (module *Module) Free() {
return
}
module.closed = true
if module.goRuntime {
// A Go c-shared runtime owns process-lifetime threads and cannot be
// unloaded safely. Close the Reflektor handle while leaving its mapping
// and reserved TLS slot pinned until process exit.
module.mapping = nil
module.symbols = nil
module.loadBias = 0
return
}
if len(module.mapping) != 0 {
_ = unix.Munmap(module.mapping)
@@ -171,7 +276,14 @@ func (module *Module) CallExport(name string) error {
return fmt.Errorf("resolve export %q: %w", name, err)
}
_ = cCall0(addr)
if module.goRuntime {
if err := cCallVoid0OnThread(addr); err != nil {
return fmt.Errorf("call Go export %q on isolated thread: %w", name, err)
}
return nil
}
cCallVoid0(addr)
return nil
}
@@ -437,17 +549,17 @@ func applyOneRelocation(machine elf.Machine, class elf.Class, mapped mappedELF,
switch machine {
case elf.EM_X86_64:
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyX8664Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_386:
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend)
return apply386Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
case elf.EM_AARCH64:
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend)
return applyAArch64Reloc(relocType, place, mapped.loadBias, symValue, addend, mapped.tlsOffset, mapped.hasTLS)
default:
return fmt.Errorf("unsupported machine for relocation: %s", machine)
}
}
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_X86_64(relocType) {
case elf.R_X86_64_NONE:
return nil
@@ -455,10 +567,10 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_X86_64_TPOFF64:
// Linux TLS local-exec relocation. The pure-Go loader does not provision
// module TLS blocks, so we apply S+A and rely on payload/runtime behavior
// that does not require a non-zero static TLS offset.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("x86_64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_X86_64_JMP_SLOT, elf.R_X86_64_GLOB_DAT, elf.R_X86_64_64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -489,7 +601,7 @@ func applyX8664Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue
}
}
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_386(relocType) {
case elf.R_386_NONE:
return nil
@@ -497,8 +609,10 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
writeU32(place, uint32(int64(loadBias)+addend))
return nil
case elf.R_386_TLS_TPOFF:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU32(place, uint32(int64(symValue)+addend))
if !hasTLS {
return errors.New("386 static TLS relocation has no reserved host TLS slot")
}
writeU32(place, uint32(tlsOffset+addend))
return nil
case elf.R_386_JMP_SLOT, elf.R_386_GLOB_DAT:
writeU32(place, uint32(symValue))
@@ -518,7 +632,7 @@ func apply386Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue u
}
}
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64) error {
func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symValue uintptr, addend int64, tlsOffset int64, hasTLS bool) error {
switch elf.R_AARCH64(relocType) {
case elf.R_AARCH64_NONE:
return nil
@@ -526,8 +640,10 @@ func applyAArch64Reloc(relocType uint32, place uintptr, loadBias uintptr, symVal
writeU64(place, uint64(int64(loadBias)+addend))
return nil
case elf.R_AARCH64_TLS_TPREL64:
// Linux TLS local-exec relocation; see R_X86_64_TPOFF64 note above.
writeU64(place, uint64(int64(symValue)+addend))
if !hasTLS {
return errors.New("arm64 static TLS relocation has no reserved host TLS slot")
}
writeU64(place, uint64(tlsOffset+addend))
return nil
case elf.R_AARCH64_JUMP_SLOT, elf.R_AARCH64_GLOB_DAT, elf.R_AARCH64_ABS64:
writeU64(place, uint64(int64(symValue)+addend))
@@ -611,24 +727,22 @@ func applySegmentProtections(mapped mappedELF) error {
return nil
}
func runELFInitializers(mapped mappedELF, f *elf.File) error {
info, err := parseDynamicInitInfo(f)
func collectELFInitializers(mapped mappedELF, class elf.Class, info dynamicInitInfo) ([]uintptr, error) {
initializers := make([]uintptr, 0)
var err error
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY")
if err != nil {
return err
return nil, err
}
argc, argv, envp := linuxInitCallArgs()
skip := collectInitSkipAddrs(f, mapped.loadBias)
if err := callDynamicInitArray(mapped, f.Class, info.preinitArr, info.preinitSz, "DT_PREINIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitFn(initializers, mapped, uintptr(info.init), "DT_INIT")
if err != nil {
return nil, err
}
if err := callDynamicInitFn(mapped, uintptr(info.init), "DT_INIT", argc, argv, envp, skip); err != nil {
return err
initializers, err = appendDynamicInitArray(initializers, mapped, class, info.initArray, info.initArraySz, "DT_INIT_ARRAY")
if err != nil {
return nil, err
}
if err := callDynamicInitArray(mapped, f.Class, info.initArray, info.initArraySz, "DT_INIT_ARRAY", argc, argv, envp, skip); err != nil {
return err
}
return nil
return initializers, nil
}
func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
@@ -703,24 +817,20 @@ func parseDynamicInitInfo(f *elf.File) (dynamicInitInfo, error) {
return info, nil
}
func callDynamicInitFn(mapped mappedELF, fn uintptr, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitFn(initializers []uintptr, mapped mappedELF, fn uintptr, source string) ([]uintptr, error) {
if fn == 0 {
return nil
return initializers, nil
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s points outside mapped image: %#x", source, fn)
return nil, fmt.Errorf("%s points outside mapped image: %#x", source, fn)
}
if _, blocked := skip[resolved]; blocked {
return nil
}
_ = cCall3(resolved, argc, argv, envp)
return nil
return append(initializers, resolved), nil
}
func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string, argc uintptr, argv uintptr, envp uintptr, skip map[uintptr]struct{}) error {
func appendDynamicInitArray(initializers []uintptr, mapped mappedELF, class elf.Class, arrayVAddr uint64, arraySz uint64, source string) ([]uintptr, error) {
if arrayVAddr == 0 || arraySz == 0 {
return nil
return initializers, nil
}
entrySize := 8
@@ -728,16 +838,16 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
entrySize = 4
}
if arraySz%uint64(entrySize) != 0 {
return fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
return nil, fmt.Errorf("%s has malformed size %#x for entry size %d", source, arraySz, entrySize)
}
arrayLen, err := u64ToInt(arraySz)
if err != nil {
return fmt.Errorf("%s size does not fit in int: %w", source, err)
return nil, fmt.Errorf("%s size does not fit in int: %w", source, err)
}
arrayAddr := mapped.loadBias + uintptr(arrayVAddr)
if !mappedAddressInRange(mapped.mapping, arrayAddr, arrayLen) {
return fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
return nil, fmt.Errorf("%s range %#x..%#x is outside mapped image", source, arrayVAddr, arrayVAddr+arraySz)
}
count := int(arraySz / uint64(entrySize))
@@ -754,57 +864,11 @@ func callDynamicInitArray(mapped mappedELF, class elf.Class, arrayVAddr uint64,
}
resolved, ok := normalizeInitFnAddress(mapped, fn)
if !ok {
return fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
return nil, fmt.Errorf("%s[%d] points outside mapped image: %#x", source, i, fn)
}
if _, blocked := skip[resolved]; blocked {
continue
}
_ = cCall3(resolved, argc, argv, envp)
initializers = append(initializers, resolved)
}
return nil
}
func collectInitSkipAddrs(f *elf.File, loadBias uintptr) map[uintptr]struct{} {
if f == nil || f.Section(".go.buildinfo") == nil {
return nil
}
targets := map[string]struct{}{
"_rt0_386_linux_lib": {},
"_rt0_amd64_linux_lib": {},
"_rt0_arm64_linux_lib": {},
"_rt0_386_lib": {},
"_rt0_amd64_lib": {},
"_rt0_arm64_lib": {},
}
out := make(map[uintptr]struct{})
add := func(symbols []elf.Symbol) {
for _, sym := range symbols {
if sym.Value == 0 || sym.Section == elf.SHN_UNDEF {
continue
}
name := sym.Name
if at := strings.IndexByte(name, '@'); at > 0 {
name = name[:at]
}
if _, ok := targets[name]; !ok {
continue
}
out[loadBias+uintptr(sym.Value)] = struct{}{}
}
}
if dynSyms, err := f.DynamicSymbols(); err == nil {
add(dynSyms)
}
if syms, err := f.Symbols(); err == nil {
add(syms)
}
if len(out) == 0 {
return nil
}
return out
return initializers, nil
}
func normalizeInitFnAddress(mapped mappedELF, fn uintptr) (uintptr, bool) {
+20
View File
@@ -2,6 +2,8 @@
package memmod
import "errors"
//go:noescape
func cCall0(fn uintptr) uintptr
@@ -13,3 +15,21 @@ func cCall2(fn, a0, a1 uintptr) uintptr
//go:noescape
func cCall3(fn, a0, a1, a2 uintptr) uintptr
func cCallVoid0(fn uintptr) {
_ = cCall0(fn)
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
_ = cCall3(fn, a0, a1, a2)
}
func cCallVoid0OnThread(fn uintptr) error {
_ = fn
return errors.New("calling a Go c-shared export requires cgo")
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
_ = slot
return 0, errors.New("loading a Go c-shared image requires cgo TLS support")
}
+109 -20
View File
@@ -3,13 +3,17 @@
package memmod
/*
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef uintptr_t (*reflektor_fn0)(void);
typedef uintptr_t (*reflektor_fn1)(uintptr_t);
typedef uintptr_t (*reflektor_fn2)(uintptr_t, uintptr_t);
typedef uintptr_t (*reflektor_fn3)(uintptr_t, uintptr_t, uintptr_t);
typedef void (*reflektor_void_fn0)(void);
typedef void (*reflektor_init_fn)(int, char **, char **);
static uintptr_t reflektor_call0(uintptr_t fn) {
return ((reflektor_fn0)fn)();
@@ -27,31 +31,89 @@ static uintptr_t reflektor_call3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintp
return ((reflektor_fn3)fn)(a0, a1, a2);
}
static uintptr_t reflektor_init_argc = 0;
static uintptr_t reflektor_init_argv = 0;
static uintptr_t reflektor_init_envp = 0;
static void reflektor_call_void0(uintptr_t fn) {
((reflektor_void_fn0)fn)();
}
// Build a tiny synthetic argv/envp/auxv vector for runtimes that expect a
// startup-like layout (argc/argv/envp/auxv). The layout is:
// argv[0] = NULL
// envp[0] = NULL
// auxv[0] = AT_NULL
// auxv[1] = 0
static void reflektor_init_call_args(uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (reflektor_init_argv == 0) {
uintptr_t *vec = (uintptr_t *)calloc(4, sizeof(uintptr_t));
if (vec != NULL) {
reflektor_init_argv = (uintptr_t)vec;
reflektor_init_envp = (uintptr_t)(vec + 1);
static void reflektor_call_void3(uintptr_t fn, uintptr_t a0, uintptr_t a1, uintptr_t a2) {
((reflektor_init_fn)fn)((int)a0, (char **)a1, (char **)a2);
}
typedef struct {
uintptr_t fn;
} reflektor_thread_call;
static void *reflektor_thread_entry(void *opaque) {
reflektor_thread_call *call = (reflektor_thread_call *)opaque;
((reflektor_void_fn0)call->fn)();
return NULL;
}
static int reflektor_call_on_thread(reflektor_thread_call *call) {
pthread_t thread;
int err = pthread_create(&thread, NULL, reflektor_thread_entry, call);
if (err != 0) {
return err;
}
return pthread_join(thread, NULL);
}
static int reflektor_call_void0_thread(uintptr_t fn) {
reflektor_thread_call call;
memset(&call, 0, sizeof(call));
call.fn = fn;
return reflektor_call_on_thread(&call);
}
// Go's ELF runtime stores thread state in up to two words of static TLS.
// Reserve independent slots so manually mapped Go runtimes do not alias the
// host Go runtime or each other. Static TLS is instantiated, zeroed, and kept
// at the same thread-pointer-relative offset for every pthread.
#define REFLEKTOR_GO_TLS_SLOTS 64
static __thread uintptr_t reflektor_go_tls_slots[REFLEKTOR_GO_TLS_SLOTS][2]
__attribute__((tls_model("initial-exec")));
static intptr_t reflektor_go_tls_offset(uintptr_t slot) {
if (slot >= REFLEKTOR_GO_TLS_SLOTS) {
return INTPTR_MIN;
}
return (intptr_t)(uintptr_t)&reflektor_go_tls_slots[slot][0] -
(intptr_t)(uintptr_t)__builtin_thread_pointer();
}
extern char **environ;
static uintptr_t reflektor_empty_init_args[4];
// Build a stable argc/argv/envp/auxv vector for runtimes that expect a
// startup-like layout. argc is zero, argv[0] is NULL, the current environment
// follows it, and a terminating AT_NULL pair follows the environment.
static void reflektor_init_call_args(int include_environment, uintptr_t *argc, uintptr_t *argv, uintptr_t *envp) {
if (!include_environment) {
*argc = 0;
*argv = (uintptr_t)reflektor_empty_init_args;
*envp = (uintptr_t)(reflektor_empty_init_args + 1);
return;
}
size_t envc = 0;
while (environ != NULL && environ[envc] != NULL) {
envc++;
}
uintptr_t *vec = (uintptr_t *)calloc(envc + 4, sizeof(uintptr_t));
if (vec != NULL) {
for (size_t i = 0; i < envc; i++) {
vec[i + 1] = (uintptr_t)environ[i];
}
}
*argc = reflektor_init_argc;
*argv = reflektor_init_argv;
*envp = reflektor_init_envp;
*argc = 0;
*argv = (uintptr_t)vec;
*envp = vec == NULL ? 0 : (uintptr_t)(vec + 1);
}
*/
import "C"
import "fmt"
func cCall0(fn uintptr) uintptr {
return uintptr(C.reflektor_call0(C.uintptr_t(fn)))
}
@@ -68,10 +130,37 @@ func cCall3(fn, a0, a1, a2 uintptr) uintptr {
return uintptr(C.reflektor_call3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2)))
}
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func cCallVoid0(fn uintptr) {
C.reflektor_call_void0(C.uintptr_t(fn))
}
func cCallVoid3(fn, a0, a1, a2 uintptr) {
C.reflektor_call_void3(C.uintptr_t(fn), C.uintptr_t(a0), C.uintptr_t(a1), C.uintptr_t(a2))
}
func cCallVoid0OnThread(fn uintptr) error {
if errno := int(C.reflektor_call_void0_thread(C.uintptr_t(fn))); errno != 0 {
return fmt.Errorf("pthread call failed with error %d", errno)
}
return nil
}
func linuxGoTLSSlotOffset(slot uintptr) (int64, error) {
if slot >= 64 {
return 0, fmt.Errorf("Go TLS slot %d is out of range", slot)
}
offset := C.reflektor_go_tls_offset(C.uintptr_t(slot))
return int64(offset), nil
}
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
var argc C.uintptr_t
var argv C.uintptr_t
var envp C.uintptr_t
C.reflektor_init_call_args(&argc, &argv, &envp)
include := C.int(0)
if includeEnvironment {
include = 1
}
C.reflektor_init_call_args(include, &argc, &argv, &envp)
return uintptr(argc), uintptr(argv), uintptr(envp)
}
@@ -2,6 +2,7 @@
package memmod
func linuxInitCallArgs() (uintptr, uintptr, uintptr) {
func linuxInitCallArgs(includeEnvironment bool) (uintptr, uintptr, uintptr) {
_ = includeEnvironment
return 0, 0, 0
}
+59 -18
View File
@@ -6,6 +6,8 @@
package memmod
import (
"bytes"
"debug/buildinfo"
"errors"
"fmt"
"strings"
@@ -28,15 +30,23 @@ func (head *addressList) free() {
}
type Module struct {
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
headers *IMAGE_NT_HEADERS
codeBase uintptr
modules []windows.Handle
initialized bool
isDLL bool
isRelocated bool
nameExports map[string]uint16
entry uintptr
blockedMemory *addressList
exceptionTable uintptr
goRuntime bool
runtimeStarted bool
}
func imageHasGoBuildInfo(data []byte) bool {
_, err := buildinfo.Read(bytes.NewReader(data))
return err == nil
}
func (module *Module) headerDirectory(idx int) *IMAGE_DATA_DIRECTORY {
@@ -161,14 +171,22 @@ func (module *Module) finalizeSection(sectionData *sectionFinalizeData) error {
return nil
}
var rtlAddFunctionTable = windows.NewLazySystemDLL("ntdll.dll").NewProc("RtlAddFunctionTable")
func (module *Module) registerExceptionHandlers() {
func (module *Module) registerExceptionHandlers() error {
directory := module.headerDirectory(IMAGE_DIRECTORY_ENTRY_EXCEPTION)
if directory.Size == 0 || directory.VirtualAddress == 0 {
return
entrySize := runtimeFunctionEntrySize()
if directory.Size == 0 || directory.VirtualAddress == 0 || entrySize == 0 {
return nil
}
rtlAddFunctionTable.Call(module.codeBase+uintptr(directory.VirtualAddress), uintptr(directory.Size)/unsafe.Sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY{}), module.codeBase)
if uintptr(directory.Size)%entrySize != 0 {
return fmt.Errorf("malformed exception table size %#x for entry size %d", directory.Size, entrySize)
}
module.exceptionTable = module.codeBase + uintptr(directory.VirtualAddress)
count := uint32(uintptr(directory.Size) / entrySize)
if !windows.RtlAddFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)), count, module.codeBase) {
module.exceptionTable = 0
return errors.New("RtlAddFunctionTable returned false")
}
return nil
}
func (module *Module) finalizeSections() error {
@@ -498,7 +516,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return nil, errors.New("Section is not page-aligned")
}
module = &Module{isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0}
module = &Module{
isDLL: (oldHeader.FileHeader.Characteristics & IMAGE_FILE_DLL) != 0,
goRuntime: imageHasGoBuildInfo(data),
}
defer func() {
if err != nil {
module.Free()
@@ -583,7 +604,10 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// Register exception tables, if they exist.
module.registerExceptionHandlers()
err = module.registerExceptionHandlers()
if err != nil {
return
}
// Register function PCs.
loadedAddressRangesMu.Lock()
@@ -598,6 +622,12 @@ func LoadLibrary(data []byte) (module *Module, err error) {
}
// TLS callbacks are executed BEFORE the main loading.
if module.goRuntime {
// Go DLL initialization starts process-lifetime runtime state. From this
// point onward, an error or Close must leave the image and its imports
// pinned rather than detach and unmap live runtime-managed threads.
module.runtimeStarted = true
}
module.executeTLS()
// Get entry point of loaded module.
@@ -619,8 +649,15 @@ func LoadLibrary(data []byte) (module *Module, err error) {
return
}
// Free releases module resources and unloads it.
// Free releases module resources. Started Go runtime images remain pinned.
func (module *Module) Free() {
if module.goRuntime && module.runtimeStarted {
if module.blockedMemory != nil {
module.blockedMemory.free()
module.blockedMemory = nil
}
return
}
if module.initialized {
// Notify library about detaching from process.
syscall.Syscall(module.entry, 3, module.codeBase, uintptr(DLL_PROCESS_DETACH), 0)
@@ -633,6 +670,10 @@ func (module *Module) Free() {
}
module.modules = nil
}
if module.exceptionTable != 0 {
windows.RtlDeleteFunctionTable((*windows.RUNTIME_FUNCTION)(unsafe.Pointer(module.exceptionTable)))
module.exceptionTable = 0
}
if module.codeBase != 0 {
windows.VirtualFree(module.codeBase, 0, windows.MEM_RELEASE)
module.codeBase = 0
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_I386
func runtimeFunctionEntrySize() uintptr { return 0 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
func runtimeFunctionEntrySize() uintptr { return 12 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
func runtimeFunctionEntrySize() uintptr { return 8 }
@@ -6,3 +6,5 @@
package memmod
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
func runtimeFunctionEntrySize() uintptr { return 8 }
+1 -1
View File
@@ -1328,7 +1328,7 @@ github.com/sliverarmory/beignet/internal/stager
## explicit; go 1.25.6
github.com/sliverarmory/malasada
github.com/sliverarmory/malasada/internal/aplib
# github.com/sliverarmory/reflektor v0.0.2
# github.com/sliverarmory/reflektor v0.0.3
## explicit; go 1.26.6
github.com/sliverarmory/reflektor/memmod
# github.com/sliverarmory/wasm-donut v0.0.3