From 2cc1f2bd54e7fec0893c6a0ce819c5c1520c9f70 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 30 Oct 2016 00:08:28 -0700 Subject: [PATCH 1/7] Updating .travis.yml --- .travis.yml | 17 ++++++++++++++--- README.md | 9 +++++---- tests.sh | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100755 tests.sh diff --git a/.travis.yml b/.travis.yml index 2ae25f2..e382cd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - 1.6.2 + - 1.7 - tip sudo: true notifications: @@ -8,7 +8,18 @@ notifications: recipients: - team@onionscan.org +install: + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go get github.com/HouzuoGuo/tiedot + - go get golang.org/x/crypto/openpgp + - go get golang.org/x/net/proxy + - go get golang.org/x/net/html + - go get github.com/rwcarlsen/goexif/exif + - go get github.com/rwcarlsen/goexif/tiff + script: - - go test -v ./... - - GOFMT=$(gofmt -d .) && echo "$GOFMT" + + - cd $TRAVIS_BUILD_DIR && ./tests.sh - test -z "$GOFMT" + - goveralls -coverprofile=./coverage.out -service travis-ci diff --git a/README.md b/README.md index 93b2bc7..298bd1b 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,12 @@ OnionScan has two primary goals: In order to install OnionScan you will need the following dependencies not provided by the core go standard library: -* golang.org/x/net/proxy - For the Tor SOCKS Proxy connection. -* golang.org/x/net/crypto - For PGP parsing -* golang.org/x/net/html - For HTML parsing -* github.com/rwcarlsen/goexif - For EXIF data extraction. * github.com/HouzuoGuo/tiedot/db - For crawl database. +* github.com/rwcarlsen/goexif/exif - For EXIF data extraction. +* github.com/rwcarlsen/goexif/tiff - For EXIF data extraction. +* golang.org/x/crypto/openpgp - For PGP parsing +* golang.org/x/net/html - For HTML parsing +* golang.org/x/net/proxy - For the Tor SOCKS Proxy connection. ### Grab with go get diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..fbdd5c1 --- /dev/null +++ b/tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +pwd +go test -coverprofile=cover.out -v . +go test -coverprofile=config.cover.out -v ./config +go test -coverprofile=crawld.cover.out -v ./crawldb +go test -coverprofile=deanonymization.cover.out -v ./deanonymization +go test -coverprofile=model.cover.out -v ./model +go test -coverprofile=onionscan.cover.out -v ./onionscan +go test -coverprofile=protocol.cover.out -v ./protocol +go test -coverprofile=report.cover.out -v ./report +go test -coverprofile=spider.cover.out -v ./spider +go test -coverprofile=utils.cover.out -v ./utils +go test -coverprofile=webui.cover.out -v ./webui +echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ +awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out +rm -rf *.cover.out From d48ca1b1c86132e40a370873fdc7035f96f22555 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 30 Oct 2016 00:54:00 -0700 Subject: [PATCH 2/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 298bd1b..38cd647 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # What is OnionScan? -[![Build Status](https://travis-ci.org/s-rah/onionscan.svg?branch=onionscan-0.2)](https://travis-ci.org/s-rah/onionscan) [![Go Report Card](https://goreportcard.com/badge/github.com/s-rah/onionscan)](https://goreportcard.com/report/github.com/s-rah/onionscan) +[![Build Status](https://travis-ci.org/s-rah/onionscan.svg?branch=onionscan-0.2)](https://travis-ci.org/s-rah/onionscan) [![Go Report Card](https://goreportcard.com/badge/github.com/s-rah/onionscan)](https://goreportcard.com/report/github.com/s-rah/onionscan) [![Coverage Status](https://coveralls.io/repos/github/s-rah/onionscan/badge.svg?branch=onionscan-0.3)](https://coveralls.io/github/s-rah/onionscan?branch=onionscan-0.3) OnionScan is a free and open source tool for investigating the Dark Web. For all the amazing technological innovations in the anonymity and privacy space, there From f5f8226ad88b906bfed369efef4cd5779d751a2e Mon Sep 17 00:00:00 2001 From: Gordon Chan Date: Sun, 30 Oct 2016 23:44:18 +1300 Subject: [PATCH 3/7] Look for cyptocurrency (Bitcoin) private keys #76 --- deanonymization/check_bitcoin_addresses.go | 134 ++++++++++-------- .../check_bitcoin_addresses_test.go | 13 +- report/anonymity_report.go | 1 + report/simple_report.go | 8 ++ webui/webui.go | 2 + 5 files changed, 101 insertions(+), 57 deletions(-) diff --git a/deanonymization/check_bitcoin_addresses.go b/deanonymization/check_bitcoin_addresses.go index 6addb20..5f16ac7 100644 --- a/deanonymization/check_bitcoin_addresses.go +++ b/deanonymization/check_bitcoin_addresses.go @@ -10,57 +10,56 @@ import ( "strings" ) -// A25 is a type for a 25 byte (not base58 encoded) bitcoin address. -type A25 [25]byte - -// Version extracts the version byte from a bitcoin address -func (a *A25) Version() byte { - return a[0] -} - -// EmbeddedChecksum returns the checksum of a bitcoin address -func (a *A25) EmbeddedChecksum() (c [4]byte) { - copy(c[:], a[21:]) - return -} +// Tmpl and Set58 are adapted from the C solution. +// Go has big integers but this techinique seems better. +var tmpl = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") -// DoubleSHA256 computes a double sha256 hash of the first 21 bytes of the -// address. This is the one function shared with the other bitcoin RC task. -// Returned is the full 32 byte sha256 hash. (The bitcoin checksum will be -// the first four bytes of the slice.) -func (a *A25) doubleSHA256() []byte { - h := sha256.New() - h.Write(a[:21]) - d := h.Sum([]byte{}) - h = sha256.New() - h.Write(d) - return h.Sum(d[:0]) +// ValidateA58 validates a base58 encoded bitcoin address. An address is valid +// if it can be decoded into a 25 byte address, the version number is 0 +// (P2PKH) or 5 (P2SH), and the checksum validates. Return value ok will be +// true for valid addresses. If ok is false, the address is invalid and the +// error value may indicate why. +func ValidateA58(a58 []byte) (ok bool) { + a := make([]byte, 25) + if err := Set58(a58, a); err != nil { + return false + } + if Version(a) != 0 && Version(a) != 5 { + return false + } + return EmbeddedChecksum(a) == ComputeChecksum(a) } -// ComputeChecksum returns a four byte checksum computed from the first 21 -// bytes of the address. The embedded checksum is not updated. -func (a *A25) ComputeChecksum() (c [4]byte) { - copy(c[:], a.doubleSHA256()) - return +// ValidateP58 validates a base58 encoded private key. A private key is valid +// if it can be decoded into a 37 byte private key, start with a 0x80 byte, +// and the checksum validates. Return value ok will be true for valid private keys. +// If ok is false, the private key is invalid and the error value may indicate why. +func ValidateP58(p58 []byte) (ok bool) { + p := make([]byte, 37) + if p58[0] != 'L' && p58[0] != 'K' { + return false + } + if err := Set58(p58[1:], p); err != nil { + return false + } + if Version(p) != 128 { + return false + } + return EmbeddedChecksum(p) == ComputeChecksum(p) } -// Tmpl and Set58 are adapted from the C solution. -// Go has big integers but this techinique seems better. -var tmpl = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") - -// Set58 takes a base58 encoded address and decodes it into the receiver. -// Errors are returned if the argument is not valid base58 or if the decoded -// value does not fit in the 25 byte address. The address is not otherwise -// checked for validity. -func (a *A25) Set58(s []byte) error { +// Set58 takes a base58 encoded string decodes it into a byte slice. +// Errors are returned if the argument is not a valid base58 or if the decoded +// value does not fit in the byte slice. +func Set58(s []byte, b []byte) error { for _, s1 := range s { c := bytes.IndexByte(tmpl, s1) if c < 0 { return errors.New("bad char") } - for j := 24; j >= 0; j-- { - c += 58 * int(a[j]) - a[j] = byte(c % 256) + for j := len(b) - 1; j >= 0; j-- { + c += 58 * int(b[j]) + b[j] = byte(c % 256) c /= 256 } if c > 0 { @@ -70,35 +69,58 @@ func (a *A25) Set58(s []byte) error { return nil } -// ValidA58 validates a base58 encoded bitcoin address. An address is valid -// if it can be decoded into a 25 byte address, the version number is 0 -// (P2PKH) or 5 (P2SH), and the checksum validates. Return value ok will be -// true for valid addresses. If ok is false, the address is invalid and the -// error value may indicate why. -func ValidA58(a58 []byte) (ok bool) { - var a A25 - if err := a.Set58(a58); err != nil { - return false - } - if a.Version() != 0 && a.Version() != 5 { - return false - } - return a.EmbeddedChecksum() == a.ComputeChecksum() +// Version extracts the version byte from the byte slice. +func Version(b []byte) byte { + return b[0] +} + +// ComputeChecksum returns a four byte checksum computed from bytes (except for +// the last 4) of the slice. The embedded checksum is not updated. +func ComputeChecksum(b []byte) (c [4]byte) { + copy(c[:], doubleSHA256(b)) + return +} + +// EmbeddedChecksum returns the checksum of the byte slice. +func EmbeddedChecksum(b []byte) (c [4]byte) { + copy(c[:], b[len(b)-4:]) + return +} + +// DoubleSHA256 computes a double sha256 hash of bytes (except for the last 4) +// of the slice. This is the one function shared with the other bitcoin RC task. +// Returned is the full 32 byte sha256 hash. (The checksum will be the first +// four bytes of the slice.) +func doubleSHA256(b []byte) []byte { + h := sha256.New() + h.Write(b[:len(b)-4]) + d := h.Sum([]byte{}) + h = sha256.New() + h.Write(d) + return h.Sum(d[:0]) } // ExtractBitcoinAddress extracts any information related to bitcoin addresses from the current crawl. func ExtractBitcoinAddress(osreport *report.OnionScanReport, anonreport *report.AnonymityReport, osc *config.OnionScanConfig) { bcaregex := regexp.MustCompile(`[13][a-km-zA-HJ-NP-Z1-9]{25,34}`) + pkregex := regexp.MustCompile(`[LK][a-km-zA-HJ-NP-Z1-9]{51}`) for _, id := range osreport.Crawls { crawlRecord, _ := osc.Database.GetCrawlRecord(id) if strings.Contains(crawlRecord.Page.Headers.Get("Content-Type"), "text/html") { foundBCID := bcaregex.FindAllString(crawlRecord.Page.Snapshot, -1) for _, result := range foundBCID { - if ValidA58([]byte(result)) { + if ValidateA58([]byte(result)) { anonreport.BitcoinAddresses = append(anonreport.BitcoinAddresses, result) osc.Database.InsertRelationship(osreport.HiddenService, "snapshot", "bitcoin-address", result) } } + foundPKID := pkregex.FindAllString(crawlRecord.Page.Snapshot, -1) + for _, result := range foundPKID { + if ValidateP58([]byte(result)) { + anonreport.BitcoinPrivateKeys = append(anonreport.BitcoinPrivateKeys, result) + osc.Database.InsertRelationship(osreport.HiddenService, "snapshot", "bitcoin-private-key", result) + } + } } } } diff --git a/deanonymization/check_bitcoin_addresses_test.go b/deanonymization/check_bitcoin_addresses_test.go index 4f677b4..1e7d008 100644 --- a/deanonymization/check_bitcoin_addresses_test.go +++ b/deanonymization/check_bitcoin_addresses_test.go @@ -42,7 +42,18 @@ func TestExtractBitcoinAddress(t *testing.T) { t.Errorf("Unexpected bitcoin address found") } - // Test 4: Multiple addresses + // Test 4: WIF bitcoin private key + ctx.CreatePage("/index.html", 200, "text/html", "L5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF") + ExtractBitcoinAddress(ctx.osreport, ctx.report, ctx.osc) + + if len(ctx.report.BitcoinPrivateKeys) != 1 { + t.Errorf("Should have detected a bitcoin private key") + } + if ctx.report.BitcoinPrivateKeys[0] != "L5Kb8kLf9zgWQnogidDA76MzPL6TsZZY36hWXMssSzNydYXYB9KF" { + t.Errorf("Unexpected bitcoin private key found") + } + + // Test 5: Multiple addresses ctx.report.BitcoinAddresses = []string{} data, err := ioutil.ReadFile("testdata/bitcoin.html") if err != nil { diff --git a/report/anonymity_report.go b/report/anonymity_report.go index 2a07209..fcb1c90 100644 --- a/report/anonymity_report.go +++ b/report/anonymity_report.go @@ -27,6 +27,7 @@ type AnonymityReport struct { EmailAddresses []string `json:"emailAddresses"` AnalyticsIDs []string `json:"analyticsIDs"` BitcoinAddresses []string `json:"bitcoinAddresses"` + BitcoinPrivateKeys []string `json:"bitcoinPrivateKeys"` LinkedOnions []string `json:"linkedOnions"` OpenDirectories []string `json:"openDirectories"` diff --git a/report/simple_report.go b/report/simple_report.go index 70687ca..346ae11 100644 --- a/report/simple_report.go +++ b/report/simple_report.go @@ -118,7 +118,15 @@ func (srt *BitcoinAddressesCheck) Check(out *SimpleReport, report *AnonymityRepo if len(report.BitcoinAddresses) > 0 { out.AddRisk(SEV_INFO, "Found Bitcoin Addresses", "", "", report.BitcoinAddresses) } +} + +// BitcoinPrivateKeysCheck implementation +type BitcoinPrivateKeysCheck struct{} +func (srt *BitcoinPrivateKeysCheck) Check(out *SimpleReport, report *AnonymityReport) { + if len(report.BitcoinPrivateKeys) > 0 { + out.AddRisk(SEV_CRITICAL, "Found Bitcoin Addresses", "", "", report.BitcoinPrivateKeys) + } } // ApacheModStatusCheck implementation diff --git a/webui/webui.go b/webui/webui.go index e25bf3d..79c719c 100644 --- a/webui/webui.go +++ b/webui/webui.go @@ -381,6 +381,8 @@ func (wui *WebUI) Index(w http.ResponseWriter, r *http.Request) { alt = "PGP Identities" case "bitcoin-address": alt = "Bitcoin Addresses" + case "bitcoin-private-key": + alt = "Bitcoin Private Keys" case "software-banner": alt = "Software Banners" case "analytics-id": From 7608dcad994f0c1800c57049efa93313fb172b7b Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Tue, 8 Nov 2016 12:00:59 +0100 Subject: [PATCH 4/7] Add more tests for utils - Add tests for `html_parsing`, `proxycheck`, `random`, `remove_duplicates`, `url_parsing` - Add a check to `GenerateRandomBytes` for negative values. --- utils/html_parsing_test.go | 19 ++++++++++++ utils/proxycheck.go | 1 + utils/proxycheck_test.go | 16 +++++++++++ utils/random.go | 4 +++ utils/random_test.go | 37 ++++++++++++++++++++++++ utils/remove_duplicates_test.go | 31 ++++++++++++++++++++ utils/url_parsing_test.go | 51 +++++++++++++++++++++++++++++++++ 7 files changed, 159 insertions(+) create mode 100644 utils/html_parsing_test.go create mode 100644 utils/proxycheck_test.go create mode 100644 utils/random_test.go create mode 100644 utils/remove_duplicates_test.go create mode 100644 utils/url_parsing_test.go diff --git a/utils/html_parsing_test.go b/utils/html_parsing_test.go new file mode 100644 index 0000000..e381379 --- /dev/null +++ b/utils/html_parsing_test.go @@ -0,0 +1,19 @@ +package utils + +import ( + "golang.org/x/net/html" + "strings" + "testing" +) + +func TestGetAttribute(t *testing.T) { + z := html.NewTokenizer(strings.NewReader("")) + z.Next() + tok := z.Token() + if GetAttribute(tok, "a") != "b" { + t.Errorf("Attribute a should have value b") + } + if GetAttribute(tok, "b") != "" { + t.Errorf("Attribute b is missing so should return empty value") + } +} diff --git a/utils/proxycheck.go b/utils/proxycheck.go index 0ce7534..1fb6f7d 100644 --- a/utils/proxycheck.go +++ b/utils/proxycheck.go @@ -15,6 +15,7 @@ const ( ProxyStatusWrongType ProxyStatusCannotConnect ProxyStatusTimeout + proxyStatusMax ) // Detect whether a proxy is connectable and is a Tor proxy diff --git a/utils/proxycheck_test.go b/utils/proxycheck_test.go new file mode 100644 index 0000000..02d0dbc --- /dev/null +++ b/utils/proxycheck_test.go @@ -0,0 +1,16 @@ +package utils + +import ( + "testing" +) + +// Missing: TestCheckTorProxy - will need to mock a proxy server to test this + +func TestProxyStatusMessage(t *testing.T) { + okmsg := ProxyStatusMessage(ProxyStatusOK) + for i := ProxyStatusOK + 1; i < proxyStatusMax; i++ { + if ProxyStatusMessage(i) == okmsg { + t.Errorf("Status message for %d returned same as for OK", i) + } + } +} diff --git a/utils/random.go b/utils/random.go index a286227..1e3d111 100644 --- a/utils/random.go +++ b/utils/random.go @@ -3,6 +3,7 @@ package utils import ( "crypto/rand" "encoding/base64" + "errors" ) // GenerateRandomBytes returns securely generated random bytes. @@ -10,6 +11,9 @@ import ( // number generator fails to function correctly, in which // case the caller should not continue. func GenerateRandomBytes(n int) ([]byte, error) { + if n < 0 { + return nil, errors.New("GenerateRandomBytes: negative number of bytes requested") + } b := make([]byte, n) _, err := rand.Read(b) // Note that err == nil only if we read len(b) bytes. diff --git a/utils/random_test.go b/utils/random_test.go new file mode 100644 index 0000000..fc1cbb9 --- /dev/null +++ b/utils/random_test.go @@ -0,0 +1,37 @@ +package utils + +import ( + "encoding/base64" + "testing" +) + +func TestGenerateRandomBytes(t *testing.T) { + bytes, err := GenerateRandomBytes(10) + if err != nil || len(bytes) != 10 { + t.Errorf("Failed to get 10 random bytes") + } + bytes, err = GenerateRandomBytes(100) + if err != nil || len(bytes) != 100 { + t.Errorf("Failed to get 100 random bytes") + } + bytes, err = GenerateRandomBytes(0) // Should not fail + if err != nil || len(bytes) != 0 { + t.Errorf("Failed to get 0 random bytes") + } + bytes, err = GenerateRandomBytes(-1) // Should fail + if err == nil { + t.Errorf("Trying to get -1 random bytes should fail") + } +} + +func TestGenerateRandomString(t *testing.T) { + s, err := GenerateRandomString(10) + if err != nil { + t.Errorf("Failed to get 10 random bytes") + } + var b []byte + b, err = base64.URLEncoding.DecodeString(s) + if err != nil || len(b) != 10 { + t.Errorf("Did not get back 10 valid encoded bytes") + } +} diff --git a/utils/remove_duplicates_test.go b/utils/remove_duplicates_test.go new file mode 100644 index 0000000..2b70ab6 --- /dev/null +++ b/utils/remove_duplicates_test.go @@ -0,0 +1,31 @@ +package utils + +import ( + "strings" + "testing" +) + +type RemoveDuplicatesTest struct { + input string + output string +} + +var RemoveDuplicatesTests = []RemoveDuplicatesTest{ + {"", ""}, + {"a,b,c,d", "a,b,c,d"}, + {"a,b,c,c", "a,b,c"}, + {"c,b,c,c", "c,b"}, + {"a,a,a,a", "a"}, +} + +func TestRemoveDuplicates(t *testing.T) { + for _, rec := range RemoveDuplicatesTests { + allTags := strings.Split(rec.input, ",") + RemoveDuplicates(&allTags) + output := strings.Join(allTags, ",") + if output != rec.output { + t.Errorf("RemoveDuplicates of \"%s\" is \"%s\" instead of expected \"%s\"", + rec.input, output, rec.output) + } + } +} diff --git a/utils/url_parsing_test.go b/utils/url_parsing_test.go new file mode 100644 index 0000000..9bddb18 --- /dev/null +++ b/utils/url_parsing_test.go @@ -0,0 +1,51 @@ +package utils + +import ( + "testing" +) + +type WithoutSubDomainsTest struct { + input string + output string +} + +var WithoutSubDomainsTests = []WithoutSubDomainsTest{ + {"", ""}, + {"com", ""}, + {"test.com", "test.com"}, + {"test.test.com", "test.com"}, + {"test.test.test.com", "test.com"}, +} + +func TestWithoutSubdomains(t *testing.T) { + for _, rec := range WithoutSubDomainsTests { + output := WithoutSubdomains(rec.input) + if output != rec.output { + t.Errorf("WithoutSubdomains of \"%s\" is \"%s\" instead of expected \"%s\"", + rec.input, output, rec.output) + } + } +} + +type WithoutProtocolTest struct { + input string + output string +} + +var WithoutProtocolTests = []WithoutProtocolTest{ + {"", ""}, + {"notactuallyan.onion", "notactuallyan.onion"}, + {"https://notactuallyan.onion", "notactuallyan.onion"}, + {"http://notactuallyan.onion", "notactuallyan.onion"}, + {"//notactuallyan.onion", "notactuallyan.onion"}, +} + +func TestWithoutProtocol(t *testing.T) { + for _, rec := range WithoutProtocolTests { + output := WithoutProtocol(rec.input) + if output != rec.output { + t.Errorf("WithoutProtocol of \"%s\" is \"%s\" instead of expected \"%s\"", + rec.input, output, rec.output) + } + } +} From 302e3a4e8827be3a0cf91824f92b58cb85b1de30 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 8 Nov 2016 15:32:57 -0800 Subject: [PATCH 5/7] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 38cd647..c33ddf0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -# What is OnionScan? - - -[![Build Status](https://travis-ci.org/s-rah/onionscan.svg?branch=onionscan-0.2)](https://travis-ci.org/s-rah/onionscan) [![Go Report Card](https://goreportcard.com/badge/github.com/s-rah/onionscan)](https://goreportcard.com/report/github.com/s-rah/onionscan) [![Coverage Status](https://coveralls.io/repos/github/s-rah/onionscan/badge.svg?branch=onionscan-0.3)](https://coveralls.io/github/s-rah/onionscan?branch=onionscan-0.3) +# What is OnionScan? [![Build Status](https://travis-ci.org/s-rah/onionscan.svg?branch=onionscan-0.3)](https://travis-ci.org/s-rah/onionscan) [![Go Report Card](https://goreportcard.com/badge/github.com/s-rah/onionscan)](https://goreportcard.com/report/github.com/s-rah/onionscan) [![Coverage Status](https://coveralls.io/repos/github/s-rah/onionscan/badge.svg?branch=onionscan-0.3)](https://coveralls.io/github/s-rah/onionscan?branch=onionscan-0.3) OnionScan is a free and open source tool for investigating the Dark Web. For all the amazing technological innovations in the anonymity and privacy space, there From 64799d34d9aef75392c028ec7cb36fa747cda7ff Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Sun, 9 Oct 2016 19:07:24 +0200 Subject: [PATCH 6/7] Check for ricochet client Use [go-ricochet](https://github.com/s-rah/go-ricochet) to check if the service is really a richoret instance. Implements #7. --- .travis.yml | 1 + protocol/ricochet_scanner.go | 131 ++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e382cd3..47a4231 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ install: - go get golang.org/x/net/html - go get github.com/rwcarlsen/goexif/exif - go get github.com/rwcarlsen/goexif/tiff + - go get github.com/s-rah/go-ricochet script: diff --git a/protocol/ricochet_scanner.go b/protocol/ricochet_scanner.go index 4a05589..9e48a6e 100644 --- a/protocol/ricochet_scanner.go +++ b/protocol/ricochet_scanner.go @@ -2,16 +2,128 @@ package protocol import ( "fmt" + "github.com/s-rah/go-ricochet" "github.com/s-rah/onionscan/config" "github.com/s-rah/onionscan/report" "github.com/s-rah/onionscan/utils" ) +// Ricochet protocol scanner instance type RicochetProtocolScanner struct { } +// Internal type used to keep track of a the protocol state for checking a +// ricochet server +type ricochetServiceChecker struct { + osc *config.OnionScanConfig + ricochet *goricochet.Ricochet + // Channel used to pass result back to main thread + status chan bool +} + +// OnReady is called once a Server has been established (by calling Listen) +func (rsc *ricochetServiceChecker) OnReady() { +} + +// OnConnect is called when a client or server sucessfully passes Version Negotiation. +func (rsc *ricochetServiceChecker) OnConnect(oc *goricochet.OpenConnection) { + rsc.osc.LogInfo(fmt.Sprintf("Ricochet version negotiation completed for %s", oc.OtherHostname)) + oc.IsAuthed = true // Connections to Servers are Considered Authenticated by Default + oc.Authenticate(1) +} + +// OnDisconnect is called when a connection is closed +func (rsc *ricochetServiceChecker) OnDisconnect(oc *goricochet.OpenConnection) { + rsc.status <- false +} + +// OnAuthenticationRequest is called when a client requests Authentication +func (rsc *ricochetServiceChecker) OnAuthenticationRequest(oc *goricochet.OpenConnection, channelID int32, clientCookie [16]byte) { +} + +// OnAuthenticationChallenge constructs a valid authentication challenge to the serverCookie +func (rsc *ricochetServiceChecker) OnAuthenticationChallenge(oc *goricochet.OpenConnection, channelID int32, serverCookie [16]byte) { + rsc.osc.LogInfo("Authentication challenge received, disconnecting\n") + rsc.status <- true + oc.Close() +} + +// OnAuthenticationProof is called when a client sends Proof for an existing authentication challenge +func (rsc *ricochetServiceChecker) OnAuthenticationProof(oc *goricochet.OpenConnection, channelID int32, publicKey []byte, signature []byte, isKnownContact bool) { +} + +// OnAuthenticationResult is called once a server has returned the result of the Proof Verification +func (rsc *ricochetServiceChecker) OnAuthenticationResult(oc *goricochet.OpenConnection, channelID int32, result bool, isKnownContact bool) { + oc.IsAuthed = result +} + +// IsKnownContact allows a caller to determine if a hostname an authorized contact. +func (rsc *ricochetServiceChecker) IsKnownContact(hostname string) bool { + return false +} + +// OnContactRequest is called when a client sends a new contact request +func (rsc *ricochetServiceChecker) OnContactRequest(oc *goricochet.OpenConnection, channelID int32, nick string, message string) { +} + +// OnContactRequestAck is called when a server sends a reply to an existing contact request +func (rsc *ricochetServiceChecker) OnContactRequestAck(oc *goricochet.OpenConnection, channelID int32, status string) { +} + +// OnOpenChannelRequest is called when a client or server requests to open a new channel +func (rsc *ricochetServiceChecker) OnOpenChannelRequest(oc *goricochet.OpenConnection, channelID int32, channelType string) { + oc.AckOpenChannel(channelID, channelType) +} + +// OnOpenChannelRequestSuccess is called when a client or server responds to an open channel request +func (rsc *ricochetServiceChecker) OnOpenChannelRequestSuccess(oc *goricochet.OpenConnection, channelID int32) { +} + +// OnChannelClose is called when a client or server closes an existing channel +func (rsc *ricochetServiceChecker) OnChannelClosed(oc *goricochet.OpenConnection, channelID int32) { +} + +// OnChatMessage is called when a new chat message is received. +func (rsc *ricochetServiceChecker) OnChatMessage(oc *goricochet.OpenConnection, channelID int32, messageID int32, message string) { + oc.AckChatMessage(channelID, messageID) +} + +// OnChatMessageAck is called when a new chat message is ascknowledged. +func (rsc *ricochetServiceChecker) OnChatMessageAck(oc *goricochet.OpenConnection, channelID int32, messageID int32) { +} + +// OnFailedChannelOpen is called when a server fails to open a channel +func (rsc *ricochetServiceChecker) OnFailedChannelOpen(oc *goricochet.OpenConnection, channelID int32, errorType string) { + oc.UnsetChannel(channelID) +} + +// OnGenericError is called when a generalized error is returned from the peer +func (rsc *ricochetServiceChecker) OnGenericError(oc *goricochet.OpenConnection, channelID int32) { + oc.RejectOpenChannel(channelID, "GenericError") +} + +//OnUnknownTypeError is called when an unknown type error is returned from the peer +func (rsc *ricochetServiceChecker) OnUnknownTypeError(oc *goricochet.OpenConnection, channelID int32) { + oc.RejectOpenChannel(channelID, "UnknownTypeError") +} + +// OnUnauthorizedError is called when an unathorized error is returned from the peer +func (rsc *ricochetServiceChecker) OnUnauthorizedError(oc *goricochet.OpenConnection, channelID int32) { + oc.RejectOpenChannel(channelID, "UnauthorizedError") +} + +// OnBadUsageError is called when a bad usage error is returned from the peer +func (rsc *ricochetServiceChecker) OnBadUsageError(oc *goricochet.OpenConnection, channelID int32) { + oc.RejectOpenChannel(channelID, "BadUsageError") +} + +// OnFailedError is called when a failed error is returned from the peer +func (rsc *ricochetServiceChecker) OnFailedError(oc *goricochet.OpenConnection, channelID int32) { + oc.RejectOpenChannel(channelID, "FailedError") +} + +// Perform scan of a hidden service for the Ricochet protocol func (rps *RicochetProtocolScanner) ScanProtocol(hiddenService string, osc *config.OnionScanConfig, report *report.OnionScanReport) { - // Ricochet osc.LogInfo(fmt.Sprintf("Checking %s ricochet(9878)\n", hiddenService)) conn, err := utils.GetNetworkConnection(hiddenService, 9878, osc.TorProxyAddress, osc.Timeout) if err != nil { @@ -19,8 +131,21 @@ func (rps *RicochetProtocolScanner) ScanProtocol(hiddenService string, osc *conf report.RicochetDetected = false } else { osc.LogInfo("Detected possible ricochet instance\n") - // TODO: Actual Analysis - report.RicochetDetected = true + r := new(goricochet.Ricochet) + rsc := &ricochetServiceChecker{osc, r, make(chan bool)} + r.Init() + go r.ProcessMessages(rsc) + oc, err := r.ConnectOpen(conn, hiddenService) + r.RequestStopMessageLoop() + if err != nil { + osc.LogInfo(fmt.Sprintf("Ricochet ConnectOpen failed: %s\n", err)) + } else { + if <-rsc.status { + osc.LogInfo("Detected working ricochet instance\n") + report.RicochetDetected = true + } + oc.Close() + } } if conn != nil { conn.Close() From 01a590c77e89005cbbe8de1f16d1ad711dee073c Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Sat, 12 Nov 2016 15:55:03 +0100 Subject: [PATCH 7/7] Dispatch onion scans from table --- main.go | 2 +- onionscan/onionscan.go | 108 +++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 65 deletions(-) diff --git a/main.go b/main.go index 5fd9208..8125eb6 100644 --- a/main.go +++ b/main.go @@ -56,7 +56,7 @@ func main() { if *scans != "" { scanslist = strings.Split(*scans, ",") } else { - scanslist = onionScan.GetAllActions() + scanslist = onionScan.GetAllActions(true) } onionScan.Config = config.Configure(*torProxyAddress, *directoryDepth, *fingerprint, *timeout, *dbdir, scanslist, *crawlconfigdir, *cookiestring, *verbose) diff --git a/onionscan/onionscan.go b/onionscan/onionscan.go index 9ed0243..2a4cf11 100644 --- a/onionscan/onionscan.go +++ b/onionscan/onionscan.go @@ -5,6 +5,7 @@ import ( "github.com/s-rah/onionscan/config" "github.com/s-rah/onionscan/protocol" "github.com/s-rah/onionscan/report" + "sort" "time" ) @@ -13,80 +14,59 @@ type OnionScan struct { Config *config.OnionScanConfig } -// GetAllActions returns a list of all possible protocol level scans. -func (os *OnionScan) GetAllActions() []string { - return []string{ - "web", - "tls", - "ssh", - "irc", - "ricochet", - "ftp", - "smtp", - "mongodb", - "vnc", - "xmpp", - "bitcoin", - "bitcoin_test", - "litecoin", - "dogecoin", - } +// Description record for a single scan type +type scanDescription struct { + scanner protocol.Scanner + runByDefault bool +} + +// List of all scan types in onionscan +var allScans = map[string]scanDescription{ + "web": {new(protocol.HTTPProtocolScanner), true}, + "tls": {new(protocol.TLSProtocolScanner), true}, + "ssh": {new(protocol.SSHProtocolScanner), true}, + "irc": {new(protocol.IRCProtocolScanner), true}, + "ricochet": {new(protocol.RicochetProtocolScanner), true}, + "ftp": {new(protocol.FTPProtocolScanner), true}, + "smtp": {new(protocol.SMTPProtocolScanner), true}, + "mongodb": {new(protocol.MongoDBProtocolScanner), true}, + "vnc": {new(protocol.VNCProtocolScanner), true}, + "xmpp": {new(protocol.XMPPProtocolScanner), true}, + "bitcoin": {protocol.NewBitcoinProtocolScanner("bitcoin"), true}, + "bitcoin_test": {protocol.NewBitcoinProtocolScanner("bitcoin_test"), true}, + "litecoin": {protocol.NewBitcoinProtocolScanner("litecoin"), true}, + "litecoin_test": {protocol.NewBitcoinProtocolScanner("litecoin_test"), false}, + "dogecoin": {protocol.NewBitcoinProtocolScanner("dogecoin"), true}, + "dogecoin_test": {protocol.NewBitcoinProtocolScanner("dogecoin_test"), false}, + "none": {nil, false}, } -// PerformNextAction determined which scan to run next, and runs it. -func (os *OnionScan) PerformNextAction(report *report.OnionScanReport, nextAction string) error { - switch nextAction { - case "web": - wps := new(protocol.HTTPProtocolScanner) - wps.ScanProtocol(report.HiddenService, os.Config, report) - case "tls": - tps := new(protocol.TLSProtocolScanner) - tps.ScanProtocol(report.HiddenService, os.Config, report) - case "ssh": - sps := new(protocol.SSHProtocolScanner) - sps.ScanProtocol(report.HiddenService, os.Config, report) - case "irc": - ips := new(protocol.IRCProtocolScanner) - ips.ScanProtocol(report.HiddenService, os.Config, report) - case "ricochet": - rps := new(protocol.RicochetProtocolScanner) - rps.ScanProtocol(report.HiddenService, os.Config, report) - case "ftp": - fps := new(protocol.FTPProtocolScanner) - fps.ScanProtocol(report.HiddenService, os.Config, report) - case "smtp": - smps := new(protocol.SMTPProtocolScanner) - smps.ScanProtocol(report.HiddenService, os.Config, report) - case "mongodb": - mdbps := new(protocol.MongoDBProtocolScanner) - mdbps.ScanProtocol(report.HiddenService, os.Config, report) - case "vnc": - vncps := new(protocol.VNCProtocolScanner) - vncps.ScanProtocol(report.HiddenService, os.Config, report) - case "xmpp": - xmppps := new(protocol.XMPPProtocolScanner) - xmppps.ScanProtocol(report.HiddenService, os.Config, report) - case "bitcoin", "bitcoin_test", "litecoin", "litecoin_test", "dogecoin", "dogecoin_test": - bps := protocol.NewBitcoinProtocolScanner(nextAction) - bps.ScanProtocol(report.HiddenService, os.Config, report) - case "none": - return nil - default: - return fmt.Errorf("Unknown scanner %s", nextAction) +// GetDefaultActions returns a list of all protocol level scans +// (or optionally only those that should be enabled by default). +func (os *OnionScan) GetAllActions(onlyDefault bool) []string { + var keys []string + for k := range allScans { + if !onlyDefault || allScans[k].runByDefault { + keys = append(keys, k) + } } - return nil + sort.Strings(keys) + return keys } // Do performs all configured protocol level scans in this run. func (os *OnionScan) Do(osreport *report.OnionScanReport) error { for _, nextAction := range os.Config.Scans { - err := os.PerformNextAction(osreport, nextAction) - if err != nil { - os.Config.LogInfo(fmt.Sprintf("Error: %s", err)) - } else { - osreport.PerformedScans = append(osreport.PerformedScans, nextAction) + scan, ok := allScans[nextAction] + if scan.scanner == nil { + if !ok { // If key was not found, give error, otherwise this was the dummy scan "none" + os.Config.LogInfo(fmt.Sprintf("Unknown scanner %s", nextAction)) + } + continue } + scan.scanner.ScanProtocol(osreport.HiddenService, os.Config, osreport) + osreport.PerformedScans = append(osreport.PerformedScans, nextAction) if time.Now().Sub(osreport.DateScanned).Seconds() > os.Config.Timeout.Seconds() { osreport.TimedOut = true break