Compare commits

...
73 Commits
Author SHA1 Message Date
Joe 902999493b Merge pull request #2351 from BishopFox/feat/wireguard-opt-in-auto-client
Make multiplayer WireGuard explicitly opt-in
2026-08-28 10:10:58 -07:00
moloch-- 14363c5d2a Make multiplayer WireGuard explicitly opt-in 2026-08-28 09:06:37 -07:00
Joe 926b7ae1a7 Merge pull request #2350 from BishopFox/feat/sgn-v0.1.0-stability
Fix SGN x64 stability and strengthen shellcode E2E coverage
2026-08-28 07:22:05 -07:00
moloch-- bf815b36ed Fix asset downloader lint findings 2026-08-27 18:04:14 -07:00
moloch-- 637853e8c2 Retry transient asset downloads safely 2026-08-27 14:48:38 -07:00
moloch-- 0c022ec722 Update SGN for x64 safe-mode stack alignment 2026-08-27 13:09:36 -07:00
moloch-- 7c83b458fa Fix SGN stability and expand shellcode E2E coverage 2026-08-27 10:39:32 -07:00
Joe 09b0540e75 Merge pull request #2349 from BishopFox/feat/armory-package-identity
Fix Armory extension updates and archive installs
2026-08-26 12:07:49 -07:00
moloch-- b4005efda8 Fix Armory PR lint failures 2026-08-26 11:03:58 -07:00
moloch-- 6d086fd7a7 Fix Armory package resolution and archive installs 2026-08-26 09:21:38 -07:00
Joe ccebd505b0 Merge pull request #2348 from BishopFox/fix/extensions-rm-external-path
Fix extensions rm deleting external directories
2026-08-25 16:21:38 -07:00
rkervella 94c5022dd0 fix(client): preserve external extension directories 2026-08-25 10:46:13 -07:00
Joe b19e383111 Merge pull request #2347 from BishopFox/feat/reflektor-default-bof-execution
Use Reflektor for default BOF execution
2026-08-25 06:56:20 -07:00
moloch-- 02ebd99c68 Fix Reflektor BOF lint failures 2026-08-24 21:30:08 -07:00
moloch-- 83d82e7161 feat: use Reflektor for default BOF execution 2026-08-24 21:13:51 -07:00
moloch-- eb5ac1b309 Only run reflektor tests on e2e 2026-08-24 16:49:00 -07:00
Joe e73618e0a5 Merge pull request #2339 from BishopFox/e2e/comprehensive
test: add comprehensive server and implant E2E coverage
2026-08-24 16:25:55 -07:00
moloch-- 0017fd2fe8 test: reduce extension callback test complexity 2026-08-24 09:53:49 -07:00
moloch-- e44111b6b4 fix: defer native extension rpc responses 2026-08-24 09:47:09 -07:00
moloch-- aa2dce510a test: run darwin shellcode e2e on macos 26 2026-08-24 06:45:15 -07:00
moloch-- 5e6acde3c7 test: surface darwin shellcode loader failures 2026-08-23 23:03:26 -07:00
moloch-- 99a8d3f859 fix: reduce xor dynamic stub complexity 2026-08-23 19:41:18 -07:00
moloch-- fc15a47b1e fix: satisfy shellcode encoder lint 2026-08-23 19:38:16 -07:00
moloch-- 8b32b6525f fix: stabilize shellcode encoders 2026-08-23 19:35:30 -07:00
moloch-- 9e0fafb785 fix: keep shellcode runner alive after return 2026-08-23 18:58:11 -07:00
moloch-- 12ebffb16c test: add shellcode execution e2e matrix 2026-08-23 18:47:39 -07:00
moloch-- b539762de7 test: cover chmod and chown on darwin 2026-08-23 15:39:14 -05:00
moloch-- e71af809d4 fix: support chmod and chown on darwin 2026-08-23 15:39:14 -05:00
moloch-- f3ffa3323b ci: fix cross-platform lint checks 2026-08-23 15:39:14 -05:00
moloch-- 778331d474 ci: export command coverage markdown 2026-08-23 15:39:14 -05:00
moloch-- 8d259577af ci: restore manual-only comprehensive trigger 2026-08-23 15:39:14 -05:00
moloch-- aa108a66a8 ci: restore comprehensive E2E matrix 2026-08-23 15:39:13 -05:00
moloch-- 44e5464950 fix Windows arm64 WireGuard reconnects 2026-08-23 15:39:13 -05:00
moloch-- 1381200b33 fix WireGuard key exchange peer readiness 2026-08-23 15:39:13 -05:00
moloch-- f81b250a04 ci: focus Windows arm64 WireGuard diagnostics 2026-08-23 15:39:13 -05:00
moloch-- 8b4ee5f5c5 test: add opt-in implant debug logging 2026-08-23 15:39:13 -05:00
moloch-- 68cb1363e8 test: preserve E2E command failure diagnostics 2026-08-23 15:39:12 -05:00
moloch-- 2344a3ae50 fix overlapping beacon check-ins 2026-08-23 15:39:12 -05:00
moloch-- 8cec5164b7 fix implant WireGuard device teardown 2026-08-23 15:39:12 -05:00
moloch-- be50c25cb2 fix WireGuard beacon result delivery receipt 2026-08-23 15:39:12 -05:00
moloch-- ee2cc83aa1 test: accept partial Windows service inventories 2026-08-23 15:39:12 -05:00
moloch-- 8e62e2dcd9 ci: temporarily run comprehensive E2E on branch pushes 2026-08-23 15:39:11 -05:00
moloch-- 1347059639 ci: add comprehensive E2E workflow 2026-08-23 15:39:11 -05:00
moloch-- 29efea561c test: add comprehensive implant command E2E suite 2026-08-23 15:39:11 -05:00
moloch-- 82deca2b0f test beacon empty check-in receive path 2026-08-23 15:39:11 -05:00
moloch-- 808b3b6408 fix beacon result delivery across reconnects 2026-08-23 15:39:11 -05:00
moloch-- eba10ebbfc fix Windows arm64 process architecture detection 2026-08-23 15:39:11 -05:00
moloch-- 13903e46a8 fix Linux arm64 memfd creation 2026-08-23 15:39:10 -05:00
moloch-- 8aaaa583bd fix Linux arm64 process architecture detection 2026-08-23 15:39:10 -05:00
moloch-- 4b428065e2 fix WireGuard key exchange response parsing 2026-08-23 15:39:10 -05:00
moloch-- 578f9a28d0 fix UDP-only netstat responses 2026-08-23 15:39:10 -05:00
moloch-- ce2f494f96 fix directory upload overwrite truncation 2026-08-23 15:39:10 -05:00
moloch-- c1c36491a6 fix cd handler error reporting 2026-08-23 15:39:09 -05:00
Joe a5c3482c6d Merge pull request #2335 from kyzzniko-lang/fix/mcp-auth-token
fix(mcp): make `mcp start` usable without the interactive confirmation
2026-08-19 14:12:23 -07:00
Joe b428274515 Merge pull request #2336 from BishopFox/reflektor-ext
Support native extensions on macOS and Linux
2026-08-19 13:24:59 -07:00
moloch-- 5dc6adae47 Pin Reflektor v0.0.6 2026-08-19 13:33:44 -05:00
moloch-- 264b00f3c7 Pin Reflektor v0.0.5 2026-08-19 12:22:06 -05:00
moloch-- ada79c8ecd Use Reflektor native loader for Unix extensions 2026-08-19 11:06:49 -05:00
moloch-- edd4c3d097 Support native extensions on macOS and Linux
Replace the Darwin universal loader with Reflektor, add Linux extension handlers and a shared Unix callback ABI, fix client registration ordering and target visibility, and extend the Reflektor integration lifecycle across supported platforms.
2026-08-19 09:36:44 -05:00
kyzzniko-lang 38f84a4f6f fix(mcp): make mcp start usable without the interactive confirmation
`mcp start` gated the whole startup behind an interactive huh confirmation
("Do you know what prompt injection is and are you an adult?"). On terminals
where those interactive forms break (#2133) the form error was discarded,
the prompt silently evaluated to false, and users were stopped with an
unrelated "not qualified" message before the server ever started: no auth
token in the output and no ~/.sliver-client/mcp.yaml, which is what #2294
reports.

- add --yes/-y to `mcp start` to skip the confirmation (the same
  flags-over-menus escape hatch recommended in #2133), and point the
  rejection message at it and at `settings autoadult`
- print the auth config path next to the token in `mcp start` and `mcp`
  status output so users can locate mcp.yaml
- surface the confirmation form error in IsUserAnAdultWithPrompt instead
  of swallowing it, so a broken terminal reports why it failed

Closes #2294
2026-08-19 19:29:47 +08:00
Joe aee0958e99 Merge pull request #2329 from BishopFox/dependabot/go_modules/charm.land/lipgloss/v2-2.0.6
chore(deps): bump charm.land/lipgloss/v2 from 2.0.4 to 2.0.6
2026-08-18 13:36:26 -07:00
Joe 23c067982a Merge pull request #2315 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/mermaid-11.16.1
chore(deps): bump mermaid from 11.15.0 to 11.16.1 in /docs/sliver-docs
2026-08-18 13:36:12 -07:00
dependabot[bot] 7e917da3ac chore(deps): bump charm.land/lipgloss/v2 from 2.0.4 to 2.0.6
Bumps [charm.land/lipgloss/v2](https://github.com/charmbracelet/lipgloss) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/charmbracelet/lipgloss/releases)
- [Commits](https://github.com/charmbracelet/lipgloss/compare/v2.0.4...v2.0.6)

---
updated-dependencies:
- dependency-name: charm.land/lipgloss/v2
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-18 03:08:02 +00:00
Joe 674b49f41f Merge pull request #2303 from BishopFox/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.8.3
chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.7.8 to 6.8.3
2026-08-17 20:05:17 -07:00
moloch-- 4ba04df621 Only run reflektor tests on changes to implant/... 2026-08-17 20:02:29 -07:00
Joe 3274382bc7 Merge pull request #2333 from BishopFox/docs-refresh
Docs refresh
2026-08-17 19:58:50 -07:00
dependabot[bot] 9c9943c94c chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.7.8 to 6.8.3
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.7.8 to 6.8.3.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.7.8...v6.8.3)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-version: 6.8.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-18 00:10:18 +00:00
Joe 2e9b52371e Merge pull request #2332 from BishopFox/reflektor-tests
Test Sliver shared libraries with Reflektor
2026-08-17 17:07:02 -07:00
moloch-- 6d6df68972 Update Reflektor to v0.0.3
Synchronize the root and implant module graphs and vendor trees with the all-platform Go c-shared loader fixes.
2026-08-17 00:29:26 -07:00
moloch-- 7f863d3172 Fix cross-platform Reflektor integration failures
Close rendered implant files before import renames, use native GCC for Linux/386 Go shared libraries, and preserve actionable integration logs.
2026-08-17 00:28:49 -07:00
moloch-- 6ecb379bce Test Sliver shared libraries with Reflektor 2026-08-16 22:09:22 -07:00
Ronan Kervella b5d3d0bad6 Merge branch 'master' into dependabot/npm_and_yarn/docs/sliver-docs/mermaid-11.16.1 2026-08-13 09:41:06 -07:00
dependabot[bot] 528b04427a chore(deps): bump mermaid from 11.15.0 to 11.16.1 in /docs/sliver-docs
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.15.0 to 11.16.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/[email protected]@11.16.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.16.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-13 04:00:22 +00:00
795 changed files with 129294 additions and 16757 deletions

No files matched your search

+449
View File
@@ -0,0 +1,449 @@
name: Comprehensive e2e Tests
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: comprehensive-e2e-${{ github.ref }}
cancel-in-progress: false
jobs:
authorize:
name: Require repository administrator
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check actor repository permission
uses: actions/github-script@v9
env:
REQUESTING_ACTOR: ${{ github.triggering_actor }}
with:
script: |
const actor = process.env.REQUESTING_ACTOR || context.actor;
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: actor,
});
core.info(`${actor} has repository permission ${data.permission} (${data.role_name})`);
if (data.permission !== "admin") {
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
}
darwin:
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
needs: authorize
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
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
with:
persist-credentials: false
- name: Prepare coverage artifact
run: |
mkdir -p e2e-results
printf 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
- 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 -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: darwin
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o sliver-server ./server
- name: Compile comprehensive E2E driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: darwin
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
run: >-
./sliver-comprehensive-e2e
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server
-server-arch ${{ matrix.server_arch }}
-target-os darwin
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-darwin-${{ matrix.goarch }}
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
linux-native:
name: comprehensive-e2e-linux-${{ matrix.goarch }}
needs: authorize
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
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
with:
persist-credentials: false
- name: Prepare coverage artifact
run: |
mkdir -p e2e-results
printf 'linux/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
- 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 -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: linux
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o sliver-server ./server
- name: Compile comprehensive E2E driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: linux
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
run: >-
./sliver-comprehensive-e2e
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server
-server-arch ${{ matrix.server_arch }}
-target-os linux
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-linux-${{ matrix.goarch }}
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
linux-386:
name: comprehensive-e2e-linux-386
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 360
env:
CONTAINER_NAME: sliver-comprehensive-e2e-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-comprehensive-e2e-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare coverage artifact
run: |
mkdir -p e2e-results
printf 'linux/386\n' > e2e-results/target-linux-386.txt
- 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 E2E 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/e2e/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
- name: Run comprehensive E2E suite
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
--volume "${PWD}/e2e-results:/results"
"${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
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-linux-386
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
windows:
name: comprehensive-e2e-windows-${{ matrix.goarch }}
needs: authorize
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-2022
goarch: amd64
server_arch: amd64
- runner: windows-11-arm
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare coverage artifact
run: |
mkdir -p e2e-results
printf 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
- 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 -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: windows
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o sliver-server.exe ./server
- name: Compile comprehensive E2E driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: windows
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e.exe ./test/e2e
- name: Run comprehensive E2E suite
run: >-
./sliver-comprehensive-e2e.exe
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server.exe
-server-arch ${{ matrix.server_arch }}
-target-os windows
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-windows-${{ matrix.goarch }}
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
aggregate-coverage:
name: Aggregate comprehensive E2E coverage
needs:
- authorize
- darwin
- linux-native
- linux-386
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
outputs:
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download target coverage
uses: actions/download-artifact@v8
with:
pattern: comprehensive-e2e-target-*
path: coverage-input
merge-multiple: true
- name: Prepare aggregate artifact
run: |
mkdir -p coverage-input coverage-summary
printf 'Comprehensive E2E coverage aggregate\n' > coverage-summary/README.txt
- name: Aggregate coverage and enforce required cells
id: aggregate
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/report
-input coverage-input
-output coverage-summary
- name: Publish command coverage table
id: publish_command_coverage
if: ${{ always() }}
run: |
detailed_report="coverage-summary/coverage-summary.md"
report="coverage-summary/command-coverage.md"
if [[ ! -s "${report}" ]]; then
printf 'Command coverage report is missing or empty: %s\n' "${report}" >&2
exit 1
fi
delimiter="COMPREHENSIVE_E2E_COMMAND_COVERAGE_EOF"
if grep -Fxq "${delimiter}" "${report}"; then
printf 'Command coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
exit 1
fi
{
printf 'command_coverage_markdown<<%s\n' "${delimiter}"
cat "${report}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
if [[ -f "${detailed_report}" ]]; then
cat "${detailed_report}" >> "${GITHUB_STEP_SUMMARY}"
fi
printf '\n## Command coverage by OS/architecture/transport\n\n' >> "${GITHUB_STEP_SUMMARY}"
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
- name: Upload aggregate coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-coverage-summary
path: coverage-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed or not-run coverage
if: ${{ steps.aggregate.outcome != 'success' }}
run: exit 1
reflektor:
name: Existing Reflektor test matrix
needs: authorize
uses: ./.github/workflows/reflektor-tests.yml
shellcode:
name: Shellcode E2E test matrix
needs: authorize
uses: ./.github/workflows/shellcode-tests.yml
+4 -4
View File
@@ -20,7 +20,6 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
lint:
@@ -41,6 +40,8 @@ jobs:
steps:
- name: Check Out Code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Go 1.26.6
uses: actions/setup-go@v6
@@ -51,6 +52,5 @@ jobs:
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
# Keep legacy findings visible without making them blockers.
only-new-issues: true
args: ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
# Gate only findings introduced since the event base revision.
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
+214
View File
@@ -0,0 +1,214 @@
name: Reflektor Integration Test Matrix
on:
workflow_call:
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 }}
+208
View File
@@ -0,0 +1,208 @@
name: Shellcode E2E Test Matrix
on:
workflow_call:
outputs:
shellcode_coverage_markdown:
description: Shellcode E2E coverage table in Markdown
value: ${{ jobs.aggregate.outputs.shellcode_coverage_markdown }}
permissions:
contents: read
jobs:
target:
name: shellcode-e2e-${{ matrix.target_os }}-${{ matrix.target_arch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
include:
- runner: macos-26
target_os: darwin
target_arch: arm64
server_os: darwin
server_arch: arm64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: ubuntu-24.04
target_os: linux
target_arch: amd64
server_os: linux
server_arch: amd64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: ubuntu-24.04-arm
target_os: linux
target_arch: arm64
server_os: linux
server_arch: arm64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: windows-2022
target_os: windows
target_arch: "386"
server_os: windows
server_arch: amd64
server_executable: ./sliver-server.exe
test_executable: ./sliver-shellcode-e2e.exe
- runner: windows-2022
target_os: windows
target_arch: amd64
server_os: windows
server_arch: amd64
server_executable: ./sliver-server.exe
test_executable: ./sliver-shellcode-e2e.exe
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare target artifact
run: |
mkdir -p shellcode-results
printf '%s/%s\n' \
'${{ matrix.target_os }}' \
'${{ matrix.target_arch }}' \
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
- 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 -buildvcs=false -mod=vendor -trimpath ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: ${{ matrix.server_os }}
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o ${{ matrix.server_executable }} ./server
- name: Compile shellcode E2E client
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.target_arch }}
GOOS: ${{ matrix.target_os }}
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o ${{ matrix.test_executable }} ./test/e2e
- name: Run shellcode generation and execution tests
run: >-
${{ matrix.test_executable }}
-test.v
-test.run=^TestShellcodeE2E$
-test.timeout=0
-repo .
-server ${{ matrix.server_executable }}
-server-arch ${{ matrix.server_arch }}
-target-os ${{ matrix.target_os }}
-target-arch ${{ matrix.target_arch }}
-results ./shellcode-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
-shellcode-sgn-samples 4
- name: Upload target shellcode results
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
path: shellcode-results
if-no-files-found: error
overwrite: true
retention-days: 14
aggregate:
name: Aggregate shellcode E2E coverage
needs: target
if: ${{ always() }}
outputs:
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Prepare aggregate artifact
run: |
mkdir -p shellcode-input shellcode-summary
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
- name: Download target shellcode results
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: shellcode-e2e-target-*
path: shellcode-input
merge-multiple: true
- name: Aggregate shellcode coverage
id: aggregate
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
-input ./shellcode-input
-output ./shellcode-summary
- name: Publish shellcode coverage table
id: publish_shellcode_coverage
if: ${{ always() }}
run: |
report="shellcode-summary/shellcode-coverage.md"
if [[ ! -s "${report}" ]]; then
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
exit 1
fi
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
if grep -Fxq "${delimiter}" "${report}"; then
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
exit 1
fi
{
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
cat "${report}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
- name: Upload aggregate shellcode coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: shellcode-e2e-coverage-summary
path: shellcode-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed or not-run shellcode coverage
if: ${{ always() && steps.aggregate.outcome != 'success' }}
run: exit 1
+1
View File
@@ -36,6 +36,7 @@ const (
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
type ClientWGConfig struct {
Enabled bool `json:"enabled,omitempty"`
ServerPubKey string `json:"server_pub_key"`
ClientPrivateKey string `json:"client_private_key"`
ClientPubKey string `json:"client_pub_key"`
+29
View File
@@ -0,0 +1,29 @@
package assets
import (
"encoding/json"
"testing"
)
func TestClientWGConfigEnabledIsPositiveOptIn(t *testing.T) {
legacyJSON := []byte(`{"wg":{"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
legacy := &ClientConfig{}
if err := json.Unmarshal(legacyJSON, legacy); err != nil {
t.Fatalf("unmarshal legacy config: %v", err)
}
if legacy.WG == nil {
t.Fatal("expected legacy WireGuard config")
}
if legacy.WG.Enabled {
t.Fatal("expected an absent enabled marker to remain disabled")
}
enabledJSON := []byte(`{"wg":{"enabled":true,"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
enabled := &ClientConfig{}
if err := json.Unmarshal(enabledJSON, enabled); err != nil {
t.Fatalf("unmarshal enabled config: %v", err)
}
if enabled.WG == nil || !enabled.WG.Enabled {
t.Fatal("expected enabled marker to opt in to WireGuard")
}
}
+3 -1
View File
@@ -52,7 +52,9 @@ func init() {
rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "connect to multiplayer through the operator config's WireGuard wrapper")
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "force multiplayer connections through the operator config's WireGuard wrapper")
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "force multiplayer connections to use direct mTLS")
rootCmd.MarkFlagsMutuallyExclusive(enableWGFlag, disableWGFlag)
// Create the console client, without any RPC or commands bound to it yet.
// This created before anything so that multiple commands can make use of
+17 -4
View File
@@ -1,17 +1,20 @@
package cli
import (
"fmt"
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
const (
enableWGFlag = "enable-wg"
enableWGFlag = "enable-wg"
disableWGFlag = "disable-wg"
)
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
if cmd == nil {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectDirect)
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
return nil
}
@@ -19,10 +22,20 @@ func applyMultiplayerConnectMode(cmd *cobra.Command) error {
if err != nil {
return err
}
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
if err != nil {
return err
}
if cmd.Flags().Changed(enableWGFlag) && cmd.Flags().Changed(disableWGFlag) {
return fmt.Errorf("--%s and --%s cannot be used together", enableWGFlag, disableWGFlag)
}
mode := transport.MultiplayerConnectDirect
if enableWG {
mode := transport.MultiplayerConnectAuto
switch {
case enableWG:
mode = transport.MultiplayerConnectEnableWG
case disableWG:
mode = transport.MultiplayerConnectDisableWG
}
transport.SetMultiplayerConnectMode(mode)
return nil
+136
View File
@@ -0,0 +1,136 @@
package cli
import (
"strings"
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
func TestWireGuardOverrideFlagsDefaultToFalse(t *testing.T) {
for _, name := range []string{enableWGFlag, disableWGFlag} {
flag := rootCmd.PersistentFlags().Lookup(name)
if flag == nil {
t.Fatalf("expected --%s persistent flag", name)
}
if flag.DefValue != "false" {
t.Fatalf("expected --%s to default to false, got %q", name, flag.DefValue)
}
}
}
func TestApplyMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := applyMultiplayerConnectMode(cmd); err != nil {
t.Fatalf("apply multiplayer mode: %v", err)
}
if !transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
t.Fatal("expected automatic mode to honor an explicitly enabled WireGuard config")
}
if transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
t.Fatal("expected automatic mode to ignore a legacy WireGuard config without the opt-in marker")
}
}
func TestApplyMultiplayerConnectModeEnableWGForcesLegacyConfig(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", enableWGFlag, err)
}
if err := applyMultiplayerConnectMode(cmd); err != nil {
t.Fatalf("apply multiplayer mode: %v", err)
}
if !transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
t.Fatal("expected --enable-wg to force a complete legacy config through WireGuard")
}
}
func TestApplyMultiplayerConnectModeDisableWGOverridesEnabledConfig(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", disableWGFlag, err)
}
if err := applyMultiplayerConnectMode(cmd); err != nil {
t.Fatalf("apply multiplayer mode: %v", err)
}
if transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
t.Fatal("expected --disable-wg to force direct mTLS")
}
}
func TestApplyMultiplayerConnectModeRejectsConflictingOverrides(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", enableWGFlag, err)
}
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", disableWGFlag, err)
}
err := applyMultiplayerConnectMode(cmd)
if err == nil {
t.Fatal("expected conflicting WireGuard overrides to fail")
}
if !strings.Contains(err.Error(), "cannot be used together") {
t.Fatalf("unexpected conflict error: %v", err)
}
}
func TestApplyMultiplayerConnectModeRejectsBothOverridesWhenExplicitlyFalse(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(enableWGFlag, "false"); err != nil {
t.Fatalf("set --%s: %v", enableWGFlag, err)
}
if err := cmd.Flags().Set(disableWGFlag, "false"); err != nil {
t.Fatalf("set --%s: %v", disableWGFlag, err)
}
err := applyMultiplayerConnectMode(cmd)
if err == nil {
t.Fatal("expected explicitly combining both WireGuard override flags to fail")
}
if !strings.Contains(err.Error(), "cannot be used together") {
t.Fatalf("unexpected conflict error: %v", err)
}
}
func newTransportModeTestCommand() *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().Bool(enableWGFlag, false, "")
cmd.Flags().Bool(disableWGFlag, false, "")
return cmd
}
func resetMultiplayerConnectModeForTest(t *testing.T) {
t.Helper()
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
t.Cleanup(func() {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
})
}
func enabledWGClientConfig() *assets.ClientConfig {
config := legacyWGClientConfig()
config.WG.Enabled = true
return config
}
func legacyWGClientConfig() *assets.ClientConfig {
return &assets.ClientConfig{
WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub",
ClientPrivateKey: "client-priv",
ClientIP: "100.64.0.2",
},
}
}
+34 -24
View File
@@ -301,32 +301,44 @@ func getCommandsInCache(armoryPK string) []string {
return commandNames
}
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
if cacheEntry.Pkg.IsAlias {
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
}
if cacheEntry.Extension == nil {
return false
}
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
return true
}
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName == name {
return true
}
}
return false
}
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
packages := []*pkgCacheEntry{}
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.LastErr == nil {
if cacheEntry.Pkg.IsAlias {
if cacheEntry.Alias.CommandName == name {
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
packages = append(packages, &cacheEntry)
}
}
}
} else {
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName == name {
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
packages = append(packages, &cacheEntry)
}
break
}
}
}
}
if cacheEntry.LastErr != nil || !packageMatchesInstallName(cacheEntry, name) {
return true
}
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
return true
}
packageVersion := ""
if cacheEntry.Pkg.IsAlias {
packageVersion = cacheEntry.Alias.Version
} else {
packageVersion = cacheEntry.Extension.Version
}
if minimumVersion == "" || packageVersion >= minimumVersion {
packages = append(packages, &cacheEntry)
}
return true
})
@@ -732,9 +744,7 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
con.Printf(console.Clearln + "\r") // Clear download message
extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
return nil
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
}
func writeArmoryTempFile(data []byte) (string, error) {
+218
View File
@@ -0,0 +1,218 @@
package armory
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/extensions"
)
func resetPackageCache(t *testing.T) {
t.Helper()
pkgCache.Range(func(key, _ any) bool {
pkgCache.Delete(key)
return true
})
t.Cleanup(func() {
pkgCache.Range(func(key, _ any) bool {
pkgCache.Delete(key)
return true
})
})
}
func testExtensionCacheEntry() pkgCacheEntry {
manifest := &extensions.ExtensionManifest{
Name: "ADCS Request (Remote)",
PackageName: "remote-adcs-package",
Version: "v0.1.5",
ArmoryName: "Default Armory",
ArmoryPK: "armory-public-key",
ExtCommand: []*extensions.ExtCommand{
{CommandName: "remote-adcs-request"},
{CommandName: "remote-adcs-request-on-behalf"},
},
}
return pkgCacheEntry{
ArmoryConfig: &assets.ArmoryConfig{PublicKey: "armory-public-key"},
Pkg: ArmoryPackage{
Name: "Remote ADCS package",
CommandName: "remote-adcs-index",
},
Extension: manifest,
ID: "remote-adcs-request-id",
}
}
func TestGetPackageForCommandAcceptsStableExtensionIdentities(t *testing.T) {
resetPackageCache(t)
entry := testExtensionCacheEntry()
pkgCache.Store(entry.ID, entry)
for _, name := range []string{
"remote-adcs-index",
"remote-adcs-request",
"remote-adcs-request-on-behalf",
"remote-adcs-package",
} {
t.Run(name, func(t *testing.T) {
found, err := getPackageForCommand(name, "armory-public-key", "v0.1.5")
if err != nil {
t.Fatalf("lookup %q failed: %v", name, err)
}
if found.ID != entry.ID {
t.Fatalf("lookup %q returned %q, want %q", name, found.ID, entry.ID)
}
})
}
if _, err := getPackageForCommand("remote-adcs-package", "other-armory", "v0.1.5"); !errors.Is(err, ErrPackageNotFound) {
t.Fatalf("wrong-armory lookup error = %v, want %v", err, ErrPackageNotFound)
}
if _, err := getPackageForCommand("remote-adcs-package", "armory-public-key", "v0.1.6"); !errors.Is(err, ErrPackageNotFound) {
t.Fatalf("newer-version lookup error = %v, want %v", err, ErrPackageNotFound)
}
for _, name := range []string{"ADCS Request (Remote)", "Remote ADCS package", "unknown-package"} {
if _, err := getPackageForCommand(name, "", ""); !errors.Is(err, ErrPackageNotFound) {
t.Fatalf("unstable identity %q lookup error = %v, want %v", name, err, ErrPackageNotFound)
}
}
}
func TestPackageMatchesInstallNameLeavesAliasesCommandOnly(t *testing.T) {
entry := pkgCacheEntry{
Pkg: ArmoryPackage{
Name: "Human Alias Name",
CommandName: "alias-command",
IsAlias: true,
},
Alias: &alias.AliasManifest{CommandName: "alias-command"},
}
if packageMatchesInstallName(entry, "Human Alias Name") {
t.Fatal("alias unexpectedly matched its display name")
}
if !packageMatchesInstallName(entry, "alias-command") {
t.Fatal("alias did not match its command name")
}
}
func TestExtensionManifestsMatch(t *testing.T) {
manifest := func(name, packageName string, commands ...string) *extensions.ExtensionManifest {
extCommands := make([]*extensions.ExtCommand, 0, len(commands))
for _, command := range commands {
extCommands = append(extCommands, &extensions.ExtCommand{CommandName: command})
}
return &extensions.ExtensionManifest{Name: name, PackageName: packageName, ExtCommand: extCommands}
}
tests := []struct {
name string
local *extensions.ExtensionManifest
latest *extensions.ExtensionManifest
want bool
}{
{
name: "v2 package identity",
local: manifest("Old Display Name", "stable-package", "old-command"),
latest: manifest("New Display Name", "stable-package", "new-command"),
want: true,
},
{
name: "different v2 packages",
local: manifest("Shared Display Name", "package-one", "shared-command"),
latest: manifest("Shared Display Name", "package-two", "shared-command"),
want: false,
},
{
name: "v2 does not downgrade to legacy identity",
local: manifest("Human Display Name", "stable-package", "shared-command"),
latest: manifest("legacy-command", "", "shared-command"),
want: false,
},
{
name: "legacy command identity",
local: manifest("legacy-command", "", "legacy-command"),
latest: manifest("Human Display Name", "stable-package", "other-command", "legacy-command"),
want: true,
},
{
name: "display names are not identity",
local: manifest("Shared Name", "", "old-command"),
latest: manifest("Shared Name", "stable-package", "new-command"),
want: false,
},
{
name: "unrelated legacy package",
local: manifest("Old Name", "", "old-command"),
latest: manifest("New Name", "stable-package", "new-command"),
want: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := extensionManifestsMatch(test.local, test.latest); got != test.want {
t.Fatalf("extensionManifestsMatch() = %t, want %t", got, test.want)
}
})
}
}
func TestExtensionUpdateDisplayNameResolvesPackage(t *testing.T) {
resetPackageCache(t)
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
installedManifest := []byte(`{
"name":"ADCS Request (Remote)",
"package_name":"remote-adcs-package",
"version":"v0.1.3",
"commands":[{
"command_name":"remote-adcs-request",
"help":"Request a certificate",
"files":[{"os":"windows","arch":"amd64","path":"adcs_request.x64.o"}]
}]
}`)
installDir := filepath.Join(assets.GetExtensionsDir(), "remote-adcs-request")
if err := os.MkdirAll(installDir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(installDir, extensions.ManifestFileName), installedManifest, 0o600); err != nil {
t.Fatal(err)
}
entry := testExtensionCacheEntry()
pkgCache.Store(entry.ID, entry)
otherEntry := testExtensionCacheEntry()
otherEntry.ID = "other-armory-package-id"
otherEntry.ArmoryConfig = &assets.ArmoryConfig{PublicKey: "other-armory"}
otherManifest := *otherEntry.Extension
otherManifest.Version = "v9.0.0"
otherManifest.ArmoryName = "Other Armory"
otherManifest.ArmoryPK = "other-armory"
otherEntry.Extension = &otherManifest
pkgCache.Store(otherEntry.ID, otherEntry)
updates := checkForExtensionUpdates("armory-public-key")
versionInfo, ok := updates["ADCS Request (Remote)"]
if !ok {
t.Fatal("display-name update was not detected")
}
if versionInfo.OldVersion != "v0.1.3" || versionInfo.NewVersion != "v0.1.5" {
t.Fatalf("unexpected versions: %#v", versionInfo)
}
if versionInfo.PackageID != entry.ID {
t.Fatalf("update package ID = %q, want %q", versionInfo.PackageID, entry.ID)
}
found := packageCacheLookupByID(versionInfo.PackageID)
if found == nil {
t.Fatal("detected update package is missing from the cache")
}
if found.ID != entry.ID {
t.Fatalf("resolved package %q, want %q", found.ID, entry.ID)
}
}
+36 -11
View File
@@ -40,6 +40,7 @@ type VersionInformation struct {
OldVersion string
NewVersion string
ArmoryName string
PackageID string
}
type PackageType uint
@@ -124,9 +125,9 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if !ok {
continue
}
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
if err != nil {
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
updatedPackage := packageCacheLookupByID(extVersionInfo.PackageID)
if updatedPackage == nil {
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, ErrPackageNotFound)
continue
}
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
@@ -173,8 +174,21 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
return results
}
func extensionManifestsMatch(localManifest, latestManifest *extensions.ExtensionManifest) bool {
if localManifest.PackageName != "" {
return latestManifest.PackageName != "" && localManifest.PackageName == latestManifest.PackageName
}
for _, localCommand := range localManifest.ExtCommand {
for _, latestCommand := range latestManifest.ExtCommand {
if localCommand.CommandName == latestCommand.CommandName {
return true
}
}
}
return false
}
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
_, cachedExtensions := packageManifestsInCache()
// Return a map of extension name to minimum version to install
results := make(map[string]VersionInformation)
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
@@ -186,7 +200,12 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
if err != nil {
continue
}
for _, latestExt := range cachedExtensions {
pkgCache.Range(func(_, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok || cacheEntry.LastErr != nil || cacheEntry.Pkg.IsAlias || cacheEntry.Extension == nil {
return true
}
latestExt := cacheEntry.Extension
/*
We used to check if the version identifiers were different between the installed version and the
version in the armory. This worked when we had one armory, but with multiple potential armories,
@@ -194,16 +213,22 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
so we will rely on the author of the package incrementing the version identifier somehow to determine
if a package is newer.
*/
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
if extensionManifestsMatch(localManifest, latestExt) && latestExt.Version > localManifest.Version {
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
results[localManifest.Name] = VersionInformation{
OldVersion: localManifest.Version,
NewVersion: latestExt.Version,
ArmoryName: latestExt.ArmoryName,
current, found := results[localManifest.Name]
if !found || latestExt.Version > current.NewVersion ||
(latestExt.Version == current.NewVersion && cacheEntry.ID < current.PackageID) {
results[localManifest.Name] = VersionInformation{
OldVersion: localManifest.Version,
NewVersion: latestExt.Version,
ArmoryName: latestExt.ArmoryName,
PackageID: cacheEntry.ID,
}
}
}
}
}
return true
})
}
return results
@@ -1,10 +1,28 @@
package extensions
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
/*
@@ -115,6 +133,101 @@ const (
}`
)
type registerExtensionTestRPC struct {
rpcpb.SliverRPCClient
register func(context.Context, *sliverpb.RegisterExtensionReq, ...grpc.CallOption) (*sliverpb.RegisterExtension, error)
}
func (f *registerExtensionTestRPC) RegisterExtension(ctx context.Context, req *sliverpb.RegisterExtensionReq, opts ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
return f.register(ctx, req, opts...)
}
func newExtensionTestConsole(t *testing.T, rpc rpcpb.SliverRPCClient, session *clientpb.Session, beacon *clientpb.Beacon) *console.SliverClient {
t.Helper()
con := console.NewConsole(false)
con.IsCLI = true
con.Rpc = rpc
con.ActiveTarget.Set(session, beacon)
return con
}
func TestRemoveExtensionByManifestNameOnlyDeletesManagedExtensions(t *testing.T) {
clientRoot := t.TempDir()
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
originalManifests := loadedManifests
originalExtensions := loadedExtensions
t.Cleanup(func() {
loadedManifests = originalManifests
loadedExtensions = originalExtensions
})
tests := []struct {
name string
extPath string
wantDeleted bool
}{
{
name: "installed extension",
extPath: filepath.Join(clientRoot, "extensions", "installed"),
wantDeleted: true,
},
{
name: "temporarily loaded extension",
extPath: t.TempDir(),
wantDeleted: false,
},
{
name: "extensions directory sibling",
extPath: filepath.Join(clientRoot, "extensions-backup"),
wantDeleted: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := os.MkdirAll(test.extPath, 0o700); err != nil {
t.Fatalf("create extension directory: %v", err)
}
markerPath := filepath.Join(test.extPath, "source-code.txt")
if err := os.WriteFile(markerPath, []byte("keep me"), 0o600); err != nil {
t.Fatalf("create marker file: %v", err)
}
manifestName := "test-extension"
commandName := "test-command"
command := &ExtCommand{CommandName: commandName}
loadedManifests = map[string]*ExtensionManifest{
manifestName: {RootPath: test.extPath, ExtCommand: []*ExtCommand{command}},
}
loadedExtensions = map[string]*ExtCommand{commandName: command}
found, err := RemoveExtensionByManifestName(manifestName, nil)
if err != nil {
t.Fatalf("remove extension: %v", err)
}
if !found {
t.Fatal("expected extension manifest to be found")
}
if _, ok := loadedManifests[manifestName]; ok {
t.Error("manifest was not unloaded")
}
if _, ok := loadedExtensions[commandName]; ok {
t.Error("command was not unloaded")
}
_, err = os.Stat(markerPath)
if test.wantDeleted && !os.IsNotExist(err) {
t.Errorf("managed extension directory was not deleted: %v", err)
}
if !test.wantDeleted && err != nil {
t.Errorf("external extension directory was modified: %v", err)
}
})
}
}
func TestParseExtensionManifest(t *testing.T) {
expectedPath := util.ResolvePath("foo/test1.dll")
@@ -189,6 +302,218 @@ func TestParseExtensionManifest(t *testing.T) {
}
func TestConvertOldManifestPreservesInit(t *testing.T) {
const initExport = "InitializeExtension"
manifest, err := ParseExtensionManifest([]byte(`{
"name": "legacy-test",
"command_name": "legacy-test",
"help": "some help",
"init": "InitializeExtension",
"files": [
{
"os": "windows",
"arch": "amd64",
"path": "foo/test.dll"
}
]
}`))
if err != nil {
t.Fatalf("Error parsing legacy extension manifest: %s", err)
}
if len(manifest.ExtCommand) != 1 {
t.Fatalf("Expected 1 extension command, got %d", len(manifest.ExtCommand))
}
if manifest.ExtCommand[0].Init != initExport {
t.Errorf("Expected init export %q, got %q", initExport, manifest.ExtCommand[0].Init)
}
}
func TestRegisterExtensionPropagatesInit(t *testing.T) {
const initExport = "InitializeExtension"
tests := []struct {
name string
session *clientpb.Session
beacon *clientpb.Beacon
wantAsync bool
wantID string
}{
{
name: "session",
session: &clientpb.Session{ID: "session-id"},
wantID: "session-id",
},
{
name: "beacon",
beacon: &clientpb.Beacon{ID: "beacon-id"},
wantAsync: true,
wantID: "beacon-id",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotReq *sliverpb.RegisterExtensionReq
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, req *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
gotReq = req
return &sliverpb.RegisterExtension{Response: &commonpb.Response{}}, nil
},
}
con := newExtensionTestConsole(t, rpc, test.session, test.beacon)
err := registerExtension("linux", &ExtCommand{CommandName: "test", Init: initExport}, []byte("extension"), nil, con)
if err != nil {
t.Fatalf("RegisterExtension returned an error: %s", err)
}
if gotReq == nil {
t.Fatal("RegisterExtension RPC was not called")
}
if gotReq.Init != initExport {
t.Errorf("Expected init export %q, got %q", initExport, gotReq.Init)
}
if gotReq.Request.GetAsync() != test.wantAsync {
t.Errorf("Expected async=%t, got %t", test.wantAsync, gotReq.Request.GetAsync())
}
if test.wantAsync && gotReq.Request.GetBeaconID() != test.wantID {
t.Errorf("Expected beacon ID %q, got %q", test.wantID, gotReq.Request.GetBeaconID())
}
if !test.wantAsync && gotReq.Request.GetSessionID() != test.wantID {
t.Errorf("Expected session ID %q, got %q", test.wantID, gotReq.Request.GetSessionID())
}
})
}
}
func TestRegisterExtensionWaitsForBeaconRegistration(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
close(started)
<-release
return &sliverpb.RegisterExtension{Response: &commonpb.Response{Async: true}}, nil
},
}
con := newExtensionTestConsole(t, rpc, nil, &clientpb.Beacon{ID: "beacon-id"})
done := make(chan error, 1)
go func() {
done <- registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con)
}()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("Timed out waiting for RegisterExtension RPC")
}
select {
case err := <-done:
t.Fatalf("RegisterExtension returned before the beacon registration was enqueued: %v", err)
case <-time.After(100 * time.Millisecond):
}
close(release)
select {
case err := <-done:
if err != nil {
t.Fatalf("RegisterExtension returned an error: %s", err)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for RegisterExtension to return")
}
}
func TestRegisterExtensionReturnsBeaconRPCError(t *testing.T) {
wantErr := errors.New("register failed")
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
return nil, wantErr
},
}
con := newExtensionTestConsole(t, rpc, nil, &clientpb.Beacon{ID: "beacon-id"})
err := registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con)
if !errors.Is(err, wantErr) {
t.Fatalf("Expected error %q, got %v", wantErr, err)
}
}
func TestRegisterExtensionRejectsNilResponse(t *testing.T) {
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
return nil, nil
},
}
con := newExtensionTestConsole(t, rpc, &clientpb.Session{ID: "session-id"}, nil)
if err := registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con); err == nil {
t.Fatal("RegisterExtension accepted a nil response")
}
}
func TestExtensionCommandVisibilityUsesExactTargetPairs(t *testing.T) {
extension := &ExtCommand{Files: []*extensionFile{
{OS: "windows", Arch: "amd64"},
{OS: "linux", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
}}
tests := []struct {
name string
session *clientpb.Session
beacon *clientpb.Beacon
wantVisible bool
}{
{
name: "cross-platform Linux session",
session: &clientpb.Session{OS: "linux", Arch: "amd64"},
wantVisible: true,
},
{
name: "cross-platform macOS beacon",
beacon: &clientpb.Beacon{OS: "darwin", Arch: "arm64"},
wantVisible: true,
},
{
name: "cross-platform Windows session",
session: &clientpb.Session{OS: "windows", Arch: "amd64"},
wantVisible: true,
},
{
name: "Linux architecture mismatch",
session: &clientpb.Session{OS: "linux", Arch: "arm64"},
wantVisible: false,
},
{
name: "macOS architecture mismatch",
beacon: &clientpb.Beacon{OS: "darwin", Arch: "amd64"},
wantVisible: false,
},
{
name: "Windows architecture mismatch",
session: &clientpb.Session{OS: "windows", Arch: "386"},
wantVisible: false,
},
{
name: "unsupported native extension target",
beacon: &clientpb.Beacon{OS: "linux", Arch: "riscv64"},
wantVisible: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
con := newExtensionTestConsole(t, nil, test.session, test.beacon)
command := &cobra.Command{Annotations: makeCommandPlatformFilters(extension)}
activeFilters := con.App.Menu(consts.ImplantMenu).ActiveFiltersFor(command)
visible := len(activeFilters) == 0
if visible != test.wantVisible {
t.Fatalf("command visibility = %t, want %t (active filters: %v)", visible, test.wantVisible, activeFilters)
}
})
}
}
func TestParseMultipleCmdManifest(t *testing.T) {
expectedPath := util.ResolvePath("ex.dll")
@@ -323,3 +648,285 @@ func TestParseExtensionManifestErrors(t *testing.T) {
}
}
}
func TestParseExtensionManifestBOFExecutor(t *testing.T) {
tests := []struct {
name string
data string
want string
}{
{
name: "multi-command manifest",
data: `{
"name":"modern-bof",
"commands":[{
"command_name":"modern-bof",
"help":"test",
"bof_executor":"reflektor",
"files":[{"os":"windows","arch":"amd64","path":"modern.o"}]
}]
}`,
want: BOFExecutorReflektor,
},
{
name: "legacy manifest",
data: `{
"name":"legacy-bof",
"command_name":"legacy-bof",
"help":"test",
"bof_executor":"coff-loader",
"depends_on":"custom-loader",
"files":[{"os":"windows","arch":"amd64","path":"legacy.o"}]
}`,
want: BOFExecutorCOFFLoader,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest, err := ParseExtensionManifest([]byte(test.data))
if err != nil {
t.Fatalf("ParseExtensionManifest returned an error: %s", err)
}
if got := manifest.ExtCommand[0].BOFExecutor; got != test.want {
t.Fatalf("BOFExecutor = %q, want %q", got, test.want)
}
})
}
}
func TestParseExtensionManifestRejectsInvalidBOFExecutor(t *testing.T) {
tests := []struct {
name string
executor string
dependsOn string
wantErrPart string
}{
{
name: "executor must be exact lowercase",
executor: "Reflektor",
wantErrPart: "invalid `bof_executor`",
},
{
name: "unknown executor",
executor: "other-loader",
wantErrPart: "invalid `bof_executor`",
},
{
name: "coff loader requires dependency",
executor: BOFExecutorCOFFLoader,
wantErrPart: "no `depends_on` fallback",
},
{
name: "coff loader rejects whitespace dependency",
executor: BOFExecutorCOFFLoader,
dependsOn: " \t",
wantErrPart: "no `depends_on` fallback",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest := &ExtensionManifest{
Name: "test",
ExtCommand: []*ExtCommand{{
CommandName: "test",
Help: "test",
BOFExecutor: test.executor,
DependsOn: test.dependsOn,
Files: []*extensionFile{{
OS: "windows", Arch: "amd64", Path: "test.o",
}},
}},
}
err := validManifest(manifest)
if err == nil || !strings.Contains(err.Error(), test.wantErrPart) {
t.Fatalf("validManifest error = %v, want error containing %q", err, test.wantErrPart)
}
})
}
}
func TestPlanExtensionExecution(t *testing.T) {
tests := []struct {
name string
ext *ExtCommand
path string
capabilities uint64
want extensionExecutionMode
wantErr bool
}{
{
name: "native artifact remains native",
ext: &ExtCommand{CommandName: "native"},
path: "native.dll",
want: extensionExecutionNative,
},
{
name: "legacy manifest keeps dependency route",
ext: &ExtCommand{CommandName: "legacy", DependsOn: "coff-loader"},
path: "legacy.o",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionCOFFLoader,
},
{
name: "explicit coff loader is authoritative",
ext: &ExtCommand{CommandName: "coff", BOFExecutor: BOFExecutorCOFFLoader, DependsOn: "custom-loader"},
path: "coff.bin",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionCOFFLoader,
},
{
name: "explicit Reflektor uses capable target",
ext: &ExtCommand{CommandName: "builtin", BOFExecutor: BOFExecutorReflektor, DependsOn: "coff-loader"},
path: "builtin.o",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionReflektor,
},
{
name: "explicit Reflektor falls back to dependency",
ext: &ExtCommand{CommandName: "fallback", BOFExecutor: BOFExecutorReflektor, DependsOn: "coff-loader"},
path: "fallback.o",
want: extensionExecutionCOFFLoader,
},
{
name: "dependency-free BOF defaults to Reflektor",
ext: &ExtCommand{CommandName: "default"},
path: "default.o",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionReflektor,
},
{
name: "uppercase object extension is a BOF",
ext: &ExtCommand{CommandName: "uppercase"},
path: "uppercase.O",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionReflektor,
},
{
name: "unsupported dependency-free BOF fails closed",
ext: &ExtCommand{CommandName: "unsupported"},
path: "unsupported.o",
wantErr: true,
},
{
name: "coff loader without dependency fails closed",
ext: &ExtCommand{CommandName: "missing", BOFExecutor: BOFExecutorCOFFLoader},
path: "missing.o",
wantErr: true,
},
{
name: "invalid executor fails closed",
ext: &ExtCommand{CommandName: "invalid", BOFExecutor: "other"},
path: "invalid.o",
capabilities: sliverpb.CapabilityBOFV1,
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := planExtensionExecution(test.ext, test.path, test.capabilities)
if test.wantErr {
if err == nil {
t.Fatalf("planExtensionExecution returned mode %d, want error", got)
}
return
}
if err != nil {
t.Fatalf("planExtensionExecution returned an error: %s", err)
}
if got != test.want {
t.Fatalf("planExtensionExecution mode = %d, want %d", got, test.want)
}
})
}
}
func TestBuildCallExtensionRequestPreservesLegacyWireFields(t *testing.T) {
root := t.TempDir()
objectData := []byte("object-data")
loaderData := []byte("loader-data")
objectPath := filepath.Join(root, "object.o")
loaderPath := filepath.Join(root, "loader.dll")
if err := os.WriteFile(objectPath, objectData, 0o600); err != nil {
t.Fatalf("write object fixture: %s", err)
}
if err := os.WriteFile(loaderPath, loaderData, 0o600); err != nil {
t.Fatalf("write loader fixture: %s", err)
}
const loaderName = "custom-loader"
previousLoader, hadPreviousLoader := loadedExtensions[loaderName]
defer func() {
if hadPreviousLoader {
loadedExtensions[loaderName] = previousLoader
} else {
delete(loadedExtensions, loaderName)
}
}()
loaderManifest := &ExtensionManifest{Name: loaderName, RootPath: root}
loadedExtensions[loaderName] = &ExtCommand{
CommandName: loaderName,
Entrypoint: "RunCOFF",
Files: []*extensionFile{{
OS: "windows", Arch: "amd64", Path: filepath.Base(loaderPath),
}},
Manifest: loaderManifest,
}
legacyArgs := []byte{0x01, 0x02, 0x03, 0x04}
ext := &ExtCommand{CommandName: "test-bof", DependsOn: loaderName}
got, err := buildCallExtensionRequest("windows", "amd64", extensionExecutionCOFFLoader, ext, objectPath, "RunCOFF", legacyArgs)
if err != nil {
t.Fatalf("buildCallExtensionRequest returned an error: %s", err)
}
loaderHash := sha256.Sum256(loaderData)
want := &sliverpb.CallExtensionReq{
Name: hex.EncodeToString(loaderHash[:]),
Export: "RunCOFF",
Args: legacyArgs,
}
gotWire, err := proto.Marshal(got)
if err != nil {
t.Fatalf("marshal actual request: %s", err)
}
wantWire, err := proto.Marshal(want)
if err != nil {
t.Fatalf("marshal expected request: %s", err)
}
if !bytes.Equal(gotWire, wantWire) {
t.Fatalf("legacy request wire bytes changed:\n got: %x\nwant: %x", gotWire, wantWire)
}
}
func TestBuildCallExtensionRequestForReflektor(t *testing.T) {
root := t.TempDir()
objectData := []byte("object-data")
objectPath := filepath.Join(root, "object.o")
if err := os.WriteFile(objectPath, objectData, 0o600); err != nil {
t.Fatalf("write object fixture: %s", err)
}
typedArgs := []byte{0x05, 0x06}
got, err := buildCallExtensionRequest("windows", "amd64", extensionExecutionReflektor, &ExtCommand{CommandName: "test-bof"}, objectPath, "go", typedArgs)
if err != nil {
t.Fatalf("buildCallExtensionRequest returned an error: %s", err)
}
objectHash := sha256.Sum256(objectData)
if got.Name != hex.EncodeToString(objectHash[:]) {
t.Errorf("Name = %q, want object hash %q", got.Name, hex.EncodeToString(objectHash[:]))
}
if got.Export != "go" {
t.Errorf("Export = %q, want %q", got.Export, "go")
}
if !bytes.Equal(got.Args, typedArgs) {
t.Errorf("Args = %x, want %x", got.Args, typedArgs)
}
if !bytes.Equal(got.BOFData, objectData) {
t.Errorf("BOFData = %q, want %q", got.BOFData, objectData)
}
if !got.IsBOF {
t.Error("IsBOF = false, want true")
}
}
+29 -23
View File
@@ -40,7 +40,21 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
con.PrintErrorf("Extension path '%s' does not exist", extLocalPath)
return
}
InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
_ = InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
}
func readExtensionManifest(extLocalPath string, isGz bool) ([]byte, error) {
if isGz {
return util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
}
return os.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
}
func extensionPackageID(manifest *ExtensionManifest) string {
if manifest.PackageName != "" {
return manifest.PackageName
}
return manifest.Name
}
// InstallFromDir installs a Sliver extension from either a local directory or gzipped archive.
@@ -54,37 +68,28 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
// - con: Sliver console client for displaying status and error messages
// - isGz: Whether the source is a gzipped archive (true) or directory (false)
//
// The function will return early with error messages printed to console if:
// The function returns errors after printing them to the console if:
// - The manifest cannot be read or parsed
// - Required directories cannot be created
// - File copy operations fail
// - User declines overwrite when prompted
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) {
var manifestData []byte
var err error
if isGz {
manifestData, err = util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
} else {
manifestData, err = os.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
}
//
// Declining an overwrite prompt cancels the installation without an error.
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) error {
manifestData, err := readExtensionManifest(extLocalPath, isGz)
if err != nil {
con.PrintErrorf("Error reading %s: %s", ManifestFileName, err)
return
return fmt.Errorf("read %s: %w", ManifestFileName, err)
}
manifestF, err := ParseExtensionManifest(manifestData)
if err != nil {
con.PrintErrorf("Error parsing %s: %s", ManifestFileName, err)
return
return fmt.Errorf("parse %s: %w", ManifestFileName, err)
}
// Use package name if available, otherwise use extension name
// (Note, for v1 manifests this will actually be command_name)
packageID := manifestF.Name
if manifestF.PackageName != "" {
packageID = manifestF.PackageName
}
packageID := extensionPackageID(manifestF)
//create repo path
minstallPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(packageID))
@@ -94,7 +99,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
confirm := false
_ = forms.Confirm("Overwrite current install?", &confirm)
if !confirm {
return
return nil
}
}
forceRemoveAll(minstallPath)
@@ -103,13 +108,13 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
err = os.MkdirAll(minstallPath, 0o700)
if err != nil {
con.PrintErrorf("Error creating extension directory: %s\n", err)
return
return fmt.Errorf("create extension directory: %w", err)
}
err = os.WriteFile(filepath.Join(minstallPath, ManifestFileName), manifestData, 0o600)
if err != nil {
con.PrintErrorf("Failed to write %s: %s\n", ManifestFileName, err)
forceRemoveAll(minstallPath)
return
return fmt.Errorf("write %s: %w", ManifestFileName, err)
}
for _, manifest := range manifestF.ExtCommand {
@@ -125,7 +130,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
if err != nil {
con.PrintErrorf("\nError creating extension directory: %s\n", err)
forceRemoveAll(installPath)
return
return fmt.Errorf("create extension artifact directory: %w", err)
}
err = util.CopyFile(src, dst)
if err != nil {
@@ -135,11 +140,12 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
if err != nil {
con.PrintErrorf("Error installing command: %s\n", err)
forceRemoveAll(installPath)
return
return fmt.Errorf("install extension artifact: %w", err)
}
}
}
}
return nil
}
func installArtifact(extGzFilePath string, installPath string, artifactPath string) error {
+135
View File
@@ -0,0 +1,135 @@
package extensions
import (
"archive/tar"
"errors"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
"github.com/klauspost/compress/gzip"
"github.com/spf13/cobra"
)
type installArchiveMember struct {
name string
data []byte
}
const installTestManifest = `{
"name": "Test Extension",
"package_name": "test-extension",
"version": "v1.0.0",
"commands": [{
"command_name": "test-command",
"help": "test command",
"files": [{"os": "windows", "arch": "amd64", "path": "foo/test.dll"}]
}]
}`
func writeInstallArchive(t *testing.T, prefix string, members ...installArchiveMember) string {
t.Helper()
archivePath := filepath.Join(t.TempDir(), "extension.tar.gz")
archive, err := os.Create(archivePath)
if err != nil {
t.Fatal(err)
}
gzipWriter := gzip.NewWriter(archive)
tarWriter := tar.NewWriter(gzipWriter)
for _, member := range members {
header := &tar.Header{
Name: prefix + member.name,
Mode: 0o600,
Size: int64(len(member.data)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatal(err)
}
if _, err := tarWriter.Write(member.data); err != nil {
t.Fatal(err)
}
}
if err := tarWriter.Close(); err != nil {
t.Fatal(err)
}
if err := gzipWriter.Close(); err != nil {
t.Fatal(err)
}
if err := archive.Close(); err != nil {
t.Fatal(err)
}
return archivePath
}
func testInstallConsole(t *testing.T) *console.SliverClient {
t.Helper()
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(
con, nil, nil, nil, emptyCommands, emptyCommands, false, "",
); err != nil {
t.Fatal(err)
}
return con
}
func TestInstallFromDirAcceptsPrefixedAndUnprefixedArchives(t *testing.T) {
for _, prefix := range []string{"./", ""} {
name := "unprefixed"
if prefix != "" {
name = "prefixed"
}
t.Run(name, func(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
archivePath := writeInstallArchive(t, prefix,
installArchiveMember{name: ManifestFileName, data: []byte(installTestManifest)},
installArchiveMember{name: "foo/test.dll", data: []byte("extension data")},
)
if err := InstallFromDir(archivePath, false, testInstallConsole(t), true); err != nil {
t.Fatal(err)
}
installRoot := filepath.Join(assets.GetExtensionsDir(), "test-extension")
if _, err := os.Stat(filepath.Join(installRoot, ManifestFileName)); err != nil {
t.Fatalf("installed manifest: %v", err)
}
artifact, err := os.ReadFile(filepath.Join(installRoot, "foo", "test.dll"))
if err != nil {
t.Fatalf("installed artifact: %v", err)
}
if string(artifact) != "extension data" {
t.Fatalf("artifact data = %q", artifact)
}
})
}
}
func TestInstallFromDirReturnsMissingManifest(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
archivePath := writeInstallArchive(t, "./",
installArchiveMember{name: "foo/test.dll", data: []byte("extension data")},
)
err := InstallFromDir(archivePath, false, testInstallConsole(t), true)
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("error = %v, want fs.ErrNotExist", err)
}
}
func TestInstallFromDirReturnsMissingArtifactAndCleansUp(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
archivePath := writeInstallArchive(t, "./",
installArchiveMember{name: ManifestFileName, data: []byte(installTestManifest)},
)
err := InstallFromDir(archivePath, false, testInstallConsole(t), true)
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("error = %v, want fs.ErrNotExist", err)
}
installRoot := filepath.Join(assets.GetExtensionsDir(), "test-extension")
if _, err := os.Stat(installRoot); !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("install directory was not cleaned up: %v", err)
}
}
+162 -98
View File
@@ -54,6 +54,20 @@ const (
// ManifestFileName - Extension manifest file name.
ManifestFileName = "extension.json"
// BOFExecutorReflektor selects the built-in Reflektor BOF executor.
BOFExecutorReflektor = "reflektor"
// BOFExecutorCOFFLoader selects the manifest's depends_on COFF loader.
BOFExecutorCOFFLoader = "coff-loader"
)
type extensionExecutionMode uint8
const (
extensionExecutionNative extensionExecutionMode = iota
extensionExecutionCOFFLoader
extensionExecutionReflektor
)
var loadedExtensions = map[string]*ExtCommand{}
@@ -72,6 +86,7 @@ type ExtensionManifest_ struct {
Arguments []*extensionArgument `json:"arguments"`
Entrypoint string `json:"entrypoint"`
DependsOn string `json:"depends_on"`
BOFExecutor string `json:"bof_executor"`
Init string `json:"init"`
RootPath string `json:"-"`
@@ -100,6 +115,7 @@ type ExtCommand struct {
Arguments []*extensionArgument `json:"arguments"`
Entrypoint string `json:"entrypoint"`
DependsOn string `json:"depends_on"`
BOFExecutor string `json:"bof_executor"`
Init string `json:"init"`
Schema *packages.OutputSchema `json:"schema"`
@@ -218,9 +234,11 @@ func convertOldManifest(old *ExtensionManifest_) *ExtensionManifest {
{
CommandName: old.CommandName,
DependsOn: old.DependsOn,
BOFExecutor: old.BOFExecutor,
Help: old.Help,
LongHelp: old.LongHelp,
Entrypoint: old.Entrypoint,
Init: old.Init,
Files: old.Files,
Arguments: old.Arguments,
Schema: nil,
@@ -270,6 +288,15 @@ func validManifest(manifest *ExtensionManifest) error {
if extManifest.CommandName == "" {
return errors.New("missing `command_name` field in extension manifest")
}
switch extManifest.BOFExecutor {
case "", BOFExecutorReflektor:
case BOFExecutorCOFFLoader:
if strings.TrimSpace(extManifest.DependsOn) == "" {
return fmt.Errorf("command %q uses bof_executor %q but has no `depends_on` fallback", extManifest.CommandName, BOFExecutorCOFFLoader)
}
default:
return fmt.Errorf("command %q has invalid `bof_executor` %q (must be %q or %q)", extManifest.CommandName, extManifest.BOFExecutor, BOFExecutorReflektor, BOFExecutorCOFFLoader)
}
if len(extManifest.Files) == 0 {
return errors.New("missing `files` field in extension manifest")
}
@@ -471,44 +498,30 @@ func registerExtension(goos string, ext *ExtCommand, binData []byte, cmd *cobra.
//set extension name to a hash of the data to avoid loading more than one instance
bd := sha256.Sum256(binData)
name := hex.EncodeToString(bd[:])
sess, beac := con.ActiveTarget.GetInteractive()
sess, _ := con.ActiveTarget.GetInteractive()
ctrl := make(chan bool)
//first time run of an extension will require some waiting depending on the size
if sess != nil {
msg := fmt.Sprintf("Sending %s to implant ...", ext.CommandName)
con.SpinUntil(msg, ctrl)
}
//don't block if we are in beacon mode
if beac != nil && sess == nil {
go func() {
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
Name: name,
Data: binData,
OS: goos,
Init: ext.Init,
Request: con.ActiveTarget.Request(cmd),
})
if err != nil {
con.PrintErrorf("Error registering extension: %s\n", err)
}
if registerResp.Response != nil && registerResp.Response.Err != "" {
con.PrintErrorf("Error registering extension: %s\n", errors.New(registerResp.Response.Err))
}
}()
return nil
}
//session mode (hopefully)
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
Name: name,
Data: binData,
OS: goos,
Init: ext.Init,
Request: con.ActiveTarget.Request(cmd),
})
ctrl <- true
<-ctrl
if sess != nil {
ctrl <- true
<-ctrl
}
if err != nil {
return err
}
if registerResp == nil {
return errors.New("received empty register extension response")
}
if registerResp.Response != nil && registerResp.Response.Err != "" {
return errors.New(registerResp.Response.Err)
}
@@ -531,6 +544,89 @@ func loadDep(goos string, goarch string, depName string, cmd *cobra.Command, con
return fmt.Errorf("missing dependency %s", depName)
}
func planExtensionExecution(ext *ExtCommand, binPath string, capabilities uint64) (extensionExecutionMode, error) {
isBOF := ext.BOFExecutor != "" || strings.EqualFold(filepath.Ext(binPath), ".o")
if !isBOF {
return extensionExecutionNative, nil
}
dependsOn := strings.TrimSpace(ext.DependsOn)
switch ext.BOFExecutor {
case BOFExecutorCOFFLoader:
if dependsOn == "" {
return extensionExecutionNative, fmt.Errorf("BOF command %q selects %q but has no depends_on loader", ext.CommandName, BOFExecutorCOFFLoader)
}
return extensionExecutionCOFFLoader, nil
case "":
// Existing manifests infer the loader from depends_on. Keep that route
// even when the target supports the built-in executor.
if dependsOn != "" {
return extensionExecutionCOFFLoader, nil
}
case BOFExecutorReflektor:
default:
return extensionExecutionNative, fmt.Errorf("BOF command %q has invalid bof_executor %q", ext.CommandName, ext.BOFExecutor)
}
if capabilities&sliverpb.CapabilityBOFV1 != 0 {
return extensionExecutionReflektor, nil
}
if dependsOn != "" {
return extensionExecutionCOFFLoader, nil
}
return extensionExecutionNative, fmt.Errorf("target does not support built-in Reflektor BOF execution and command %q has no depends_on fallback", ext.CommandName)
}
func activeTargetCapabilities(session *clientpb.Session, beacon *clientpb.Beacon) uint64 {
if session != nil {
return session.Capabilities
}
if beacon != nil {
return beacon.Capabilities
}
return 0
}
func buildCallExtensionRequest(goos string, goarch string, mode extensionExecutionMode, ext *ExtCommand, binPath string, entryPoint string, extensionArgs []byte) (*sliverpb.CallExtensionReq, error) {
extData, err := os.ReadFile(binPath)
if err != nil {
return nil, err
}
bd := sha256.Sum256(extData)
name := hex.EncodeToString(bd[:])
if mode == extensionExecutionCOFFLoader {
loader, found := loadedExtensions[ext.DependsOn]
if !found {
return nil, fmt.Errorf("attempted to load non-existing extension: %s", ext.DependsOn)
}
loaderPath, err := loader.getFileForTarget(goos, goarch)
if err != nil {
return nil, fmt.Errorf("could not get file for extension %s: %w", ext.DependsOn, err)
}
loaderData, err := os.ReadFile(loaderPath)
if err != nil {
return nil, fmt.Errorf("dep read file error: %w", err)
}
if len(loaderData) == 0 {
return nil, fmt.Errorf("read empty file: %s", loaderPath)
}
bd = sha256.Sum256(loaderData)
name = hex.EncodeToString(bd[:])
}
req := &sliverpb.CallExtensionReq{
Name: name,
Export: entryPoint,
Args: extensionArgs,
}
if mode == extensionExecutionReflektor {
req.BOFData = extData
req.IsBOF = true
}
return req, nil
}
func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// The command runs with DisableFlagParsing, so args is the raw, unstripped
// slice. Separate Sliver-owned flags from the extension's own arguments and
@@ -570,27 +666,32 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
return
}
checkCache := session != nil
if err = loadExtension(goos, goarch, checkCache, ext, cmd, con); err != nil {
con.PrintErrorf("Could not load extension: %s\n", err)
return
}
binPath, err := ext.getFileForTarget(goos, goarch)
if err != nil {
con.PrintErrorf("Failed to read extension file: %s\n", err)
return
}
isBOF := filepath.Ext(binPath) == ".o"
executionMode, err := planExtensionExecution(ext, binPath, activeTargetCapabilities(session, beacon))
if err != nil {
con.PrintErrorf("Could not execute extension: %s\n", err)
return
}
if executionMode != extensionExecutionReflektor {
checkCache := session != nil
if err = loadExtension(goos, goarch, checkCache, ext, cmd, con); err != nil {
con.PrintErrorf("Could not load extension: %s\n", err)
return
}
}
// BOFs (Beacon Object Files) are a specific kind of extensions
// that require another extension (a COFF loader) to be present.
// that either use the built-in Reflektor executor or another extension.
// BOFs also have strongly typed arguments that need to be parsed in the proper way.
// This block will pack both the BOF data and its arguments into a single buffer that
// the loader will extract and load.
if isBOF {
// Beacon Object File -- requires a COFF loader
switch executionMode {
case extensionExecutionCOFFLoader:
// Legacy COFF loaders expect the entrypoint, object, and typed argument
// buffer packed into the existing outer argument envelope.
extensionArgs, err = getBOFArgs(cmd, args, binPath, ext)
if err != nil {
con.PrintErrorf("BOF args error: %s\n", err)
@@ -598,7 +699,15 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
}
extName = ext.DependsOn
entryPoint = loadedExtensions[extName].Entrypoint // should exist at this point
} else {
case extensionExecutionReflektor:
extensionArgs, err = ParseFlagArgumentsToBuffer(cmd, args, binPath, ext)
if err != nil {
con.PrintErrorf("BOF args error: %s\n", err)
return
}
extName = ext.CommandName
entryPoint = ext.Entrypoint
default:
// Regular DLL - Just join the arguments with spaces
if len(args) > 0 {
extensionArgs = []byte(strings.Join(args, " "))
@@ -612,45 +721,15 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
ctrl := make(chan bool)
msg := fmt.Sprintf("Executing %s ...", cmd.Name())
con.SpinUntil(msg, ctrl)
extdata, err := os.ReadFile(binPath)
callExtReq, err := buildCallExtensionRequest(goos, goarch, executionMode, ext, binPath, entryPoint, extensionArgs)
if err != nil {
con.PrintErrorf("ext read file error: %s\n", err)
ctrl <- true
<-ctrl
con.PrintErrorf("Could not prepare extension call: %s\n", err)
return
}
bd := sha256.Sum256(extdata)
name := hex.EncodeToString(bd[:])
if isBOF {
//if we are using a bof, we are actually calling the coffloader extension - so get the file from dep ref and use that shasum
if extension, found := loadedExtensions[ext.DependsOn]; found {
dep, err := extension.getFileForTarget(goos, goarch)
if err != nil {
con.PrintErrorf("could not get file for extension %s", ext.DependsOn)
return
}
depdata, err := os.ReadFile(dep)
if err != nil {
con.PrintErrorf("dep read file error: %s\n", err)
return
}
if len(depdata) == 0 {
con.PrintErrorf("read empty file: %s\n", dep)
return
}
bd = sha256.Sum256(depdata)
name = hex.EncodeToString(bd[:])
} else {
// handle error
con.PrintErrorf("attempted to load non-existing extension: %s", ext.DependsOn)
return
}
}
callExtResp, err := con.Rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
Name: name,
Export: entryPoint,
Args: extensionArgs,
Request: con.ActiveTarget.Request(cmd),
})
callExtReq.Request = con.ActiveTarget.Request(cmd)
callExtResp, err := con.Rpc.CallExtension(context.Background(), callExtReq)
ctrl <- true
<-ctrl
if err != nil {
@@ -865,37 +944,22 @@ func makeExtensionFlagNameCompletion(extCmd *ExtCommand) carapace.Action {
}
func makeCommandPlatformFilters(extCmd *ExtCommand) map[string]string {
filtersOS := make(map[string]bool)
filtersArch := make(map[string]bool)
var all []string
// Only add filters for architectures when there OS matters.
supportedTargets := make(map[string]struct{}, len(extCmd.Files))
for _, file := range extCmd.Files {
filtersOS[file.OS] = true
if filtersOS[file.OS] {
filtersArch[file.Arch] = true
}
supportedTargets[file.OS+"/"+file.Arch] = struct{}{}
}
for os, enabled := range filtersOS {
if enabled {
all = append(all, os)
// Console command filters are hide-on-match, so annotate the command with
// every exact target pair it does not support. A separate fallback hides all
// native extensions on target pairs outside the supported loader matrix.
filters := []string{consts.NativeExtensionUnsupportedTargetFilter}
for _, target := range consts.NativeExtensionTargets() {
if _, supported := supportedTargets[target.GOOS+"/"+target.GOARCH]; !supported {
filters = append(filters, target.Filter)
}
}
for arch, enabled := range filtersArch {
if enabled {
all = append(all, arch)
}
}
if len(all) == 0 {
return map[string]string{}
}
return map[string]string{
appConsole.CommandFilterKey: strings.Join(all, ","),
appConsole.CommandFilterKey: strings.Join(filters, ","),
}
}
+12 -15
View File
@@ -23,7 +23,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
@@ -100,21 +99,11 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
return true, nil
}
// If path is outside extensions directory, prompt for confirmation
if !strings.HasPrefix(extPath, assets.GetExtensionsDir()) {
confirm := false
_ = forms.Confirm(fmt.Sprintf("Remove '%s' extension directory from filesystem?", manifestName), &confirm)
if !confirm {
// Skip the forceRemoveAll but continue with the rest
delete(loadedManifests, manifestName)
for _, cmd := range man.ExtCommand {
delete(loadedExtensions, cmd.CommandName)
}
return true, nil
}
// Only installed extensions are owned by Sliver and may be deleted.
// Temporarily loaded extensions can live in arbitrary source directories.
if isWithinExtensionsDir(extPath) {
forceRemoveAll(extPath)
}
forceRemoveAll(extPath)
delete(loadedManifests, manifestName)
for _, cmd := range man.ExtCommand {
delete(loadedExtensions, cmd.CommandName)
@@ -124,6 +113,14 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
return false, nil
}
func isWithinExtensionsDir(extPath string) bool {
relPath, err := filepath.Rel(assets.GetExtensionsDir(), extPath)
if err != nil {
return false
}
return relPath != "." && filepath.IsLocal(relPath)
}
func forceRemoveAll(rootPath string) {
util.ChmodR(rootPath, 0o600, 0o700)
os.RemoveAll(rootPath)
+1
View File
@@ -87,6 +87,7 @@ var (
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
ErrNoExternalBuilder = errors.New("no external builders are available")
+1
View File
@@ -38,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.String("listen", defaultConfig.ListenAddress, "listen address for the MCP server")
f.String("name", defaultConfig.ServerName, "server name for MCP initialize")
f.String("version", defaultConfig.ServerVersion, "server version for MCP initialize")
f.BoolP("yes", "y", false, "skip the prompt-injection confirmation (broken or non-interactive terminals)")
})
flags.BindFlagCompletions(startCmd, func(comp *carapace.ActionMap) {
(*comp)["transport"] = carapace.ActionValues("http", "sse")
+3
View File
@@ -32,6 +32,9 @@ func McpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if status.AuthToken != "" {
con.Printf("Auth Token: %s\n", status.AuthToken)
}
if status.AuthConfigPath != "" {
con.Printf("Auth Config: %s\n", status.AuthConfigPath)
}
if status.Running && !status.StartedAt.IsZero() {
uptime := time.Since(status.StartedAt).Truncate(time.Second)
+6 -1
View File
@@ -28,8 +28,10 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}.WithDefaults()
msg := `Do you know what prompt injection is and are you an adult?`
if !settings.IsUserAnAdultWithPrompt(con, msg) {
skipConfirm, _ := cmd.Flags().GetBool("yes")
if !skipConfirm && !settings.IsUserAnAdultWithPrompt(con, msg) {
con.PrintErrorf("Failed to start MCP server, the user is not qualified to use feature\n")
con.PrintInfof("Use --yes to skip the confirmation prompt, or toggle it with 'settings autoadult'\n")
return
}
@@ -50,4 +52,7 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if status.AuthToken != "" {
con.PrintInfof("Auth Token: %s\n", status.AuthToken)
}
if status.AuthConfigPath != "" {
con.PrintInfof("Auth Config: %s\n", status.AuthConfigPath)
}
}
+6 -1
View File
@@ -50,7 +50,12 @@ func IsUserAnAdultWithPrompt(con *console.SliverClient, prompt string) bool {
return true
}
confirm := false
_ = forms.Confirm(prompt, &confirm)
if err := forms.Confirm(prompt, &confirm); err != nil {
// The interactive form is known to fail on some terminals (#2133);
// surface the error instead of silently treating it as a rejection.
con.PrintErrorf("Confirmation prompt failed: %s\n", err)
return false
}
return confirm
}
+7 -1
View File
@@ -61,7 +61,7 @@ func (con *SliverClient) SetConnection(rpc rpcpb.SliverRPCClient, grpcConn *grpc
con.BeaconTaskCallbacksMutex.Unlock()
con.ActiveTarget.Set(nil, nil)
if details != nil && details.Config != nil && details.Config.WG != nil {
if useDedicatedCommandConnection(details) {
con.backgroundDedicated = true
commandRPC, commandConn, err := transport.MTLSConnect(details.Config)
if err != nil {
@@ -110,6 +110,12 @@ func (con *SliverClient) SetConnection(rpc rpcpb.SliverRPCClient, grpcConn *grpc
return nil
}
func useDedicatedCommandConnection(details *ConnectionDetails) bool {
return details != nil &&
details.Config != nil &&
transport.MultiplayerConnectUsesWireGuard(details.Config)
}
// CloseConnection stops background loops and closes the current gRPC connection.
func (con *SliverClient) CloseConnection() error {
con.connMu.Lock()
+62
View File
@@ -0,0 +1,62 @@
package console
import (
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/transport"
)
func TestDedicatedCommandConnectionUsesEffectiveWireGuardMode(t *testing.T) {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
t.Cleanup(func() {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
})
legacyDetails := &ConnectionDetails{Config: completeWireGuardClientConfig(false)}
if useDedicatedCommandConnection(legacyDetails) {
t.Fatal("expected auto mode to keep an unmarked legacy config on direct mTLS")
}
enabledDetails := &ConnectionDetails{Config: completeWireGuardClientConfig(true)}
if !useDedicatedCommandConnection(enabledDetails) {
t.Fatal("expected auto mode to use a dedicated connection for an explicitly enabled config")
}
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectEnableWG)
if !useDedicatedCommandConnection(legacyDetails) {
t.Fatal("expected force-enable mode to use a dedicated connection with a complete legacy config")
}
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectDisableWG)
if useDedicatedCommandConnection(enabledDetails) {
t.Fatal("expected force-disable mode to keep an enabled config on direct mTLS")
}
}
func TestDedicatedCommandConnectionRejectsIncompleteEnabledConfig(t *testing.T) {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
t.Cleanup(func() {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
})
details := &ConnectionDetails{
Config: &assets.ClientConfig{
WG: &assets.ClientWGConfig{Enabled: true},
},
}
if useDedicatedCommandConnection(details) {
t.Fatal("expected an incomplete enabled config not to select a dedicated connection")
}
}
func completeWireGuardClientConfig(enabled bool) *assets.ClientConfig {
return &assets.ClientConfig{
WG: &assets.ClientWGConfig{
Enabled: enabled,
ServerPubKey: "server-pub",
ClientPrivateKey: "client-priv",
ClientIP: "100.64.0.2",
},
}
}
+2
View File
@@ -1147,6 +1147,7 @@ func (con *SliverClient) ExposeCommands() {
case con.ActiveTarget.session != nil:
session := con.ActiveTarget.session
filters = append(filters, consts.BeaconCmdsFilter)
filters = append(filters, consts.NativeExtensionTargetFilter(session.OS, session.Arch))
// Operating system
if session.OS != "windows" {
@@ -1161,6 +1162,7 @@ func (con *SliverClient) ExposeCommands() {
case con.ActiveTarget.beacon != nil:
beacon := con.ActiveTarget.beacon
filters = append(filters, consts.SessionCmdsFilter)
filters = append(filters, consts.NativeExtensionTargetFilter(beacon.OS, beacon.Arch))
// Operating system
if beacon.OS != "windows" {
+45
View File
@@ -363,8 +363,53 @@ const (
BeaconCmdsFilter = "beacon"
WindowsCmdsFilter = "windows"
WireguardCmdsFilter = "wireguard"
NativeExtensionUnsupportedTargetFilter = "native-extension:unsupported"
NativeExtensionDarwinAMD64Filter = "native-extension:darwin/amd64"
NativeExtensionDarwinARM64Filter = "native-extension:darwin/arm64"
NativeExtensionLinux386Filter = "native-extension:linux/386"
NativeExtensionLinuxAMD64Filter = "native-extension:linux/amd64"
NativeExtensionLinuxARM64Filter = "native-extension:linux/arm64"
NativeExtensionWindows386Filter = "native-extension:windows/386"
NativeExtensionWindowsAMD64Filter = "native-extension:windows/amd64"
NativeExtensionWindowsARM64Filter = "native-extension:windows/arm64"
)
// NativeExtensionTarget identifies a GOOS/GOARCH pair supported by native
// implant extensions and the command filter associated with that exact pair.
type NativeExtensionTarget struct {
GOOS string
GOARCH string
Filter string
}
var nativeExtensionTargets = [8]NativeExtensionTarget{
{GOOS: "darwin", GOARCH: "amd64", Filter: NativeExtensionDarwinAMD64Filter},
{GOOS: "darwin", GOARCH: "arm64", Filter: NativeExtensionDarwinARM64Filter},
{GOOS: "linux", GOARCH: "386", Filter: NativeExtensionLinux386Filter},
{GOOS: "linux", GOARCH: "amd64", Filter: NativeExtensionLinuxAMD64Filter},
{GOOS: "linux", GOARCH: "arm64", Filter: NativeExtensionLinuxARM64Filter},
{GOOS: "windows", GOARCH: "386", Filter: NativeExtensionWindows386Filter},
{GOOS: "windows", GOARCH: "amd64", Filter: NativeExtensionWindowsAMD64Filter},
{GOOS: "windows", GOARCH: "arm64", Filter: NativeExtensionWindowsARM64Filter},
}
// NativeExtensionTargets returns the supported native extension target matrix.
func NativeExtensionTargets() [8]NativeExtensionTarget {
return nativeExtensionTargets
}
// NativeExtensionTargetFilter returns the filter for an exact supported target.
// Unsupported pairs share a filter that hides all native extension commands.
func NativeExtensionTargetFilter(goos string, goarch string) string {
for _, target := range nativeExtensionTargets {
if target.GOOS == goos && target.GOARCH == goarch {
return target.Filter
}
}
return NativeExtensionUnsupportedTargetFilter
}
// Creds (needed here to avoid recursive imports).
const (
UserColonHashNewlineFormat = "user:hash" // username:hash\n
+15 -2
View File
@@ -11,8 +11,21 @@ import (
type MultiplayerConnectMode int
const (
MultiplayerConnectDirect MultiplayerConnectMode = iota
// MultiplayerConnectAuto follows the explicit WireGuard opt-in recorded in
// the operator config.
MultiplayerConnectAuto MultiplayerConnectMode = iota
// MultiplayerConnectEnableWG forces WireGuard for complete configs,
// including legacy configs that predate the explicit opt-in marker.
MultiplayerConnectEnableWG
// MultiplayerConnectDisableWG forces direct multiplayer mTLS.
MultiplayerConnectDisableWG
// MultiplayerConnectDirect is retained as a source-compatible alias for
// callers that explicitly select direct multiplayer mTLS.
MultiplayerConnectDirect = MultiplayerConnectDisableWG
// MultiplayerConnectRequireWG is retained for source compatibility.
// Deprecated: use MultiplayerConnectEnableWG.
MultiplayerConnectRequireWG = MultiplayerConnectEnableWG
)
type connectionCloser interface {
@@ -21,7 +34,7 @@ type connectionCloser interface {
var (
multiplayerConnectModeMu sync.RWMutex
multiplayerConnectMode = MultiplayerConnectDirect
multiplayerConnectMode = MultiplayerConnectAuto
connClosers sync.Map // *grpc.ClientConn -> connectionCloser
)
+21 -2
View File
@@ -104,17 +104,36 @@ func selectMultiplayerDialStrategy(config *assets.ClientConfig) (multiplayerDial
return multiplayerDialDirect, errors.New("client config is required")
}
switch getMultiplayerConnectMode() {
mode := getMultiplayerConnectMode()
switch mode {
case MultiplayerConnectDisableWG:
return multiplayerDialDirect, nil
case MultiplayerConnectEnableWG:
if err := validateWireGuardConfig(config); err != nil {
return multiplayerDialDirect, err
}
return multiplayerDialWireGuard, nil
case MultiplayerConnectAuto:
if config.WG == nil || !config.WG.Enabled {
return multiplayerDialDirect, nil
}
if err := validateWireGuardConfig(config); err != nil {
return multiplayerDialDirect, err
}
return multiplayerDialWireGuard, nil
default:
return multiplayerDialDirect, nil
return multiplayerDialDirect, fmt.Errorf("invalid multiplayer connect mode %d", mode)
}
}
// MultiplayerConnectUsesWireGuard reports whether the current mode and
// operator config select the WireGuard wrapper. Invalid WireGuard configs
// report false; the initial multiplayer dial returns the validation error.
func MultiplayerConnectUsesWireGuard(config *assets.ClientConfig) bool {
strategy, err := selectMultiplayerDialStrategy(config)
return err == nil && strategy == multiplayerDialWireGuard
}
func directMTLSConnect(config *assets.ClientConfig, statusFn ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
notifyConnectStatus(statusFn, connectStatusGRPCMTLS)
options, err := newMTLSDialOptions(config)
+67 -28
View File
@@ -10,8 +10,14 @@ import (
"google.golang.org/grpc/credentials/insecure"
)
func TestSelectMultiplayerDialStrategyLegacyConfigUsesDirectMTLS(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectDirect)
func TestMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
if mode := getMultiplayerConnectMode(); mode != MultiplayerConnectAuto {
t.Fatalf("expected auto to be the default multiplayer mode, got %v", mode)
}
}
func TestSelectMultiplayerDialStrategyAutoWithoutWGConfigUsesDirectMTLS(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{})
if err != nil {
@@ -22,15 +28,10 @@ func TestSelectMultiplayerDialStrategyLegacyConfigUsesDirectMTLS(t *testing.T) {
}
}
func TestSelectMultiplayerDialStrategyDefaultIgnoresIncompleteWGConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectDirect)
func TestSelectMultiplayerDialStrategyAutoLegacyWGConfigUsesDirectMTLS(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub",
ClientIP: "100.64.0.2",
},
})
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(false))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
@@ -39,6 +40,33 @@ func TestSelectMultiplayerDialStrategyDefaultIgnoresIncompleteWGConfig(t *testin
}
}
func TestSelectMultiplayerDialStrategyAutoEnabledWGConfigRequiresCompleteConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
_, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
WG: &assets.ClientWGConfig{
Enabled: true,
ServerPubKey: "server-pub",
ClientIP: "100.64.0.2",
},
})
if !errors.Is(err, ErrIncompleteWireGuardConfig) {
t.Fatalf("expected incomplete WG config error, got %v", err)
}
}
func TestSelectMultiplayerDialStrategyAutoEnabledWGConfigUsesWireGuard(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectAuto)
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(true))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialWireGuard {
t.Fatalf("expected WireGuard strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyEnableWGRejectsMissingWGConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectEnableWG)
@@ -62,16 +90,10 @@ func TestSelectMultiplayerDialStrategyEnableWGRejectsIncompleteWGConfig(t *testi
}
}
func TestSelectMultiplayerDialStrategyEnableWGUsesWireGuardWhenConfigComplete(t *testing.T) {
func TestSelectMultiplayerDialStrategyEnableWGUsesCompleteLegacyConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectEnableWG)
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub",
ClientPrivateKey: "client-priv",
ClientIP: "100.64.0.2",
},
})
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(false))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
@@ -80,21 +102,38 @@ func TestSelectMultiplayerDialStrategyEnableWGUsesWireGuardWhenConfigComplete(t
}
}
func TestSelectMultiplayerDialStrategyDefaultUsesDirectMTLSEvenWhenWGConfigComplete(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectDirect)
func TestSelectMultiplayerDialStrategyDisableWGOverridesEnabledConfig(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectDisableWG)
strategy, err := selectMultiplayerDialStrategy(&assets.ClientConfig{
WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub",
ClientPrivateKey: "client-priv",
ClientIP: "100.64.0.2",
},
})
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(true))
if err != nil {
t.Fatalf("select dial strategy: %v", err)
}
if strategy != multiplayerDialDirect {
t.Fatalf("expected direct strategy when WG is not explicitly enabled, got %v", strategy)
t.Fatalf("expected direct strategy, got %v", strategy)
}
}
func TestSelectMultiplayerDialStrategyRejectsInvalidMode(t *testing.T) {
setTestMultiplayerConnectMode(t, MultiplayerConnectMode(255))
strategy, err := selectMultiplayerDialStrategy(completeWGClientConfig(true))
if strategy != multiplayerDialDirect {
t.Fatalf("expected invalid mode to fall back to direct strategy, got %v", strategy)
}
if err == nil || err.Error() != "invalid multiplayer connect mode 255" {
t.Fatalf("expected invalid mode error, got %v", err)
}
}
func completeWGClientConfig(enabled bool) *assets.ClientConfig {
return &assets.ClientConfig{
WG: &assets.ClientWGConfig{
Enabled: enabled,
ServerPubKey: "server-pub",
ClientPrivateKey: "client-priv",
ClientIP: "100.64.0.2",
},
}
}
+14 -14
View File
@@ -22,7 +22,7 @@
"asciinema-player": "^3.15.1",
"fuse.js": "^7.3.0",
"lunr": "^2.3.9",
"mermaid": "^11.15.0",
"mermaid": "^11.16.1",
"next": "^16.2.6",
"next-themes": "^0.4.6",
"react": "^19.2.5",
@@ -1498,12 +1498,12 @@
}
},
"node_modules/@mermaid-js/parser": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz",
"integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz",
"integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==",
"license": "MIT",
"dependencies": {
"@chevrotain/types": "~11.1.1"
"@chevrotain/types": "~11.1.2"
}
},
"node_modules/@monaco-editor/loader": {
@@ -7707,26 +7707,26 @@
}
},
"node_modules/mermaid": {
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz",
"integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==",
"version": "11.16.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz",
"integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
"@braintree/sanitize-url": "^7.1.2",
"@iconify/utils": "^3.0.2",
"@mermaid-js/parser": "^1.1.1",
"@mermaid-js/parser": "^1.2.0",
"@types/d3": "^7.4.3",
"@upsetjs/venn.js": "^2.0.0",
"cytoscape": "^3.33.1",
"cytoscape": "^3.33.3",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.14",
"dayjs": "^1.11.19",
"dompurify": "^3.3.1",
"dayjs": "^1.11.20",
"dompurify": "^3.3.3",
"es-toolkit": "^1.45.1",
"katex": "^0.16.25",
"katex": "^0.16.45",
"khroma": "^2.1.0",
"marked": "^16.3.0",
"roughjs": "^4.6.6",
+1 -1
View File
@@ -26,7 +26,7 @@
"asciinema-player": "^3.15.1",
"fuse.js": "^7.3.0",
"lunr": "^2.3.9",
"mermaid": "^11.15.0",
"mermaid": "^11.16.1",
"next": "^16.2.6",
"next-themes": "^0.4.6",
"react": "^19.2.5",
@@ -168,6 +168,8 @@ As the alias support relies on Sliver side loading capabilities, please make sur
## Extensions
⚠️ Extensions are currently only supported when using Windows implants, though we're actively working on MacOS/Linux support.
Native extensions are supported by Windows implants (`386`, `amd64`, and `arm64`), Linux implants (`386`, `amd64`, and `arm64`), and macOS implants (`amd64` and `arm64`). Each manifest must include a platform-native shared library for every target it supports: `.dll` on Windows, `.so` on Linux, and `.dylib` on macOS.
Extensions are similar in structure to an alias, but internally work differently. An extension is an artifact of native code that is reflectively loaded by the implant and passed certain callbacks. These callbacks allow the extension to return data to the C2 server. Extensions may also have dependencies (other extensions), which Sliver will load prior to the extension; circular dependencies are not allowed. For example, all BOF extensions (`.o` files) rely on the COFF Loader extension (a `.dll`).
Native exports use the same Sliver extension ABI on every supported operating system. The export receives the argument buffer, its size, and a callback used to return output. The optional `init` export in the extension manifest is called once after the library is loaded and before any command export is invoked.
@@ -5,7 +5,7 @@ This document describes the technical design of Sliver.
There are four major components to the Sliver ecosystem:
- **Server Console -** The server console is the main interface, which is started when you run the `sliver-server` executable. The server console is a superset of the client console. All code is shared between the client/server consoles except server-specific commands related to client (i.e., operator) management. The server console talks over an in-memory gRPC interface to the server.
- **Sliver Server -** The Sliver server is also part of the `sliver-server` executable and manages the internal database, starts/stops network listeners (such as C2 listeners, though there are other types). The main interface used to interact with the server is the gRPC interface, thru which all functionality is implemented. By default the server will only start an in-memory gRPC listener that can only be communicated with from the server console. However, the gRPC interface can also be exposed to the network in multiplayer mode, either directly over mutual TLS (mTLS) or, by default, through a WireGuard-protected wrapper that carries the same gRPC/mTLS service.
- **Sliver Server -** The Sliver server is also part of the `sliver-server` executable and manages the internal database, starts/stops network listeners (such as C2 listeners, though there are other types). The main interface used to interact with the server is the gRPC interface, thru which all functionality is implemented. By default the server will only start an in-memory gRPC listener that can only be communicated with from the server console. However, the gRPC interface can also be exposed to the network in multiplayer mode, directly over mutual TLS (mTLS) by default or through an optional WireGuard-protected wrapper that carries the same gRPC/mTLS service.
- **Sliver Client -** The client console is the primary user interface that is used to interact with the Sliver server. Note that most of the code in the server console is actually the client console. The client console can also be compiled into a separate client binary file `sliver-client`, which is generally used to connect to the multiplayer gRPC network listener.
- **Implant -** The implant is the actual malicious code run on the target system you want remote access to.
@@ -17,8 +17,8 @@ There are four major components to the Sliver ecosystem:
└─────────┘ └───────────────┘
▲
│
│WireGuard (default)
│or direct gRPC/mTLS
│gRPC/mTLS (default)
│or optional WireGuard wrapper
│
┌────┴────┐
│ Sliver │
@@ -2,11 +2,11 @@ Sliver supports the loading and execution of BOFs and COFFs, generally no code c
### BOF Extensions
BOF support is provided via the [COFF Loader](https://github.com/sliverarmory/COFFLoader) extension, you'll need it installed to run pretty much any BOF. However, the COFF Loader will be installed automatically if you install a BOF extension from the [armory](/docs?name=Armory).
New Sliver implants execute BOFs with the built-in [Reflektor](https://github.com/sliverarmory/reflektor) executor by default. Armory packages can retain a [COFF Loader](https://github.com/sliverarmory/COFFLoader) dependency as a compatibility fallback for older clients, servers, and implants; Armory installs that dependency automatically.
The easiest way to install a BOF extension, for example [`nanodump`](https://github.com/sliverarmory/nanodump), is using the [armory](/docs?name=Armory) package manager:
**IMPORTANT:** BOF extensions are still installed per-sliver client for normal operator use, so they are not shared across operators automatically. If you want the server-side AI agent to use a BOF, copy the unpacked BOF directory into `~/.sliver/ai/extensions` on the server as well, and copy any dependency such as `coff-loader` into that same server-side AI store.
**IMPORTANT:** BOF extensions are still installed per-sliver client for normal operator use, so they are not shared across operators automatically. If you want the server-side AI agent to use a BOF, copy the unpacked BOF directory into `~/.sliver/ai/extensions` on the server as well. Copy its `depends_on` package too when the manifest retains a compatibility fallback or explicitly selects a COFF loader.
```
sliver > armory install nanodump
@@ -75,7 +75,7 @@ beacon_command_register(
| z | zero-terminated+encoded string | `string` |
| Z | zero-terminated wide-char string | `wstring` |
Looking at the script we can see the BOF requires a single integer argument. The corresponding Sliver `extension.json` file is shown below. Note that BOFs will always rely on the `coff-loader` extension but other kinds of extensions may not. If the `coff-loader` extension is not already installed on your system, it can be installed using `armory install coff-loader`. You can list installed extensions by simply running the `extensions` command with no arguments.
Looking at the script we can see the BOF requires a single integer argument. The corresponding Sliver `extension.json` file is shown below. This manifest prefers Reflektor while retaining `coff-loader` for old-component compatibility. If the loader is not already installed, Armory installs it with the BOF package.
```json
{
@@ -87,6 +87,7 @@ Looking at the script we can see the BOF requires a single integer argument. The
"repo_url": "https://github.com/sliverarmory/CredManBOF",
"help": "Dump credentials using the CredsBackupCredentials API",
"long_help": "",
"bof_executor": "reflektor",
"depends_on": "coff-loader",
"entrypoint": "go",
"files": [
@@ -112,10 +113,17 @@ Looking at the script we can see the BOF requires a single integer argument. The
}
```
`bof_executor` accepts two values:
- `reflektor` prefers built-in execution when both the server and implant advertise support. If `depends_on` is present, upgraded clients use it as the fallback for an older server or implant, while old clients ignore `bof_executor` and retain their existing loader behavior.
- `coff-loader` always uses the existing loader path. `depends_on` is required and names the concrete loader, including custom loaders.
When `bof_executor` is omitted, existing `.o` manifests with `depends_on` keep their legacy loader behavior. A dependency-free `.o` defaults to Reflektor and therefore requires a compatible client, server, and implant.
Once the manifest is defined load it into your client using `extensions load`, locally loaded extensions do not need to be cryptographically signed. The paths in the manifest should be relative to the manifest file, parent directories are not allowed.
If you want the server-side AI agent to discover and run the same BOF, copy that unpacked directory into `~/.sliver/ai/extensions/<name>/` so the `extension.json` file and `.o` artifact are available on the server.
More details can be found on the [Aliases & Extensions](/docs?name=Aliases+and+Extensions) page.
**IMPORTANT:** For BOF extensions to be properly detected by the Sliver client, the `path` must use the `.o` file extension.
**IMPORTANT:** Legacy manifests should keep the `.o` file extension. An explicit `bof_executor` also identifies the command as a BOF, but older clients still rely on the `.o` suffix.
@@ -55,6 +55,7 @@ cxx: {}
- `port` - Listen port for the multiplayer listener.
- `tailscale` - Enable Tailscale for daemon listener setup. When enabled, Sliver does not wrap multiplayer in its own WireGuard layer.
- `enable_wg` - Wrap multiplayer in WireGuard. If omitted or false, multiplayer is exposed directly over mTLS.
- The legacy `disable_wg` field from the withdrawn WireGuard-by-default release does not enable WireGuard and is normalized to `enable_wg: false`. Set `enable_wg: true` explicitly to opt in.
- `logs` - Server logging options.
- `level` - `logrus` level (`0`-`6`, clamped by Sliver). `4` is `INFO`, `5` is `DEBUG`, `6` is `TRACE`.
- `grpc_unary_payloads` - Log gRPC unary payloads.
@@ -142,21 +143,31 @@ For details on generating them, see [multiplayer mode](/docs?name=Multi-player+M
"lport": 31337,
"ca_certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
"private_key": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n",
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
"wg": {
"server_pub_key": "<multiplayer-wg-server-pubkey>",
"client_private_key": "<operator-wg-private-key>",
"client_pub_key": "<operator-wg-public-key>",
"client_ip": "100.65.0.10",
"server_ip": "100.65.0.1"
}
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"
}
```
**Note:** The `operator` field is mostly a local label in the client. Server-side identity is derived from the client certificate (`CommonName`) and token material generated by the server. If you need to rename an operator, generate a new client configuration.
The `wg` block is optional:
The default operator profile does not contain a `wg` block, and the client connects directly to the multiplayer mTLS listener.
- When present, `sliver-client` automatically brings up the multiplayer WireGuard wrapper and then dials the in-tunnel gRPC/mTLS service.
- When absent, the client connects directly to the multiplayer mTLS listener.
- Generate a WireGuard-enabled profile with `new-operator --enable-wg` or `sliver-server operator --enable-wg` when you want the multiplayer WireGuard wrapper.
Multiplayer WireGuard is opt-in. Generate a WireGuard-enabled profile with `new-operator --enable-wg` or `sliver-server operator --enable-wg`; this adds the following block:
```json
"wg": {
"enabled": true,
"server_pub_key": "<multiplayer-wg-server-pubkey>",
"client_private_key": "<operator-wg-private-key>",
"client_pub_key": "<operator-wg-public-key>",
"client_ip": "100.65.0.10",
"server_ip": "100.65.0.1"
}
```
The listener must also be started with `multiplayer --enable-wg` or `sliver-server daemon --enable-wg`. The official client reads the profile and automatically uses WireGuard only when `wg.enabled` is true; otherwise it connects directly. A profile from the withdrawn WireGuard-by-default release may contain a `wg` block without the `enabled` field. Such a profile stays in direct mode unless you deliberately force WireGuard with `sliver-client --enable-wg`.
The client flags override the profile selection:
- `sliver-client --enable-wg` forces WireGuard and requires usable tunnel settings in the selected profile.
- `sliver-client --disable-wg` forces a direct gRPC/mTLS connection even when `wg.enabled` is true.
- Without either flag, `sliver-client` follows `wg.enabled`. The two override flags cannot be used together.
@@ -2,18 +2,18 @@ Although the `sliver-client` is the default way to interact with a `sliver-serve
To do so, you can use [sliver-script](https://github.com/moloch--/sliver-script), [sliver-py](https://github.com/moloch--/sliver-py), or write a custom client in another language.
Current Sliver releases generate multiplayer operator configs with a `wg` block by default. The official `sliver-client`, and Go clients that reuse Sliver's `client/assets` plus `client/transport` packages, will automatically bring up that WireGuard wrapper before dialing the in-tunnel gRPC/mTLS service.
Sliver exposes multiplayer directly over gRPC/mTLS and generates direct operator configs by default. The multiplayer WireGuard wrapper is enabled explicitly on the listener and when the operator profile is generated. A WireGuard-enabled profile records that choice as `wg.enabled: true`.
If you are writing a custom client in another language, you have two options:
- Implement the multiplayer WireGuard wrapper described in [multiplayer mode](/docs?name=Multi-player+Mode), then connect to the in-tunnel gRPC/mTLS service.
- Keep multiplayer in direct mode by default, or explicitly opt into the wrapper with `multiplayer --enable-wg` or `sliver-server daemon --enable-wg`. Generate matching operator profiles with `new-operator --enable-wg` or `sliver-server operator --enable-wg` when you want the client-side WireGuard wrapper.
- Use the default direct gRPC/mTLS multiplayer mode, which works with a standard gRPC client and a default operator profile.
- Implement the multiplayer WireGuard wrapper described in [multiplayer mode](/docs?name=Multi-player+Mode). Opt into it with `multiplayer --enable-wg` or `sliver-server daemon --enable-wg`, generate a matching profile with `new-operator --enable-wg` or `sliver-server operator --enable-wg`, and honor `wg.enabled` when selecting the transport. A legacy profile that has a `wg` block but no `enabled` field should remain direct.
Once connected, the client/server API is still gRPC, so any language with gRPC support can in theory be used to create a custom client.
## Writing a Go client
This example uses Sliver's Go client libraries, so it works with either a direct multiplayer profile or a WireGuard-enabled profile.
This example uses Sliver's Go client libraries. `transport.MTLSConnect` automatically follows the selected profile in `transport.MultiplayerConnectAuto` mode: `wg.enabled: true` selects WireGuard, while a missing or disabled marker selects direct gRPC/mTLS. Applications that expose manual transport controls can call `transport.SetMultiplayerConnectMode` with `transport.MultiplayerConnectEnableWG` or `transport.MultiplayerConnectDisableWG` before connecting.
In this example, we will focus on writing a custom Go client that executes a new system command on every new implant that connects to the sliver server.
@@ -1,6 +1,6 @@
Sliver supports running in daemon mode, which automatically starts the multiplayer listener but does not provide an interactive console. To connect to a daemonized server, use [multiplayer mode](/docs?name=Multi-player+Mode) operator configs.
By default, daemon mode starts the same WireGuard-protected multiplayer listener used by the interactive `multiplayer` command. That means the outer listener is UDP on `31337` by default, while the authenticated gRPC/mTLS service stays inside the WireGuard tunnel.
By default, daemon mode exposes the multiplayer listener directly over TCP gRPC/mTLS on port `31337`. The multiplayer WireGuard wrapper is opt-in with `--enable-wg` or `daemon.enable_wg: true`.
There are two ways to run in daemon mode:
@@ -49,7 +49,7 @@ Since daemon mode does not provide an interactive server console, generate opera
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
```
The `operator` CLI generates direct multiplayer profiles by default. Add `--enable-wg` when the daemon is running with the WireGuard wrapper.
The `operator` CLI generates direct multiplayer profiles by default. When the daemon is running with the WireGuard wrapper, add `--enable-wg` while generating the profile. That profile contains `wg.enabled: true`, so `sliver-client` selects WireGuard automatically. Use the client's `--enable-wg` or `--disable-wg` only when you need to override the profile.
### Shutdown Behavior
@@ -21,6 +21,10 @@ Any `sliver-server` binary can be started as a builder process using [operator c
./sliver-server builder -c operator-multiplayer.cfg
```
Builders follow the operator profile automatically: direct profiles use gRPC/mTLS, while profiles generated with `new-operator --enable-wg` (or `sliver-server operator --enable-wg`) contain `wg.enabled: true` and use WireGuard. No builder flag is required for a correctly generated profile.
Use `--enable-wg` to force WireGuard for a profile with tunnel settings, or `--disable-wg` to force a direct connection; the flags cannot be combined. Profiles from the withdrawn WireGuard-by-default release may have a `wg` block without `enabled`; builders treat those profiles as direct unless WireGuard is forced explicitly.
When started as a builder, the Sliver process will mirror log output to stdout by default, however this can be disabled (see `sliver-server builder --help`).
**⚠️ IMPORTANT:** Builders must have unique names, by default the builder's hostname will be used, but this can be changed using the `--name` cli flag.
@@ -4,7 +4,7 @@ https://sliver.sh/install
This script should work on Kali, Ubuntu, and RHEL (CentOS, etc) distributions of Linux.
**⚠️ OPSEC:** By default the Linux install script will bind the multiplayer listener to `:31337` on all interfaces. In current releases that is the WireGuard-protected multiplayer listener, so the outer service is UDP/31337 and the authenticated gRPC/mTLS server only exists inside the tunnel. Ensure your firewalls are properly configured if this is a concern, or reconfigure the server to bind to localhost if you only wish to allow local users. Publicly exposing the multiplayer listener still makes the server easier to discover and fingerprint.
**⚠️ OPSEC:** By default the Linux install script binds the direct multiplayer gRPC/mTLS listener to TCP `:31337` on all interfaces. WireGuard is not enabled by the installer. Ensure your firewalls are properly configured, or reconfigure the server to bind to localhost if you only wish to allow local users. Publicly exposing the multiplayer listener makes the server easier to discover and fingerprint.
### One Liner
@@ -10,8 +10,7 @@ Sliver exposes the operator-facing multiplayer listener directly over gRPC/mTLS
└──────────────────┘
▲
│
UDP/WireGuard (default) │
gRPC/mTLS inside │
TCP/gRPC/mTLS (default) │
┌────────────┬────────┴─────┬───────────┐
│ │ │ │
┌─────┴──┐ │ │ ┌──┴─────┐
@@ -21,7 +20,7 @@ Sliver exposes the operator-facing multiplayer listener directly over gRPC/mTLS
└────────┘ └────────┘
```
## Default WireGuard-Protected Mode
## Default Direct mTLS Mode
Operators will need to download their preferred platform's client software from the releases page. Clients can connect to servers of differing platforms (e.g. a Windows client can connect to a Linux server). Operators and servers authenticate using Mutual TLS, however all the certificates are managed automatically for you.
@@ -32,11 +31,9 @@ Operators need a configuration file generated by the Sliver server to connect in
In the default mode:
- `multiplayer` exposes a WireGuard listener on `--lport` (default `31337`) over UDP.
- The authenticated gRPC/mTLS server only listens inside that WireGuard tunnel.
- `new-operator` generates a per-operator WireGuard keypair and tunnel IP, then embeds that information in the operator config's `wg` block.
- The multiplayer listener uses WireGuard keys that are separate from Sliver's implant `wg` C2 listener.
- `sliver-client` automatically uses the WireGuard wrapper when the imported config contains that `wg` block.
- `multiplayer` exposes the authenticated gRPC/mTLS listener directly over TCP on `--lport` (default `31337`).
- `new-operator` generates a direct multiplayer profile without a `wg` block.
- `sliver-client` connects directly over gRPC/mTLS because the selected profile does not opt into WireGuard.
From the server, start the multiplayer listener and generate an operator config with the server's public address or hostname in `--lhost`. Use `--permissions all` for a standard interactive operator profile:
@@ -66,28 +63,50 @@ $ ./sliver-client
localhost
```
## Direct mTLS Mode
## Optional WireGuard Wrapper
If you want the old behavior, or you need compatibility with third-party gRPC clients that do not implement the multiplayer WireGuard wrapper, disable it on both the listener and the generated operator profile:
Multiplayer WireGuard is opt-in. Enable it explicitly on the listener and include matching WireGuard settings in the operator profile:
```
[server] sliver > multiplayer
[server] sliver > multiplayer --enable-wg
[*] Multiplayer mode enabled!
[server] sliver > new-operator --name tester --lhost 1.2.3.4 --permissions all
[server] sliver > new-operator --name tester --lhost 1.2.3.4 --permissions all --enable-wg
[*] Generating new client certificate, please wait ...
[*] Saved new client config to: /Users/tester/Desktop/tester_example.com.cfg
```
In direct mode:
Import the profile and start the client normally:
- Multiplayer is exposed directly over TCP on `--lport` (default `31337`).
- Generated operator configs omit the `wg` block.
- `sliver-client --enable-wg` opts into the multiplayer WireGuard wrapper when the operator config includes a `wg` block.
```
$ ./sliver-client import ./tester_example.com.cfg
The listener mode, operator config, and client flag need to match. A WireGuard-wrapped listener needs a `wg` block in the operator profile plus `sliver-client --enable-wg`. Direct multiplayer works without the `wg` block and without the flag.
$ ./sliver-client
```
In WireGuard mode:
- `multiplayer --enable-wg` exposes a WireGuard listener on `--lport` (default `31337`) over UDP.
- The authenticated gRPC/mTLS server only listens inside that WireGuard tunnel.
- `new-operator --enable-wg` generates a per-operator WireGuard keypair and tunnel IP, then embeds that information with `wg.enabled: true` in the operator profile.
- `sliver-client` sees `wg.enabled: true` and automatically uses the profile's WireGuard settings before dialing the in-tunnel gRPC/mTLS service.
- The multiplayer listener uses WireGuard keys that are separate from Sliver's implant `wg` C2 listener.
The listener mode and operator profile need to match. A WireGuard-wrapped listener needs a profile generated with `--enable-wg`; direct multiplayer uses a normal profile. No client flag is required in either case.
The client provides explicit overrides for troubleshooting and migration:
- `sliver-client --enable-wg` forces WireGuard and requires usable WireGuard settings in the profile.
- `sliver-client --disable-wg` forces direct gRPC/mTLS even when `wg.enabled` is true.
- Without either flag, the client follows `wg.enabled`. The flags cannot be used together.
Profiles from the withdrawn WireGuard-by-default release may contain a `wg` block without the new `enabled` marker. The client treats those profiles as direct rather than considering block presence an opt-in. To keep using WireGuard, explicitly recreate the listener and profile with `--enable-wg`. The client override can temporarily force a legacy profile only when its listener is also running in WireGuard mode.
When upgrading from the withdrawn WireGuard-by-default release, legacy persisted multiplayer jobs restart in direct mode because they do not contain an explicit WireGuard opt-in. Recreate the listener with `multiplayer --enable-wg` if you intentionally want the wrapper.
Upgrade affected clients along with the server. The withdrawn client automatically selected WireGuard whenever a `wg` block was present, so it can mismatch a listener that the new server safely migrated to direct mode. Until that client is upgraded, use its `--disable-wg` override for a migrated direct listener, or explicitly re-enable WireGuard on both the listener and profile.
### Server CLI / Daemon Mode Multiplayer
@@ -99,7 +118,7 @@ Use the server CLI to generate operator configuration files:
./sliver-server operator --name zer0cool --lhost 1.2.3.4 --permissions all --save zer0cool.cfg
```
Operator profiles omit the multiplayer `wg` block by default. Add `--enable-wg` when the listener is wrapped in WireGuard.
Operator profiles omit the multiplayer `wg` block by default. Add `--enable-wg` when the listener is wrapped in WireGuard; the resulting `wg.enabled: true` marker makes the client select the wrapper automatically.
The installation script places the `sliver-server` binary in `/root` by default.
@@ -107,7 +126,7 @@ The installation script places the `sliver-server` binary in `/root` by default.
Sliver supports multiplayer integration with [Tailscale](https://tailscale.com/), which is an excellent option when you want to keep the multiplayer listener off the public Internet.
Tailscale mode is separate from the default multiplayer WireGuard wrapper. When you use `multiplayer -T`, Sliver exposes the listener only through Tailscale instead of layering its own multiplayer WireGuard tunnel on top.
Tailscale mode is separate from the optional multiplayer WireGuard wrapper. When you use `multiplayer -T`, Sliver exposes the listener only through Tailscale instead of layering its own multiplayer WireGuard tunnel on top.
```
$ export TS_AUTHKEY=<tailscale authkey>
@@ -61,14 +61,14 @@ pip3 install sliver-py
Since our extension is essentially going to be another client connection to the sliver server, you’ll also need to enable multiplayer mode and generate a new profile.
Current Sliver releases default to a WireGuard-protected multiplayer listener. Because this tutorial uses a third-party Python client, we’ll force direct multiplayer mode so the profile only contains the plain gRPC/mTLS settings:
Sliver defaults to direct gRPC/mTLS multiplayer, which is compatible with this third-party Python client. Start the listener and generate the operator profile without the optional WireGuard flag:
```bash
[server] sliver > multiplayer --disable-wg
[server] sliver > multiplayer
[*] Multiplayer mode enabled!
[server] sliver > new-operator -n tester -l 127.0.0.1 --permissions all --disable-wg
[server] sliver > new-operator -n tester -l 127.0.0.1 --permissions all
[*] Generating new client certificate, please wait ...
[*] Saved new client config to: /Users/tester/tools/tester_127.0.0.1.cfg
+1 -1
View File
@@ -1 +1 @@
{"tutorials":[{"name":"1 - Getting Started","content":"`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n![Alt text](/images/Architecture.png)\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-In Next Check-In\n========== =============== =========== ================= ==========Line truncated
{"tutorials":[{"name":"1 - Getting Started","content":"`sliver-server` is the binary you want to use to run the Sliver C2 server, `sliver-client` is solely a client to connect to a Sliver C2 server. Sliver server also acts as a client on its own, so you don’t necessarily run sliver server and client separately.\n\nFirst time running Sliver will take a couple seconds as it's retrieving its dependencies. Consecutive executions will be much faster. Go ahead and launch the `sliver-server`.\n\n```asciinema\n{\"src\": \"/asciinema/startup.cast\", \"cols\": \"132\", \"rows\": \"28\", \"idleTimeLimit\": 8}\n```\n\nLet's take a couple minutes to discuss what Sliver actually is and how it's set up.\n\n![Alt text](/images/Architecture.png)\n\nNow that Sliver is running, lets generate and execute your first implant to try out some of the basic features of Sliver, for now we’re going to run everything on the local host.\n\nHere's what we're going to do: \n* Generate your implant using the `generate` command as shown below.\n* Start HTTP listener on port 80\n* Execute implant in a separate terminal\n\n```asciinema\n{\"src\": \"/asciinema/first-implant.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNow let’s select our implant and run our first command using the `use` command.\n\n```bash\n[server] sliver > use\n? Select a session or beacon: \nSESSION 1884a365 RELATED_EARDRUM [::1]:49153 test.local tester darwin/amd64\n[*] Active session RELATED_EARDRUM (1884a365-085f-4506-b28e-80c481730fd0)\n\n[server] sliver (RELATED_EARDRUM) > pwd\n\n[*] /Users/tester/tools\n```\n\nOnce you have reached this point, go ahead and explore some of the commands listed below. In each case, first check out the command's help using the **`-h`** flag then try it out!\n\n```bash\nExploring and interacting with the filesystem\n\nFilesystem\n cat Dump file to stdout\n cd Change directory\n cp Copy a file\n download Download a file\n grep Search for strings that match a regex within a file or directory\n head Grab the first number of bytes or lines from a file\n ls List current directory\n mkdir Make a directory\n mv Move or rename a file\n pwd Print working directory\n rm Remove a file or directory\n tail Grab the last number of bytes or lines from a file\n upload Upload a file\n```\n\n```asciinema\n{\"src\": \"/asciinema/filesystem.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nGetting some environmental information\n```bash\nInfo\n env List environment variables\n getgid Get session process GID\n getpid Get session pid\n getuid Get session process UID\n info Get session info\n ping Send round trip message to implant (does not use ICMP)\n whoami Get session user execution context\n```\nExecute a binary\n\n```asciinema\n{\"src\": \"/asciinema/execute.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nRunning an interactive shell\n\n```asciinema\n{\"src\": \"/asciinema/shell.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nNot all commands are supported for all operating systems or meant to be used on all operating systems, here are a few examples:\n\n```\nmount Get information on mounted filesystems (only for Windows and Linux)\nscreenshot Take a screenshot (only for Windows and Linux)\nmemfiles List current memfiles (Linux only)\n```\n\nThis is an open-source project if you see a bug please file an issue! Pull requests are always welcome."},{"name":"2 - Beacons vs Sessions","content":"Sliver implants support two types of connections, sessions and beacons.\n\nSessions use long-polling connections, which means they constantly maintain a single TCP connection open while communicating with the server. Beacons on the other hand call back periodically and will sleep when not active which can help keep their presence hidden. You could limit callbacks to once every 6 hours for example and only 'activate' the beacon when needed by switching its sleeping time to a shorter duration.\n\nTypically during an engagement you will want to deploy a beacon on the target system and switch to a session while doing more active enumeration.\n\nLet’s start with generating and deploying a beacon using `http`.\n\n```asciinema\n{\"src\": \"/asciinema/beacon_generation.cast\", \"cols\": \"132\", \"rows\": \"14\", \"idleTimeLimit\": 8}\n```\n\nYou can see the beacon callback times either in the `info` command or using `beacons watch`.\n\n```bash\n[server] sliver > beacons watch\n\n ID Name Transport Username Operating System Last Check-In Next Check-In\n========== =============== =========== ================= ==========Line truncated
+31 -8
View File
@@ -125,19 +125,29 @@ run_test_cmd() {
should_skip_package() {
local pkg="$1"
local tags="${2:-}"
local go_list_cmd=(go list -e -f '{{if .Error}}{{.Error}}{{end}}')
if [ -n "$tags" ]; then
go_list_cmd+=("-tags=$tags")
fi
go_list_cmd+=("$pkg")
local go_list_error
go_list_error="$("${go_list_cmd[@]}" 2>/dev/null || true)"
local test_file_counts
if [ -n "$tags" ]; then
go_list_error="$(go list -e -f '{{if .Error}}{{.Error}}{{end}}' "-tags=$tags" "$pkg" 2>/dev/null || true)"
test_file_counts="$(go list -e -f '{{len .TestGoFiles}} {{len .XTestGoFiles}}' "-tags=$tags" "$pkg" 2>/dev/null || true)"
else
go_list_error="$(go list -e -f '{{if .Error}}{{.Error}}{{end}}' "$pkg" 2>/dev/null || true)"
test_file_counts="$(go list -e -f '{{len .TestGoFiles}} {{len .XTestGoFiles}}' "$pkg" 2>/dev/null || true)"
fi
if [[ "$go_list_error" == *"build constraints exclude all Go files"* ]]; then
echo
echo "==> Skipping $pkg (unsupported on current platform)"
return 0
fi
# File discovery intentionally sees tests for every OS. Avoid forcing a
# package build when all of its tests are excluded by the current target's
# build constraints; those tests remain discoverable on their target OS.
if [ "$test_file_counts" = "0 0" ]; then
echo
echo "==> Skipping $pkg (no tests for current platform)"
return 0
fi
return 1
}
@@ -145,7 +155,7 @@ collect_test_dirs() {
if command -v rg >/dev/null 2>&1; then
rg --files -g '*_test.go'
else
find client implant server util -type f -name '*_test.go' -print
find client implant server test util -type f -name '*_test.go' -print
fi
}
@@ -169,6 +179,7 @@ done < <(collect_test_dirs | xargs -n1 dirname | sort -u)
CLIENT_TEST_PKGS=()
IMPLANT_TEST_PKGS=()
SERVER_UTIL_TEST_PKGS=()
E2E_SUPPORT_TEST_PKGS=()
for test_dir in "${TEST_DIRS[@]}"; do
pkg="./$test_dir"
@@ -185,6 +196,9 @@ for test_dir in "${TEST_DIRS[@]}"; do
./server/* | ./util*)
SERVER_UTIL_TEST_PKGS+=("$pkg")
;;
./test/e2e | ./test/e2e/*)
E2E_SUPPORT_TEST_PKGS+=("$pkg")
;;
esac
done
@@ -196,6 +210,15 @@ for pkg in "${CLIENT_TEST_PKGS[@]}"; do
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
done
## Comprehensive E2E support packages. The top-level lifecycle test skips
## unless a server binary is supplied; coverage/report unit tests run here.
for pkg in "${E2E_SUPPORT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
continue
fi
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
done
## Implant
for pkg in "${IMPLANT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg"; then
+10 -12
View File
@@ -12,11 +12,10 @@ require (
charm.land/bubbles/v2 v2.0.0
charm.land/glamour/v2 v2.0.1
charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.4
charm.land/lipgloss/v2 v2.0.6
filippo.io/age v1.3.1
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0
github.com/Ne0nd0g/go-clr v1.0.3
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/alecthomas/chroma v0.10.0
@@ -24,6 +23,7 @@ require (
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
github.com/ebitengine/purego v0.10.1
github.com/glebarez/sqlite v1.11.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
@@ -31,7 +31,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/hashicorp/yamux v0.1.2
github.com/jedib0t/go-pretty/v6 v6.7.8
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/klauspost/compress v1.18.4
github.com/lesnuages/go-winio v0.4.19
@@ -39,7 +39,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.32
github.com/miekg/dns v1.1.70
github.com/moloch--/asciicast v0.1.1
github.com/moloch--/sgn v0.0.5
github.com/moloch--/sgn v0.1.2
github.com/ncruces/go-sqlite3 v0.30.4
github.com/nikoksr/notify v1.5.0
github.com/openai/openai-go/v2 v2.7.1
@@ -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.7
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
@@ -63,7 +63,7 @@ require (
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/sys v0.46.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.44.0
golang.org/x/text v0.40.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
@@ -126,7 +126,7 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
@@ -232,8 +232,6 @@ require (
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/atotto/clipboard v0.1.4
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 // indirect
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
@@ -241,7 +239,7 @@ require (
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.7
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
@@ -281,12 +279,12 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/mark3labs/mcp-go v0.45.0
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
+22 -24
View File
@@ -12,8 +12,8 @@ charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q=
charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik=
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
@@ -49,14 +49,11 @@ firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw
firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb h1:c1Bb1n1wr2xgTHkNVxkUHBshff33m+IXgY6lE6oYttg=
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb/go.mod h1:fSQ9asCJK/lIGXUnoRwJNMXcnzel53AsZMCtlVBIn3k=
github.com/Binject/debug v0.0.0-20210225042342-c9b8b45728d2/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/Binject/debug v0.0.0-20210312092933-6277045c2fdf/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a h1:4c0nc0krv8eh7gD809n+swLaCuFyHpxdrxwx0ZmHvBw=
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 h1:T8z8Wz/fqaPPANF8Unv4LjWLoa0TAvsjYAsRpvSkOqs=
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563/go.mod h1:HMbzsKPz1sF7H4Hmeovh+d2PH2iKPYpAB4XMOVz8wmM=
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0 h1:1sdnMvqxxlMXsu8ku/Xrh6gi3V40ykksst5fd0lfWnc=
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0/go.mod h1:J3XDRlam5pPYca3i6EqgQ35GCCEoyxafpCbLkta0ozc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -114,10 +111,6 @@ github.com/atc0005/go-teams-notify/v2 v2.14.0 h1:7N+xw+COnYANLREaAveQ65rsNQ12nIZ
github.com/atc0005/go-teams-notify/v2 v2.14.0/go.mod h1:EECsWM2b0Hvoz7O+QdlsvyN2KCUOFQCGj8bUBXv3A3Q=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 h1:JjwxKkxzcBk4k8147g0eBQRCIy0UN1Be8AAv6RaIj4Q=
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0/go.mod h1:IbERvuyb387Hppp8hX0SQTFt/mkej8+OhuS8L0nC2CI=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 h1:cIAK2NNf2yafdgpFRNJrgZMwvy61BEVpGoHc2n4/yWs=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4/go.mod h1:SalMPBCab3yuID8nIhLfzwoBV+lBRyaC7NhuN8qL8xE=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
@@ -186,10 +179,10 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
@@ -274,6 +267,8 @@ github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 h1:uh1GSejOhVPR
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -462,8 +457,8 @@ github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFr
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=
github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ=
github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g=
github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
@@ -521,8 +516,8 @@ github.com/lesnuages/snitch v0.6.0 h1:vOao32MdYDHYDcTAq2CKszDh6Dn8Bv1Gmgc8uVh193
github.com/lesnuages/snitch v0.6.0/go.mod h1:RfXlAVJYL7gUoZ9VMMon4LqrPiAjC8x62Ntbse4a4s0=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
@@ -543,8 +538,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
@@ -580,6 +575,10 @@ github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a h1:Q69DxLWF+Z8oI
github.com/moloch--/readline v0.0.0-20260129035512-cb5b3e87d51a/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY=
github.com/moloch--/sgn v0.0.5 h1:92GFaRpe+353uhqzFiNKXKgmTiD4dBxnvb8gd4MaV/g=
github.com/moloch--/sgn v0.0.5/go.mod h1:FyWfCtdynywgLmVR71vTJztZ3WkGSkR91Y4EOijUutE=
github.com/moloch--/sgn v0.1.1 h1:ok67KAIkEpiatl6pesQAjpjqXUZwqI2l7CIJfXH4N0Q=
github.com/moloch--/sgn v0.1.1/go.mod h1:qrMzA8Tf9oiGZvWLaZvfKUetiUM61srSZdmq3gINies=
github.com/moloch--/sgn v0.1.2 h1:tWmhDNpiZfWBg5+3uPnfJDRBmB219TnVto/RESOqoFo=
github.com/moloch--/sgn v0.1.2/go.mod h1:qrMzA8Tf9oiGZvWLaZvfKUetiUM61srSZdmq3gINies=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -678,8 +677,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.7 h1:VpH5s9KXM5GOZXtT7Iz5tW1MdMu5YnFL+NnCOT5UtOQ=
github.com/sliverarmory/reflektor v0.0.7/go.mod h1:+lCyQy0xHVqx6D5nYbcz5XIDVtAGbDMIMU0Yc6Brb1s=
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=
@@ -927,7 +926,6 @@ golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -945,8 +943,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+2 -5
View File
@@ -4,14 +4,14 @@ go 1.26.6
require (
filippo.io/age v1.1.1
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0
github.com/Ne0nd0g/go-clr v1.0.3
github.com/ebitengine/purego v0.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/hashicorp/yamux v0.1.2
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.7
github.com/stretchr/testify v1.8.4
github.com/tetratelabs/wazero v1.7.1
github.com/things-go/go-socks5 v0.0.4
@@ -25,11 +25,8 @@ require (
)
require (
github.com/Binject/debug v0.0.0-20210225042342-c9b8b45728d2 // indirect
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 // indirect
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 // indirect
github.com/google/btree v1.1.2 // indirect
+4 -12
View File
@@ -1,22 +1,16 @@
filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg=
filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE=
github.com/Binject/debug v0.0.0-20210225042342-c9b8b45728d2 h1:8kQNJC9AxAaNs0JkXnWUfbNeDnIO1QLYYWjYqC6JEE4=
github.com/Binject/debug v0.0.0-20210225042342-c9b8b45728d2/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0 h1:1sdnMvqxxlMXsu8ku/Xrh6gi3V40ykksst5fd0lfWnc=
github.com/Binject/universal v0.0.0-20220519011857-bea739e758c0/go.mod h1:J3XDRlam5pPYca3i6EqgQ35GCCEoyxafpCbLkta0ozc=
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk=
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/Ne0nd0g/go-clr v1.0.3 h1:xt92wwuqY23ZSC7RuHD3mKu3K22Bk5NNbxI803vojK4=
github.com/Ne0nd0g/go-clr v1.0.3/go.mod h1:TKYSQ/5xT25EvBUttAlUrzpR8yHuI0qTRK495I5xG/I=
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0 h1:JjwxKkxzcBk4k8147g0eBQRCIy0UN1Be8AAv6RaIj4Q=
github.com/awgh/cppgo v0.0.0-20210224085512-3d24bca8edc0/go.mod h1:IbERvuyb387Hppp8hX0SQTFt/mkej8+OhuS8L0nC2CI=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 h1:cIAK2NNf2yafdgpFRNJrgZMwvy61BEVpGoHc2n4/yWs=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4/go.mod h1:SalMPBCab3yuID8nIhLfzwoBV+lBRyaC7NhuN8qL8xE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@@ -43,12 +37,11 @@ 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.7 h1:VpH5s9KXM5GOZXtT7Iz5tW1MdMu5YnFL+NnCOT5UtOQ=
github.com/sliverarmory/reflektor v0.0.7/go.mod h1:+lCyQy0xHVqx6D5nYbcz5XIDVtAGbDMIMU0Yc6Brb1s=
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=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
@@ -82,7 +75,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+6 -4
View File
@@ -2,13 +2,15 @@
## Overview
Extension host runtime allowing implants to load optional capabilities. Manages sandboxing, lifecycle, and communication with extension modules. Runtime components handle extension darwin, extension windows, memfs, and WASM for implant-side extension features.
Extension host runtime allowing implants to load optional capabilities. Manages native shared-library and WASM extension lifecycle, registration, and communication across supported implant platforms.
## Go Files
- `extension.go` – Core extension host that loads modules and brokers RPC communication.
- `extension_darwin.go` – macOS-specific stubs and build tags for extension support.
- `extension_windows.go` – Windows-specific integration for loading and managing extensions.
- `extension.go` – Thread-safe native extension registry and single-flight loading lifecycle.
- `extension_darwin.go` – macOS constructor for the shared Reflektor-backed Unix loader.
- `extension_linux.go` – Linux constructor for the shared Reflektor-backed Unix loader.
- `extension_unix.go` – CGO-free macOS/Linux loading, export invocation, and callback bridge.
- `extension_windows.go` – Windows-specific Reflektor integration for loading and invoking extensions.
- `memfs.go` – Implements the in-memory filesystem backing extension assets.
- `memfs_test.go` *(tests)* – Tests the in-memory filesystem behavior for extensions.
- `wasm.go` – Sets up the WASM runtime for implant extensions and handles module lifecycle.
+63 -2
View File
@@ -20,13 +20,31 @@ package extension
import (
"errors"
"sync"
// {{if .Config.Debug}}
"log"
// {{end}}
)
var extensions map[string]Extension
const (
// Success indicates that a native extension callback completed successfully.
Success = 0
// Failure indicates that a native extension callback failed.
Failure = 1
)
var (
extensions map[string]Extension
extensionLoads map[string]*extensionLoad
extensionsMu sync.RWMutex
)
type extensionLoad struct {
done chan struct{}
err error
waiters int
}
type Extension interface {
Load() error
@@ -36,10 +54,47 @@ type Extension interface {
}
func Add(e Extension) {
extensionsMu.Lock()
defer extensionsMu.Unlock()
extensions[e.GetID()] = e
}
// Register loads an extension once per ID and publishes it only after loading
// succeeds. Concurrent registrations for the same ID wait for the in-flight
// load instead of mapping duplicate libraries and allocating duplicate callback
// trampolines.
func Register(e Extension) error {
id := e.GetID()
extensionsMu.Lock()
if _, found := extensions[id]; found {
extensionsMu.Unlock()
return nil
}
if loading, found := extensionLoads[id]; found {
loading.waiters++
extensionsMu.Unlock()
<-loading.done
return loading.err
}
loading := &extensionLoad{done: make(chan struct{})}
extensionLoads[id] = loading
extensionsMu.Unlock()
err := e.Load()
extensionsMu.Lock()
if err == nil {
extensions[id] = e
}
loading.err = err
close(loading.done)
delete(extensionLoads, id)
extensionsMu.Unlock()
return err
}
func List() []string {
extensionsMu.RLock()
defer extensionsMu.RUnlock()
var extList []string
for id := range extensions {
extList = append(extList, id)
@@ -48,10 +103,15 @@ func List() []string {
}
func Run(extID string, funcName string, arguments []byte, callback func([]byte)) error {
if ext, found := extensions[extID]; found {
extensionsMu.RLock()
ext, found := extensions[extID]
extensionsMu.RUnlock()
if found {
return ext.Call(funcName, arguments, callback)
}
// {{if .Config.Debug}}
extensionsMu.RLock()
defer extensionsMu.RUnlock()
for id, ext := range extensions {
log.Printf("Extension '%s' (%s)", id, ext.GetArch())
}
@@ -61,4 +121,5 @@ func Run(extID string, funcName string, arguments []byte, callback func([]byte))
func init() {
extensions = make(map[string]Extension)
extensionLoads = make(map[string]*extensionLoad)
}
+4 -107
View File
@@ -18,113 +18,10 @@ package extension
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"runtime"
"sync"
"unsafe"
// {{if .Config.Debug}}
"log"
// {{end}}
"github.com/Binject/universal"
)
type DarwinExtension struct {
id string
data []byte
module *universal.Library
arch string
serverStore bool
init string
sync.Mutex
}
type extensionArguments struct {
inDataSize uintptr
inDataBuff uintptr
outDataSize uintptr
outDataBuff uintptr
}
// DarwinExtension is a Reflektor-backed native macOS extension.
type DarwinExtension = UnixExtension
// NewDarwinExtension creates a native macOS extension backed by Reflektor.
func NewDarwinExtension(data []byte, id string, arch string, init string) *DarwinExtension {
return &DarwinExtension{
id: id,
data: data,
arch: arch,
init: init,
}
}
func (d *DarwinExtension) GetID() string {
return d.id
}
func (d *DarwinExtension) GetArch() string {
return d.arch
}
func (d *DarwinExtension) Load() error {
var err error
d.Lock()
defer d.Unlock()
loader, err := universal.NewLoader()
if err != nil {
return err
}
d.module, err = loader.LoadLibrary(d.id, &d.data)
if err != nil {
return err
}
if d.init != "" {
if _, errInit := d.module.Call(d.init); errInit != nil {
return errInit
}
}
return nil
}
func (d *DarwinExtension) Call(export string, arguments []byte, onFinish func([]byte)) error {
// We currently have 2 issues with Darwin extensions:
// - cppgo (used by universal) fucks up when calling a function with more than 1 argument
// - we don't have Go callback support for the loaded extension,
// so we have to wait for the call to finish to get the results
// To circumvent these issues, we pass the extensionArguments structure
// as the only argument to the call, so we can pass args in and extract
// the result at the same time.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
extArgs := extensionArguments{}
if len(arguments) > 0 {
extArgs.inDataBuff = uintptr(unsafe.Pointer(&arguments[0]))
extArgs.inDataSize = uintptr(uint64(len(arguments)))
}
// {{if .Config.Debug}}
log.Printf("Calling %s, arg size: %d\n", export, extArgs.inDataSize)
// {{end}}
d.Lock()
defer d.Unlock()
_, err := d.module.Call(export, uintptr(unsafe.Pointer(&extArgs)))
if err != nil {
return err
}
// {{if .Config.Debug}}
log.Printf("%s done!\n", export)
// {{end}}
outData := new(bytes.Buffer)
outDataSize := int(extArgs.outDataSize)
// {{if .Config.Debug}}
log.Printf("Out data size: %d\n", outDataSize)
// {{end}}
for i := 0; i < outDataSize; i++ {
b := (*byte)(unsafe.Pointer(uintptr(i) + extArgs.outDataBuff))
outData.WriteByte(*b)
}
// We currently don't have a way to trigger a callback
// in the loaded code for Darwin.
if outData.Len() > 0 {
onFinish(outData.Bytes())
}
return nil
return newUnixExtension(data, id, arch, init)
}
@@ -0,0 +1,29 @@
//go:build 386 || amd64 || arm64
package extension
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// LinuxExtension is a Reflektor-backed native Linux extension.
type LinuxExtension = UnixExtension
// NewLinuxExtension creates a native Linux extension backed by Reflektor.
func NewLinuxExtension(data []byte, id string, arch string, init string) *LinuxExtension {
return newUnixExtension(data, id, arch, init)
}
@@ -0,0 +1,55 @@
//go:build !(386 || amd64 || arm64)
package extension
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "errors"
// LinuxExtension reports unsupported native-extension operations on Linux
// architectures without a Reflektor backend.
type LinuxExtension struct {
id string
arch string
}
// NewLinuxExtension creates an unsupported-architecture extension placeholder.
func NewLinuxExtension(_ []byte, id string, arch string, _ string) *LinuxExtension {
return &LinuxExtension{id: id, arch: arch}
}
// GetID returns the extension identifier.
func (l *LinuxExtension) GetID() string {
return l.id
}
// GetArch returns the extension architecture.
func (l *LinuxExtension) GetArch() string {
return l.arch
}
// Load reports that native extensions are unsupported on this architecture.
func (l *LinuxExtension) Load() error {
return errors.New("{{if .Config.Debug}} native extensions are not supported on this Linux architecture {{end}}")
}
// Call reports that native extensions are unsupported on this architecture.
func (l *LinuxExtension) Call(_ string, _ []byte, _ func([]byte)) error {
return errors.New("{{if .Config.Debug}} native extensions are not supported on this Linux architecture {{end}}")
}
+236
View File
@@ -0,0 +1,236 @@
package extension
import (
"errors"
"fmt"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
type testExtension struct {
id string
}
func (e *testExtension) Load() error {
return nil
}
func (e *testExtension) Call(_ string, arguments []byte, callback func([]byte)) error {
callback(arguments)
return nil
}
func (e *testExtension) GetID() string {
return e.id
}
func (e *testExtension) GetArch() string {
return "test"
}
type registeringTestExtension struct {
testExtension
loadCalls *atomic.Int32
loadErr error
started chan struct{}
release chan struct{}
}
func (e *registeringTestExtension) Load() error {
e.loadCalls.Add(1)
if e.started != nil {
close(e.started)
}
if e.release != nil {
<-e.release
}
return e.loadErr
}
func resetExtensionRegistry(t *testing.T) {
t.Helper()
extensionsMu.Lock()
previousExtensions := extensions
previousLoads := extensionLoads
extensions = make(map[string]Extension)
extensionLoads = make(map[string]*extensionLoad)
extensionsMu.Unlock()
t.Cleanup(func() {
extensionsMu.Lock()
extensions = previousExtensions
extensionLoads = previousLoads
extensionsMu.Unlock()
})
}
func waitForExtensionWaiters(t *testing.T, extensionID string, want int) {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
extensionsMu.RLock()
loading := extensionLoads[extensionID]
got := 0
if loading != nil {
got = loading.waiters
}
extensionsMu.RUnlock()
if got == want {
return
}
if time.Now().After(deadline) {
t.Fatalf("registration waiters = %d, want %d", got, want)
}
runtime.Gosched()
}
}
func TestExtensionRegistryConcurrentAccess(t *testing.T) {
resetExtensionRegistry(t)
const count = 64
errCh := make(chan error, count)
var waitGroup sync.WaitGroup
for index := 0; index < count; index++ {
index := index
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
id := fmt.Sprintf("extension-%d", index)
Add(&testExtension{id: id})
argument := []byte(id)
if err := Run(id, "run", argument, func(output []byte) {
if string(output) != id {
errCh <- fmt.Errorf("callback output = %q, want %q", output, id)
}
}); err != nil {
errCh <- err
}
_ = List()
}()
}
waitGroup.Wait()
close(errCh)
for err := range errCh {
t.Error(err)
}
if got := len(List()); got != count {
t.Fatalf("List returned %d extensions, want %d", got, count)
}
}
func TestRegisterLoadsExtensionOnlyOnce(t *testing.T) {
resetExtensionRegistry(t)
const extensionID = "duplicate-extension"
var loadCalls atomic.Int32
started := make(chan struct{})
release := make(chan struct{})
first := &registeringTestExtension{
testExtension: testExtension{id: extensionID},
loadCalls: &loadCalls,
started: started,
release: release,
}
const registrations = 16
errorsCh := make(chan error, registrations)
go func() {
errorsCh <- Register(first)
}()
<-started
for index := 1; index < registrations; index++ {
go func() {
errorsCh <- Register(&registeringTestExtension{
testExtension: testExtension{id: extensionID},
loadCalls: &loadCalls,
})
}()
}
waitForExtensionWaiters(t, extensionID, registrations-1)
select {
case err := <-errorsCh:
t.Fatalf("Register returned before the in-flight load completed: %v", err)
default:
}
close(release)
for index := 0; index < registrations; index++ {
if err := <-errorsCh; err != nil {
t.Fatalf("Register returned an error: %v", err)
}
}
if got := loadCalls.Load(); got != 1 {
t.Fatalf("Load called %d times, want 1", got)
}
if got := List(); len(got) != 1 || got[0] != extensionID {
t.Fatalf("List returned %q, want [%q]", got, extensionID)
}
if err := Register(&registeringTestExtension{
testExtension: testExtension{id: extensionID},
loadCalls: &loadCalls,
}); err != nil {
t.Fatalf("repeat Register returned an error: %v", err)
}
if got := loadCalls.Load(); got != 1 {
t.Fatalf("repeat Register called Load; total calls = %d, want 1", got)
}
}
func TestRegisterRetriesAfterFailedLoad(t *testing.T) {
resetExtensionRegistry(t)
const extensionID = "retry-extension"
var loadCalls atomic.Int32
wantErr := errors.New("load failed")
started := make(chan struct{})
release := make(chan struct{})
const registrations = 8
errorsCh := make(chan error, registrations)
go func() {
errorsCh <- Register(&registeringTestExtension{
testExtension: testExtension{id: extensionID},
loadCalls: &loadCalls,
loadErr: wantErr,
started: started,
release: release,
})
}()
<-started
for index := 1; index < registrations; index++ {
go func() {
errorsCh <- Register(&registeringTestExtension{
testExtension: testExtension{id: extensionID},
loadCalls: &loadCalls,
loadErr: errors.New("unexpected follower load"),
})
}()
}
waitForExtensionWaiters(t, extensionID, registrations-1)
close(release)
for index := 0; index < registrations; index++ {
if err := <-errorsCh; !errors.Is(err, wantErr) {
t.Fatalf("Register error = %v, want shared error %v", err, wantErr)
}
}
if got := loadCalls.Load(); got != 1 {
t.Fatalf("failed Load called %d times, want 1", got)
}
if got := List(); len(got) != 0 {
t.Fatalf("failed extension was published: %q", got)
}
if err := Register(&registeringTestExtension{
testExtension: testExtension{id: extensionID},
loadCalls: &loadCalls,
}); err != nil {
t.Fatalf("retry Register returned an error: %v", err)
}
if got := loadCalls.Load(); got != 2 {
t.Fatalf("Load called %d times, want 2", got)
}
}
+174
View File
@@ -0,0 +1,174 @@
//go:build (darwin && (amd64 || arm64)) || (linux && (386 || amd64 || arm64))
package extension
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"errors"
"runtime"
"sync"
"sync/atomic"
"unsafe"
// {{if .Config.Debug}}
"log"
// {{end}}
"github.com/ebitengine/purego"
reflektor "github.com/sliverarmory/reflektor/native"
)
type extensionCall struct {
onFinish func([]byte)
}
// UnixExtension is a Reflektor-backed native extension for supported Unix
// targets.
type UnixExtension struct {
id string
data []byte
module *reflektor.Library
arch string
init string
callback uintptr
active atomic.Pointer[extensionCall]
sync.Mutex
}
func newUnixExtension(data []byte, id string, arch string, init string) *UnixExtension {
return &UnixExtension{
id: id,
data: data,
arch: arch,
init: init,
}
}
// GetID returns the extension identifier.
func (u *UnixExtension) GetID() string {
return u.id
}
// GetArch returns the extension architecture.
func (u *UnixExtension) GetArch() string {
return u.arch
}
// Load maps the extension and invokes its optional initialization export.
func (u *UnixExtension) Load() error {
if len(u.data) == 0 {
return errors.New("{{if .Config.Debug}} extension data is empty {{end}}")
}
u.Lock()
defer u.Unlock()
if u.module != nil {
return errors.New("{{if .Config.Debug}} module already loaded {{end}}")
}
module, err := reflektor.LoadLibrary(u.data)
if err != nil {
return err
}
if u.init != "" {
// {{if .Config.Debug}}
log.Printf("Calling %s\n", u.init)
// {{end}}
if err := module.CallExport(u.init); err != nil {
_ = module.Close()
return err
}
}
callback, err := newUnixExtensionCallback(u)
if err != nil {
_ = module.Close()
return err
}
u.callback = callback
u.module = module
return nil
}
// Call invokes an extension export with the native extension callback ABI.
func (u *UnixExtension) Call(export string, arguments []byte, onFinish func([]byte)) error {
u.Lock()
defer u.Unlock()
if u.module == nil {
return errors.New("{{if .Config.Debug}} module not loaded {{end}}")
}
if uint64(len(arguments)) > uint64(^uint32(0)) {
return errors.New("{{if .Config.Debug}} extension arguments exceed uint32 size {{end}}")
}
var argumentsPtr uintptr
if len(arguments) > 0 {
argumentsPtr = uintptr(unsafe.Pointer(&arguments[0]))
}
argumentsSize := uintptr(uint32(len(arguments)))
call := &extensionCall{onFinish: onFinish}
u.active.Store(call)
defer u.active.Store(nil)
// {{if .Config.Debug}}
log.Printf("Calling %s, arguments addr: 0x%08x, args size: %08x\n", export, argumentsPtr, argumentsSize)
// {{end}}
_, err := u.module.CallExportWithArgs(export, argumentsPtr, argumentsSize, u.callback)
runtime.KeepAlive(arguments)
runtime.KeepAlive(call)
return err
}
func newUnixExtensionCallback(extension *UnixExtension) (callback uintptr, err error) {
defer func() {
if recovered := recover(); recovered != nil {
// {{if .Config.Debug}}
log.Printf("Failed to create extension callback: %v\n", recovered)
// {{end}}
callback = 0
err = errors.New("{{if .Config.Debug}} failed to create extension callback {{end}}")
}
}()
return purego.NewCallback(extension.extensionCallback), nil
}
func (u *UnixExtension) extensionCallback(data unsafe.Pointer, dataLen int32) (result int32) {
result = Failure
defer func() {
if recover() != nil {
result = Failure
}
}()
call := u.active.Load()
if call == nil || call.onFinish == nil {
return Failure
}
if dataLen == 0 {
return Success
}
if data == nil || dataLen < 0 {
return Failure
}
output := append([]byte(nil), unsafe.Slice((*byte)(data), int(dataLen))...)
call.onFinish(output)
return Success
}
@@ -0,0 +1,59 @@
//go:build (darwin && (amd64 || arm64)) || (linux && (386 || amd64 || arm64))
package extension
import (
"bytes"
"testing"
"unsafe"
)
func TestUnixExtensionCallbackCopiesOutput(t *testing.T) {
extension := &UnixExtension{}
var got []byte
call := &extensionCall{onFinish: func(output []byte) {
got = output
}}
extension.active.Store(call)
defer extension.active.Store(nil)
source := []byte("extension output")
result := extension.extensionCallback(unsafe.Pointer(&source[0]), int32(len(source)))
if result != Success {
t.Fatalf("extension callback returned %d, want %d", result, Success)
}
source[0] = 'X'
if !bytes.Equal(got, []byte("extension output")) {
t.Fatalf("extension callback output = %q, want an independent copy", got)
}
}
func TestUnixExtensionCallbackRejectsInvalidCalls(t *testing.T) {
extension := &UnixExtension{}
if result := extension.extensionCallback(nil, 1); result != Failure {
t.Fatalf("callback without an active call returned %d, want %d", result, Failure)
}
extension.active.Store(&extensionCall{onFinish: func([]byte) {}})
defer extension.active.Store(nil)
if result := extension.extensionCallback(nil, 1); result != Failure {
t.Fatalf("callback with a nil data pointer returned %d, want %d", result, Failure)
}
source := byte(0)
if result := extension.extensionCallback(unsafe.Pointer(&source), -1); result != Failure {
t.Fatalf("callback with a negative length returned %d, want %d", result, Failure)
}
}
func TestUnixExtensionCallbackContainsPanics(t *testing.T) {
extension := &UnixExtension{}
extension.active.Store(&extensionCall{onFinish: func([]byte) {
panic("callback failed")
}})
defer extension.active.Store(nil)
source := []byte("output")
if result := extension.extensionCallback(unsafe.Pointer(&source[0]), int32(len(source))); result != Failure {
t.Fatalf("panicking callback returned %d, want %d", result, Failure)
}
}
@@ -31,11 +31,6 @@ import (
reflektor "github.com/sliverarmory/reflektor/memmod"
)
const (
Success = 0
Failure = 1
)
type WindowsExtension struct {
id string
data []byte
+3 -1
View File
@@ -2,16 +2,18 @@
## Overview
Runtime message handlers that react to server instructions. Dispatches inbound RPC messages to feature-specific executors. Runtime components handle extensions WASM, handlers wireguard, handlers darwin, and handlers generic for implant-side handlers features.
Runtime message handlers that react to server instructions. Dispatches inbound RPC messages to feature-specific executors, including native and WASM extension lifecycle requests.
## Go Files
- `extensions-wasm.go` – Routes extension-related RPC messages to the WASM runtime.
- `handlers_extensions.go` – Shared Windows, macOS, and Linux native extension registration, invocation, and listing handlers.
- `handlers-wireguard.go` – Handles WireGuard control messages and state updates.
- `handlers.go` – Registers core handler functions and shared dispatch utilities.
- `handlers_darwin.go` – macOS-specific handler bindings and feature toggles.
- `handlers_generic.go` – Generic handler implementations used when no platform specialization is required.
- `handlers_linux.go` – Linux-focused handler logic and build tags.
- `handlers_unix.go` – Shared macOS and Linux permission-change handlers.
- `handlers_windows.go` – Windows-specific handler implementations.
- `kill-handlers.go` – Processes kill commands for sessions and tasks in a platform-agnostic way.
- `kill-handlers_windows.go` – Windows-specific kill command handling.
+8 -7
View File
@@ -444,12 +444,13 @@ func cdHandler(data []byte, resp RPCResponse) {
return
}
os.Chdir(cdReq.Path)
dir, err := os.Getwd()
pwd := &sliverpb.Pwd{Path: dir}
if err != nil {
resp([]byte{}, err)
return
chdirErr := os.Chdir(cdReq.Path)
dir, getwdErr := os.Getwd()
pwd := &sliverpb.Pwd{Path: dir, Response: &commonpb.Response{}}
if chdirErr != nil {
pwd.Response.Err = chdirErr.Error()
} else if getwdErr != nil {
pwd.Response.Err = getwdErr.Error()
}
// {{if .Config.Debug}}
@@ -984,7 +985,7 @@ func extractFiles(data []byte, path string, overwrite bool) (int, int, error) {
return filesWritten, filesSkipped, err
}
}
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
file.Close()
return filesWritten, filesSkipped, err
+65
View File
@@ -0,0 +1,65 @@
//go:build linux || darwin || windows
package handlers
import (
"errors"
"fmt"
"github.com/sliverarmory/reflektor/bof"
)
type bofObject interface {
Execute([]byte) ([]bof.Output, error)
Close() error
}
type bofLoadFunc func([]byte, bof.LoadOptions) (bofObject, error)
type bofExecuteFunc func([]byte, string, []byte) ([]byte, error)
func loadReflektorBOF(data []byte, options bof.LoadOptions) (bofObject, error) {
if options.EntryPoint == "" {
return bof.Load(data)
}
return bof.LoadWithOptions(data, options)
}
func executeBOF(data []byte, entryPoint string, args []byte) ([]byte, error) {
return executeBOFWithLoader(data, entryPoint, args, loadReflektorBOF)
}
func executeBOFWithLoader(data []byte, entryPoint string, args []byte, load bofLoadFunc) (output []byte, err error) {
options := bof.LoadOptions{}
if !isDefaultBOFEntryPoint(entryPoint) {
options.EntryPoint = entryPoint
}
object, err := load(data, options)
if err != nil {
return nil, fmt.Errorf("load BOF: %w", err)
}
defer func() {
if closeErr := object.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("close BOF: %w", closeErr))
}
}()
outputs, executeErr := object.Execute(args)
for _, record := range outputs {
output = append(output, record.Data...)
}
if executeErr != nil {
err = fmt.Errorf("execute BOF: %w", executeErr)
}
return output, err
}
func isDefaultBOFEntryPoint(entryPoint string) bool {
switch entryPoint {
case "", "go", "_go", "coffee", "_coffee":
return true
default:
return false
}
}
@@ -0,0 +1,114 @@
//go:build linux || darwin || windows
package handlers
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/sliverarmory/reflektor/bof"
)
type fakeBOFObject struct {
outputs []bof.Output
executeErr error
closeErr error
executeArgs []byte
executeCalls int
closeCalls int
}
func (object *fakeBOFObject) Execute(args []byte) ([]bof.Output, error) {
object.executeCalls++
object.executeArgs = append([]byte(nil), args...)
return object.outputs, object.executeErr
}
func (object *fakeBOFObject) Close() error {
object.closeCalls++
return object.closeErr
}
func TestExecuteBOFUsesDefaultEntryPointLookup(t *testing.T) {
for _, entryPoint := range []string{"", "go", "_go", "coffee", "_coffee"} {
t.Run(entryPoint, func(t *testing.T) {
object := &fakeBOFObject{}
var gotOptions bof.LoadOptions
_, err := executeBOFWithLoader([]byte("object"), entryPoint, nil, func(_ []byte, options bof.LoadOptions) (bofObject, error) {
gotOptions = options
return object, nil
})
if err != nil {
t.Fatalf("executeBOF() error = %v", err)
}
if gotOptions.EntryPoint != "" {
t.Fatalf("entry point option = %q, want default lookup", gotOptions.EntryPoint)
}
if object.closeCalls != 1 {
t.Fatalf("Close() calls = %d, want 1", object.closeCalls)
}
})
}
}
func TestExecuteBOFUsesExactCustomEntryPointAndConcatenatesOutput(t *testing.T) {
wantArgs := []byte{4, 0, 0, 0, 1, 2, 3, 4}
object := &fakeBOFObject{outputs: []bof.Output{
{Type: bof.OutputDefault, Data: []byte("first")},
{Type: bof.OutputError, Data: []byte("second")},
}}
var gotOptions bof.LoadOptions
output, err := executeBOFWithLoader([]byte("object"), " CustomExport ", wantArgs, func(_ []byte, options bof.LoadOptions) (bofObject, error) {
gotOptions = options
return object, nil
})
if err != nil {
t.Fatalf("executeBOF() error = %v", err)
}
if gotOptions.EntryPoint != " CustomExport " {
t.Fatalf("entry point option = %q, want exact custom symbol", gotOptions.EntryPoint)
}
if !bytes.Equal(object.executeArgs, wantArgs) {
t.Fatalf("Execute() args = %v, want %v", object.executeArgs, wantArgs)
}
if !bytes.Equal(output, []byte("firstsecond")) {
t.Fatalf("output = %q, want ordered concatenation", output)
}
if object.executeCalls != 1 || object.closeCalls != 1 {
t.Fatalf("Execute() calls = %d, Close() calls = %d, want 1 each", object.executeCalls, object.closeCalls)
}
}
func TestExecuteBOFReportsExecuteAndCloseErrors(t *testing.T) {
object := &fakeBOFObject{
outputs: []bof.Output{
{Data: []byte("partial")},
},
executeErr: errors.New("execute failed"),
closeErr: errors.New("close failed"),
}
output, err := executeBOFWithLoader([]byte("object"), "go", nil, func([]byte, bof.LoadOptions) (bofObject, error) {
return object, nil
})
if !bytes.Equal(output, []byte("partial")) {
t.Fatalf("output = %q, want partial output", output)
}
if err == nil || !strings.Contains(err.Error(), "execute failed") || !strings.Contains(err.Error(), "close failed") {
t.Fatalf("error = %v, want execute and close failures", err)
}
if object.closeCalls != 1 {
t.Fatalf("Close() calls = %d, want 1", object.closeCalls)
}
}
func TestExecuteBOFReportsLoadErrorWithoutClosing(t *testing.T) {
wantErr := errors.New("load failed")
_, err := executeBOFWithLoader([]byte("object"), "go", nil, func([]byte, bof.LoadOptions) (bofObject, error) {
return nil, wantErr
})
if !errors.Is(err, wantErr) {
t.Fatalf("error = %v, want wrapped load error", err)
}
}
+4 -63
View File
@@ -53,6 +53,8 @@ var (
pb.MsgUnsetEnvReq: unsetEnvHandler,
pb.MsgChtimesReq: chtimesHandler,
pb.MsgGrepReq: grepHandler,
pb.MsgChmodReq: chmodHandler,
pb.MsgChownReq: chownHandler,
pb.MsgScreenshotReq: screenshotHandler,
pb.MsgNetstatReq: netstatHandler,
@@ -92,69 +94,8 @@ func GetSystemHandlers() map[uint32]RPCHandler {
// Extensions
func registerExtensionHandler(data []byte, resp RPCResponse) {
registerReq := &pb.RegisterExtensionReq{}
err := proto.Unmarshal(data, registerReq)
if err != nil {
return
}
ext := extension.NewDarwinExtension(registerReq.Data, registerReq.Name, registerReq.OS, registerReq.Init)
extension.Add(ext)
err = ext.Load()
registerResp := &pb.RegisterExtension{
Response: &commonpb.Response{},
}
if err != nil {
registerResp.Response.Err = err.Error()
}
data, err = proto.Marshal(registerResp)
resp(data, err)
}
func callExtensionHandler(data []byte, resp RPCResponse) {
callReq := &pb.CallExtensionReq{}
err := proto.Unmarshal(data, callReq)
if err != nil {
return
}
callResp := &pb.CallExtension{
Response: &commonpb.Response{},
}
gotOutput := false
err = extension.Run(callReq.Name, callReq.Export, callReq.Args, func(out []byte) {
gotOutput = true
callResp.Output = out
data, err = proto.Marshal(callResp)
resp(data, err)
})
// Only send back synchronously if there was an error
if err != nil || !gotOutput {
if err != nil {
callResp.Response.Err = err.Error()
}
data, err = proto.Marshal(callResp)
resp(data, err)
}
}
func listExtensionsHandler(data []byte, resp RPCResponse) {
lstReq := &pb.ListExtensionsReq{}
err := proto.Unmarshal(data, lstReq)
if err != nil {
return
}
exts := extension.List()
lstResp := &pb.ListExtensions{
Response: &commonpb.Response{},
Names: exts,
}
data, err = proto.Marshal(lstResp)
resp(data, err)
func newExtension(data []byte, id string, arch string, init string) extension.Extension {
return extension.NewDarwinExtension(data, id, arch, init)
}
func mountHandler(data []byte, resp RPCResponse) {
@@ -0,0 +1,107 @@
//go:build linux || darwin || windows
package handlers
/*
Sliver Implant Framework
Copyright (C) 2021 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"github.com/bishopfox/sliver/implant/sliver/extension"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
type extensionFactory func(data []byte, id string, arch string, init string) extension.Extension
func registerExtensionHandler(data []byte, resp RPCResponse) {
registerExtension(data, resp, newExtension)
}
func registerExtension(data []byte, resp RPCResponse, factory extensionFactory) {
registerReq := &sliverpb.RegisterExtensionReq{}
if err := proto.Unmarshal(data, registerReq); err != nil {
return
}
ext := factory(registerReq.Data, registerReq.Name, registerReq.OS, registerReq.Init)
err := extension.Register(ext)
registerResp := &sliverpb.RegisterExtension{Response: &commonpb.Response{}}
if err != nil {
registerResp.Response.Err = err.Error()
}
data, err = proto.Marshal(registerResp)
resp(data, err)
}
func callExtensionHandler(data []byte, resp RPCResponse) {
callExtension(data, resp, executeBOF)
}
func callExtension(data []byte, resp RPCResponse, execute bofExecuteFunc) {
callReq := &sliverpb.CallExtensionReq{}
if err := proto.Unmarshal(data, callReq); err != nil {
return
}
callResp := &sliverpb.CallExtension{Response: &commonpb.Response{}}
if callReq.IsBOF {
output, err := execute(callReq.BOFData, callReq.Export, callReq.Args)
callResp.Output = output
if err != nil {
callResp.Response.Err = err.Error()
}
data, err = proto.Marshal(callResp)
resp(data, err)
return
}
gotOutput := false
var output []byte
err := extension.Run(callReq.Name, callReq.Export, callReq.Args, func(out []byte) {
// Native callbacks run on the extension's call stack. Keep the first
// result, copy it before the native buffer expires, and respond only
// after the extension call has returned.
if gotOutput {
return
}
gotOutput = true
output = append([]byte(nil), out...)
})
if gotOutput {
callResp.Output = output
} else if err != nil {
callResp.Response.Err = err.Error()
}
data, err = proto.Marshal(callResp)
resp(data, err)
}
func listExtensionsHandler(data []byte, resp RPCResponse) {
lstReq := &sliverpb.ListExtensionsReq{}
if err := proto.Unmarshal(data, lstReq); err != nil {
return
}
lstResp := &sliverpb.ListExtensions{
Response: &commonpb.Response{},
Names: extension.List(),
}
data, err := proto.Marshal(lstResp)
resp(data, err)
}
@@ -0,0 +1,271 @@
//go:build linux || darwin || windows
package handlers
import (
"bytes"
"errors"
"slices"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/extension"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
type failingExtension struct {
id string
loadErr error
}
type callbackLifetimeExtension struct {
id string
output []byte
nativeOutput []byte
callActive atomic.Bool
callbackReturned chan struct{}
allowCallReturn chan struct{}
allowReturnOnce sync.Once
}
func (f *failingExtension) Load() error {
return f.loadErr
}
func (f *failingExtension) Call(string, []byte, func([]byte)) error {
return nil
}
func (f *failingExtension) GetID() string {
return f.id
}
func (f *failingExtension) GetArch() string {
return "test"
}
func (e *callbackLifetimeExtension) Load() error {
return nil
}
func (e *callbackLifetimeExtension) Call(_ string, _ []byte, callback func([]byte)) error {
e.callActive.Store(true)
defer e.callActive.Store(false)
e.nativeOutput = append([]byte(nil), e.output...)
callback(e.nativeOutput)
for index := range e.nativeOutput {
e.nativeOutput[index] = 0xa5
}
close(e.callbackReturned)
<-e.allowCallReturn
return nil
}
func (e *callbackLifetimeExtension) GetID() string {
return e.id
}
func (e *callbackLifetimeExtension) GetArch() string {
return "test"
}
func (e *callbackLifetimeExtension) allowReturn() {
e.allowReturnOnce.Do(func() {
close(e.allowCallReturn)
})
}
func TestRegisterExtensionDoesNotAddFailedLoad(t *testing.T) {
const extensionID = "handler-test-failed-load"
wantErr := errors.New("load failed")
request, err := proto.Marshal(&sliverpb.RegisterExtensionReq{Name: extensionID})
if err != nil {
t.Fatalf("marshal request: %v", err)
}
var (
responseData []byte
responseErr error
responses int
)
registerExtension(request, func(data []byte, err error) {
responses++
responseData = data
responseErr = err
}, func(_ []byte, id string, _, _ string) extension.Extension {
return &failingExtension{id: id, loadErr: wantErr}
})
if responseErr != nil {
t.Fatalf("response callback returned error: %v", responseErr)
}
if responses != 1 {
t.Fatalf("got %d responses, want 1", responses)
}
response := &sliverpb.RegisterExtension{}
if err := proto.Unmarshal(responseData, response); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if response.GetResponse().GetErr() != wantErr.Error() {
t.Fatalf("got response error %q, want %q", response.GetResponse().GetErr(), wantErr)
}
if slices.Contains(extension.List(), extensionID) {
t.Fatalf("extension %q was registered after its load failed", extensionID)
}
}
func TestCallExtensionHandlerDefersResponseAndCopiesCallbackOutput(t *testing.T) {
const extensionID = "handler-test-callback-lifetime"
wantOutput := []byte("native extension output")
ext := &callbackLifetimeExtension{
id: extensionID,
output: wantOutput,
callbackReturned: make(chan struct{}),
allowCallReturn: make(chan struct{}),
}
extension.Add(ext)
defer ext.allowReturn()
request, err := proto.Marshal(&sliverpb.CallExtensionReq{Name: extensionID, Export: "Run"})
if err != nil {
t.Fatalf("marshal request: %v", err)
}
type handlerResponse struct {
data []byte
err error
callActive bool
}
responses := make(chan handlerResponse, 2)
handlerDone := make(chan struct{})
go func() {
defer close(handlerDone)
callExtensionHandler(request, func(data []byte, err error) {
responses <- handlerResponse{
data: append([]byte(nil), data...),
err: err,
callActive: ext.callActive.Load(),
}
})
}()
select {
case <-ext.callbackReturned:
case <-time.After(5 * time.Second):
t.Fatal("extension callback did not return")
}
select {
case <-responses:
t.Fatal("RPC response was delivered before Extension.Call returned")
default:
}
ext.allowReturn()
var response handlerResponse
select {
case response = <-responses:
case <-time.After(5 * time.Second):
t.Fatal("callExtensionHandler did not deliver a response")
}
if response.callActive {
t.Fatal("RPC response was delivered while Extension.Call was active")
}
if response.err != nil {
t.Fatalf("response callback returned error: %v", response.err)
}
callResponse := &sliverpb.CallExtension{}
if err := proto.Unmarshal(response.data, callResponse); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if !bytes.Equal(callResponse.Output, wantOutput) {
t.Fatalf("response output = %q, want %q", callResponse.Output, wantOutput)
}
if bytes.Equal(ext.nativeOutput, wantOutput) {
t.Fatal("fake native output was not poisoned after the callback")
}
select {
case <-handlerDone:
case <-time.After(5 * time.Second):
t.Fatal("callExtensionHandler did not return")
}
select {
case <-responses:
t.Fatal("callExtensionHandler delivered more than one response")
default:
}
}
func TestCallExtensionRoutesBOFDataToBuiltInExecutor(t *testing.T) {
wantData := []byte("bof object")
wantArgs := []byte{4, 0, 0, 0, 1, 2, 3, 4}
wantOutput := []byte("partial BOF output")
wantErr := errors.New("BOF execution failed")
request, err := proto.Marshal(&sliverpb.CallExtensionReq{
Name: "must-not-use-native-registry",
Export: "CustomExport",
Args: wantArgs,
BOFData: wantData,
IsBOF: true,
})
if err != nil {
t.Fatalf("marshal request: %v", err)
}
var (
responseData []byte
responseErr error
responses int
)
callExtension(request, func(data []byte, err error) {
responses++
responseData = data
responseErr = err
}, func(data []byte, entryPoint string, args []byte) ([]byte, error) {
if !bytes.Equal(data, wantData) {
t.Fatalf("BOF data = %q, want %q", data, wantData)
}
if entryPoint != "CustomExport" {
t.Fatalf("entry point = %q, want CustomExport", entryPoint)
}
if !bytes.Equal(args, wantArgs) {
t.Fatalf("args = %v, want %v", args, wantArgs)
}
return wantOutput, wantErr
})
if responseErr != nil {
t.Fatalf("response callback returned error: %v", responseErr)
}
if responses != 1 {
t.Fatalf("got %d responses, want 1", responses)
}
response := &sliverpb.CallExtension{}
if err := proto.Unmarshal(responseData, response); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if !bytes.Equal(response.Output, wantOutput) {
t.Fatalf("output = %q, want %q", response.Output, wantOutput)
}
if response.GetResponse().GetErr() != wantErr.Error() {
t.Fatalf("response error = %q, want %q", response.GetResponse().GetErr(), wantErr)
}
}
func TestSystemHandlersIncludeExtensions(t *testing.T) {
handlers := GetSystemHandlers()
for _, messageType := range []uint32{
sliverpb.MsgRegisterExtensionReq,
sliverpb.MsgCallExtensionReq,
sliverpb.MsgListExtensionsReq,
} {
if handlers[messageType] == nil {
t.Errorf("handler for message type %d is not registered", messageType)
}
}
}
@@ -0,0 +1,77 @@
package handlers
import (
"archive/tar"
"bytes"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
func TestCdHandlerReportsChdirFailure(t *testing.T) {
workingDirectory := t.TempDir()
t.Chdir(workingDirectory)
request, err := proto.Marshal(&sliverpb.CdReq{Path: filepath.Join(workingDirectory, "missing")})
if err != nil {
t.Fatal(err)
}
called := false
cdHandler(request, func(data []byte, handlerErr error) {
called = true
if handlerErr != nil {
t.Fatalf("cdHandler callback error: %v", handlerErr)
}
response := &sliverpb.Pwd{}
if err := proto.Unmarshal(data, response); err != nil {
t.Fatalf("decode cd response: %v", err)
}
if response.GetResponse().GetErr() == "" {
t.Fatal("cdHandler returned success for a missing directory")
}
if response.Path != workingDirectory {
t.Fatalf("working directory changed: got %q, want %q", response.Path, workingDirectory)
}
})
if !called {
t.Fatal("cdHandler did not invoke its response callback")
}
}
func TestExtractFilesTruncatesOverwrittenFile(t *testing.T) {
destination := t.TempDir()
archive := func(content string) []byte {
t.Helper()
buffer := bytes.NewBuffer(nil)
writer := tar.NewWriter(buffer)
header := &tar.Header{Name: "bundle/item.txt", Mode: 0o600, Size: int64(len(content)), Typeflag: tar.TypeReg}
if err := writer.WriteHeader(header); err != nil {
t.Fatal(err)
}
if _, err := writer.Write([]byte(content)); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return buffer.Bytes()
}
if written, skipped, err := extractFiles(archive("a deliberately long first payload\n"), destination, false); err != nil || written != 1 || skipped != 0 {
t.Fatalf("initial extract: written=%d skipped=%d err=%v", written, skipped, err)
}
if written, skipped, err := extractFiles(archive("short\n"), destination, true); err != nil || written != 1 || skipped != 0 {
t.Fatalf("overwrite extract: written=%d skipped=%d err=%v", written, skipped, err)
}
content, err := os.ReadFile(filepath.Join(destination, "bundle", "item.txt"))
if err != nil {
t.Fatal(err)
}
if string(content) != "short\n" {
t.Fatalf("overwritten content = %q, want exact shorter payload", content)
}
}
+21 -185
View File
@@ -20,22 +20,20 @@ package handlers
import (
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
"github.com/bishopfox/sliver/implant/sliver/extension"
"github.com/bishopfox/sliver/implant/sliver/mount"
"github.com/bishopfox/sliver/implant/sliver/procdump"
"github.com/bishopfox/sliver/implant/sliver/taskrunner"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"golang.org/x/sys/unix"
"google.golang.org/protobuf/proto"
// {{if .Config.Debug}}
@@ -76,6 +74,11 @@ var (
sliverpb.MsgMountReq: mountHandler,
sliverpb.MsgGrepReq: grepHandler,
// Extensions
sliverpb.MsgRegisterExtensionReq: registerExtensionHandler,
sliverpb.MsgCallExtensionReq: callExtensionHandler,
sliverpb.MsgListExtensionsReq: listExtensionsHandler,
// Wasm Extensions - Note that execution can be done via a tunnel handler
sliverpb.MsgRegisterWasmExtensionReq: registerWasmExtensionHandler,
sliverpb.MsgDeregisterWasmExtensionReq: deregisterWasmExtensionHandler,
@@ -91,11 +94,12 @@ var (
sliverpb.MsgWGListSocksReq: wgListSocksServersHandler,
// {{end}}
// Linux Only
// Unix permissions
sliverpb.MsgChmodReq: chmodHandler,
sliverpb.MsgChownReq: chownHandler,
sliverpb.MsgChtimesReq: chtimesHandler,
// Linux Only
sliverpb.MsgMemfilesListReq: memfilesListHandler,
sliverpb.MsgMemfilesAddReq: memfilesAddHandler,
sliverpb.MsgMemfilesRmReq: memfilesRmHandler,
@@ -107,6 +111,10 @@ func GetSystemHandlers() map[uint32]RPCHandler {
return linuxHandlers
}
func newExtension(data []byte, id string, arch string, init string) extension.Extension {
return extension.NewLinuxExtension(data, id, arch, init)
}
func dumpHandler(data []byte, resp RPCResponse) {
procDumpReq := &sliverpb.ProcessDumpReq{}
err := proto.Unmarshal(data, procDumpReq)
@@ -251,34 +259,24 @@ func memfilesListHandler(_ []byte, resp RPCResponse) {
}
func memfilesAddHandler(_ []byte, resp RPCResponse) {
var nrMemfdCreate int
memfilesAdd := &sliverpb.MemfilesAdd{}
memfilesAdd.Response = &commonpb.Response{}
memfilesAdd := &sliverpb.MemfilesAdd{Response: &commonpb.Response{}}
memfdName := taskrunner.RandomString(8)
memfd, err := syscall.BytePtrFromString(memfdName)
fd, err := unix.MemfdCreate(memfdName, unix.MFD_CLOEXEC)
if err != nil {
//{{if .Config.Debug}}
log.Printf("Error during conversion: %s\n", err)
log.Printf("Error creating memfd: %s\n", err)
//{{end}}
return
}
if runtime.GOARCH == "386" {
nrMemfdCreate = 356
memfilesAdd.Response.Err = err.Error()
} else {
nrMemfdCreate =
319
memfilesAdd.Fd = int64(fd)
}
fd, _, _ := syscall.Syscall(uintptr(nrMemfdCreate), uintptr(unsafe.Pointer(memfd)), 1, 0)
fd_str := fmt.Sprintf("%d", fd)
fd_int, _ := strconv.ParseInt(fd_str, 0, 64)
memfilesAdd.Fd = fd_int
data, err := proto.Marshal(memfilesAdd)
if err != nil && fd >= 0 {
_ = unix.Close(fd)
}
resp(data, err)
}
func memfilesRmHandler(data []byte, resp RPCResponse) {
@@ -320,165 +318,3 @@ func memfilesRmHandler(data []byte, resp RPCResponse) {
resp(data, err)
}
func chmodHandler(data []byte, resp RPCResponse) {
chmodReq := &sliverpb.ChmodReq{}
err := proto.Unmarshal(data, chmodReq)
if err != nil {
// {{if .Config.Debug}}
log.Printf("error decoding message: %v", err)
// {{end}}
return
}
chmod := &sliverpb.Chmod{}
target, _ := filepath.Abs(chmodReq.Path)
chmod.Path = target
// Make sure file exists
_, err = os.Stat(target)
chmod.Response = &commonpb.Response{}
if err == nil {
// Convert string to octal number
octal, err := strconv.ParseInt(chmodReq.FileMode, 8, 32)
if err == nil {
setuid := octal & 04000
setgid := octal & 02000
setstcky := octal & 01000
// Cast the octal number to fs.FileMode
fileMode := os.FileMode(octal)
// Found this was necessary because the constructor above doesn't set special permissions
if setuid > 0 {
fileMode = fileMode | os.ModeSetuid
}
if setgid > 0 {
fileMode = fileMode | os.ModeSetgid
}
if setstcky > 0 {
fileMode = fileMode | os.ModeSticky
}
if chmodReq.Recursive {
err := filepath.WalkDir(target, func(file string, d fs.DirEntry, err error) error {
if err == nil {
err = os.Chmod(file, fileMode)
if err != nil {
return err
}
} else {
return err
}
return nil
})
if err != nil {
chmod.Response.Err = err.Error()
}
} else {
err = os.Chmod(target, fileMode)
if err != nil {
chmod.Response.Err = err.Error()
}
}
} else {
chmod.Response.Err = err.Error()
}
} else {
chmod.Response.Err = err.Error()
}
data, err = proto.Marshal(chmod)
resp(data, err)
}
func chownHandler(data []byte, resp RPCResponse) {
// variable definitions so goto won't break
var uid_str string
var gid_str string
var gid uint64
var uid uint64
var err error
var usr *user.User
var grp *user.Group
chownReq := &sliverpb.ChownReq{}
err = proto.Unmarshal(data, chownReq)
if err != nil {
// {{if .Config.Debug}}
log.Printf("error decoding message: %v", err)
// {{end}}
return
}
chown := &sliverpb.Chown{}
target, _ := filepath.Abs(chownReq.Path)
chown.Path = target
_, err = os.Stat(target)
chown.Response = &commonpb.Response{}
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
uid_str = chownReq.Uid
usr, err = user.Lookup(uid_str)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
uid, err = strconv.ParseUint(usr.Uid, 10, 32)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
gid_str = chownReq.Gid
grp, err = user.LookupGroup(gid_str)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
gid, err = strconv.ParseUint(grp.Gid, 10, 32)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
// Check if the recursive flag is set and the path is a directory
if chownReq.Recursive {
err := filepath.WalkDir(target, func(file string, d fs.DirEntry, err error) error {
if err == nil {
err = os.Chown(file, int(uid), int(gid))
if err != nil {
return err
}
} else {
return err
}
return nil
})
if err != nil {
chown.Response.Err = err.Error()
}
} else {
err = os.Chown(target, int(uid), int(gid))
if err != nil {
chown.Response.Err = err.Error()
}
}
finished:
data, err = proto.Marshal(chown)
resp(data, err)
}
@@ -0,0 +1,69 @@
package handlers
import (
"fmt"
"os"
"strings"
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"golang.org/x/sys/unix"
"google.golang.org/protobuf/proto"
)
func TestMemfilesAddAndRemoveLifecycle(t *testing.T) {
var addResponse *sliverpb.MemfilesAdd
memfilesAddHandler(nil, func(data []byte, handlerErr error) {
if handlerErr != nil {
t.Fatalf("memfilesAddHandler callback error: %v", handlerErr)
}
addResponse = &sliverpb.MemfilesAdd{}
if err := proto.Unmarshal(data, addResponse); err != nil {
t.Fatalf("decode add response: %v", err)
}
})
if addResponse == nil {
t.Fatal("memfilesAddHandler did not invoke its response callback")
}
if errText := addResponse.GetResponse().GetErr(); errText != "" {
t.Fatalf("memfilesAddHandler returned error: %s", errText)
}
if addResponse.Fd < 3 {
t.Fatalf("memfilesAddHandler returned invalid fd %d", addResponse.Fd)
}
fd := int(addResponse.Fd)
defer func() { _ = unix.Close(fd) }()
fdPath := fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), fd)
link, err := os.Readlink(fdPath)
if err != nil {
t.Fatalf("read memfd link: %v", err)
}
if !strings.Contains(link, "/memfd:") {
t.Fatalf("fd link %q does not identify a memfd", link)
}
request, err := proto.Marshal(&sliverpb.MemfilesRmReq{Fd: addResponse.Fd})
if err != nil {
t.Fatal(err)
}
var removeResponse *sliverpb.MemfilesRm
memfilesRmHandler(request, func(data []byte, handlerErr error) {
if handlerErr != nil {
t.Fatalf("memfilesRmHandler callback error: %v", handlerErr)
}
removeResponse = &sliverpb.MemfilesRm{}
if err := proto.Unmarshal(data, removeResponse); err != nil {
t.Fatalf("decode remove response: %v", err)
}
})
if removeResponse == nil {
t.Fatal("memfilesRmHandler did not invoke its response callback")
}
if errText := removeResponse.GetResponse().GetErr(); errText != "" {
t.Fatalf("memfilesRmHandler returned error: %s", errText)
}
if _, err := os.Readlink(fdPath); !os.IsNotExist(err) {
t.Fatalf("memfd still accessible after removal: %v", err)
}
}
@@ -0,0 +1,30 @@
package handlers
import (
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
func TestNetstatHandlerRespondsWithoutTCP(t *testing.T) {
request, err := proto.Marshal(&sliverpb.NetstatReq{UDP: true})
if err != nil {
t.Fatal(err)
}
called := false
netstatHandler(request, func(data []byte, handlerErr error) {
called = true
if handlerErr != nil {
t.Fatalf("netstatHandler callback error: %v", handlerErr)
}
response := &sliverpb.Netstat{}
if err := proto.Unmarshal(data, response); err != nil {
t.Fatalf("decode netstat response: %v", err)
}
})
if !called {
t.Fatal("netstatHandler did not respond to a request without TCP")
}
}
+199
View File
@@ -0,0 +1,199 @@
//go:build darwin || linux
package handlers
/*
Sliver Implant Framework
Copyright (C) 2019 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"io/fs"
"os"
"os/user"
"path/filepath"
"strconv"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
// {{if .Config.Debug}}
"log"
// {{end}}
)
func chmodHandler(data []byte, resp RPCResponse) {
chmodReq := &sliverpb.ChmodReq{}
err := proto.Unmarshal(data, chmodReq)
if err != nil {
// {{if .Config.Debug}}
log.Printf("error decoding message: %v", err)
// {{end}}
return
}
chmod := &sliverpb.Chmod{}
target, _ := filepath.Abs(chmodReq.Path)
chmod.Path = target
// Make sure file exists
_, err = os.Stat(target)
chmod.Response = &commonpb.Response{}
if err == nil {
// Convert string to octal number
octal, err := strconv.ParseInt(chmodReq.FileMode, 8, 32)
if err == nil {
setuid := octal & 04000
setgid := octal & 02000
setstcky := octal & 01000
// Cast the octal number to fs.FileMode
fileMode := os.FileMode(octal)
// Found this was necessary because the constructor above doesn't set special permissions
if setuid > 0 {
fileMode = fileMode | os.ModeSetuid
}
if setgid > 0 {
fileMode = fileMode | os.ModeSetgid
}
if setstcky > 0 {
fileMode = fileMode | os.ModeSticky
}
if chmodReq.Recursive {
err := filepath.WalkDir(target, func(file string, _ fs.DirEntry, err error) error {
if err == nil {
err = os.Chmod(file, fileMode)
if err != nil {
return err
}
} else {
return err
}
return nil
})
if err != nil {
chmod.Response.Err = err.Error()
}
} else {
err = os.Chmod(target, fileMode)
if err != nil {
chmod.Response.Err = err.Error()
}
}
} else {
chmod.Response.Err = err.Error()
}
} else {
chmod.Response.Err = err.Error()
}
data, err = proto.Marshal(chmod)
resp(data, err)
}
func chownHandler(data []byte, resp RPCResponse) {
// variable definitions so goto won't break
var uidStr string
var gidStr string
var gid uint64
var uid uint64
var err error
var usr *user.User
var grp *user.Group
chownReq := &sliverpb.ChownReq{}
err = proto.Unmarshal(data, chownReq)
if err != nil {
// {{if .Config.Debug}}
log.Printf("error decoding message: %v", err)
// {{end}}
return
}
chown := &sliverpb.Chown{}
target, _ := filepath.Abs(chownReq.Path)
chown.Path = target
_, err = os.Stat(target)
chown.Response = &commonpb.Response{}
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
uidStr = chownReq.Uid
usr, err = user.Lookup(uidStr)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
uid, err = strconv.ParseUint(usr.Uid, 10, 32)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
gidStr = chownReq.Gid
grp, err = user.LookupGroup(gidStr)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
gid, err = strconv.ParseUint(grp.Gid, 10, 32)
if err != nil {
chown.Response.Err = err.Error()
goto finished
}
// Check if the recursive flag is set and the path is a directory
if chownReq.Recursive {
err := filepath.WalkDir(target, func(file string, _ fs.DirEntry, err error) error {
if err == nil {
err = os.Chown(file, int(uid), int(gid))
if err != nil {
return err
}
} else {
return err
}
return nil
})
if err != nil {
chown.Response.Err = err.Error()
}
} else {
err = os.Chown(target, int(uid), int(gid))
if err != nil {
chown.Response.Err = err.Error()
}
}
finished:
data, err = proto.Marshal(chown)
resp(data, err)
}
@@ -0,0 +1,153 @@
//go:build darwin || linux
package handlers
import (
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
func TestUnixPermissionHandlersRegistered(t *testing.T) {
handlers := GetSystemHandlers()
for _, messageType := range []uint32{sliverpb.MsgChmodReq, sliverpb.MsgChownReq} {
if _, ok := handlers[messageType]; !ok {
t.Fatalf("message type %d is not registered", messageType)
}
}
}
func TestChmodHandlerRecursive(t *testing.T) {
target := filepath.Join(t.TempDir(), "target")
nested := filepath.Join(target, "nested")
file := filepath.Join(nested, "fixture.txt")
if err := os.MkdirAll(nested, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file, []byte("fixture"), 0o600); err != nil {
t.Fatal(err)
}
request, err := proto.Marshal(&sliverpb.ChmodReq{Path: target, FileMode: "0750", Recursive: true})
if err != nil {
t.Fatal(err)
}
called := false
chmodHandler(request, func(data []byte, handlerErr error) {
called = true
if handlerErr != nil {
t.Fatalf("chmodHandler callback error: %v", handlerErr)
}
response := &sliverpb.Chmod{}
if err := proto.Unmarshal(data, response); err != nil {
t.Fatalf("decode chmod response: %v", err)
}
if response.GetResponse().GetErr() != "" {
t.Fatalf("chmodHandler response error: %s", response.GetResponse().GetErr())
}
if response.Path != target {
t.Fatalf("chmodHandler path = %q, want %q", response.Path, target)
}
})
if !called {
t.Fatal("chmodHandler did not invoke its response callback")
}
for _, path := range []string{target, nested, file} {
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != 0o750 {
t.Errorf("%s mode = %o, want 750", path, got)
}
}
}
func TestChownHandlerRecursiveCurrentOwner(t *testing.T) {
current, group, wantUID, wantGID := currentOwner(t)
target := filepath.Join(t.TempDir(), "target")
file := filepath.Join(target, "fixture.txt")
if err := os.MkdirAll(target, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file, []byte("fixture"), 0o600); err != nil {
t.Fatal(err)
}
request, err := proto.Marshal(&sliverpb.ChownReq{
Path: target, Uid: current.Username, Gid: group.Name, Recursive: true,
})
if err != nil {
t.Fatal(err)
}
called := false
chownHandler(request, func(data []byte, handlerErr error) {
called = true
if handlerErr != nil {
t.Fatalf("chownHandler callback error: %v", handlerErr)
}
response := &sliverpb.Chown{}
if err := proto.Unmarshal(data, response); err != nil {
t.Fatalf("decode chown response: %v", err)
}
if response.GetResponse().GetErr() != "" {
t.Fatalf("chownHandler response error: %s", response.GetResponse().GetErr())
}
if response.Path != target {
t.Fatalf("chownHandler path = %q, want %q", response.Path, target)
}
})
if !called {
t.Fatal("chownHandler did not invoke its response callback")
}
assertOwner(t, []string{target, file}, wantUID, wantGID)
}
func currentOwner(t *testing.T) (*user.User, *user.Group, uint64, uint64) {
t.Helper()
current, err := user.Current()
if err != nil {
t.Fatal(err)
}
group, err := user.LookupGroupId(current.Gid)
if err != nil {
t.Fatal(err)
}
wantUID, err := strconv.ParseUint(current.Uid, 10, 32)
if err != nil {
t.Fatal(err)
}
wantGID, err := strconv.ParseUint(group.Gid, 10, 32)
if err != nil {
t.Fatal(err)
}
return current, group, wantUID, wantGID
}
func assertOwner(t *testing.T, paths []string, wantUID uint64, wantGID uint64) {
t.Helper()
for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
t.Fatalf("%s stat type = %T, want *syscall.Stat_t", path, info.Sys())
}
if uint64(stat.Uid) != wantUID || uint64(stat.Gid) != wantGID {
t.Errorf("%s owner = %d:%d, want %d:%d", path, stat.Uid, stat.Gid, wantUID, wantGID)
}
}
}
+2 -57
View File
@@ -912,63 +912,8 @@ func mountHandler(data []byte, resp RPCResponse) {
// Extensions
func registerExtensionHandler(data []byte, resp RPCResponse) {
registerReq := &sliverpb.RegisterExtensionReq{}
err := proto.Unmarshal(data, registerReq)
if err != nil {
return
}
ext := extension.NewWindowsExtension(registerReq.Data, registerReq.Name, registerReq.OS, registerReq.Init)
err = ext.Load()
registerResp := &sliverpb.RegisterExtension{Response: &commonpb.Response{}}
if err != nil {
registerResp.Response.Err = err.Error()
} else {
extension.Add(ext)
}
data, err = proto.Marshal(registerResp)
resp(data, err)
}
func callExtensionHandler(data []byte, resp RPCResponse) {
callReq := &sliverpb.CallExtensionReq{}
err := proto.Unmarshal(data, callReq)
if err != nil {
return
}
callResp := &sliverpb.CallExtension{Response: &commonpb.Response{}}
gotOutput := false
err = extension.Run(callReq.Name, callReq.Export, callReq.Args, func(out []byte) {
gotOutput = true
callResp.Output = out
data, err = proto.Marshal(callResp)
resp(data, err)
})
// Only send back synchronously if there was an error
if err != nil || !gotOutput {
if err != nil {
callResp.Response.Err = err.Error()
}
data, err = proto.Marshal(callResp)
resp(data, err)
}
}
func listExtensionsHandler(data []byte, resp RPCResponse) {
lstReq := &sliverpb.ListExtensionsReq{}
err := proto.Unmarshal(data, lstReq)
if err != nil {
return
}
exts := extension.List()
lstResp := &sliverpb.ListExtensions{
Response: &commonpb.Response{},
Names: exts,
}
data, err = proto.Marshal(lstResp)
resp(data, err)
func newExtension(data []byte, id string, arch string, init string) extension.Extension {
return extension.NewWindowsExtension(data, id, arch, init)
}
// Stub since Windows doesn't support UID
+3 -3
View File
@@ -201,10 +201,10 @@ func netstatHandler(data []byte, resp RPCResponse) {
}
entries = append(entries, buildEntries("tcp6", tabs)...)
}
result.Entries = entries
data, err := proto.Marshal(result)
resp(data, err)
}
result.Entries = entries
data, err = proto.Marshal(result)
resp(data, err)
}
func buildEntries(proto string, s []netstat.SockTabEntry) []*sliverpb.SockTabEntry {
+42
View File
@@ -0,0 +1,42 @@
// Package ps provides process enumeration and architecture detection.
package ps
import "testing"
func TestArchitectureFromELFMachine(t *testing.T) {
tests := map[uint16]string{
3: "x86",
62: "x86_64",
183: "aarch64",
179: "",
}
for machine, expected := range tests {
if actual := architectureFromELFMachine(machine); actual != expected {
t.Errorf("machine %d: got %q, want %q", machine, actual, expected)
}
}
}
func TestArchitectureFromWindowsMachines(t *testing.T) {
tests := []struct {
name string
processMachine uint16
nativeMachine uint16
expected string
}{
{name: "x86 on amd64", processMachine: 0x014c, nativeMachine: 0x8664, expected: "x86"},
{name: "x86 on arm64", processMachine: 0x014c, nativeMachine: 0xaa64, expected: "x86"},
{name: "native amd64", nativeMachine: 0x8664, expected: "x86_64"},
{name: "native arm64", nativeMachine: 0xaa64, expected: "arm64"},
{name: "exact amd64", processMachine: 0x8664, nativeMachine: 0xaa64, expected: "x86_64"},
{name: "unknown", processMachine: 0xffff, nativeMachine: 0xaa64, expected: ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := architectureFromWindowsMachines(test.processMachine, test.nativeMachine)
if actual != test.expected {
t.Fatalf("got %q, want %q", actual, test.expected)
}
})
}
}
+33
View File
@@ -56,3 +56,36 @@ func Kill(pid int) error {
}
return p.Kill()
}
func architectureFromELFMachine(machine uint16) string {
switch machine {
case 3:
return "x86"
case 62:
return "x86_64"
case 183:
return "aarch64"
default:
return ""
}
}
func architectureFromWindowsMachine(machine uint16) string {
switch machine {
case 0x014c:
return "x86"
case 0x8664:
return "x86_64"
case 0xaa64:
return "arm64"
default:
return ""
}
}
func architectureFromWindowsMachines(processMachine, nativeMachine uint16) string {
if processMachine != 0 {
return architectureFromWindowsMachine(processMachine)
}
return architectureFromWindowsMachine(nativeMachine)
}
+2 -10
View File
@@ -4,6 +4,7 @@
package ps
import (
"encoding/binary"
"fmt"
"io"
"os"
@@ -114,16 +115,7 @@ func getProcessArchitecture(pid int) (string, error) {
return "", nil
}
if mach[0] == 0xb3 {
return "aarch64", nil
}
if mach[0] == 0x03 {
return "x86", nil
}
if mach[0] == 0x3e {
return "x86_64", nil
}
return "", err
return architectureFromELFMachine(binary.LittleEndian.Uint16(mach)), nil
}
// Refresh reloads all the data associated with this process.
+73 -35
View File
@@ -4,6 +4,7 @@
package ps
import (
"errors"
"fmt"
"runtime"
"strings"
@@ -14,6 +15,16 @@ import (
"golang.org/x/sys/windows"
)
const processMachineTypeInfo = 9
type processMachineInformation struct {
ProcessMachine uint16
Reserved uint16
MachineAttributes uint32
}
var getProcessInformation = windows.NewLazySystemDLL("kernel32.dll").NewProc("GetProcessInformation")
// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
pid int
@@ -267,32 +278,7 @@ func getCmdLine(pid uint32) ([]string, error) {
return strings.Fields(syscall.UTF16ToString(module.SzExePath[:]) + " : " + syscall.UTF16ToString(cmdLine[:])), nil
}
var nativeArch string = ""
var nativeArchLookup bool = true
func getProcessArchitecture(pid uint32) (string, error) {
if nativeArchLookup {
nativeArchLookup = false
if runtime.GOARCH == "amd64" {
nativeArch = "x86_64"
} else {
var is64Bit bool
pHandle := windows.CurrentProcess()
if uint(pHandle) == 0 {
nativeArch = ""
} else if err := windows.IsWow64Process(pHandle, &is64Bit); err != nil {
nativeArch = ""
} else {
if !is64Bit {
nativeArch = "x86"
} else {
nativeArch = "x86_64"
}
}
}
}
proc, err := syscall.OpenProcess(
windows.PROCESS_QUERY_INFORMATION,
false,
@@ -309,22 +295,74 @@ func getProcessArchitecture(pid uint32) (string, error) {
}
}
arch := ""
var isWow64 bool
if err := windows.IsWow64Process(windows.Handle(proc), &isWow64); err == nil {
if isWow64 {
arch = "x86"
} else {
arch = nativeArch
arch, queryErr := queryProcessArchitecture(windows.Handle(proc))
closeErr := syscall.CloseHandle(proc)
if queryErr != nil {
return "", queryErr
}
if closeErr != nil {
return "", closeErr
}
return arch, nil
}
func queryProcessArchitecture(handle windows.Handle) (string, error) {
if arch, err := processMachineArchitecture(handle); err == nil && arch != "" {
return arch, nil
}
var processMachine, nativeMachine uint16
if err := windows.IsWow64Process2(handle, &processMachine, &nativeMachine); err == nil {
if arch := architectureFromWindowsMachines(processMachine, nativeMachine); arch != "" {
return arch, nil
}
}
err = syscall.CloseHandle(proc)
if err != nil {
var isWow64 bool
if err := windows.IsWow64Process(handle, &isWow64); err != nil {
return "", err
}
if isWow64 {
return "x86", nil
}
return arch, err
switch runtime.GOARCH {
case "amd64":
return "x86_64", nil
case "arm64":
return "arm64", nil
case "386":
var currentIsWow64 bool
if err := windows.IsWow64Process(windows.CurrentProcess(), &currentIsWow64); err != nil {
return "", err
}
if currentIsWow64 {
return "x86_64", nil
}
return "x86", nil
default:
return "", nil
}
}
func processMachineArchitecture(handle windows.Handle) (string, error) {
if err := getProcessInformation.Find(); err != nil {
return "", err
}
info := processMachineInformation{}
result, _, callErr := getProcessInformation.Call(
uintptr(handle),
uintptr(processMachineTypeInfo),
uintptr(unsafe.Pointer(&info)),
unsafe.Sizeof(info),
)
if result == 0 {
if callErr != nil && !errors.Is(callErr, syscall.Errno(0)) {
return "", callErr
}
return "", syscall.EINVAL
}
return architectureFromWindowsMachine(info.ProcessMachine), nil
}
func getSessionID(pid uint32) (int, error) {
+26
View File
@@ -0,0 +1,26 @@
package ps
import (
"os"
"runtime"
"testing"
)
func TestGetProcessArchitectureCurrentProcess(t *testing.T) {
expected := map[string]string{
"386": "x86",
"amd64": "x86_64",
"arm64": "arm64",
}[runtime.GOARCH]
if expected == "" {
t.Skipf("unsupported test architecture %s", runtime.GOARCH)
}
actual, err := getProcessArchitecture(uint32(os.Getpid()))
if err != nil {
t.Fatal(err)
}
if actual != expected {
t.Fatalf("got %q, want %q", actual, expected)
}
}
@@ -0,0 +1,9 @@
//go:build !sliver_lint && ((darwin && (amd64 || arm64)) || (linux && (386 || amd64 || arm64)) || (windows && (386 || amd64 || arm64)))
package runner
import "github.com/bishopfox/sliver/protobuf/sliverpb"
func implantCapabilities() uint64 {
return uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_BOF_V1)
}
@@ -0,0 +1,7 @@
//go:build !sliver_lint && !((darwin && (amd64 || arm64)) || (linux && (386 || amd64 || arm64)) || (windows && (386 || amd64 || arm64)))
package runner
func implantCapabilities() uint64 {
return 0
}
+80 -55
View File
@@ -149,6 +149,7 @@ func beaconStartup() {
abort <- struct{}{}
}()
beacons := transports.StartBeaconLoop(abort)
pendingResults := &beaconResultQueue{}
for beacon := range beacons {
// {{if .Config.Debug}}
log.Printf("Next beacon = %v", beacon)
@@ -161,7 +162,7 @@ func beaconStartup() {
// {{end}}
continue
}
err := beaconMainLoop(beacon)
err := beaconMainLoop(beacon, pendingResults)
if err != nil {
connectionErrors++
if transports.GetMaxConnectionErrors() < connectionErrors {
@@ -217,7 +218,38 @@ func sessionStartup() {
// {{end}}
// {{if .Config.IsBeacon}}
func beaconMainLoop(beacon *transports.Beacon) error {
type beaconResultQueue struct {
mu sync.Mutex
results []*sliverpb.Envelope
}
func (q *beaconResultQueue) add(result *sliverpb.Envelope) {
q.mu.Lock()
defer q.mu.Unlock()
q.results = append(q.results, result)
}
func (q *beaconResultQueue) drain() []*sliverpb.Envelope {
q.mu.Lock()
defer q.mu.Unlock()
results := q.results
q.results = nil
return results
}
func (q *beaconResultQueue) prepend(results []*sliverpb.Envelope) {
if len(results) == 0 {
return
}
q.mu.Lock()
defer q.mu.Unlock()
combined := make([]*sliverpb.Envelope, 0, len(results)+len(q.results))
combined = append(combined, results...)
combined = append(combined, q.results...)
q.results = combined
}
func beaconMainLoop(beacon *transports.Beacon, pendingResults *beaconResultQueue) error {
// Register beacon
err := beacon.Init()
if err != nil {
@@ -250,7 +282,6 @@ func beaconMainLoop(beacon *transports.Beacon) error {
// {{if .Config.Debug}}
log.Printf("Registering beacon with server")
// {{end}}
nextCheckin := time.Now().Add(beacon.Duration())
register := registerSliver()
register.ActiveC2 = beacon.ActiveC2
register.ProxyURL = beacon.ProxyURL
@@ -264,44 +295,40 @@ func beaconMainLoop(beacon *transports.Beacon) error {
time.Sleep(time.Second)
beacon.Close()
// BeaconMain - Tasks run in background goroutines, check-ins happen on schedule
// Results are collected and sent on subsequent check-ins
pendingResults := make(chan *sliverpb.Envelope, 100)
errors := make(chan error)
shortCircuit := make(chan struct{})
return beaconCheckinLoop(beacon, pendingResults, beaconMain)
}
type beaconCheckinFunc func(*transports.Beacon, time.Time, *beaconResultQueue) error
// beaconCheckinLoop schedules check-ins without overlapping them. Beacon
// transport callbacks share per-check-in connection state, so the previous
// check-in must finish closing its resources before the next one can start.
func beaconCheckinLoop(beacon *transports.Beacon, pendingResults *beaconResultQueue, checkin beaconCheckinFunc) error {
for {
duration := beacon.Duration()
nextCheckin = time.Now().Add(duration)
go func() {
oldInterval := beacon.Interval()
err := beaconMain(beacon, nextCheckin, pendingResults)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[beacon] main error: %v", nextCheckin)
// {{end}}
errors <- err
} else if oldInterval != beacon.Interval() {
// The beacon's interval was modified so we need to short circuit
// the current sleep and tell the server when the next checkin will
// be based on the new interval.
shortCircuit <- struct{}{}
}
}()
nextCheckin := time.Now().Add(duration)
oldInterval := beacon.Interval()
err := checkin(beacon, nextCheckin, pendingResults)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[beacon] main error: %v", nextCheckin)
// {{end}}
return err
}
// {{if .Config.Debug}}
log.Printf("[beacon] sleep until %v", nextCheckin)
// {{end}}
select {
case <-errors:
return err
case <-time.After(duration):
case <-shortCircuit:
// Short circuit current duration with no error
if oldInterval == beacon.Interval() {
// Keep the original start-to-start cadence when the check-in
// finishes before its deadline. If it overruns, begin the next
// check-in immediately without overlapping shared transport state.
if remaining := time.Until(nextCheckin); 0 < remaining {
time.Sleep(remaining)
}
}
// check if reconfig used to set a new C2-URI
// check if reconfig used to set a new C2-URI
if c2 := transports.GetC2URI(); c2 != "" && c2 != beacon.ActiveC2 {
// {{if .Config.Debug}}
log.Printf("[beacon] C2 URI changed to %s, reconnecting...", c2)
@@ -309,10 +336,9 @@ func beaconMainLoop(beacon *transports.Beacon) error {
return nil
}
}
return nil
}
func beaconMain(beacon *transports.Beacon, nextCheckin time.Time, pendingResults chan *sliverpb.Envelope) error {
func beaconMain(beacon *transports.Beacon, nextCheckin time.Time, pendingResults *beaconResultQueue) error {
err := beacon.Start()
if err != nil {
// {{if .Config.Debug}}
@@ -328,17 +354,8 @@ func beaconMain(beacon *transports.Beacon, nextCheckin time.Time, pendingResults
beacon.Close()
}()
// Collect any pending results from background tasks
var completedResults []*sliverpb.Envelope
collecting:
for {
select {
case result := <-pendingResults:
completedResults = append(completedResults, result)
default:
break collecting
}
}
// Collect any pending results from background tasks.
completedResults := pendingResults.drain()
// {{if .Config.Debug}}
log.Printf("[beacon] sending check in with %d pending results...", len(completedResults))
@@ -349,11 +366,18 @@ collecting:
Tasks: completedResults,
}))
if err != nil {
pendingResults.prepend(completedResults)
// {{if .Config.Debug}}
log.Printf("[beacon] send failure %s", err)
// {{end}}
return err
}
// The server deliberately does not return pending tasks on a result-only
// check-in. Waiting for a response here blocks this loop until another
// check-in replaces the transport connection.
if len(completedResults) > 0 {
return nil
}
// {{if .Config.Debug}}
log.Printf("[beacon] recv task(s) ...")
// {{end}}
@@ -401,7 +425,7 @@ collecting:
// Dispatch tasks to background - results sent to pendingResults as each completes
// Extensions must be registered synchronously before other tasks can use them
for _, r := range beaconHandleTasklist(tasksExtensionRegister) {
pendingResults <- r
pendingResults.add(r)
}
// Dispatch other tasks individually - each sends its result when done
@@ -414,7 +438,7 @@ collecting:
}
// beaconDispatchTasks dispatches tasks to run in background, sending results as each completes
func beaconDispatchTasks(tasks []*sliverpb.Envelope, pendingResults chan *sliverpb.Envelope) {
func beaconDispatchTasks(tasks []*sliverpb.Envelope, pendingResults *beaconResultQueue) {
sysHandlers := handlers.GetSystemHandlers()
specHandlers := handlers.GetKillHandlers()
@@ -434,10 +458,10 @@ func beaconDispatchTasks(tasks []*sliverpb.Envelope, pendingResults chan *sliver
}
log.Printf("[beacon] task completed (id: %d)", taskID)
// {{end}}
pendingResults <- &sliverpb.Envelope{
pendingResults.add(&sliverpb.Envelope{
ID: taskID,
Data: data,
}
})
})
}()
// {{else}}
@@ -449,26 +473,26 @@ func beaconDispatchTasks(tasks []*sliverpb.Envelope, pendingResults chan *sliver
}
log.Printf("[beacon] task completed (id: %d)", taskID)
// {{end}}
pendingResults <- &sliverpb.Envelope{
pendingResults.add(&sliverpb.Envelope{
ID: taskID,
Data: data,
}
})
})
}()
// {{end}}
} else if task.Type == sliverpb.MsgOpenSession {
go openSessionHandler(task.Data)
pendingResults <- &sliverpb.Envelope{
pendingResults.add(&sliverpb.Envelope{
ID: task.ID,
Data: []byte{},
}
})
} else if handler, ok := specHandlers[task.Type]; ok {
go handler(task.Data, nil)
} else {
pendingResults <- &sliverpb.Envelope{
pendingResults.add(&sliverpb.Envelope{
ID: task.ID,
UnknownMessageType: true,
}
})
}
}
}
@@ -783,5 +807,6 @@ func registerSliver() *sliverpb.Register {
ConfigID: "{{ .Config.ID }}",
PeerID: pivots.MyPeerID,
Locale: locale.GetLocale(),
Capabilities: implantCapabilities(),
}
}
+220
View File
@@ -0,0 +1,220 @@
//go:build windows && !sliver_lint
package runner
import (
"errors"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func setBeaconTimingForTest(t *testing.T, interval time.Duration) {
t.Helper()
oldInterval := transports.GetInterval()
oldJitter := transports.GetJitter()
oldC2URI := transports.GetC2URI()
transports.SetInterval(int64(interval))
transports.SetJitter(1) // Int63n(1) always yields zero jitter.
transports.SetC2URI("")
t.Cleanup(func() {
transports.SetInterval(oldInterval)
transports.SetJitter(oldJitter)
transports.SetC2URI(oldC2URI)
})
}
func TestBeaconCheckinLoopSerializesSlowCheckins(t *testing.T) {
const interval = 10 * time.Millisecond
setBeaconTimingForTest(t, interval)
firstStarted := make(chan struct{})
secondStarted := make(chan struct{})
releaseFirst := make(chan struct{})
var releaseOnce sync.Once
release := func() { releaseOnce.Do(func() { close(releaseFirst) }) }
defer release()
stopErr := errors.New("stop check-in loop")
var stateMu sync.Mutex
active := 0
maxActive := 0
calls := 0
checkin := func(*transports.Beacon, time.Time, *beaconResultQueue) error {
stateMu.Lock()
active++
calls++
call := calls
if maxActive < active {
maxActive = active
}
stateMu.Unlock()
defer func() {
stateMu.Lock()
active--
stateMu.Unlock()
}()
switch call {
case 1:
close(firstStarted)
<-releaseFirst
return nil
case 2:
close(secondStarted)
return stopErr
default:
return errors.New("unexpected extra check-in")
}
}
done := make(chan error, 1)
go func() {
done <- beaconCheckinLoop(&transports.Beacon{}, &beaconResultQueue{}, checkin)
}()
select {
case <-firstStarted:
case <-time.After(time.Second):
t.Fatal("first check-in did not start")
}
overlapped := false
select {
case <-secondStarted:
overlapped = true
case <-time.After(5 * interval):
}
release()
select {
case err := <-done:
if !errors.Is(err, stopErr) {
t.Fatalf("got error %v, want %v", err, stopErr)
}
case <-time.After(time.Second):
t.Fatal("check-in loop did not stop")
}
stateMu.Lock()
defer stateMu.Unlock()
if overlapped {
t.Error("second check-in overlapped the first")
}
if calls != 2 {
t.Fatalf("check-in called %d times, want 2", calls)
}
if maxActive != 1 {
t.Fatalf("maximum concurrent check-ins = %d, want 1", maxActive)
}
}
func TestBeaconCheckinLoopShortCircuitsReconfiguredInterval(t *testing.T) {
const originalInterval = 500 * time.Millisecond
setBeaconTimingForTest(t, originalInterval)
stopErr := errors.New("stop check-in loop")
calls := 0
start := time.Now()
err := beaconCheckinLoop(&transports.Beacon{}, &beaconResultQueue{}, func(*transports.Beacon, time.Time, *beaconResultQueue) error {
calls++
if calls == 1 {
transports.SetInterval(int64(time.Millisecond))
return nil
}
return stopErr
})
if !errors.Is(err, stopErr) {
t.Fatalf("got error %v, want %v", err, stopErr)
}
if elapsed := time.Since(start); originalInterval/2 <= elapsed {
t.Fatalf("reconfigured interval did not short circuit sleep: %s", elapsed)
}
}
func TestBeaconResultQueuePrependPreservesOrder(t *testing.T) {
queue := &beaconResultQueue{}
first := &sliverpb.Envelope{ID: 1}
second := &sliverpb.Envelope{ID: 2}
third := &sliverpb.Envelope{ID: 3}
queue.add(third)
queue.prepend([]*sliverpb.Envelope{first, second})
actual := queue.drain()
if len(actual) != 3 || actual[0] != first || actual[1] != second || actual[2] != third {
t.Fatalf("unexpected result order: %+v", actual)
}
if remaining := queue.drain(); len(remaining) != 0 {
t.Fatalf("drain left %d result(s)", len(remaining))
}
}
func TestBeaconMainResultCheckinDoesNotWaitForTasks(t *testing.T) {
queue := &beaconResultQueue{}
queue.add(&sliverpb.Envelope{ID: 41, Data: []byte("result")})
var recvCalled bool
beacon := &transports.Beacon{
Start: func() error { return nil },
Send: func(*sliverpb.Envelope) error { return nil },
Recv: func() (*sliverpb.Envelope, error) {
recvCalled = true
return nil, errors.New("unexpected receive")
},
Close: func() error { return nil },
}
if err := beaconMain(beacon, time.Now(), queue); err != nil {
t.Fatal(err)
}
if recvCalled {
t.Fatal("result-only check-in waited for a server task response")
}
if remaining := queue.drain(); len(remaining) != 0 {
t.Fatalf("successful check-in retained %d result(s)", len(remaining))
}
}
func TestBeaconMainEmptyCheckinWaitsForTasks(t *testing.T) {
queue := &beaconResultQueue{}
var recvCalled bool
beacon := &transports.Beacon{
Start: func() error { return nil },
Send: func(*sliverpb.Envelope) error { return nil },
Recv: func() (*sliverpb.Envelope, error) {
recvCalled = true
return nil, nil
},
Close: func() error { return nil },
}
if err := beaconMain(beacon, time.Now(), queue); err != nil {
t.Fatal(err)
}
if !recvCalled {
t.Fatal("empty check-in did not wait for a server task response")
}
}
func TestBeaconMainRequeuesResultsAfterSendFailure(t *testing.T) {
wantErr := errors.New("send failed")
result := &sliverpb.Envelope{ID: 73, Data: []byte("result")}
queue := &beaconResultQueue{}
queue.add(result)
beacon := &transports.Beacon{
Start: func() error { return nil },
Send: func(*sliverpb.Envelope) error { return wantErr },
Recv: func() (*sliverpb.Envelope, error) { return nil, nil },
Close: func() error { return nil },
}
if err := beaconMain(beacon, time.Now(), queue); !errors.Is(err, wantErr) {
t.Fatalf("got error %v, want %v", err, wantErr)
}
actual := queue.drain()
if len(actual) != 1 || actual[0] != result {
t.Fatalf("send failure did not preserve exact result: %+v", actual)
}
}
+11 -14
View File
@@ -29,6 +29,8 @@ import (
"syscall"
"unsafe"
"golang.org/x/sys/unix"
//{{if .Config.Debug}}
"log"
//{{end}}
@@ -98,26 +100,21 @@ func RemoteTask(processID int, data []byte, rwxPages bool) error {
// Sideload - Side load a library and return its output
func Sideload(procName string, procArgs []string, _ uint32, data []byte, args []string, kill bool) (string, error) {
var (
nrMemfdCreate int
stdOut bytes.Buffer
stdErr bytes.Buffer
wg sync.WaitGroup
cmd *exec.Cmd
stdOut bytes.Buffer
stdErr bytes.Buffer
wg sync.WaitGroup
cmd *exec.Cmd
)
memfdName := RandomString(8)
memfd, err := syscall.BytePtrFromString(memfdName)
fd, err := unix.MemfdCreate(memfdName, unix.MFD_CLOEXEC)
if err != nil {
//{{if .Config.Debug}}
log.Printf("Error during conversion: %s\n", err)
log.Printf("Error creating memfd: %s\n", err)
//{{end}}
return "", err
return "", fmt.Errorf("create memfd: %w", err)
}
if runtime.GOARCH == "386" {
nrMemfdCreate = 356
} else {
nrMemfdCreate = 319
}
fd, _, _ := syscall.Syscall(uintptr(nrMemfdCreate), uintptr(unsafe.Pointer(memfd)), 1, 0)
defer func() { _ = unix.Close(fd) }()
pid := os.Getpid()
fdPath := fmt.Sprintf("/proc/%d/fd/%d", pid, fd)
err = os.WriteFile(fdPath, data, 0755)
+76 -22
View File
@@ -325,8 +325,9 @@ func wgBeacon(uri *url.URL) *Beacon {
return err
}
if _, err := conn.Write([]byte(wireguard.YamuxPreface)); err != nil {
_ = conn.Close()
_ = dev.Down()
closeWGBeaconResources(nil, conn, dev)
conn = nil
dev = nil
return err
}
cfg := yamux.DefaultConfig()
@@ -339,8 +340,9 @@ func wgBeacon(uri *url.URL) *Beacon {
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
_ = conn.Close()
_ = dev.Down()
closeWGBeaconResources(nil, conn, dev)
conn = nil
dev = nil
return err
}
return nil
@@ -354,26 +356,13 @@ func wgBeacon(uri *url.URL) *Beacon {
return wireguard.ReadEnvelope(stream)
},
Send: func(envelope *pb.Envelope) error {
stream, err := muxSession.Open()
if err != nil {
return err
}
defer stream.Close()
return wireguard.WriteEnvelope(stream, envelope)
return sendWGBeaconEnvelope(muxSession, envelope)
},
Close: func() error {
if muxSession != nil {
_ = muxSession.Close()
muxSession = nil
}
if conn != nil {
_ = conn.Close()
conn = nil
}
if dev != nil {
_ = dev.Down()
dev = nil
}
closeWGBeaconResources(muxSession, conn, dev)
muxSession = nil
conn = nil
dev = nil
return nil
},
Cleanup: func() error {
@@ -383,6 +372,71 @@ func wgBeacon(uri *url.URL) *Beacon {
return beacon
}
// closeWGBeaconResources permanently closes each per-check-in resource. A
// device.Down only stops the bind and peers; it leaves the netstack TUN and
// WireGuard worker goroutines alive, so beacon reconnects must use Close.
func closeWGBeaconResources(muxSession *yamux.Session, conn net.Conn, dev *device.Device) {
if muxSession != nil {
_ = muxSession.Close()
}
if conn != nil {
_ = conn.Close()
}
if dev != nil {
closeWGBeaconDevice(dev.Close)
}
}
func closeWGBeaconDevice(closeDevice func()) {
if closeDevice != nil {
closeDevice()
}
}
const wgBeaconStreamReceiptTimeout = 5 * time.Second
func sendWGBeaconEnvelope(muxSession *yamux.Session, envelope *pb.Envelope) error {
if envelope == nil {
return errors.New("[wg] nil beacon envelope")
}
return sendWGBeaconStream(muxSession, wgBeaconStreamReceiptTimeout, func(stream net.Conn) error {
return wireguard.WriteEnvelope(stream, envelope)
})
}
// sendWGBeaconStream waits for the peer's FIN after writing the request. A
// successful yamux write only means the frame reached the local transport; the
// remote FIN proves the server read the complete request before the beacon
// tears down the WireGuard session.
func sendWGBeaconStream(muxSession *yamux.Session, receiptTimeout time.Duration, write func(net.Conn) error) error {
if muxSession == nil {
return errors.New("[wg] send called with nil yamux session")
}
if receiptTimeout <= 0 {
return errors.New("[wg] invalid beacon stream receipt timeout")
}
stream, err := muxSession.Open()
if err != nil {
return err
}
if isNilInterface(stream) {
return errors.New("[wg] open returned nil stream")
}
defer func() { _ = stream.Close() }()
if err := write(stream); err != nil {
return err
}
if err := stream.SetReadDeadline(time.Now().Add(receiptTimeout)); err != nil {
return err
}
if err := stream.Close(); err != nil {
return err
}
_, err = io.Copy(io.Discard, stream)
return err
}
// {{end}}
// {{if .Config.IncludeHTTP}}
@@ -0,0 +1,159 @@
//go:build windows || darwin || linux
package transports
import (
"errors"
"io"
"net"
"sync"
"testing"
"time"
"github.com/hashicorp/yamux"
)
type recordingWGBeaconDevice struct {
closeCalls int
}
func (d *recordingWGBeaconDevice) Close() {
d.closeCalls++
}
func TestCloseWGBeaconDeviceUsesPermanentClose(t *testing.T) {
dev := &recordingWGBeaconDevice{}
closeWGBeaconDevice(dev.Close)
if dev.closeCalls != 1 {
t.Fatalf("wireguard device Close called %d times, want 1", dev.closeCalls)
}
}
func TestSendWGBeaconStreamWaitsForRemoteClose(t *testing.T) {
client, server := newBeaconYamuxPair(t)
marker := []byte("beacon-result")
serverRead := make(chan struct{})
releaseServer := make(chan struct{})
var releaseOnce sync.Once
release := func() { releaseOnce.Do(func() { close(releaseServer) }) }
t.Cleanup(release)
serverErr := make(chan error, 1)
go func() {
stream, err := server.Accept()
if err != nil {
serverErr <- err
return
}
defer func() { _ = stream.Close() }()
got := make([]byte, len(marker))
if _, err := io.ReadFull(stream, got); err != nil {
serverErr <- err
return
}
if string(got) != string(marker) {
serverErr <- errors.New("server received an unexpected payload")
return
}
close(serverRead)
<-releaseServer
serverErr <- nil
}()
sendErr := make(chan error, 1)
go func() {
sendErr <- sendWGBeaconStream(client, time.Second, func(stream net.Conn) error {
_, err := stream.Write(marker)
return err
})
}()
select {
case err := <-serverErr:
t.Fatalf("server failed before reading the request: %v", err)
case err := <-sendErr:
t.Fatalf("send returned before the server read the request: %v", err)
case <-serverRead:
case <-time.After(time.Second):
t.Fatal("server did not receive the request")
}
select {
case err := <-sendErr:
t.Fatalf("send returned before the server closed its stream: %v", err)
case <-time.After(100 * time.Millisecond):
}
release()
select {
case err := <-sendErr:
if err != nil {
t.Fatalf("send failed after the server closed its stream: %v", err)
}
case <-time.After(time.Second):
t.Fatal("send did not observe the server stream close")
}
if err := <-serverErr; err != nil {
t.Fatalf("server failed: %v", err)
}
}
func TestSendWGBeaconStreamReceiptTimeout(t *testing.T) {
client, server := newBeaconYamuxPair(t)
marker := []byte("beacon-result")
releaseServer := make(chan struct{})
var releaseOnce sync.Once
release := func() { releaseOnce.Do(func() { close(releaseServer) }) }
t.Cleanup(release)
serverErr := make(chan error, 1)
go func() {
stream, err := server.Accept()
if err != nil {
serverErr <- err
return
}
defer func() { _ = stream.Close() }()
got := make([]byte, len(marker))
if _, err := io.ReadFull(stream, got); err != nil {
serverErr <- err
return
}
<-releaseServer
serverErr <- nil
}()
err := sendWGBeaconStream(client, 50*time.Millisecond, func(stream net.Conn) error {
_, err := stream.Write(marker)
return err
})
if !errors.Is(err, yamux.ErrTimeout) {
t.Fatalf("got receipt error %v, want %v", err, yamux.ErrTimeout)
}
release()
if err := <-serverErr; err != nil {
t.Fatalf("server failed: %v", err)
}
}
func newBeaconYamuxPair(t *testing.T) (*yamux.Session, *yamux.Session) {
t.Helper()
clientConn, serverConn := net.Pipe()
config := yamux.DefaultConfig()
config.EnableKeepAlive = false
config.LogOutput = io.Discard
server, err := yamux.Server(serverConn, config)
if err != nil {
t.Fatal(err)
}
client, err := yamux.Client(clientConn, config)
if err != nil {
_ = server.Close()
t.Fatal(err)
}
t.Cleanup(func() {
_ = client.Close()
_ = server.Close()
})
return client, server
}
+14 -13
View File
@@ -426,6 +426,17 @@ func wgConnect(uri *url.URL) (*Connection, error) {
var conn net.Conn
var dev *device.Device
var muxSession *yamux.Session
closeTransport := func() {
if muxSession != nil {
_ = muxSession.Close()
}
if conn != nil {
_ = conn.Close()
}
if dev != nil {
dev.Close()
}
}
connection := &Connection{
Send: send,
Recv: recv,
@@ -440,15 +451,7 @@ func wgConnect(uri *url.URL) (*Connection, error) {
log.Printf("[wg] lost connection, cleanup...")
// {{end}}
close(done)
if muxSession != nil {
muxSession.Close()
}
if conn != nil {
conn.Close()
}
if dev != nil {
dev.Down()
}
closeTransport()
close(recv)
},
}
@@ -488,8 +491,7 @@ func wgConnect(uri *url.URL) (*Connection, error) {
return err
}
if _, err := conn.Write([]byte(wireguard.YamuxPreface)); err != nil {
conn.Close()
dev.Down()
closeTransport()
return err
}
cfg := yamux.DefaultConfig()
@@ -502,8 +504,7 @@ func wgConnect(uri *url.URL) (*Connection, error) {
// {{end}}
muxSession, err = yamux.Client(conn, cfg)
if err != nil {
conn.Close()
dev.Down()
closeTransport()
return err
}
connection.IsOpen = true
@@ -0,0 +1,9 @@
//go:build (windows && !arm64) || darwin || linux
package wireguard
import "golang.zx2c4.com/wireguard/conn"
func newWGUDPBind() conn.Bind {
return conn.NewDefaultBind()
}
@@ -0,0 +1,12 @@
//go:build windows && arm64
package wireguard
import "golang.zx2c4.com/wireguard/conn"
// WinRingBind can stop exchanging handshakes after rapid userspace-device
// recreation on Windows ARM64. Beacon callbacks recreate their device at each
// interval, so use wireguard-go's portable socket bind on this target.
func newWGUDPBind() conn.Bind {
return conn.NewStdNetBind()
}
@@ -0,0 +1,15 @@
//go:build windows && arm64
package wireguard
import (
"testing"
"golang.zx2c4.com/wireguard/conn"
)
func TestWindowsARM64WGUDPBindUsesStandardSockets(t *testing.T) {
if _, ok := newWGUDPBind().(*conn.StdNetBind); !ok {
t.Fatal("Windows ARM64 WireGuard bind did not use standard sockets")
}
}
@@ -0,0 +1,201 @@
//go:build windows || darwin || linux
package wireguard
import (
"bytes"
"errors"
"io"
"net"
"strings"
"testing"
)
type ownershipTestDevice struct {
upErr error
configureErr error
upCalls int
closeCalls int
configured string
}
func (d *ownershipTestDevice) Up() error {
d.upCalls++
return d.upErr
}
func (d *ownershipTestDevice) Close() {
d.closeCalls++
}
func (d *ownershipTestDevice) IpcSetOperation(config io.Reader) error {
data, err := io.ReadAll(config)
if err != nil {
return err
}
d.configured = string(data)
return d.configureErr
}
type ownershipTestDialer struct {
dial func(network string, address string) (net.Conn, error)
dialCalls int
network string
address string
}
func (d *ownershipTestDialer) Dial(network string, address string) (net.Conn, error) {
d.dialCalls++
d.network = network
d.address = address
return d.dial(network, address)
}
func TestGetSessKeysWithDeviceClosesTemporaryDevice(t *testing.T) {
originalPrivate, originalPublic, originalAddress := wgSessPrivKey, wgSessPubKey, tunAddress
t.Cleanup(func() {
wgSessPrivKey, wgSessPubKey, tunAddress = originalPrivate, originalPublic, originalAddress
})
privateKey := strings.Repeat("a", 64)
publicKey := strings.Repeat("b", 64)
dialer := &ownershipTestDialer{dial: func(string, string) (net.Conn, error) {
client, server := net.Pipe()
go func() {
_, _ = server.Write([]byte(privateKey + "|" + publicKey + "|100.64.0.42"))
_ = server.Close()
}()
return client, nil
}}
dev := &ownershipTestDevice{}
if err := getSessKeysWithDevice(dev, dialer); err != nil {
t.Fatal(err)
}
if dev.upCalls != 1 || dev.closeCalls != 1 {
t.Fatalf("temporary device calls: Up=%d Close=%d, want 1 each", dev.upCalls, dev.closeCalls)
}
if wgSessPrivKey != privateKey || wgSessPubKey != publicKey || tunAddress != "100.64.0.42" {
t.Fatalf("unexpected session keys: private=%q public=%q address=%q", wgSessPrivKey, wgSessPubKey, tunAddress)
}
}
func TestGetSessKeysWithDeviceClosesTemporaryDeviceOnErrors(t *testing.T) {
originalPrivate, originalPublic, originalAddress := wgSessPrivKey, wgSessPubKey, tunAddress
t.Cleanup(func() {
wgSessPrivKey, wgSessPubKey, tunAddress = originalPrivate, originalPublic, originalAddress
})
upErr := errors.New("up failed")
dialErr := errors.New("dial failed")
tests := []struct {
name string
dev *ownershipTestDevice
dial func(string, string) (net.Conn, error)
wantErr error
wantDialed bool
}{
{
name: "device up",
dev: &ownershipTestDevice{upErr: upErr},
dial: func(string, string) (net.Conn, error) { return nil, errors.New("unexpected dial") },
wantErr: upErr,
},
{
name: "key exchange dial",
dev: &ownershipTestDevice{},
dial: func(string, string) (net.Conn, error) { return nil, dialErr },
wantErr: dialErr,
wantDialed: true,
},
{
name: "key exchange response",
dev: &ownershipTestDevice{},
dial: func(string, string) (net.Conn, error) {
client, server := net.Pipe()
go func() {
_, _ = server.Write([]byte("malformed"))
_ = server.Close()
}()
return client, nil
},
wantDialed: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dialer := &ownershipTestDialer{dial: test.dial}
err := getSessKeysWithDevice(test.dev, dialer)
if err == nil {
t.Fatal("expected key exchange to fail")
}
if test.wantErr != nil && !errors.Is(err, test.wantErr) {
t.Fatalf("got error %v, want %v", err, test.wantErr)
}
if test.dev.closeCalls != 1 {
t.Fatalf("temporary device Close called %d times, want 1", test.dev.closeCalls)
}
if got := dialer.dialCalls > 0; got != test.wantDialed {
t.Fatalf("dial called=%v, want %v", got, test.wantDialed)
}
})
}
}
func TestDialDeviceTransfersOrReleasesOwnership(t *testing.T) {
t.Run("success", func(t *testing.T) {
client, server := net.Pipe()
defer func() { _ = server.Close() }()
dev := &ownershipTestDevice{}
dialer := &ownershipTestDialer{dial: func(string, string) (net.Conn, error) { return client, nil }}
connection, err := dialDevice(dialer, dev, "tcp", "100.64.0.1:8888")
if err != nil {
t.Fatal(err)
}
defer func() { _ = connection.Close() }()
if dev.closeCalls != 0 {
t.Fatalf("successful dial closed returned device %d times", dev.closeCalls)
}
})
t.Run("failure", func(t *testing.T) {
wantErr := errors.New("dial failed")
dev := &ownershipTestDevice{}
dialer := &ownershipTestDialer{dial: func(string, string) (net.Conn, error) { return nil, wantErr }}
connection, err := dialDevice(dialer, dev, "tcp", "100.64.0.1:8888")
if connection != nil || !errors.Is(err, wantErr) {
t.Fatalf("got connection=%v error=%v, want nil and %v", connection, err, wantErr)
}
if dev.closeCalls != 1 {
t.Fatalf("failed dial closed device %d times, want 1", dev.closeCalls)
}
})
}
func TestConfigureDeviceTransfersOrReleasesOwnership(t *testing.T) {
t.Run("success", func(t *testing.T) {
dev := &ownershipTestDevice{}
if err := configureDevice(dev, bytes.NewBufferString("private_key=test\n")); err != nil {
t.Fatal(err)
}
if dev.closeCalls != 0 {
t.Fatalf("successful configuration closed returned device %d times", dev.closeCalls)
}
if dev.configured != "private_key=test\n" {
t.Fatalf("unexpected configuration %q", dev.configured)
}
})
t.Run("failure", func(t *testing.T) {
wantErr := errors.New("configuration failed")
dev := &ownershipTestDevice{configureErr: wantErr}
err := configureDevice(dev, bytes.NewBufferString("private_key=test\n"))
if !errors.Is(err, wantErr) {
t.Fatalf("got error %v, want %v", err, wantErr)
}
if dev.closeCalls != 1 {
t.Fatalf("failed configuration closed device %d times, want 1", dev.closeCalls)
}
})
}
@@ -0,0 +1,52 @@
//go:build windows || darwin || linux
package wireguard
import (
"net"
"strings"
"testing"
)
func exchangeResponse(t *testing.T, response string) (string, string, string, error) {
t.Helper()
client, server := net.Pipe()
go func() {
_, _ = server.Write([]byte(response))
_ = server.Close()
}()
defer func() { _ = client.Close() }()
return doKeyExchange(client)
}
func TestDoKeyExchangeParsesTextTunnelIP(t *testing.T) {
privateKey := strings.Repeat("a", 64)
publicKey := strings.Repeat("b", 64)
gotPrivate, gotPublic, gotIP, err := exchangeResponse(t, privateKey+"|"+publicKey+"|100.64.0.23")
if err != nil {
t.Fatal(err)
}
if gotPrivate != privateKey || gotPublic != publicKey || gotIP != "100.64.0.23" {
t.Fatalf("unexpected exchange result: private=%q public=%q ip=%q", gotPrivate, gotPublic, gotIP)
}
}
func TestDoKeyExchangeRejectsMalformedResponses(t *testing.T) {
key := strings.Repeat("a", 64)
tests := map[string]string{
"missing field": key + "|" + key,
"short key": "aa|" + key + "|100.64.0.2",
"non-hex key": strings.Repeat("z", 64) + "|" + key + "|100.64.0.2",
"invalid IP": key + "|" + key + "|not-an-ip",
"IPv6 tunnel IP": key + "|" + key + "|fd00::2",
"oversized payload": strings.Repeat("x", 257),
}
for name, response := range tests {
t.Run(name, func(t *testing.T) {
_, _, _, err := exchangeResponse(t, response)
if err == nil {
t.Fatal("expected malformed key exchange response to fail")
}
})
}
}
@@ -28,6 +28,7 @@ import (
"crypto/ed25519"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
@@ -47,7 +48,6 @@ import (
"github.com/bishopfox/sliver/implant/sliver/netstack"
"golang.org/x/crypto/blake2b"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"google.golang.org/protobuf/proto"
@@ -251,12 +251,35 @@ func formatWGEndpoint(address string, port uint16) string {
return net.JoinHostPort(address, strconv.Itoa(int(port)))
}
type permanentDevice interface {
Close()
}
type sessionKeyDevice interface {
permanentDevice
Up() error
}
type netDialer interface {
Dial(network string, address string) (net.Conn, error)
}
type deviceConfigurator interface {
permanentDevice
IpcSetOperation(io.Reader) error
}
// getSessKeys - Connect to the wireguard server and retrieve session specific keys and IP
func getSessKeys(address string, port uint16) error {
_, dev, tNet, err := bringUpWGInterface(address, port, wgImplantPrivKey, wgServerPubKey, wgPeerTunIP)
if err != nil {
return err
}
return getSessKeysWithDevice(dev, tNet)
}
func getSessKeysWithDevice(dev sessionKeyDevice, tNet netDialer) error {
defer dev.Close()
if err := dev.Up(); err != nil {
return err
@@ -271,25 +294,17 @@ func getSessKeys(address string, port uint16) error {
// {{if .Config.Debug}}
log.Printf("Unable to connect to wg key exchange listener: %v", err)
// {{end}}
_ = dev.Down()
return err
}
wgSessPrivKey, wgSessPubKey, tunAddress = doKeyExchange(keyExchangeConnection)
wgSessPrivKey, wgSessPubKey, tunAddress, err = doKeyExchange(keyExchangeConnection)
if err != nil {
return err
}
// {{if .Config.Debug}}
log.Printf("Signaling wg device to go down")
log.Printf("Closing temporary wg key exchange device")
// {{end}}
// Close initial wireguard connection
err = dev.Down()
if err != nil {
// {{if .Config.Debug}}
log.Printf("Failed to close device.Device: %s", err)
// {{end}}
return err
}
return nil
}
@@ -309,7 +324,7 @@ func WGConnect(address string, port uint16) (net.Conn, *device.Device, error) {
return nil, nil, err
}
connection, err := tNet.Dial("tcp", fmt.Sprintf("%s:%d", serverTunIP, wgTcpCommsPort))
connection, err := dialDevice(tNet, dev, "tcp", fmt.Sprintf("%s:%d", serverTunIP, wgTcpCommsPort))
if err != nil {
// {{if .Config.Debug}}
log.Printf("Unable to connect to sliver listener: %v", err)
@@ -326,6 +341,15 @@ func WGConnect(address string, port uint16) (net.Conn, *device.Device, error) {
return connection, dev, nil
}
func dialDevice(dialer netDialer, dev permanentDevice, network string, address string) (net.Conn, error) {
connection, err := dialer.Dial(network, address)
if err != nil {
dev.Close()
return nil, err
}
return connection, nil
}
// bringUpWGInterface - First creates an inet.af network stack.
// then creates a Wireguard device/interface and applies configuration
func bringUpWGInterface(address string, port uint16, implantPrivKey string, serverPubKey string, netstackTunIP string) (tun.Device, *device.Device, *netstack.Net, error) {
@@ -342,6 +366,7 @@ func bringUpWGInterface(address string, port uint16, implantPrivKey string, serv
// {{if .Config.Debug}}
log.Panic(err)
// {{end}}
return nil, nil, nil, err
}
wgLogLevel := device.LogLevelSilent
@@ -349,7 +374,7 @@ func bringUpWGInterface(address string, port uint16, implantPrivKey string, serv
wgLogLevel = device.LogLevelVerbose
// {{end}}
dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(wgLogLevel, "[c2/wg] "))
dev := device.NewDevice(tun, newWGUDPBind(), device.NewLogger(wgLogLevel, "[c2/wg] "))
wgConf := bytes.NewBuffer(nil)
fmt.Fprintf(wgConf, "private_key=%s\n", implantPrivKey)
fmt.Fprintf(wgConf, "public_key=%s\n", serverPubKey)
@@ -360,7 +385,7 @@ func bringUpWGInterface(address string, port uint16, implantPrivKey string, serv
log.Printf("Configuring wg device with: %s", wgConf.String())
// {{end}}
if err := dev.IpcSetOperation(bufio.NewReader(wgConf)); err != nil {
if err := configureDevice(dev, bufio.NewReader(wgConf)); err != nil {
// {{if .Config.Debug}}
log.Printf("Failed to set wg device config: %s", err)
// {{end}}
@@ -373,29 +398,53 @@ func bringUpWGInterface(address string, port uint16, implantPrivKey string, serv
return tun, dev, tNet, nil
}
func configureDevice(dev deviceConfigurator, config io.Reader) error {
if err := dev.IpcSetOperation(config); err != nil {
dev.Close()
return err
}
return nil
}
// doKeyExchange - Connect to key exchange listener and retrieve new dynamic wg keys
func doKeyExchange(conn net.Conn) (string, string, string) {
func doKeyExchange(conn net.Conn) (string, string, string, error) {
// {{if .Config.Debug}}
log.Printf("Connected to key exchange listener")
// {{end}}
defer conn.Close()
// 129 = 64 byte key + 1 byte delimiter + 64 byte key + 1 byte delimiter + 16 byte ip address
buff := make([]byte, 146)
buffReader := bufio.NewReader(conn)
_, err := io.ReadFull(buffReader, buff)
const maxMessageSize = 256
buff, err := io.ReadAll(io.LimitReader(conn, maxMessageSize+1))
if err != nil {
// {{if .Config.Debug}}
log.Printf("Failed to read wg keys from key exchange listener: %s", err)
// {{end}}
return "", "", "", fmt.Errorf("read WireGuard key exchange response: %w", err)
}
if len(buff) > maxMessageSize {
return "", "", "", errors.New("WireGuard key exchange response exceeds maximum length")
}
stringSlice := strings.Split(string(buff), "|")
if len(stringSlice) != 3 {
return "", "", "", fmt.Errorf("invalid WireGuard key exchange response: got %d fields", len(stringSlice))
}
for index, key := range stringSlice[:2] {
if len(key) != 64 {
return "", "", "", fmt.Errorf("invalid WireGuard key exchange key %d length", index+1)
}
if _, err := hex.DecodeString(key); err != nil {
return "", "", "", fmt.Errorf("invalid WireGuard key exchange key %d encoding", index+1)
}
}
tunIP, err := netip.ParseAddr(stringSlice[2])
if err != nil || !tunIP.Is4() {
return "", "", "", fmt.Errorf("invalid WireGuard key exchange tunnel IP %q", stringSlice[2])
}
// {{if .Config.Debug}}
log.Printf("Retrieved new keys, priv:%s, pub:%s, ip:%s", stringSlice[0], stringSlice[1], net.IP(stringSlice[2]).String())
log.Printf("Retrieved new WireGuard session keys for tunnel IP %s", tunIP.String())
// {{end}}
return stringSlice[0], stringSlice[1], net.IP(stringSlice[2]).String()
return stringSlice[0], stringSlice[1], tunIP.String(), nil
}
func getWgKeyExchangePort() int {
-27
View File
@@ -1,27 +0,0 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
File diff suppressed because it is too large. Load diff
-32
View File
@@ -1,32 +0,0 @@
package elf
/*
Any symbol in the dynamic symbol table (in .dynsym) for which .st_shndx == SHN_UNDEF
(references undefined section) is an import, and every other symbol is defined and exported.
*/
// Export - describes a single export entry
type Export struct {
Name string
VirtualAddress uint64
}
// Exports - gets exports
func (f *File) Exports() ([]Export, error) {
var exports []Export
symbols, err := f.DynamicSymbols()
if err != nil {
return nil, err
}
for _, s := range symbols {
if s.Section != SHN_UNDEF {
exports = append(exports, Export{
Name: s.Name,
VirtualAddress: s.Value,
})
}
}
return exports, nil
}
-964
View File
@@ -1,964 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package elf implements access to ELF object files.
package elf
import (
"bytes"
"compress/zlib"
"debug/dwarf"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strings"
)
// seekStart, seekCurrent, seekEnd are copies of
// io.SeekStart, io.SeekCurrent, and io.SeekEnd.
// We can't use the ones from package io because
// we want this code to build with Go 1.4 during
// cmd/dist bootstrap.
const (
seekStart int = 0
seekCurrent int = 1
seekEnd int = 2
)
// TODO: error reporting detail
/*
* Internal ELF representation
*/
// A FileHeader represents an ELF file header.
type FileHeader struct {
Class Class
Data Data
Version Version
OSABI OSABI
ABIVersion uint8
ByteOrder binary.ByteOrder
Type Type
Machine Machine
Entry uint64
SHTOffset int64
ShStrIndex int
}
// A File represents an open ELF file.
type File struct {
FileHeader
Sections []*Section
Progs []*Prog
closer io.Closer
gnuNeed []verneed
gnuVersym []byte
Insertion []byte
InsertionEOF []byte
DynTags []DynTagValue
}
// A SectionHeader represents a single ELF section header.
type SectionHeader struct {
Name string
Type SectionType
Flags SectionFlag
Addr uint64
Offset uint64
Size uint64
Link uint32
Info uint32
Addralign uint64
Entsize uint64
Shnum int // Section Header Number
Shname uint32 // Section Header Name (index)
// FileSize is the size of this section in the file in bytes.
// If a section is compressed, FileSize is the size of the
// compressed data, while Size (above) is the size of the
// uncompressed data.
FileSize uint64
}
// A Section represents a single section in an ELF file.
type Section struct {
SectionHeader
// Embed ReaderAt for ReadAt method.
// Do not embed SectionReader directly
// to avoid having Read and Seek.
// If a client wants Read and Seek it must use
// Open() to avoid fighting over the seek offset
// with other clients.
//
// ReaderAt may be nil if the section is not easily available
// in a random-access form. For example, a compressed section
// may have a nil ReaderAt.
io.ReaderAt
sr *io.SectionReader
compressionType CompressionType
compressionOffset int64
}
// Data reads and returns the contents of the ELF section.
// Even if the section is stored compressed in the ELF file,
// Data returns uncompressed data.
func (s *Section) Data() ([]byte, error) {
dat := make([]byte, s.Size)
n, err := io.ReadFull(s.Open(), dat)
return dat[0:n], err
}
// stringTable reads and returns the string table given by the
// specified link value.
func (f *File) stringTable(link uint32) ([]byte, error) {
if link <= 0 || link >= uint32(len(f.Sections)) {
return nil, errors.New("section has invalid string table link")
}
return f.Sections[link].Data()
}
// Open returns a new ReadSeeker reading the ELF section.
// Even if the section is stored compressed in the ELF file,
// the ReadSeeker reads uncompressed data.
func (s *Section) Open() io.ReadSeeker {
if s.Flags&SHF_COMPRESSED == 0 {
return io.NewSectionReader(s.sr, 0, 1<<63-1)
}
if s.compressionType == COMPRESS_ZLIB {
return &readSeekerFromReader{
reset: func() (io.Reader, error) {
fr := io.NewSectionReader(s.sr, s.compressionOffset, int64(s.FileSize)-s.compressionOffset)
return zlib.NewReader(fr)
},
size: int64(s.Size),
}
}
err := &FormatError{int64(s.Offset), "unknown compression type", s.compressionType}
return errorReader{err}
}
// A ProgHeader represents a single ELF program header.
type ProgHeader struct {
Type ProgType
Flags ProgFlag
Off uint64
Vaddr uint64
Paddr uint64
Filesz uint64
Memsz uint64
Align uint64
}
// A Prog represents a single ELF program header in an ELF binary.
type Prog struct {
ProgHeader
// Embed ReaderAt for ReadAt method.
// Do not embed SectionReader directly
// to avoid having Read and Seek.
// If a client wants Read and Seek it must use
// Open() to avoid fighting over the seek offset
// with other clients.
io.ReaderAt
sr *io.SectionReader
}
// Open returns a new ReadSeeker reading the ELF program body.
func (p *Prog) Open() io.ReadSeeker { return io.NewSectionReader(p.sr, 0, 1<<63-1) }
// A Symbol represents an entry in an ELF symbol table section.
type Symbol struct {
Name string
NameIndex uint32
Info, Other byte
Section SectionIndex
SectIndex uint16
Value, Size uint64
}
// ToSym64 - Convert to a Sym64
func (s Symbol) ToSym64() (retval Sym64) {
retval.Name = s.NameIndex
retval.Info = s.Info
retval.Other = s.Other
retval.Shndx = s.SectIndex
retval.Value = s.Value
retval.Size = s.Size
return
}
// ToSym32 - Convert to a Sym32
func (s Symbol) ToSym32() (retval Sym32) {
retval.Name = s.NameIndex
retval.Info = s.Info
retval.Other = s.Other
retval.Shndx = s.SectIndex
retval.Value = uint32(s.Value)
retval.Size = uint32(s.Size)
return
}
/*
* ELF reader
*/
type FormatError struct {
off int64
msg string
val interface{}
}
func (e *FormatError) Error() string {
msg := e.msg
if e.val != nil {
msg += fmt.Sprintf(" '%v' ", e.val)
}
msg += fmt.Sprintf("in record at byte %#x", e.off)
return msg
}
// Open opens the named file using os.Open and prepares it for use as an ELF binary.
func Open(name string) (*File, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
ff, err := NewFile(f)
if err != nil {
f.Close()
return nil, err
}
ff.closer = f
return ff, nil
}
// Close closes the File.
// If the File was created using NewFile directly instead of Open,
// Close has no effect.
func (f *File) Close() error {
var err error
if f.closer != nil {
err = f.closer.Close()
f.closer = nil
}
return err
}
// SectionByType returns the first section in f with the
// given type, or nil if there is no such section.
func (f *File) SectionByType(typ SectionType) *Section {
for _, s := range f.Sections {
if s.Type == typ {
return s
}
}
return nil
}
// SectionByName returns the first section in f with the
// given name, or nil if there is no such section.
func (f *File) SectionByName(name string) *Section {
for _, s := range f.Sections {
if s.Name == name {
return s
}
}
return nil
}
// NewFile creates a new File for accessing an ELF binary in an underlying reader.
// The ELF binary is expected to start at position 0 in the ReaderAt.
func NewFile(r io.ReaderAt) (*File, error) {
sr := io.NewSectionReader(r, 0, 1<<63-1)
// Read and decode ELF identifier
var ident [16]uint8
if _, err := r.ReadAt(ident[0:], 0); err != nil {
return nil, err
}
if ident[0] != '\x7f' || ident[1] != 'E' || ident[2] != 'L' || ident[3] != 'F' {
return nil, &FormatError{0, "bad magic number", ident[0:4]}
}
f := new(File)
f.Class = Class(ident[EI_CLASS])
switch f.Class {
case ELFCLASS32:
case ELFCLASS64:
// ok
default:
return nil, &FormatError{0, "unknown ELF class", f.Class}
}
f.Data = Data(ident[EI_DATA])
switch f.Data {
case ELFDATA2LSB:
f.ByteOrder = binary.LittleEndian
case ELFDATA2MSB:
f.ByteOrder = binary.BigEndian
default:
return nil, &FormatError{0, "unknown ELF data encoding", f.Data}
}
f.Version = Version(ident[EI_VERSION])
if f.Version != EV_CURRENT {
return nil, &FormatError{0, "unknown ELF version", f.Version}
}
f.OSABI = OSABI(ident[EI_OSABI])
f.ABIVersion = ident[EI_ABIVERSION]
// Read ELF file header
var phoff int64
var phentsize, phnum int
var shentsize, shnum int
f.ShStrIndex = -1
switch f.Class {
case ELFCLASS32:
hdr := new(Header32)
sr.Seek(0, seekStart)
if err := binary.Read(sr, f.ByteOrder, hdr); err != nil {
return nil, err
}
f.Type = Type(hdr.Type)
f.Machine = Machine(hdr.Machine)
f.Entry = uint64(hdr.Entry)
if v := Version(hdr.Version); v != f.Version {
return nil, &FormatError{0, "mismatched ELF version", v}
}
phoff = int64(hdr.Phoff)
phentsize = int(hdr.Phentsize)
phnum = int(hdr.Phnum)
f.SHTOffset = int64(hdr.Shoff)
shentsize = int(hdr.Shentsize)
shnum = int(hdr.Shnum)
f.ShStrIndex = int(hdr.Shstrndx)
case ELFCLASS64:
hdr := new(Header64)
sr.Seek(0, seekStart)
if err := binary.Read(sr, f.ByteOrder, hdr); err != nil {
return nil, err
}
f.Type = Type(hdr.Type)
f.Machine = Machine(hdr.Machine)
f.Entry = hdr.Entry
if v := Version(hdr.Version); v != f.Version {
return nil, &FormatError{0, "mismatched ELF version", v}
}
phoff = int64(hdr.Phoff)
phentsize = int(hdr.Phentsize)
phnum = int(hdr.Phnum)
f.SHTOffset = int64(hdr.Shoff)
shentsize = int(hdr.Shentsize)
shnum = int(hdr.Shnum)
f.ShStrIndex = int(hdr.Shstrndx)
}
if shnum > 0 && f.SHTOffset > 0 && (f.ShStrIndex < 0 || f.ShStrIndex >= shnum) {
return nil, &FormatError{0, "invalid ELF shstrndx", f.ShStrIndex}
}
// Read program headers
f.Progs = make([]*Prog, phnum)
for i := 0; i < phnum; i++ {
off := phoff + int64(i)*int64(phentsize)
sr.Seek(off, seekStart)
p := new(Prog)
switch f.Class {
case ELFCLASS32:
ph := new(Prog32)
if err := binary.Read(sr, f.ByteOrder, ph); err != nil {
return nil, err
}
p.ProgHeader = ProgHeader{
Type: ProgType(ph.Type),
Flags: ProgFlag(ph.Flags),
Off: uint64(ph.Off),
Vaddr: uint64(ph.Vaddr),
Paddr: uint64(ph.Paddr),
Filesz: uint64(ph.Filesz),
Memsz: uint64(ph.Memsz),
Align: uint64(ph.Align),
}
case ELFCLASS64:
ph := new(Prog64)
if err := binary.Read(sr, f.ByteOrder, ph); err != nil {
return nil, err
}
p.ProgHeader = ProgHeader{
Type: ProgType(ph.Type),
Flags: ProgFlag(ph.Flags),
Off: ph.Off,
Vaddr: ph.Vaddr,
Paddr: ph.Paddr,
Filesz: ph.Filesz,
Memsz: ph.Memsz,
Align: ph.Align,
}
}
p.sr = io.NewSectionReader(r, int64(p.Off), int64(p.Filesz))
p.ReaderAt = p.sr
f.Progs[i] = p
}
// Read section headers
f.Sections = make([]*Section, shnum)
names := make([]uint32, shnum)
for i := 0; i < shnum; i++ {
off := f.SHTOffset + int64(i)*int64(shentsize)
sr.Seek(off, seekStart)
s := new(Section)
switch f.Class {
case ELFCLASS32:
sh := new(Section32)
if err := binary.Read(sr, f.ByteOrder, sh); err != nil {
return nil, err
}
names[i] = sh.Name
s.SectionHeader = SectionHeader{
Type: SectionType(sh.Type),
Flags: SectionFlag(sh.Flags),
Addr: uint64(sh.Addr),
Offset: uint64(sh.Off),
FileSize: uint64(sh.Size),
Link: sh.Link,
Info: sh.Info,
Addralign: uint64(sh.Addralign),
Entsize: uint64(sh.Entsize),
Shnum: i,
Shname: sh.Name,
}
case ELFCLASS64:
sh := new(Section64)
if err := binary.Read(sr, f.ByteOrder, sh); err != nil {
return nil, err
}
names[i] = sh.Name
s.SectionHeader = SectionHeader{
Type: SectionType(sh.Type),
Flags: SectionFlag(sh.Flags),
Offset: sh.Off,
FileSize: sh.Size,
Addr: sh.Addr,
Link: sh.Link,
Info: sh.Info,
Addralign: sh.Addralign,
Entsize: sh.Entsize,
Shnum: i,
Shname: sh.Name,
}
}
s.sr = io.NewSectionReader(r, int64(s.Offset), int64(s.FileSize))
if s.Flags&SHF_COMPRESSED == 0 {
s.ReaderAt = s.sr
s.Size = s.FileSize
} else {
// Read the compression header.
switch f.Class {
case ELFCLASS32:
ch := new(Chdr32)
if err := binary.Read(s.sr, f.ByteOrder, ch); err != nil {
return nil, err
}
s.compressionType = CompressionType(ch.Type)
s.Size = uint64(ch.Size)
s.Addralign = uint64(ch.Addralign)
s.compressionOffset = int64(binary.Size(ch))
case ELFCLASS64:
ch := new(Chdr64)
if err := binary.Read(s.sr, f.ByteOrder, ch); err != nil {
return nil, err
}
s.compressionType = CompressionType(ch.Type)
s.Size = ch.Size
s.Addralign = ch.Addralign
s.compressionOffset = int64(binary.Size(ch))
}
}
f.Sections[i] = s
}
if err := f.parseDynTags(); err != nil {
return nil, err
}
if len(f.Sections) == 0 {
return f, nil
}
// Load section header string table.
shstrtab, err := f.Sections[f.ShStrIndex].Data()
if err != nil {
return nil, err
}
for i, s := range f.Sections {
var ok bool
s.Name, ok = getString(shstrtab, int(names[i]))
if !ok {
return nil, &FormatError{f.SHTOffset + int64(i*shentsize), "bad section name index", names[i]}
}
}
return f, nil
}
// getSymbols returns a slice of Symbols from parsing the symbol table
// with the given type, along with the associated string table.
func (f *File) getSymbols(typ SectionType) ([]Symbol, []byte, error) {
switch f.Class {
case ELFCLASS64:
return f.getSymbols64(typ)
case ELFCLASS32:
return f.getSymbols32(typ)
}
return nil, nil, errors.New("not implemented")
}
// ErrNoSymbols is returned by File.Symbols and File.DynamicSymbols
// if there is no such section in the File.
var ErrNoSymbols = errors.New("no symbol section")
func (f *File) getSymbols32(typ SectionType) ([]Symbol, []byte, error) {
symtabSection := f.SectionByType(typ)
if symtabSection == nil {
return nil, nil, ErrNoSymbols
}
data, err := symtabSection.Data()
if err != nil {
return nil, nil, errors.New("cannot load symbol section")
}
symtab := bytes.NewReader(data)
if symtab.Len()%Sym32Size != 0 {
return nil, nil, errors.New("length of symbol section is not a multiple of SymSize")
}
strdata, err := f.stringTable(symtabSection.Link)
if err != nil {
return nil, nil, errors.New("cannot load string table section")
}
// The first entry is all zeros.
var skip [Sym32Size]byte
symtab.Read(skip[:])
symbols := make([]Symbol, symtab.Len()/Sym32Size)
i := 0
var sym Sym32
for symtab.Len() > 0 {
binary.Read(symtab, f.ByteOrder, &sym)
str, _ := getString(strdata, int(sym.Name))
symbols[i].Name = str
symbols[i].Info = sym.Info
symbols[i].Other = sym.Other
symbols[i].Section = SectionIndex(sym.Shndx)
symbols[i].Value = uint64(sym.Value)
symbols[i].Size = uint64(sym.Size)
i++
}
return symbols, strdata, nil
}
func (f *File) getSymbols64(typ SectionType) ([]Symbol, []byte, error) {
symtabSection := f.SectionByType(typ)
if symtabSection == nil {
return nil, nil, ErrNoSymbols
}
data, err := symtabSection.Data()
if err != nil {
return nil, nil, errors.New("cannot load symbol section")
}
symtab := bytes.NewReader(data)
if symtab.Len()%Sym64Size != 0 {
return nil, nil, errors.New("length of symbol section is not a multiple of Sym64Size")
}
strdata, err := f.stringTable(symtabSection.Link)
if err != nil {
return nil, nil, errors.New("cannot load string table section")
}
// The first entry is all zeros.
var skip [Sym64Size]byte
symtab.Read(skip[:])
symbols := make([]Symbol, symtab.Len()/Sym64Size)
i := 0
var sym Sym64
for symtab.Len() > 0 {
binary.Read(symtab, f.ByteOrder, &sym)
str, _ := getString(strdata, int(sym.Name))
symbols[i].Name = str
symbols[i].NameIndex = sym.Name
symbols[i].Info = sym.Info
symbols[i].Other = sym.Other
symbols[i].Section = SectionIndex(sym.Shndx)
symbols[i].SectIndex = sym.Shndx
symbols[i].Value = sym.Value
symbols[i].Size = sym.Size
i++
}
return symbols, strdata, nil
}
// getString extracts a string from an ELF string table.
func getString(section []byte, start int) (string, bool) {
if start < 0 || start >= len(section) {
return "", false
}
for end := start; end < len(section); end++ {
if section[end] == 0 {
return string(section[start:end]), true
}
}
return "", false
}
// Section returns a section with the given name, or nil if no such
// section exists.
func (f *File) Section(name string) *Section {
for _, s := range f.Sections {
if s.Name == name {
return s
}
}
return nil
}
// DWARF - No idea what this does
func (f *File) DWARF() (*dwarf.Data, error) {
dwarfSuffix := func(s *Section) string {
switch {
case strings.HasPrefix(s.Name, ".debug_"):
return s.Name[7:]
case strings.HasPrefix(s.Name, ".zdebug_"):
return s.Name[8:]
default:
return ""
}
}
// sectionData gets the data for s, checks its size, and
// applies any applicable relations.
sectionData := func(i int, s *Section) ([]byte, error) {
b, err := s.Data()
if err != nil && uint64(len(b)) < s.Size {
return nil, err
}
if len(b) >= 12 && string(b[:4]) == "ZLIB" {
dlen := binary.BigEndian.Uint64(b[4:12])
dbuf := make([]byte, dlen)
r, err := zlib.NewReader(bytes.NewBuffer(b[12:]))
if err != nil {
return nil, err
}
if _, err := io.ReadFull(r, dbuf); err != nil {
return nil, err
}
if err := r.Close(); err != nil {
return nil, err
}
b = dbuf
}
for _, r := range f.Sections {
if r.Type != SHT_RELA && r.Type != SHT_REL {
continue
}
if int(r.Info) != i {
continue
}
rd, err := r.Data()
if err != nil {
return nil, err
}
err = f.applyRelocations(b, rd)
if err != nil {
return nil, err
}
}
return b, nil
}
// There are many other DWARF sections, but these
// are the ones the debug/dwarf package uses.
// Don't bother loading others.
var dat = map[string][]byte{"abbrev": nil, "info": nil, "str": nil, "line": nil, "ranges": nil}
for i, s := range f.Sections {
suffix := dwarfSuffix(s)
if suffix == "" {
continue
}
if _, ok := dat[suffix]; !ok {
continue
}
b, err := sectionData(i, s)
if err != nil {
return nil, err
}
dat[suffix] = b
}
d, err := dwarf.New(dat["abbrev"], nil, nil, dat["info"], dat["line"], nil, dat["ranges"], dat["str"])
if err != nil {
return nil, err
}
// Look for DWARF4 .debug_types sections.
for i, s := range f.Sections {
suffix := dwarfSuffix(s)
if suffix != "types" {
continue
}
b, err := sectionData(i, s)
if err != nil {
return nil, err
}
err = d.AddTypes(fmt.Sprintf("types-%d", i), b)
if err != nil {
return nil, err
}
}
return d, nil
}
// Symbols returns the symbol table for f. The symbols will be listed in the order
// they appear in f.
//
// For compatibility with Go 1.0, Symbols omits the null symbol at index 0.
// After retrieving the symbols as symtab, an externally supplied index x
// corresponds to symtab[x-1], not symtab[x].
func (f *File) Symbols() ([]Symbol, error) {
sym, _, err := f.getSymbols(SHT_SYMTAB)
return sym, err
}
// DynamicSymbols returns the dynamic symbol table for f. The symbols
// will be listed in the order they appear in f.
//
// For compatibility with Symbols, DynamicSymbols omits the null symbol at index 0.
// After retrieving the symbols as symtab, an externally supplied index x
// corresponds to symtab[x-1], not symtab[x].
func (f *File) DynamicSymbols() ([]Symbol, error) {
sym, _, err := f.getSymbols(SHT_DYNSYM)
return sym, err
}
type ImportedSymbol struct {
Name string
Version string
Library string
}
// ImportedSymbols returns the names of all symbols
// referred to by the binary f that are expected to be
// satisfied by other libraries at dynamic load time.
// It does not return weak symbols.
func (f *File) ImportedSymbols() ([]ImportedSymbol, error) {
sym, str, err := f.getSymbols(SHT_DYNSYM)
if err != nil {
return nil, err
}
f.gnuVersionInit(str)
var all []ImportedSymbol
for i, s := range sym {
if ST_BIND(s.Info) == STB_GLOBAL && s.Section == SHN_UNDEF {
all = append(all, ImportedSymbol{Name: s.Name})
f.gnuVersion(i, &all[len(all)-1])
}
}
return all, nil
}
type verneed struct {
File string
Name string
}
// gnuVersionInit parses the GNU version tables
// for use by calls to gnuVersion.
func (f *File) gnuVersionInit(str []byte) {
// Accumulate verneed information.
vn := f.SectionByType(SHT_GNU_VERNEED)
if vn == nil {
return
}
d, _ := vn.Data()
var need []verneed
i := 0
for {
if i+16 > len(d) {
break
}
vers := f.ByteOrder.Uint16(d[i : i+2])
if vers != 1 {
break
}
cnt := f.ByteOrder.Uint16(d[i+2 : i+4])
fileoff := f.ByteOrder.Uint32(d[i+4 : i+8])
aux := f.ByteOrder.Uint32(d[i+8 : i+12])
next := f.ByteOrder.Uint32(d[i+12 : i+16])
file, _ := getString(str, int(fileoff))
var name string
j := i + int(aux)
for c := 0; c < int(cnt); c++ {
if j+16 > len(d) {
break
}
// hash := f.ByteOrder.Uint32(d[j:j+4])
// flags := f.ByteOrder.Uint16(d[j+4:j+6])
other := f.ByteOrder.Uint16(d[j+6 : j+8])
nameoff := f.ByteOrder.Uint32(d[j+8 : j+12])
next := f.ByteOrder.Uint32(d[j+12 : j+16])
name, _ = getString(str, int(nameoff))
ndx := int(other)
if ndx >= len(need) {
a := make([]verneed, 2*(ndx+1))
copy(a, need)
need = a
}
need[ndx] = verneed{file, name}
if next == 0 {
break
}
j += int(next)
}
if next == 0 {
break
}
i += int(next)
}
// Versym parallels symbol table, indexing into verneed.
vs := f.SectionByType(SHT_GNU_VERSYM)
if vs == nil {
return
}
d, _ = vs.Data()
f.gnuNeed = need
f.gnuVersym = d
}
// gnuVersion adds Library and Version information to sym,
// which came from offset i of the symbol table.
func (f *File) gnuVersion(i int, sym *ImportedSymbol) {
// Each entry is two bytes.
i = (i + 1) * 2
if i >= len(f.gnuVersym) {
return
}
j := int(f.ByteOrder.Uint16(f.gnuVersym[i:]))
if j < 2 || j >= len(f.gnuNeed) {
return
}
n := &f.gnuNeed[j]
sym.Library = n.File
sym.Version = n.Name
}
// ImportedLibraries returns the names of all libraries
// referred to by the binary f that are expected to be
// linked with the binary at dynamic link time.
func (f *File) ImportedLibraries() ([]string, error) {
return f.DynString(DT_NEEDED)
}
func (f *File) parseDynTags() error {
s := f.SectionByType(SHT_DYNAMIC)
if s == nil {
return nil // nothing to do
}
var m []DynTagValue
d, err := s.Data()
if err != nil {
return err
}
//fmt.Printf("%+v\nd: %x len(%d)\n", s, d, len(d))
r := bytes.NewBuffer(d)
for {
var t, v uint64
if err := binary.Read(r, f.ByteOrder, &t); err != nil {
if err == io.EOF {
break
} else if err != nil {
return err
}
}
if err := binary.Read(r, f.ByteOrder, &v); err != nil {
if err == io.EOF {
break
} else if err != nil {
return err
}
}
m = append(m, DynTagValue{Tag: DynTag(t), Value: v})
//fmt.Printf("%x -> %x\n", t, v)
}
f.DynTags = m
return nil
}
// DynString returns the strings listed for the given tag in the file's dynamic
// section.
//
// The tag must be one that takes string values: DT_NEEDED, DT_SONAME, DT_RPATH, or
// DT_RUNPATH.
func (f *File) DynString(tag DynTag) ([]string, error) {
switch tag {
case DT_NEEDED, DT_SONAME, DT_RPATH, DT_RUNPATH:
default:
return nil, fmt.Errorf("non-string-valued tag %v", tag)
}
ds := f.SectionByType(SHT_DYNAMIC)
if ds == nil {
// not dynamic, so no libraries
return nil, nil
}
str, err := f.stringTable(ds.Link)
if err != nil {
return nil, err
}
var all []string
for _, taggedValue := range f.DynTags {
if taggedValue.Tag == tag {
s, ok := getString(str, int(taggedValue.Value))
if ok {
all = append(all, s)
}
}
}
return all, nil
}
-108
View File
@@ -1,108 +0,0 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package elf
import (
"io"
"os"
)
// errorReader returns error from all operations.
type errorReader struct {
error
}
func (r errorReader) Read(p []byte) (n int, err error) {
return 0, r.error
}
func (r errorReader) ReadAt(p []byte, off int64) (n int, err error) {
return 0, r.error
}
func (r errorReader) Seek(offset int64, whence int) (int64, error) {
return 0, r.error
}
func (r errorReader) Close() error {
return r.error
}
// readSeekerFromReader converts an io.Reader into an io.ReadSeeker.
// In general Seek may not be efficient, but it is optimized for
// common cases such as seeking to the end to find the length of the
// data.
type readSeekerFromReader struct {
reset func() (io.Reader, error)
r io.Reader
size int64
offset int64
}
func (r *readSeekerFromReader) start() {
x, err := r.reset()
if err != nil {
r.r = errorReader{err}
} else {
r.r = x
}
r.offset = 0
}
func (r *readSeekerFromReader) Read(p []byte) (n int, err error) {
if r.r == nil {
r.start()
}
n, err = r.r.Read(p)
r.offset += int64(n)
return n, err
}
func (r *readSeekerFromReader) Seek(offset int64, whence int) (int64, error) {
var newOffset int64
switch whence {
case seekStart:
newOffset = offset
case seekCurrent:
newOffset = r.offset + offset
case seekEnd:
newOffset = r.size + offset
default:
return 0, os.ErrInvalid
}
switch {
case newOffset == r.offset:
return newOffset, nil
case newOffset < 0, newOffset > r.size:
return 0, os.ErrInvalid
case newOffset == 0:
r.r = nil
case newOffset == r.size:
r.r = errorReader{io.EOF}
default:
if newOffset < r.offset {
// Restart at the beginning.
r.start()
}
// Read until we reach offset.
var buf [512]byte
for r.offset < newOffset {
b := buf[:]
if newOffset-r.offset < int64(len(buf)) {
b = buf[:newOffset-r.offset]
}
if _, err := r.Read(b); err != nil {
return 0, err
}
}
}
r.offset = newOffset
return r.offset, nil
}
-529
View File
@@ -1,529 +0,0 @@
package elf
import (
"bytes"
"encoding/binary"
"errors"
)
// applyRelocations applies relocations to dst. rels is a relocations section
// in REL or RELA format.
func (f *File) applyRelocations(dst []byte, rels []byte) error {
switch {
case f.Class == ELFCLASS64 && f.Machine == EM_X86_64:
return f.applyRelocationsAMD64(dst, rels)
case f.Class == ELFCLASS32 && f.Machine == EM_386:
return f.applyRelocations386(dst, rels)
case f.Class == ELFCLASS32 && f.Machine == EM_ARM:
return f.applyRelocationsARM(dst, rels)
case f.Class == ELFCLASS64 && f.Machine == EM_AARCH64:
return f.applyRelocationsARM64(dst, rels)
case f.Class == ELFCLASS32 && f.Machine == EM_PPC:
return f.applyRelocationsPPC(dst, rels)
case f.Class == ELFCLASS64 && f.Machine == EM_PPC64:
return f.applyRelocationsPPC64(dst, rels)
case f.Class == ELFCLASS32 && f.Machine == EM_MIPS:
return f.applyRelocationsMIPS(dst, rels)
case f.Class == ELFCLASS64 && f.Machine == EM_MIPS:
return f.applyRelocationsMIPS64(dst, rels)
case f.Class == ELFCLASS64 && f.Machine == EM_RISCV:
return f.applyRelocationsRISCV64(dst, rels)
case f.Class == ELFCLASS64 && f.Machine == EM_S390:
return f.applyRelocationss390x(dst, rels)
case f.Class == ELFCLASS64 && f.Machine == EM_SPARCV9:
return f.applyRelocationsSPARC64(dst, rels)
default:
return errors.New("applyRelocations: not implemented")
}
}
func (f *File) applyRelocationsAMD64(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 32
t := R_X86_64(rela.Info & 0xffff)
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if SymType(sym.Info&0xf) != STT_SECTION {
// We don't handle non-section relocations for now.
continue
}
// There are relocations, so this must be a normal
// object file, and we only look at section symbols,
// so we assume that the symbol value is 0.
switch t {
case R_X86_64_64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], uint64(rela.Addend))
case R_X86_64_32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], uint32(rela.Addend))
}
}
return nil
}
func (f *File) applyRelocations386(dst []byte, rels []byte) error {
// 8 is the size of Rel32.
if len(rels)%8 != 0 {
return errors.New("length of relocation section is not a multiple of 8")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rel Rel32
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rel)
symNo := rel.Info >> 8
t := R_386(rel.Info & 0xff)
if symNo == 0 || symNo > uint32(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if t == R_386_32 {
if rel.Off+4 >= uint32(len(dst)) {
continue
}
val := f.ByteOrder.Uint32(dst[rel.Off : rel.Off+4])
val += uint32(sym.Value)
f.ByteOrder.PutUint32(dst[rel.Off:rel.Off+4], val)
}
}
return nil
}
func (f *File) applyRelocationsARM(dst []byte, rels []byte) error {
// 8 is the size of Rel32.
if len(rels)%8 != 0 {
return errors.New("length of relocation section is not a multiple of 8")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rel Rel32
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rel)
symNo := rel.Info >> 8
t := R_ARM(rel.Info & 0xff)
if symNo == 0 || symNo > uint32(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
switch t {
case R_ARM_ABS32:
if rel.Off+4 >= uint32(len(dst)) {
continue
}
val := f.ByteOrder.Uint32(dst[rel.Off : rel.Off+4])
val += uint32(sym.Value)
f.ByteOrder.PutUint32(dst[rel.Off:rel.Off+4], val)
}
}
return nil
}
func (f *File) applyRelocationsARM64(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 32
t := R_AARCH64(rela.Info & 0xffff)
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if SymType(sym.Info&0xf) != STT_SECTION {
// We don't handle non-section relocations for now.
continue
}
// There are relocations, so this must be a normal
// object file, and we only look at section symbols,
// so we assume that the symbol value is 0.
switch t {
case R_AARCH64_ABS64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], uint64(rela.Addend))
case R_AARCH64_ABS32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], uint32(rela.Addend))
}
}
return nil
}
func (f *File) applyRelocationsPPC(dst []byte, rels []byte) error {
// 12 is the size of Rela32.
if len(rels)%12 != 0 {
return errors.New("length of relocation section is not a multiple of 12")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela32
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 8
t := R_PPC(rela.Info & 0xff)
if symNo == 0 || symNo > uint32(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if SymType(sym.Info&0xf) != STT_SECTION {
// We don't handle non-section relocations for now.
continue
}
switch t {
case R_PPC_ADDR32:
if rela.Off+4 >= uint32(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], uint32(rela.Addend))
}
}
return nil
}
func (f *File) applyRelocationsPPC64(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 32
t := R_PPC64(rela.Info & 0xffff)
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if SymType(sym.Info&0xf) != STT_SECTION {
// We don't handle non-section relocations for now.
continue
}
switch t {
case R_PPC64_ADDR64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], uint64(rela.Addend))
case R_PPC64_ADDR32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], uint32(rela.Addend))
}
}
return nil
}
func (f *File) applyRelocationsMIPS(dst []byte, rels []byte) error {
// 8 is the size of Rel32.
if len(rels)%8 != 0 {
return errors.New("length of relocation section is not a multiple of 8")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rel Rel32
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rel)
symNo := rel.Info >> 8
t := R_MIPS(rel.Info & 0xff)
if symNo == 0 || symNo > uint32(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
switch t {
case R_MIPS_32:
if rel.Off+4 >= uint32(len(dst)) {
continue
}
val := f.ByteOrder.Uint32(dst[rel.Off : rel.Off+4])
val += uint32(sym.Value)
f.ByteOrder.PutUint32(dst[rel.Off:rel.Off+4], val)
}
}
return nil
}
func (f *File) applyRelocationsMIPS64(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
var symNo uint64
var t R_MIPS
if f.ByteOrder == binary.BigEndian {
symNo = rela.Info >> 32
t = R_MIPS(rela.Info & 0xff)
} else {
symNo = rela.Info & 0xffffffff
t = R_MIPS(rela.Info >> 56)
}
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if SymType(sym.Info&0xf) != STT_SECTION {
// We don't handle non-section relocations for now.
continue
}
switch t {
case R_MIPS_64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], uint64(rela.Addend))
case R_MIPS_32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], uint32(rela.Addend))
}
}
return nil
}
func (f *File) applyRelocationsRISCV64(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 32
t := R_RISCV(rela.Info & 0xffff)
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
switch SymType(sym.Info & 0xf) {
case STT_SECTION, STT_NOTYPE:
break
default:
continue
}
switch t {
case R_RISCV_64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
val := sym.Value + uint64(rela.Addend)
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val)
case R_RISCV_32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
val := uint32(sym.Value) + uint32(rela.Addend)
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val)
}
}
return nil
}
func (f *File) applyRelocationss390x(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 32
t := R_390(rela.Info & 0xffff)
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
switch SymType(sym.Info & 0xf) {
case STT_SECTION, STT_NOTYPE:
break
default:
continue
}
switch t {
case R_390_64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
val := sym.Value + uint64(rela.Addend)
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val)
case R_390_32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
val := uint32(sym.Value) + uint32(rela.Addend)
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val)
}
}
return nil
}
func (f *File) applyRelocationsSPARC64(dst []byte, rels []byte) error {
// 24 is the size of Rela64.
if len(rels)%24 != 0 {
return errors.New("length of relocation section is not a multiple of 24")
}
symbols, _, err := f.getSymbols(SHT_SYMTAB)
if err != nil {
return err
}
b := bytes.NewReader(rels)
var rela Rela64
for b.Len() > 0 {
binary.Read(b, f.ByteOrder, &rela)
symNo := rela.Info >> 32
t := R_SPARC(rela.Info & 0xff)
if symNo == 0 || symNo > uint64(len(symbols)) {
continue
}
sym := &symbols[symNo-1]
if SymType(sym.Info&0xf) != STT_SECTION {
// We don't handle non-section relocations for now.
continue
}
switch t {
case R_SPARC_64, R_SPARC_UA64:
if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], uint64(rela.Addend))
case R_SPARC_32, R_SPARC_UA32:
if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 {
continue
}
f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], uint32(rela.Addend))
}
}
return nil
}
-281
View File
@@ -1,281 +0,0 @@
package elf
import (
"bufio"
"bytes"
"encoding/binary"
"io/ioutil"
"log"
"os"
)
// Bytes - returns the bytes of an Elf file
func (elfFile *File) Bytes() ([]byte, error) {
bytesWritten := uint64(0)
elfBuf := bytes.NewBuffer(nil)
w := bufio.NewWriter(elfBuf)
// Write Elf Magic
w.WriteByte('\x7f')
w.WriteByte('E')
w.WriteByte('L')
w.WriteByte('F')
bytesWritten += 4
// ident[EI_CLASS]
w.WriteByte(byte(elfFile.Class))
// ident[EI_DATA]
w.WriteByte(byte(elfFile.Data))
// ident[EI_VERSION]
w.WriteByte(byte(elfFile.Version))
// ident[EI_OSABI]
w.WriteByte(byte(elfFile.OSABI))
// ident[EI_ABIVERSION]
w.WriteByte(byte(elfFile.ABIVersion))
// ident[EI_PAD] ( 7 bytes )
w.Write([]byte{0, 0, 0, 0, 0, 0, 0})
bytesWritten += 12
// Type
binary.Write(w, elfFile.ByteOrder, uint16(elfFile.Type))
// Machine
binary.Write(w, elfFile.ByteOrder, uint16(elfFile.Machine))
// Version
binary.Write(w, elfFile.ByteOrder, uint32(elfFile.Version))
bytesWritten += 8
phsize := 0
switch elfFile.Class {
case ELFCLASS32:
phsize = 0x20
// Entry 32
binary.Write(w, elfFile.ByteOrder, uint32(elfFile.Entry))
// PH Offset 32
binary.Write(w, elfFile.ByteOrder, uint32(0x34))
// SH Offset 32 // 0x20 0x28 4 8 e_shoff Points to the start of the section header table.
binary.Write(w, elfFile.ByteOrder, int32(elfFile.FileHeader.SHTOffset))
// Flags
binary.Write(w, elfFile.ByteOrder, uint32(0)) // todo
// EH Size
binary.Write(w, elfFile.ByteOrder, uint16(52))
// PH Size // 0x2A 0x36 2 e_phentsize Contains the size of a program header table entry.
binary.Write(w, elfFile.ByteOrder, uint16(phsize))
// PH Num // 0x2C 0x38 2 e_phnum Contains the number of entries in the program header table.
binary.Write(w, elfFile.ByteOrder, uint16(len(elfFile.Progs)))
// SH Size // 0x2E 0x3A 2 e_shentsize Contains the size of a section header table entry.
binary.Write(w, elfFile.ByteOrder, uint16(0x28))
bytesWritten += 24
case ELFCLASS64:
phsize = 0x38
// Entry 64
binary.Write(w, elfFile.ByteOrder, uint64(elfFile.Entry))
// PH Offset 64
binary.Write(w, elfFile.ByteOrder, uint64(0x40))
// SH Offset 64 // 0x20 0x28 4 8 e_shoff Points to the start of the section header table.
binary.Write(w, elfFile.ByteOrder, int64(elfFile.FileHeader.SHTOffset))
// Flags
binary.Write(w, elfFile.ByteOrder, uint32(0)) // I think right?
// EH Size
binary.Write(w, elfFile.ByteOrder, uint16(64))
// PH Size // 0x2A 0x36 2 e_phentsize Contains the size of a program header table entry.
binary.Write(w, elfFile.ByteOrder, uint16(phsize))
// PH Num // 0x2C 0x38 2 e_phnum Contains the number of entries in the program header table.
binary.Write(w, elfFile.ByteOrder, uint16(len(elfFile.Progs)))
// SH Size // 0x2E 0x3A 2 e_shentsize Contains the size of a section header table entry.
binary.Write(w, elfFile.ByteOrder, uint16(0x40))
bytesWritten += 36
}
// SH Num // 0x30 0x3C 2 e_shnum Contains the number of entries in the section header table.
binary.Write(w, elfFile.ByteOrder, uint16(len(elfFile.Sections)))
// SH Str Ndx // 0x32 0x3E 2 e_shstrndx Contains index of the section header table entry that contains the section names.
binary.Write(w, elfFile.ByteOrder, uint16(elfFile.ShStrIndex))
bytesWritten += 4
// Program Header
for _, p := range elfFile.Progs {
// Type (segment)
binary.Write(w, elfFile.ByteOrder, uint32(p.Type))
bytesWritten += 4
switch elfFile.Class {
case ELFCLASS32:
// Offset of Segment in File
binary.Write(w, elfFile.ByteOrder, uint32(p.Off))
// Vaddr
binary.Write(w, elfFile.ByteOrder, uint32(p.Vaddr))
// Paddr
binary.Write(w, elfFile.ByteOrder, uint32(p.Paddr))
// File Size
binary.Write(w, elfFile.ByteOrder, uint32(p.Filesz))
// Memory Size
binary.Write(w, elfFile.ByteOrder, uint32(p.Memsz))
// Flags (segment)
binary.Write(w, elfFile.ByteOrder, uint32(p.Flags))
// Alignment
binary.Write(w, elfFile.ByteOrder, uint32(p.Align))
bytesWritten += 28
case ELFCLASS64:
// Flags (segment)
binary.Write(w, elfFile.ByteOrder, uint32(p.Flags))
// Offset of Segment in File
binary.Write(w, elfFile.ByteOrder, uint64(p.Off))
// Vaddr
binary.Write(w, elfFile.ByteOrder, uint64(p.Vaddr))
// Paddr
binary.Write(w, elfFile.ByteOrder, uint64(p.Paddr))
// File Size
binary.Write(w, elfFile.ByteOrder, uint64(p.Filesz))
// Memory Size
binary.Write(w, elfFile.ByteOrder, uint64(p.Memsz))
// Alignment
binary.Write(w, elfFile.ByteOrder, uint64(p.Align))
bytesWritten += 52
}
}
sortedSections := elfFile.Sections[:]
//sort.Slice(sortedSections, func(a, b int) bool { return elfFile.Sections[a].Link < elfFile.Sections[b].Link })
for _, s := range sortedSections {
//log.Printf("Writing section: %s type: %+v\n", s.Name, s.Type)
//log.Printf("written: %x offset: %x\n", bytesWritten, s.Offset)
if s.Type == SHT_NULL || s.Type == SHT_NOBITS || s.FileSize == 0 {
//log.Println("continuing...")
continue
}
if bytesWritten > s.Offset {
log.Printf("Overlapping Sections in Generated Elf: %+v\n", s.Name)
continue
}
if s.Offset != 0 && bytesWritten < s.Offset {
pad := make([]byte, s.Offset-bytesWritten)
w.Write(pad)
//log.Printf("Padding before section %s at %x: length:%x to:%x\n", s.Name, bytesWritten, len(pad), s.Offset)
bytesWritten += uint64(len(pad))
}
slen := 0
switch s.Type {
case SHT_DYNAMIC:
for _, taggedValue := range elfFile.DynTags {
//log.Printf("writing %d (%x) -> %d (%x)\n", taggedValue.Tag, taggedValue.Tag, taggedValue.Value, taggedValue.Value)
switch elfFile.Class {
case ELFCLASS32:
binary.Write(w, elfFile.ByteOrder, uint32(taggedValue.Tag))
binary.Write(w, elfFile.ByteOrder, uint32(taggedValue.Value))
bytesWritten += 8
case ELFCLASS64:
binary.Write(w, elfFile.ByteOrder, uint64(taggedValue.Tag))
binary.Write(w, elfFile.ByteOrder, uint64(taggedValue.Value))
bytesWritten += 16
}
}
default:
section, err := ioutil.ReadAll(s.Open())
if err != nil {
return nil, err
}
binary.Write(w, elfFile.ByteOrder, section)
slen = len(section)
//log.Printf("Wrote %s section at %x, length %x\n", s.Name, bytesWritten, slen)
bytesWritten += uint64(slen)
}
// todo: elfFile.Insertion should be renamed InsertionLoadEnd or similar
if s.Type == SHT_PROGBITS && len(elfFile.Insertion) > 0 && s.Size-uint64(slen) >= uint64(len(elfFile.Insertion)) {
binary.Write(w, elfFile.ByteOrder, elfFile.Insertion)
bytesWritten += uint64(len(elfFile.Insertion))
}
w.Flush()
}
// Pad to Section Header Table
if bytesWritten < uint64(elfFile.FileHeader.SHTOffset) {
pad := make([]byte, uint64(elfFile.FileHeader.SHTOffset)-bytesWritten)
w.Write(pad)
//log.Printf("Padding before SHT at %x: length:%x to:%x\n", bytesWritten, len(pad), elfFile.FileHeader.SHTOffset)
bytesWritten += uint64(len(pad))
}
// Write Section Header Table
for _, s := range elfFile.Sections[:] {
switch elfFile.Class {
case ELFCLASS32:
binary.Write(w, elfFile.ByteOrder, &Section32{
Name: s.Shname,
Type: uint32(s.Type),
Flags: uint32(s.Flags),
Addr: uint32(s.Addr),
Off: uint32(s.Offset),
Size: uint32(s.Size),
Link: s.Link,
Info: s.Info,
Addralign: uint32(s.Addralign),
Entsize: uint32(s.Entsize)})
case ELFCLASS64:
binary.Write(w, elfFile.ByteOrder, &Section64{
Name: s.Shname,
Type: uint32(s.Type),
Flags: uint64(s.Flags),
Addr: s.Addr,
Off: s.Offset,
Size: s.Size,
Link: s.Link,
Info: s.Info,
Addralign: s.Addralign,
Entsize: s.Entsize})
}
}
// Do I have a PT_NOTE segment to add at the end?
if len(elfFile.InsertionEOF) > 0 {
binary.Write(w, elfFile.ByteOrder, elfFile.InsertionEOF)
bytesWritten += uint64(len(elfFile.InsertionEOF))
}
w.Flush()
return elfBuf.Bytes(), nil
}
// WriteFile - Creates a new file and writes it using the Bytes func above
func (elfFile *File) WriteFile(destFile string) error {
f, err := os.Create(destFile)
if err != nil {
return err
}
defer f.Close()
elfData, err := elfFile.Bytes()
if err != nil {
return err
}
_, err = f.Write(elfData)
if err != nil {
return err
}
return nil
}
-3
View File
@@ -1,3 +0,0 @@
## More Info
More info on the Mach-O file structure here: [Mach-O Deep Dive](https://www.symbolcrash.com/2019/02/25/so-you-want-to-be-a-mach-o-man/)
-27
View File
@@ -1,27 +0,0 @@
package macho
const N_SECT = 0x0E
const N_PEXT = 0x10
const N_EXT = 0x01
// Export - describes a single export entry (similar to PE version for future refactoring)
type Export struct {
//Ordinal uint32 // no ordinals for Mach-O
Name string
VirtualAddress uint64
}
// Exports - gets exports, including private exports
func (f *File) Exports() []Export {
var exports []Export
for _, symbol := range f.Symtab.Syms {
if (symbol.Type&N_PEXT == N_PEXT ||
symbol.Type&N_EXT == N_EXT) && symbol.Value != 0 {
var export Export
export.Name = symbol.Name
export.VirtualAddress = symbol.Value
exports = append(exports, export)
}
}
return exports
}
-146
View File
@@ -1,146 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package macho
import (
"encoding/binary"
"fmt"
"io"
"os"
)
// A FatFile is a Mach-O universal binary that contains at least one architecture.
type FatFile struct {
Magic uint32
Arches []FatArch
closer io.Closer
}
// A FatArchHeader represents a fat header for a specific image architecture.
type FatArchHeader struct {
Cpu Cpu
SubCpu uint32
Offset uint32
Size uint32
Align uint32
}
const fatArchHeaderSize = 5 * 4
// A FatArch is a Mach-O File inside a FatFile.
type FatArch struct {
FatArchHeader
*File
}
// ErrNotFat is returned from NewFatFile or OpenFat when the file is not a
// universal binary but may be a thin binary, based on its magic number.
var ErrNotFat = &FormatError{0, "not a fat Mach-O file", nil}
// NewFatFile creates a new FatFile for accessing all the Mach-O images in a
// universal binary. The Mach-O binary is expected to start at position 0 in
// the ReaderAt.
func NewFatFile(r io.ReaderAt) (*FatFile, error) {
var ff FatFile
sr := io.NewSectionReader(r, 0, 1<<63-1)
// Read the fat_header struct, which is always in big endian.
// Start with the magic number.
err := binary.Read(sr, binary.BigEndian, &ff.Magic)
if err != nil {
return nil, &FormatError{0, "error reading magic number", nil}
} else if ff.Magic != MagicFat {
// See if this is a Mach-O file via its magic number. The magic
// must be converted to little endian first though.
var buf [4]byte
binary.BigEndian.PutUint32(buf[:], ff.Magic)
leMagic := binary.LittleEndian.Uint32(buf[:])
if leMagic == Magic32 || leMagic == Magic64 {
return nil, ErrNotFat
} else {
return nil, &FormatError{0, "invalid magic number", nil}
}
}
offset := int64(4)
// Read the number of FatArchHeaders that come after the fat_header.
var narch uint32
err = binary.Read(sr, binary.BigEndian, &narch)
if err != nil {
return nil, &FormatError{offset, "invalid fat_header", nil}
}
offset += 4
if narch < 1 {
return nil, &FormatError{offset, "file contains no images", nil}
}
// Combine the Cpu and SubCpu (both uint32) into a uint64 to make sure
// there are not duplicate architectures.
seenArches := make(map[uint64]bool, narch)
// Make sure that all images are for the same MH_ type.
var machoType Type
// Following the fat_header comes narch fat_arch structs that index
// Mach-O images further in the file.
ff.Arches = make([]FatArch, narch)
for i := uint32(0); i < narch; i++ {
fa := &ff.Arches[i]
err = binary.Read(sr, binary.BigEndian, &fa.FatArchHeader)
if err != nil {
return nil, &FormatError{offset, "invalid fat_arch header", nil}
}
offset += fatArchHeaderSize
fr := io.NewSectionReader(r, int64(fa.Offset), int64(fa.Size))
fa.File, err = NewFile(fr)
if err != nil {
return nil, err
}
// Make sure the architecture for this image is not duplicate.
seenArch := (uint64(fa.Cpu) << 32) | uint64(fa.SubCpu)
if o, k := seenArches[seenArch]; o || k {
return nil, &FormatError{offset, fmt.Sprintf("duplicate architecture cpu=%v, subcpu=%#x", fa.Cpu, fa.SubCpu), nil}
}
seenArches[seenArch] = true
// Make sure the Mach-O type matches that of the first image.
if i == 0 {
machoType = fa.Type
} else {
if fa.Type != machoType {
return nil, &FormatError{offset, fmt.Sprintf("Mach-O type for architecture #%d (type=%#x) does not match first (type=%#x)", i, fa.Type, machoType), nil}
}
}
}
return &ff, nil
}
// OpenFat opens the named file using os.Open and prepares it for use as a Mach-O
// universal binary.
func OpenFat(name string) (*FatFile, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
ff, err := NewFatFile(f)
if err != nil {
f.Close()
return nil, err
}
ff.closer = f
return ff, nil
}
func (ff *FatFile) Close() error {
var err error
if ff.closer != nil {
err = ff.closer.Close()
ff.closer = nil
}
return err
}
Loaded 100 of 795 files, more files were not shown because too many files have changed in this diff. Show more