mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce |
No files matched your search
@@ -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 }}
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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()}
|
||||
<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>
|
||||
|
||||
@@ -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
@@ -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 & 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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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: "{query?.slice(0, 50)}"
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 & 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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
Generated
Vendored
+33
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+2
-1
@@ -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
@@ -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
|
||||
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_I386
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 0 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 12 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
+33
@@ -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
@@ -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
@@ -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
@@ -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
-1
@@ -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
@@ -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
|
||||
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_I386
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 0 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 12 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARMNT
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
+2
@@ -6,3 +6,5 @@
|
||||
package memmod
|
||||
|
||||
const imageFileProcess = IMAGE_FILE_MACHINE_ARM64
|
||||
|
||||
func runtimeFunctionEntrySize() uintptr { return 8 }
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user