First published Milk Sad website commit
|
@ -0,0 +1,3 @@
|
||||||
|
Dockerfile
|
||||||
|
Makefile
|
||||||
|
_site
|
|
@ -0,0 +1,8 @@
|
||||||
|
_site
|
||||||
|
.sass-cache
|
||||||
|
.jekyll-cache
|
||||||
|
.jekyll-metadata
|
||||||
|
_vendor
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
*.swp
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "_vendor/jekyll-theme-console"]
|
||||||
|
path = _vendor/jekyll-theme-console
|
||||||
|
url = https://github.com/b2a3e8/jekyll-theme-console
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
permalink: /404.html
|
||||||
|
layout: home
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>404</h1>
|
||||||
|
|
||||||
|
<p><strong>You're in the wrong place.</strong> Go <a href="/">/home</a>.</p>
|
||||||
|
<p>:wq</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM ruby:3.2.2-alpine AS builder
|
||||||
|
LABEL stage=distrust-co-builder
|
||||||
|
RUN apk update && apk add g++ make git git-lfs
|
||||||
|
RUN mkdir -p /home
|
||||||
|
COPY Gemfile /home
|
||||||
|
COPY Gemfile.lock /home
|
||||||
|
COPY _vendor /home/_vendor
|
||||||
|
WORKDIR /home
|
||||||
|
RUN bundle install
|
||||||
|
COPY . /home
|
||||||
|
RUN jekyll build
|
||||||
|
|
||||||
|
FROM nginx:1.25.1
|
||||||
|
COPY --from=builder /home/_site /usr/share/nginx/html
|
|
@ -0,0 +1,4 @@
|
||||||
|
source "https://rubygems.org"
|
||||||
|
# gem "jekyll-theme-console", path: "./_vendor/jekyll-theme-console"
|
||||||
|
|
||||||
|
gem "jekyll"
|
|
@ -0,0 +1,73 @@
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
addressable (2.8.4)
|
||||||
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
colorator (1.1.0)
|
||||||
|
concurrent-ruby (1.2.2)
|
||||||
|
em-websocket (0.5.3)
|
||||||
|
eventmachine (>= 0.12.9)
|
||||||
|
http_parser.rb (~> 0)
|
||||||
|
eventmachine (1.2.7)
|
||||||
|
ffi (1.15.5)
|
||||||
|
forwardable-extended (2.6.0)
|
||||||
|
google-protobuf (3.23.4-x86_64-linux)
|
||||||
|
http_parser.rb (0.8.0)
|
||||||
|
i18n (1.14.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
jekyll (4.3.2)
|
||||||
|
addressable (~> 2.4)
|
||||||
|
colorator (~> 1.0)
|
||||||
|
em-websocket (~> 0.5)
|
||||||
|
i18n (~> 1.0)
|
||||||
|
jekyll-sass-converter (>= 2.0, < 4.0)
|
||||||
|
jekyll-watch (~> 2.0)
|
||||||
|
kramdown (~> 2.3, >= 2.3.1)
|
||||||
|
kramdown-parser-gfm (~> 1.0)
|
||||||
|
liquid (~> 4.0)
|
||||||
|
mercenary (>= 0.3.6, < 0.5)
|
||||||
|
pathutil (~> 0.9)
|
||||||
|
rouge (>= 3.0, < 5.0)
|
||||||
|
safe_yaml (~> 1.0)
|
||||||
|
terminal-table (>= 1.8, < 4.0)
|
||||||
|
webrick (~> 1.7)
|
||||||
|
jekyll-sass-converter (3.0.0)
|
||||||
|
sass-embedded (~> 1.54)
|
||||||
|
jekyll-watch (2.2.1)
|
||||||
|
listen (~> 3.0)
|
||||||
|
kramdown (2.4.0)
|
||||||
|
rexml
|
||||||
|
kramdown-parser-gfm (1.1.0)
|
||||||
|
kramdown (~> 2.0)
|
||||||
|
liquid (4.0.4)
|
||||||
|
listen (3.8.0)
|
||||||
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
|
mercenary (0.4.0)
|
||||||
|
pathutil (0.16.2)
|
||||||
|
forwardable-extended (~> 2.6)
|
||||||
|
public_suffix (5.0.3)
|
||||||
|
rake (13.0.6)
|
||||||
|
rb-fsevent (0.11.2)
|
||||||
|
rb-inotify (0.10.1)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
rexml (3.2.5)
|
||||||
|
rouge (4.1.2)
|
||||||
|
safe_yaml (1.0.5)
|
||||||
|
sass-embedded (1.63.6)
|
||||||
|
google-protobuf (~> 3.23)
|
||||||
|
rake (>= 13.0.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
unicode-display_width (2.4.2)
|
||||||
|
webrick (1.8.1)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
x86_64-linux
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
jekyll
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.4.17
|
|
@ -0,0 +1,9 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Distrust, LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,31 @@
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
# Build Docker image
|
||||||
|
docker build -t milksad-distrust-co .
|
||||||
|
|
||||||
|
.PHONY: fullclean
|
||||||
|
fullclean: clean
|
||||||
|
docker rmi milksad-distrust-co -f
|
||||||
|
docker image prune --filter label=stage=distrust-co-builder
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -r _site
|
||||||
|
|
||||||
|
_site: build
|
||||||
|
mkdir -p _site
|
||||||
|
docker run milksad-distrust-co tar c -C /usr/share/nginx/html . | tar x -C _site
|
||||||
|
|
||||||
|
.PHONY: serve
|
||||||
|
serve: build
|
||||||
|
# Run Docker container with listener for current dir and port mapping
|
||||||
|
docker run --rm -p 0.0.0.0:4000:80 -it milksad-distrust-co
|
||||||
|
|
||||||
|
.PHONY: build-dev
|
||||||
|
build-dev:
|
||||||
|
# Build Docker image
|
||||||
|
docker build --target builder -t dev-milksad-distrust-co .
|
||||||
|
|
||||||
|
.PHONY: dev
|
||||||
|
dev: build-dev
|
||||||
|
docker run --rm --expose 4000 -p 127.0.0.1:4000:4000 -v ${PWD}:/home -it dev-milksad-distrust-co jekyll serve -H 0.0.0.0
|
|
@ -0,0 +1,62 @@
|
||||||
|
# [distrust.co](https://distrust.co)
|
||||||
|
|
||||||
|
Web page source.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
0. If the repo was not cloned with submodules:
|
||||||
|
```shell
|
||||||
|
$ git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
You can manually clone the submodule into the dir if the above command doesn't work:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/b2a3e8/jekyll-theme-console _vendor/jekyll-theme-console
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Install `make`.
|
||||||
|
2. Install `docker`
|
||||||
|
3. Build Docker container
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ make build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Serve site at `0.0.0.0:4000`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ make serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publish Docker container to Codeberg
|
||||||
|
|
||||||
|
1. Use `docker login` with a Codeberg PAT with write package scope
|
||||||
|
2. Tag container with Codeberg repo name
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker tag milksad-distrust-co codeberg.org/distrust/milksad-distrust-co
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Push container to Codeberg container registry
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dockerb push codeberg.org/distrust/milksad-distrust-co
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Generate Static Files
|
||||||
|
|
||||||
|
1. Output static files in `_site` directory:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ make _site
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
|
||||||
|
1. Remove all build artifacts:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ make fullclean
|
||||||
|
```
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Welcome to Jekyll!
|
||||||
|
#
|
||||||
|
# This config file is meant for settings that affect your whole blog, values
|
||||||
|
# which you are expected to set up once and rarely edit after that. If you find
|
||||||
|
# yourself editing this file very often, consider using Jekyll's data files
|
||||||
|
# feature for the data you need to update frequently.
|
||||||
|
#
|
||||||
|
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||||
|
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
|
||||||
|
#
|
||||||
|
# If you need help with YAML syntax, here are some quick references for you:
|
||||||
|
# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
|
||||||
|
# https://learnxinyminutes.com/docs/yaml/
|
||||||
|
#
|
||||||
|
# Site settings
|
||||||
|
# These are used to personalize your new site. If you look in the HTML files,
|
||||||
|
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
||||||
|
# You can create any custom variable you would like, and they will be accessible
|
||||||
|
# in the templates via {{ site.myvariable }}.
|
||||||
|
|
||||||
|
title: milk sad Disclosure
|
||||||
|
email: team@milksad.info
|
||||||
|
description: >- # this means to ignore newlines until "baseurl:"
|
||||||
|
An explanation of how weak entropy can ruin your day.
|
||||||
|
baseurl: "" # the subpath of your site, e.g. /blog
|
||||||
|
url: "https://milksad.info" # the base hostname & protocol for your site, e.g. http://example.com
|
||||||
|
|
||||||
|
header_pages:
|
||||||
|
- index.md
|
||||||
|
- disclosure.md
|
||||||
|
- lookup.md
|
||||||
|
- faq.md
|
||||||
|
|
||||||
|
style: dark # dark (default), light or hacker
|
||||||
|
listen_for_clients_preferred_style: false # false (default) or true
|
||||||
|
|
||||||
|
footer: '2023'
|
||||||
|
|
||||||
|
# Build settings
|
||||||
|
|
||||||
|
# included locally instead
|
||||||
|
# theme: jekyll-theme-console
|
||||||
|
|
||||||
|
# for Table of Contents
|
||||||
|
kramdown:
|
||||||
|
toc_levels: 2..3
|
||||||
|
|
||||||
|
sass:
|
||||||
|
sass_dir: _sass
|
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
* Core Team
|
||||||
|
* Distrust
|
||||||
|
* Anton Livaja - [anton@distrust.co](mailto:anton@distrust.co)
|
||||||
|
* Lance R. Vick - [lance@distrust.co](mailto:lance@distrust.co), [https://lance.dev](https://lance.dev)
|
||||||
|
* Ryan Heywood - [ryan@distrust.co](mailto:ryan@distrust.co), [https://ryansquared.pub](https://ryansquared.pub)
|
||||||
|
* Shane Engelman - [shane@distrust.co](mailto:shane@distrust.co)
|
||||||
|
* Independent
|
||||||
|
* Christian Reitter - [https://inhq.net](https://inhq.net)
|
||||||
|
* Daniel Grove - [danny@dannygrove.com](mailto:danny@dannygrove.com)
|
||||||
|
* Dustin Johnson - [\_@di0.io](mailto:_@di0.io)
|
||||||
|
* Heiko Schaefer - [heiko@schaefer.name](mailto:heiko@schaefer.name)
|
||||||
|
* James Callahan - [james@wavesquid.com](mailto:james@wavesquid.com)
|
||||||
|
* Jochen Hoenicke - [https://jhoenicke.de](https://jhoenicke.de)
|
||||||
|
* John Naulty - [jnaulty@dendritictech.com](mailto:jnaulty@dendritictech.com)
|
||||||
|
* Matthew Brooks - [\*@logicwax.com](mailto:*@logicwax.com)
|
||||||
|
* Special Thanks
|
||||||
|
* Jack Kearney - [Turnkey](https://turnkey.com)
|
||||||
|
* Several trusted advisors that wish to remain uncredited. You know who you are.
|
|
@ -0,0 +1,3 @@
|
||||||
|
<footer>
|
||||||
|
<span><img src="assets/base/milksad_bottle_transparent.svg" height="12px" alt="Milk Sad logo as icon"/></span> {{ site.footer }}
|
||||||
|
</footer>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicons/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="/assets/favicons/favicon-96x96.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicons/favicon-16x16.png">
|
||||||
|
<title>Milk Sad: {{ page.title }}</title>
|
||||||
|
|
||||||
|
{% if page.robots %}
|
||||||
|
<meta name="robots" content="{{page.robots}}" />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ "/assets/main.css" | relative_url }}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ "/assets/main-dark.css" | relative_url }}">
|
||||||
|
|
||||||
|
<!-- "Really, there is nothing interesting to see here. It is a static website. Here is the terraform code that deployed it, and here is the site source repo. If you find anything interesting or want to talk to us, reach out!" -->
|
||||||
|
<!-- https://git.distrust.co/public/infra -->
|
||||||
|
<!-- https://git.distrust.co/public/website -->
|
||||||
|
|
||||||
|
</head>
|
|
@ -0,0 +1,22 @@
|
||||||
|
{%- assign page_paths = site.header_pages | default: default_paths -%}
|
||||||
|
<header>
|
||||||
|
<div class="menu">
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li style="float: left">
|
||||||
|
<a href="/" style="display: flex">
|
||||||
|
<img height="30px" src="/assets/base/milksad_transparent.svg" />
|
||||||
|
<div style="padding: 5px 0px 0px 5px">Milk Sad</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{%- for path in page_paths -%}
|
||||||
|
{%- assign my_page = site.pages | where: "path", path | first -%}
|
||||||
|
{%- if my_page.title -%}
|
||||||
|
<li><a href="{{ my_page.url | relative_url }}">
|
||||||
|
{{ my_page.title | escape }}
|
||||||
|
</a></li>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
|
@ -0,0 +1,10 @@
|
||||||
|
* [Distrust, LLC](https://distrust.co/contact.html)
|
||||||
|
* Infosec firm focusing on high risk clients
|
||||||
|
* Pentesting, threat modeling, hands-on security engineering
|
||||||
|
* Full stack security evaluations and advisory retainer contracts
|
||||||
|
* Covers: Shane Engelman, Anton Livaja, Ryan Heywood, and Lance Vick
|
||||||
|
* [Christian Reitter](https://blog.inhq.net/consulting/)
|
||||||
|
* Freelance InfoSec Consultant
|
||||||
|
* Pentesting, Code Audits, Security Research, Fuzzing
|
||||||
|
* [Heiko Schaefer](https://codeberg.org/heiko)
|
||||||
|
* Focus on OpenPGP: OpenPGP CA, Sequoia PGP, OpenPGP on HSM devices (OpenPGP card, PKCS #11, PIV)
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ page.lang | default: site.lang | default: "en" }}">
|
||||||
|
|
||||||
|
{%- include head.html -%}
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
{%- include header.html -%}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{{ content }}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{%- include footer.html -%}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
|
{{ content }}
|
||||||
|
{% assign contentwonl = content | strip_newlines %} {% unless contentwonl == "" %} <br /> {% endunless %}
|
||||||
|
{%- assign date_format = "%Y-%m-%d" -%}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Dark theme variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--base-color: #ffffff;
|
||||||
|
--border: solid 2px rgba(219, 219, 219, 0.9);
|
||||||
|
--selection-background: rgba(219, 219, 219, 0.99);
|
||||||
|
--selection-text: #000;
|
||||||
|
--background-color: #04355d;
|
||||||
|
--text-color: var(--base-color);
|
||||||
|
--placeholder-color: var(--background-color);
|
||||||
|
--link-color: var(--base-color);
|
||||||
|
--code-color-1: #ebdadac5;
|
||||||
|
--code-color-2: #ffffcc;
|
||||||
|
--code-color-3: #F00000;
|
||||||
|
--code-color-4: #F0A0A0;
|
||||||
|
--code-color-5: #b38aff;
|
||||||
|
--code-color-6: #35c652;
|
||||||
|
--code-color-7: #858500;
|
||||||
|
--code-color-8: #000080;
|
||||||
|
--code-color-9: #32ef32;
|
||||||
|
--code-color-10: #888888;
|
||||||
|
--code-color-11: #555555;
|
||||||
|
--code-color-12: #800080;
|
||||||
|
--code-color-13: #006917;
|
||||||
|
--code-color-14: #00c1c1;
|
||||||
|
--code-color-15: #fcc971;
|
||||||
|
--code-color-16: #1e90ff;
|
||||||
|
--code-color-17: #800000;
|
||||||
|
--code-color-18: #bbbbbb;
|
||||||
|
--code-color-19: black;
|
||||||
|
--code-color-20: #1294ff;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Base variables
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--base-color: #000;
|
||||||
|
--border: dashed 1px rgba(0, 0, 0, 1);
|
||||||
|
--selection-background: rgba(0, 0, 0, 0.99);
|
||||||
|
--selection-text: #FFF;
|
||||||
|
--background-color: #FFF;
|
||||||
|
--text-color: var(--base-color);
|
||||||
|
--placeholder-color: var(--base-color);
|
||||||
|
--link-color: var(--base-color);
|
||||||
|
--code-color-1: #aaaaaa;
|
||||||
|
--code-color-2: #ffffcc;
|
||||||
|
--code-color-3: #F00000;
|
||||||
|
--code-color-4: #F0A0A0;
|
||||||
|
--code-color-5: #0000aa;
|
||||||
|
--code-color-6: #4c8317;
|
||||||
|
--code-color-7: #aa0000;
|
||||||
|
--code-color-8: #000080;
|
||||||
|
--code-color-9: #00aa00;
|
||||||
|
--code-color-10: #888888;
|
||||||
|
--code-color-11: #555555;
|
||||||
|
--code-color-12: #800080;
|
||||||
|
--code-color-13: #00aaaa;
|
||||||
|
--code-color-14: #009999;
|
||||||
|
--code-color-15: #aa5500;
|
||||||
|
--code-color-16: #1e90ff;
|
||||||
|
--code-color-17: #800000;
|
||||||
|
--code-color-18: #bbbbbb;
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
@charset "utf-8";
|
||||||
|
// @import url('https://fonts.googleapis.com/css?family=Rubik:400,700');
|
||||||
|
// @import url('Rubik-VariableFont_wght.ttf');
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rubik';
|
||||||
|
src: url('fonts/Rubik-VariableFont_wght.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style variables
|
||||||
|
*/
|
||||||
|
$base-font-family: 'Rubik', monospace !default;
|
||||||
|
$base-font-size: 16px !default;
|
||||||
|
$mobile-font-size: 16px !default;
|
||||||
|
$base-line-height: 1.5 !default;
|
||||||
|
$container-width: 90% !default;
|
||||||
|
$container-max-width: 1150px !default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global
|
||||||
|
*/
|
||||||
|
body { background-color: var(--background-color); margin: 0 auto; padding: 0; font-family: $base-font-family; font-size: $base-font-size; color: var(--text-color); text-align: left; line-height: $base-line-height !important; }
|
||||||
|
h1 { font-size: 32px; }
|
||||||
|
h2 { font-size: 28px; }
|
||||||
|
h3 { font-size: 24px; }
|
||||||
|
h4 { font-size: 20px; }
|
||||||
|
h5 { font-size: 18px; }
|
||||||
|
h6 { font-size: 16px; }
|
||||||
|
h1, h2, h3, h4, h5, h6 { margin: 0px; margin-top: 12px; margin-bottom: 12px; font-weight: bold; color: var(--text-color); }
|
||||||
|
p, ul, ol { margin: 0px; color: var(--text-color); }
|
||||||
|
a { text-decoration: underline; color: var(--link-color); }
|
||||||
|
a:hover { color: var(--background-color); background-color: var(--base-color); }
|
||||||
|
a:focus { color: var(--background-color); background-color: var(--base-color); }
|
||||||
|
@media only screen and (max-device-width: 500px) { * { font-size: $mobile-font-size; } }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout
|
||||||
|
*/
|
||||||
|
.container { width: $container-width; max-width: $container-max-width; margin-right: auto; margin-left: auto; }
|
||||||
|
p { word-wrap: break-word; word-break: break-word; white-space: pre-wrap; margin-bottom: 15px; }
|
||||||
|
footer { color: var(--text-color); border-top: var(--border); margin-top: 0; padding-top: 10px; text-align: right; }
|
||||||
|
header { margin-top: 20px; margin-bottom: 10px; }
|
||||||
|
header p { text-align: left; margin: 0; }
|
||||||
|
footer { margin-bottom: 20px; }
|
||||||
|
hr { margin-top: 20px; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu Logo
|
||||||
|
*/
|
||||||
|
.menu-logo { position: absolute; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight/Markup
|
||||||
|
*/
|
||||||
|
::selection { background: var(--selection-background); color: var(--selection-text); }
|
||||||
|
::-moz-selection { background: var(--selection-background); color: var(--selection-text); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
:not(.menu) > ul { list-style: none; }
|
||||||
|
:not(.menu) > ul { list-style-type: none; }
|
||||||
|
:not(.menu) > ul > li:before { content: "-"; margin-right: 9px; }
|
||||||
|
ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header/Navigation
|
||||||
|
*/
|
||||||
|
.menu { border-bottom: var(--border); margin-bottom: 20px; }
|
||||||
|
.menu ul { margin-top: 12px; margin-bottom: 12px; padding-left: 0px; list-style-type: none; text-align: right; }
|
||||||
|
.menu ul li { display: inline; margin-left: 10px; }
|
||||||
|
.menu ul li a { text-decoration: none; color: var(--text-color); }
|
||||||
|
.menu ul li a:hover { text-decoration: none; color: var(--background-color); background-color: var(--base-color); }
|
||||||
|
.menu ul li a:focus { text-decoration: none; color: var(--background-color); background-color: var(--base-color); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form
|
||||||
|
*/
|
||||||
|
input, select, textarea { padding: 0; margin: 0; -webkit-appearance: none; -webkit-border-radius: 0; border: none; }
|
||||||
|
input[type=text], select, textarea { width: 100%; resize: none; background-color: var(--base-color); color: var(--code-color-17); caret-color: var(--code-color-17); font-size: $base-font-size; font-family: $base-font-family; line-height: $base-line-height; }
|
||||||
|
input, select, textarea, textarea::-webkit-input-placeholder { text-indent: 0px; }
|
||||||
|
::placeholder { color: var(--placeholder-color); opacity: 1; }
|
||||||
|
:-ms-input-placeholder { color: var(--placeholder-color); }
|
||||||
|
::-ms-input-placeholder { color: var(--placeholder-color); }
|
||||||
|
input[type=submit] { font-size: $base-font-size; font-family: $base-font-family; line-height: $base-line-height; cursor: pointer; color: var(--base-color); background-color: var(--code-color-1); }
|
||||||
|
input[type=submit]:hover { color: var(--background-color); background-color: var(--base-color); }
|
||||||
|
input[type=submit]:focus { color: var(--background-color); background-color: var(--base-color); }
|
||||||
|
*:focus { outline: none; }
|
||||||
|
textarea { vertical-align: top; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code and syntax highlighting
|
||||||
|
*/
|
||||||
|
.lineno { color: var(--code-color-1); margin-right: 15px; }
|
||||||
|
figure.highlight { margin: 5px 0; }
|
||||||
|
pre { background-color: var(--background-color); border: none; padding: 0; margin: 0; overflow:auto; font-size: $base-font-size; color: var(--text-color); line-height: 1.7 !important; font-family: $base-font-family !important; }
|
||||||
|
.highlight .hll { background-color: var(--code-color-2); }
|
||||||
|
.highlight .c { color: var(--code-color-8); font-style: italic } /* Comment */
|
||||||
|
.highlight .err { color: var(--code-color-3); background-color: var(--code-color-4); } /* Error */
|
||||||
|
.highlight .k { color: var(--code-color-5); } /* Keyword */
|
||||||
|
.highlight .cm { color: var(--code-color-1); font-style: italic } /* Comment.Multiline */
|
||||||
|
.highlight .cp { color: var(--code-color-6); } /* Comment.Preproc */
|
||||||
|
.highlight .c1 { color: var(--code-color-1); font-style: italic } /* Comment.Single */
|
||||||
|
.highlight .cs { color: var(--code-color-5); font-style: italic } /* Comment.Special */
|
||||||
|
.highlight .gd { color: var(--code-color-7); } /* Generic.Deleted */
|
||||||
|
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||||
|
.highlight .gr { color: var(--code-color-7); } /* Generic.Error */
|
||||||
|
.highlight .gh { color: var(--code-color-8); font-weight: bold } /* Generic.Heading */
|
||||||
|
.highlight .gi { color: var(--code-color-9); } /* Generic.Inserted */
|
||||||
|
.highlight .go { color: var(--code-color-10); } /* Generic.Output */
|
||||||
|
.highlight .gp { color: var(--code-color-11); } /* Generic.Prompt */
|
||||||
|
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||||
|
.highlight .gu { color: var(--code-color-12); font-weight: bold } /* Generic.Subheading */
|
||||||
|
.highlight .gt { color: var(--code-color-7); } /* Generic.Traceback */
|
||||||
|
.highlight .kc { color: var(--code-color-5); } /* Keyword.Constant */
|
||||||
|
.highlight .kd { color: var(--code-color-5); } /* Keyword.Declaration */
|
||||||
|
.highlight .kn { color: var(--code-color-5); } /* Keyword.Namespace */
|
||||||
|
.highlight .kp { color: var(--code-color-5); } /* Keyword.Pseudo */
|
||||||
|
.highlight .kr { color: var(--code-color-5); } /* Keyword.Reserved */
|
||||||
|
.highlight .kt { color: var(--code-color-13); } /* Keyword.Type */
|
||||||
|
.highlight .m { color: var(--code-color-14); } /* Literal.Number */
|
||||||
|
.highlight .s { color: var(--code-color-15); } /* Literal.String */
|
||||||
|
.highlight .na { color: var(--code-color-16); } /* Name.Attribute */
|
||||||
|
.highlight .nb { color: var(--code-color-13); } /* Name.Builtin */
|
||||||
|
.highlight .nc { color: var(--code-color-9); text-decoration: underline } /* Name.Class */
|
||||||
|
.highlight .no { color: var(--code-color-7); } /* Name.Constant */
|
||||||
|
.highlight .nd { color: var(--code-color-10); } /* Name.Decorator */
|
||||||
|
.highlight .ni { color: var(--code-color-17); font-weight: bold } /* Name.Entity */
|
||||||
|
.highlight .nf { color: var(--code-color-9); } /* Name.Function */
|
||||||
|
.highlight .nn { color: var(--code-color-13); text-decoration: underline } /* Name.Namespace */
|
||||||
|
.highlight .nt { color: var(--code-color-16); font-weight: bold } /* Name.Tag */
|
||||||
|
.highlight .nv { color: var(--code-color-7); } /* Name.Variable */
|
||||||
|
.highlight .ow { color: var(--code-color-5); } /* Operator.Word */
|
||||||
|
.highlight .w { color: var(--code-color-18); } /* Text.Whitespace */
|
||||||
|
.highlight .mf { color: var(--code-color-14); } /* Literal.Number.Float */
|
||||||
|
.highlight .mh { color: var(--code-color-14); } /* Literal.Number.Hex */
|
||||||
|
.highlight .mi { color: var(--code-color-14); } /* Literal.Number.Integer */
|
||||||
|
.highlight .mo { color: var(--code-color-14); } /* Literal.Number.Oct */
|
||||||
|
.highlight .sb { color: var(--code-color-15); } /* Literal.String.Backtick */
|
||||||
|
.highlight .sc { color: var(--code-color-15); } /* Literal.String.Char */
|
||||||
|
.highlight .sd { color: var(--code-color-15); } /* Literal.String.Doc */
|
||||||
|
.highlight .s2 { color: var(--code-color-15); } /* Literal.String.Double */
|
||||||
|
.highlight .se { color: var(--code-color-15); } /* Literal.String.Escape */
|
||||||
|
.highlight .sh { color: var(--code-color-15); } /* Literal.String.Heredoc */
|
||||||
|
.highlight .si { color: var(--code-color-15); } /* Literal.String.Interpol */
|
||||||
|
.highlight .sx { color: var(--code-color-15); } /* Literal.String.Other */
|
||||||
|
.highlight .sr { color: var(--code-color-14); } /* Literal.String.Regex */
|
||||||
|
.highlight .s1 { color: var(--code-color-15); } /* Literal.String.Single */
|
||||||
|
.highlight .ss { color: var(--code-color-5); } /* Literal.String.Symbol */
|
||||||
|
.highlight .bp { color: var(--code-color-13); } /* Name.Builtin.Pseudo */
|
||||||
|
.highlight .vc { color: var(--code-color-7); } /* Name.Variable.Class */
|
||||||
|
.highlight .vg { color: var(--code-color-7); } /* Name.Variable.Global */
|
||||||
|
.highlight .vi { color: var(--code-color-7); } /* Name.Variable.Instance */
|
||||||
|
.highlight .il { color: var(--code-color-14); } /* Literal.Number.Integer.Long */
|
||||||
|
|
||||||
|
// Custom Overrides
|
||||||
|
pre {
|
||||||
|
background: rgba(0, 0, 0, 0.3490196078);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 20px 20px 30px;
|
||||||
|
margin: 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code:not(pre > code) {
|
||||||
|
background: #00000059;
|
||||||
|
color: #E8912d;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: var(--code-color-1);
|
||||||
|
margin: 40px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
background: rgba(0, 0, 0, 0.3490196078);
|
||||||
|
width: 100%;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, ul, ol {
|
||||||
|
// workaround, improve
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid lightgrey;
|
||||||
|
border-collapse: collapse;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background: rgba(0, 0, 0, 0.3490196078);
|
||||||
|
padding: 14px 25px 1px 25px;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 76983908faa6f06ffd87b70165f2632790d36687
|
After Width: | Height: | Size: 733 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg id="svg1681" xmlns="http://www.w3.org/2000/svg" width="300" height="77.54" viewBox="0 0 300 77.54"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M56.15,0H21.39A21.39,21.39,0,0,0,0,21.39V56.15A21.39,21.39,0,0,0,21.39,77.54H56.15A21.38,21.38,0,0,0,77.54,56.15V21.39A21.39,21.39,0,0,0,56.15,0ZM5.76,40.46V37.89a2.62,2.62,0,0,1,1-2.35L22.21,20.06a2.6,2.6,0,0,1,2.35-1h2.58a2.6,2.6,0,0,1,2.35,1l9.56,9.6-4.7,4.69-8.5-8.53L12.49,39.18,21,47.72l-4.7,4.69-9.57-9.6A2.63,2.63,0,0,1,5.76,40.46Zm9.83,24.83a1.12,1.12,0,0,1-1.59,0l-2.65-2.65a1.12,1.12,0,0,1,0-1.59L60.54,11.86a1.11,1.11,0,0,1,1.59,0l2.65,2.64a1.13,1.13,0,0,1,0,1.6ZM71.78,40.47a2.6,2.6,0,0,1-1,2.34L55.33,58.29a2.62,2.62,0,0,1-2.35,1H50.41a2.62,2.62,0,0,1-2.35-1L37.21,47.35l4.69-4.69,9.8,9.86L65.06,39.17l-9.8-9.87L60,24.61,70.82,35.55a2.66,2.66,0,0,1,1,2.34Z"/><g id="layer2"><g id="text2555-4-2-6"><path id="path2692-8-9-2" class="cls-1" d="M107.08,27.21h-6.39v24h6.72q11.78,0,11.78-12.5,0-6-2.89-8.76t-9.22-2.79ZM91,59.39V19.08H108.3q10.33,0,15.38,5.15t5.09,14.53q0,9.24-5.45,15t-15,5.67Z"/><path id="path2694-4-8-6" class="cls-1" d="M140.84,24.19h-9.55v-6h9.55Zm-.13,35.2h-9.22V27.54h9.22Z"/><path id="path2696-0-2-4" class="cls-1" d="M157.93,60.34a23.8,23.8,0,0,1-5.61-.59,14,14,0,0,1-4-1.51,10.25,10.25,0,0,1-2.75-2.36A11.37,11.37,0,0,1,143.82,53a16.49,16.49,0,0,1-.88-3.38l8.53-1.67a8.24,8.24,0,0,0,2.26,4.23,6.35,6.35,0,0,0,4.39,1.41c2.93,0,4.43-1,4.5-3a2.55,2.55,0,0,0-1.41-2.23,19.62,19.62,0,0,0-5.51-1.67q-6.57-1.38-9.25-3.74a8.06,8.06,0,0,1-2.69-6.4,8.59,8.59,0,0,1,3.34-7.21q3.38-2.59,9.94-2.59c4.37,0,7.66.83,9.84,2.49a11.52,11.52,0,0,1,4.27,7.18l-8.73,1.94a7.43,7.43,0,0,0-1.87-3.84A4.9,4.9,0,0,0,157,33.25a5.38,5.38,0,0,0-3.09.75,2.38,2.38,0,0,0-1.08,2.07,2.72,2.72,0,0,0,.43,1.6,4.79,4.79,0,0,0,2,1.28,24.33,24.33,0,0,0,4.59,1.22c4.35.85,7.41,2.1,9.15,3.74a8.21,8.21,0,0,1,2.63,6.26,9.05,9.05,0,0,1-3.38,7.51q-3.37,2.67-10.33,2.66Z"/><path id="path2698-80-9-7" class="cls-1" d="M176.1,49.39V35.21h-3.7V27.54h3.7v-8h9.12v8h8.4v7.67h-8.4V47.65A4.1,4.1,0,0,0,186,50.5a3.9,3.9,0,0,0,2.89.89,23.86,23.86,0,0,0,4.16-.72l1,8.46a30.76,30.76,0,0,1-3.64.92,21.64,21.64,0,0,1-3.9.29q-5.25,0-7.88-3.05A11.82,11.82,0,0,1,176.1,49.39Z"/><path id="path2700-6-5-4" class="cls-1" d="M205.26,59.39h-9.21V27.54h9.21V38.89h.13a35.72,35.72,0,0,1,1.35-5.84,9.57,9.57,0,0,1,1.9-3.48A6,6,0,0,1,211,28a9.07,9.07,0,0,1,3-.43h1.54V38h-3.48c-2.6,0-4.38.43-5.34,1.31s-1.45,2.49-1.45,4.85Z"/><path id="path2702-9-9-3" class="cls-1" d="M237.61,59.39V50.53h-.13q-1,5.22-3.71,7.52a9.75,9.75,0,0,1-6.66,2.29c-3.34,0-5.83-1-7.44-3.05s-2.43-4.85-2.43-8.46V27.54h9.21v18.7a6.65,6.65,0,0,0,1.28,4.26,5,5,0,0,0,4,1.58,5.31,5.31,0,0,0,5.84-5.84V27.54h9.12V59.39Z"/><path id="path2704-7-46-3" class="cls-1" d="M263.82,60.34a23.8,23.8,0,0,1-5.61-.59,13.93,13.93,0,0,1-4-1.51,10.1,10.1,0,0,1-2.75-2.36A11.37,11.37,0,0,1,249.71,53a15.87,15.87,0,0,1-.88-3.38l8.53-1.67a8.17,8.17,0,0,0,2.26,4.23A6.35,6.35,0,0,0,264,53.55c2.93,0,4.43-1,4.5-3a2.55,2.55,0,0,0-1.41-2.23,19.62,19.62,0,0,0-5.51-1.67q-6.57-1.38-9.25-3.74a8.06,8.06,0,0,1-2.69-6.4A8.59,8.59,0,0,1,253,29.28q3.38-2.59,9.94-2.59t9.84,2.49A11.52,11.52,0,0,1,277,36.36l-8.73,1.94a7.43,7.43,0,0,0-1.87-3.84,4.9,4.9,0,0,0-3.57-1.21,5.41,5.41,0,0,0-3.09.75,2.38,2.38,0,0,0-1.08,2.07,2.72,2.72,0,0,0,.43,1.6,4.79,4.79,0,0,0,2,1.28,24.33,24.33,0,0,0,4.59,1.22q6.53,1.28,9.16,3.74a8.24,8.24,0,0,1,2.62,6.26,9.05,9.05,0,0,1-3.38,7.51q-3.37,2.67-10.33,2.66Z"/><path id="path2706-2-4-1" class="cls-1" d="M282,49.39V35.21h-3.71V27.54H282v-8h9.12v8h8.4v7.67h-8.4V47.65a4.1,4.1,0,0,0,.82,2.85,3.9,3.9,0,0,0,2.89.89,24.16,24.16,0,0,0,4.16-.72l1,8.46a31.25,31.25,0,0,1-3.64.92,21.72,21.72,0,0,1-3.9.29q-5.25,0-7.88-3.05A11.77,11.77,0,0,1,282,49.39Z"/></g></g></svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg id="svg1681" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 393.45 322.36"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="layer1"><path id="path1927-7-5-9-8-2-7-6-3-3-4-5-3-2-2-0" class="cls-1" d="M112.06,45.08c-5.26,0-8.16,0-14,5.8L5.8,143.13C0,148.91,0,151.87,0,157.11v15.32c0,5.24,0,8.19,5.77,14l57.05,57.26,28-28L40.13,164.81l79.58-79.58L170.4,136.1l28-28-57-57.18c-5.9-5.89-8.75-5.82-14-5.83Z" transform="translate(0 0)"/><path id="path1931-4-9-5-1-0-2-97-3-8-9-0-1-4-7-55" class="cls-1" d="M323,78l-28,28,58.41,58.81L273.8,244.33l-58.4-58.81-28,28,64.68,65.12c5.86,5.86,8.74,5.83,14,5.84l15.28,0c5.26,0,8.22,0,14-5.8l92.26-92.25c5.78-5.78,5.78-8.72,5.79-14V157.14c0-5.23-.11-8.3-5.74-14Z" transform="translate(0 0)"/><g id="g2384"><path id="path2388" class="cls-1" d="M336,2l15.78,15.78a6.69,6.69,0,0,1,0,9.49L58.59,320.39a6.69,6.69,0,0,1-9.49,0L33.32,304.62a6.71,6.71,0,0,1,0-9.5L326.47,2A6.69,6.69,0,0,1,336,2Z" transform="translate(0 0)"/></g></g></svg>
|
After Width: | Height: | Size: 974 B |
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-1, .cls-2, .cls-3 {
|
||||||
|
stroke: #424242;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke-width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4, .cls-3 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #349ff7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="Bottle_Background" data-name="Bottle Background">
|
||||||
|
<path class="cls-4" d="m201.08,150.74h197.85c18.7,29.99,28.62,64.62,28.62,99.96v288.15c0,27.42-22.23,49.65-49.65,49.65h-155.79c-27.42,0-49.65-22.23-49.65-49.65V250.7c0-35.34,9.92-69.97,28.62-99.96Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Bottle">
|
||||||
|
<path class="cls-1" d="m201.08,150.74l56.37-90.37h85.11l56.37,90.37c18.7,29.99,28.62,64.62,28.62,99.96v288.15c0,27.42-22.23,49.65-49.65,49.65h-155.79c-27.42,0-49.65-22.23-49.65-49.65V250.7c0-35.34,9.92-69.97,28.62-99.96Z"/>
|
||||||
|
<rect class="cls-1" x="244.14" y="19.66" width="111.72" height="40.71" rx="11.53" ry="11.53"/>
|
||||||
|
</g>
|
||||||
|
<g id="Bottle_Label" data-name="Bottle Label">
|
||||||
|
<rect class="cls-2" x="172.46" y="287.33" width="255.09" height="212.26"/>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="m198.56,322.55h17.13l11.26,30.85c1.39,4.1,2.61,8.55,4,12.82h.43c1.39-4.27,2.57-8.72,3.96-12.82l10.91-30.85h17.13v69.43h-14.29v-25.48c0-6.77,1.27-16.87,2.01-23.59h-.42l-5.7,16.43-9.82,26.6h-8.71l-9.84-26.6-5.54-16.43h-.43c.74,6.72,2.01,16.82,2.01,23.59v25.48h-14.08v-69.43Z"/>
|
||||||
|
<path class="cls-4" d="m273.34,322.55h15.7v69.43h-15.7v-69.43Z"/>
|
||||||
|
<path class="cls-4" d="m299.01,322.55h15.7v56.27h27.41v13.16h-43.1v-69.43Z"/>
|
||||||
|
<path class="cls-4" d="m347.8,322.55h15.7v28.37h.39l20.7-28.37h17.25l-21,27.71,24.86,41.72h-17.21l-16.88-29.33-8.11,10.73v18.61h-15.7v-69.43Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-4" d="m238.46,466.21c6.9-2.39,10.54-6.76,10.46-11.55l-.23-9.17,4.47,7.67c-1.36,1.15-3.09,1.72-4.97,1.72-4.32,0-8.26-2.8-8.26-7.8,0-4.69,3.82-7.8,8.57-7.8,6.1,0,9.47,4.84,9.47,12.98,0,10.17-5.87,17.96-16.7,21.24l-2.8-7.3Zm1.4-48.47c0-4.88,3.52-8.63,8.26-8.63s8.26,3.76,8.26,8.63-3.52,8.6-8.26,8.6-8.26-3.76-8.26-8.6Z"/>
|
||||||
|
<path class="cls-4" d="m273.19,460.74h44.32v7.74h-44.32v-7.74Z"/>
|
||||||
|
<path class="cls-4" d="m332.93,466.21c6.9-2.39,10.54-6.76,10.46-11.55l-.23-9.17,4.47,7.67c-1.36,1.15-3.09,1.72-4.97,1.72-4.32,0-8.26-2.8-8.26-7.8,0-4.69,3.82-7.8,8.57-7.8,6.1,0,9.47,4.84,9.47,12.98,0,10.17-5.87,17.96-16.7,21.24l-2.8-7.3Zm1.4-48.47c0-4.88,3.52-8.63,8.26-8.63s8.26,3.76,8.26,8.63-3.52,8.6-8.26,8.6-8.26-3.76-8.26-8.6Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Bottle_Level" data-name="Bottle Level">
|
||||||
|
<line class="cls-3" x1="201.08" y1="150.74" x2="398.92" y2="150.74"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,161 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
viewBox="0 0 600 600"
|
||||||
|
version="1.1"
|
||||||
|
id="svg283"
|
||||||
|
sodipodi:docname="milksad_final-01.svg"
|
||||||
|
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
||||||
|
<metadata
|
||||||
|
id="metadata287">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="2256"
|
||||||
|
inkscape:window-height="1449"
|
||||||
|
id="namedview285"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.225"
|
||||||
|
inkscape:cx="300"
|
||||||
|
inkscape:cy="300"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="55"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg283" />
|
||||||
|
<defs
|
||||||
|
id="defs239">
|
||||||
|
<style
|
||||||
|
id="style237">
|
||||||
|
.cls-1 {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #ef5eb4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #349ff7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="Carton_Background"
|
||||||
|
data-name="Carton Background"
|
||||||
|
transform="matrix(1.2618615,0,0,1.2618615,-78.194497,-79.433761)">
|
||||||
|
<polygon
|
||||||
|
class="cls-3"
|
||||||
|
points="96.63,228.11 187.24,115.42 187.24,70.08 503.37,228.11 503.37,431.48 300,533.17 96.63,431.48 "
|
||||||
|
id="polygon244" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Carton_Label"
|
||||||
|
data-name="Carton Label"
|
||||||
|
transform="matrix(1.2618615,0,0,1.2618615,-78.194497,-79.433761)">
|
||||||
|
<polygon
|
||||||
|
class="cls-5"
|
||||||
|
points="103.13,398.13 293.5,493.32 293.5,365.06 103.13,269.88 "
|
||||||
|
id="polygon247" />
|
||||||
|
<path
|
||||||
|
class="cls-4"
|
||||||
|
d="M 299.5,503.03 97.13,401.84 V 260.17 L 299.5,361.36 Z M 99.13,391.02 295.21,487.01 V 372.17 l -196.08,-96 v 114.84 z"
|
||||||
|
id="path249" />
|
||||||
|
<polygon
|
||||||
|
class="cls-1"
|
||||||
|
points="95.39,376.88 297.3,477.63 297.3,390.26 93.93,285.94 "
|
||||||
|
id="polygon251" />
|
||||||
|
<polygon
|
||||||
|
class="cls-5"
|
||||||
|
points="496.87,399.15 306.5,494.34 306.5,366.08 496.87,270.89 "
|
||||||
|
id="polygon253" />
|
||||||
|
<path
|
||||||
|
class="cls-4"
|
||||||
|
d="M 300.5,504.04 V 362.37 L 502.87,261.18 v 141.67 z m 4.25,-130.17 V 488.71 L 502.2,388.63 V 273.79 Z"
|
||||||
|
id="path255" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Carton_Edit"
|
||||||
|
data-name="Carton Edit"
|
||||||
|
transform="matrix(1.2618615,0,0,1.2618615,-78.194497,-79.433761)">
|
||||||
|
<path
|
||||||
|
class="cls-4"
|
||||||
|
d="m 509.87,241.35 c 0,-10.57 -5.97,-20.24 -15.43,-24.97 L 302.91,120.61 v 0.03 c 0,0 -106.08,-53.03 -106.08,-53.03 -7.39,-3.7 -16.09,1.68 -16.09,9.94 v 35.58 L 96.3,217.98 c -4,4.96 -6.17,11.14 -6.17,17.51 v 182.76 c 0,10.57 5.97,20.24 15.43,24.97 l 181.95,90.98 c 7.86,3.93 17.11,3.93 24.97,0 l 181.95,-90.98 c 9.46,-4.73 15.43,-14.4 15.43,-24.97 v -176.9 z m -116.3,-17.27 86.71,8.31 -156.57,78.28 69.85,-86.59 z m 3.54,-12.87 v -28.96 l 71.97,35.98 -71.97,-7.03 z m -13,-28.55 v 23.93 L 193.74,111.4 V 85.85 c 0,-2.41 2.54,-3.98 4.7,-2.9 l 179.53,89.76 c 3.77,1.88 6.14,5.73 6.14,9.94 z m -195.09,-59.08 3.65,1.83 187.82,93.91 -82.27,102.33 -191.47,-95.74 z M 103.13,420.09 V 238.63 L 293.5,333.82 V 522.66 L 109.73,430.77 c -4.05,-2.02 -6.6,-6.16 -6.6,-10.68 z M 490.27,430.77 306.5,522.66 V 333.82 l 190.37,-95.19 v 181.46 c 0,4.52 -2.56,8.66 -6.6,10.68 z"
|
||||||
|
id="path258" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Carton_Face"
|
||||||
|
data-name="Carton Face"
|
||||||
|
transform="matrix(1.2618615,0,0,1.2618615,-78.194497,-79.433761)">
|
||||||
|
<g
|
||||||
|
id="g269">
|
||||||
|
<g
|
||||||
|
id="g265">
|
||||||
|
<path
|
||||||
|
class="cls-3"
|
||||||
|
d="m 338.96,441.09 c 8.28,-2.66 12.46,-7.56 12.36,-12.67 v -11.04 l 5.21,9.3 c -1.63,1.23 -3.68,1.94 -5.93,1.94 -5.31,0 -10.22,-3.47 -10.22,-9.6 0,-5.72 4.7,-9.6 10.63,-9.6 7.56,0 11.65,5.93 11.65,15.94 0,12.06 -6.95,21.25 -20.23,24.93 z m 1.43,-52.72 c 0,-5.93 4.29,-10.63 10.22,-10.63 5.93,0 10.22,4.7 10.22,10.63 0,5.93 -4.29,10.63 -10.22,10.63 -5.93,0 -10.22,-4.7 -10.22,-10.63 z"
|
||||||
|
id="path261" />
|
||||||
|
<path
|
||||||
|
class="cls-3"
|
||||||
|
d="m 444.28,388.91 c 8.28,-2.66 12.46,-7.56 12.36,-12.67 V 365.2 l 5.21,9.3 c -1.63,1.23 -3.68,1.94 -5.93,1.94 -5.31,0 -10.22,-3.47 -10.22,-9.6 0,-5.72 4.7,-9.6 10.63,-9.6 7.56,0 11.65,5.93 11.65,15.94 0,12.06 -6.95,21.25 -20.23,24.93 z m 1.43,-52.72 c 0,-5.93 4.29,-10.63 10.22,-10.63 5.93,0 10.22,4.7 10.22,10.63 0,5.93 -4.29,10.63 -10.22,10.63 -5.93,0 -10.22,-4.7 -10.22,-10.63 z"
|
||||||
|
id="path263" />
|
||||||
|
</g>
|
||||||
|
<polygon
|
||||||
|
class="cls-3"
|
||||||
|
points="426.29,385.48 426.77,401.1 378.46,425.25 377.98,409.63 "
|
||||||
|
id="polygon267" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="Carton_Label-2"
|
||||||
|
data-name="Carton Label"
|
||||||
|
transform="matrix(1.2618615,0,0,1.2618615,-78.194497,-79.433761)">
|
||||||
|
<g
|
||||||
|
id="g280">
|
||||||
|
<path
|
||||||
|
class="cls-3"
|
||||||
|
d="m 130.7,303.88 11.65,5.83 5.21,38.18 c 0.63,5.13 1.11,10.72 1.73,16.11 l 0.25,0.13 c 0.9,-4.63 1.68,-9.59 2.58,-13.96 l 6.94,-32.1 11.65,5.83 -2.43,85.71 -9.88,-4.94 0.72,-25.32 c 0.25,-8.7 1.51,-21.58 2.26,-29.89 l -0.25,-0.13 -4,18.86 -5.87,25.82 -5.82,-2.91 -4.26,-30.88 -2.71,-22.22 -0.25,-0.13 c 0.26,8.82 0.77,22.58 0.53,31.29 l -0.72,25.32 -9.75,-4.88 2.43,-85.71 z"
|
||||||
|
id="path272" />
|
||||||
|
<path
|
||||||
|
class="cls-3"
|
||||||
|
d="m 179.83,328.45 10.89,5.45 -2.43,85.71 -10.89,-5.45 z"
|
||||||
|
id="path274" />
|
||||||
|
<path
|
||||||
|
class="cls-3"
|
||||||
|
d="m 199.84,338.45 10.89,5.45 -1.89,66.72 15.57,7.79 -0.54,18.99 -26.46,-13.23 2.43,-85.71 z"
|
||||||
|
id="path276" />
|
||||||
|
<path
|
||||||
|
class="cls-3"
|
||||||
|
d="m 233.39,355.24 10.89,5.45 -0.9,31.91 0.25,0.13 11.29,-26.72 11.9,5.95 -13.27,28.67 13.25,58.24 -11.9,-5.95 -8.18,-37.52 -4.25,9.64 -0.6,21.36 -10.89,-5.45 2.43,-85.71 z"
|
||||||
|
id="path278" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
# Only the main Sass file needs front matter (the dashes are enough)
|
||||||
|
---
|
||||||
|
|
||||||
|
@import "dark";
|
||||||
|
@import "base";
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
# Only the main Sass file needs front matter (the dashes are enough)
|
||||||
|
---
|
||||||
|
|
||||||
|
@import "light";
|
||||||
|
@import "base";
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@import "dark";
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
jekyll server --host 0.0.0.0 --watch --drafts --future
|
||||||
|
|
|
@ -0,0 +1,542 @@
|
||||||
|
---
|
||||||
|
title: /full write-up
|
||||||
|
layout: home
|
||||||
|
permalink: /disclosure.html
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# Technical write-up
|
||||||
|
|
||||||
|
This is the story of a wallet theft enabled by bad cryptography. It covers our research on problems with `Libbitcoin Explorer` `3.x` (CVE-2023-39910), outlines how it is related to the `Trust Wallet` vulnerability (CVE-2023-31290), and shows some of the real-world impact that we were able to confirm. Additionally, it has some early research on problems with `bx` `2.x` that we became aware of late in the disclosure process.
|
||||||
|
If you're looking for a less-technical summary, head over to the [summary](/) page and the [FAQs](/faq.html).
|
||||||
|
|
||||||
|
|
||||||
|
<div id="toc-container" markdown="1">
|
||||||
|
<h2 class="no_toc">Table of Contents</h2>
|
||||||
|
* placeholder
|
||||||
|
{:toc}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Part I - Tracing the Issue to the Source
|
||||||
|
|
||||||
|
Please note that throughout this article, minor details relating to the victims have been omitted or changed.
|
||||||
|
### Dude, Where's my Cryptocurrency?
|
||||||
|
|
||||||
|
Our story starts on Friday, 21 July 2023. Upon attempting to use a well-protected cryptocurrency wallet, the wallet owner realizes that all of their funds stored in their wallet are gone.
|
||||||
|
This was no accident -- they were the victim of a sophisticated theft. The funds were sent to the attacker's addresses on July 12th, at a time when the hardware wallet wasn't in use for several days. (Details below)
|
||||||
|
|
||||||
|
The generation and use of the affected wallet was unusually strict:
|
||||||
|
* Generated on an [air-gapped](https://en.wikipedia.org/wiki/Air_gap_(networking)) Linux laptop with self-compiled software
|
||||||
|
* Use of BIP39 24 mnemonic word phrase
|
||||||
|
* Mnemonic securely entered into Ledger & Trezor hardware wallets
|
||||||
|
* Good PIN and physical security on the hardware wallets
|
||||||
|
* Mnemonic seed phrase never touched a non-air-gapped computer
|
||||||
|
* Mnemonic seed backup well-protected
|
||||||
|
|
||||||
|
|
||||||
|
### Dude, Where's my Friend's Cryptocurrency?
|
||||||
|
|
||||||
|
The victim reached out to their network of friends with similar key generation and management protocols, and a second victim was identified! The second victim also had the contents of their cryptocurrency wallet stolen during the same period of time -- both victims Bitcoin (BTC) was stolen in the same _minute_ on-chain. The victims realized this was no accident. They had fallen victim to a some type of hack.
|
||||||
|
|
||||||
|
The victims discovered their Bitcoin (BTC) holdings were not the only things stolen. The attackers had also taken Ethereum and other distinct cryptocurrency types from the same wallets. The victims realized this could only happen with an underlying leak of their main wallet private keys. Tricking their hardware wallets into authorizing incorrect transfers or breaking individual private keys of sub-accounts would manifest with a more limited impact.
|
||||||
|
|
||||||
|
A theft like this affecting two people at once despite their thorough precautions should be very unlikely. Even worse, the two victims weren't the only ones affected by this. The publicly visible Bitcoin transactions of the theft pull in funds from what looks like many different wallets, possibly by up to a thousand different wallet owners on Bitcoin alone.
|
||||||
|
|
||||||
|
So, what in the world is going on!? Had someone found a remotely exploitable hardware wallet vulnerability, used it on a wide scale, and waited for months before executing the on-chain sweeping transactions collectively? Even worse, could one of the underlying cryptographic primitives be broken? Could Quantum Computer magic be involved? 😱
|
||||||
|
|
||||||
|
Tensions were running high - thus began the search for the source of compromise.
|
||||||
|
|
||||||
|
### Our Cryptocurrency is Gone, But How!?!?
|
||||||
|
|
||||||
|
After coordination and communication, the two victims realized that their affected wallets were generated on a similar airgap laptop setup -- although the individual victims' wallets were generated several years apart. At that point, the issue seemed hard to pin down and could have originated from many sources. Our victims decided to start at the beginning -- their wallet generation steps, from the first commands used and working their way up from there.
|
||||||
|
|
||||||
|
An essential tool that was involved in the wallet creation in both cases was the [Libbitcoin Explorer](https://github.com/libbitcoin/libbitcoin-explorer/tree/version3) in a 3.x version, via its `bx` binary. The Libbitcoin project has been around for a very long time (2011 !), is Open Source, and `bx` brings everything needed for an offline wallet generation in one self-contained binary.
|
||||||
|
|
||||||
|
Despite being a specialized tool that most wallet users won't have heard of, `bx` has some popularity and is dedicated an [appendix section](https://github.com/bitcoinbook/bitcoinbook/blob/97df56f77c06813b1e028b5b1f2dbc036f27b1fc/appdx-bx.asciidoc) in the "Mastering Bitcoin" book. In other words, it appeared to be a reasonable tool to use.
|
||||||
|
|
||||||
|
Brief example of the wallet generation workflow used in a Linux shell:
|
||||||
|
```
|
||||||
|
# generate 256 bits of entropy, turn it into BIP39 mnemonics
|
||||||
|
bx seed -b 256 | bx mnemonic-new
|
||||||
|
<output of secret BIP39 mnemonic words>
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command produces a 24 word BIP39 mnemonic phrase comparable to those of the victims' wallet. This private key is the foundation of all wallet related security.
|
||||||
|
|
||||||
|
Could the `bx` binary command the victims used have something to do with the problem? The victims ensured that their `/dev/random` Random Number Generator (RNG) subsystem of the Linux laptops had sufficient entropy, but perhaps that wasn't sufficient after all ..? Was there a major system configuration issue or virus?
|
||||||
|
|
||||||
|
At this point in time, a handful of friends with Information Security backgrounds were called in to help review the situation and the relevant wallet generation code paths 🕵️♂️.
|
||||||
|
|
||||||
|
As more eyes settle on the situation, the first signs of a major problem emerge.
|
||||||
|
|
||||||
|
#### Given Enough Eyes, All Bugs are Shallow?
|
||||||
|
|
||||||
|
The team decided that source code review of `bx` of the `bx seed` command is the logical place to start looking.
|
||||||
|
|
||||||
|
Running `bx seed` calls the `new_seed(size_t bit_length)` function in libbitcoin-explorer [src/utility.cpp](
|
||||||
|
https://github.com/libbitcoin/libbitcoin-explorer/blob/20eba4db9a8a3476949d6fd08a589abda7fde3e3/src/utility.cpp#L78), which calls a `pseudo_random_fill(data_chunk& out)` function in the [libbitcoin-system](https://github.com/libbitcoin/libbitcoin-system/blob/a1b777fc51d9c04e0c7a1dec5cc746b82a6afe64/src/crypto/pseudo_random.cpp#L35) library:
|
||||||
|
|
||||||
|
```c
|
||||||
|
console_result seed::invoke(std::ostream& output, std::ostream& error)
|
||||||
|
{
|
||||||
|
const auto bit_length = get_bit_length_option();
|
||||||
|
|
||||||
|
// These are soft requirements for security and rationality.
|
||||||
|
// We use bit vs. byte length input as the more familiar convention.
|
||||||
|
if (bit_length < minimum_seed_size * byte_bits ||
|
||||||
|
bit_length % byte_bits != 0)
|
||||||
|
{
|
||||||
|
error << BX_SEED_BIT_LENGTH_UNSUPPORTED << std::endl;
|
||||||
|
return console_result::failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto seed = new_seed(bit_length);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```c
|
||||||
|
data_chunk new_seed(size_t bit_length)
|
||||||
|
{
|
||||||
|
size_t fill_seed_size = bit_length / byte_bits;
|
||||||
|
data_chunk seed(fill_seed_size);
|
||||||
|
pseudo_random_fill(seed);
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only _pseudo_-random? Alright, a pseudo-random number generator (PRNG) doesn't have to be bad if it's a Cryptographically Secure Pseudo Random Number Generator ([CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator)). Perhaps everything is fine, but let's take a closer look.
|
||||||
|
|
||||||
|
We follow the call path:
|
||||||
|
`pseudo_random::fill(data_chunk& out)` -> `pseudo_random::next()` -> `pseudo_random::next(uint8_t begin, uint8_t end)` -> `std::mt19937& pseudo_random::get_twister()`
|
||||||
|
|
||||||
|
Wait a moment. `mt19937`, `twister` - this uses the [Mersenne Twister](https://en.wikipedia.org/wiki/Mersenne_Twister) PRNG? 🤔
|
||||||
|
At this point, the first alarm bells are going off. Mersenne Twister is not a CSPRNG, so it shouldn't be in any code path that generates secrets. One alarming property of the Mersenne Twister is that its internal state can be reversed by an attacker who knows a few hundred outputs, endangering the secrecy of the other outputs of the same stream that the attacker doesn't know (in simplified terms).
|
||||||
|
|
||||||
|
However, if the PRNG is re-seeded before every wallet generation, only one output is fetched, and the single result is kept secret, would the weak construction of MT19937 be fatal enough for a remote theft if everything else is done well?
|
||||||
|
|
||||||
|
The search within this the `pseudo_random.cpp` code file continues, and we don't have to go much further into the `pseudo_random::get_twister()` details to learn the actual problem.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Use the clock for seeding.
|
||||||
|
const auto get_clock_seed = []() NOEXCEPT
|
||||||
|
{
|
||||||
|
const auto now = high_resolution_clock::now();
|
||||||
|
return static_cast<uint32_t>(now.time_since_epoch().count());
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is thread safe because the instance is thread static.
|
||||||
|
if (twister.get() == nullptr)
|
||||||
|
{
|
||||||
|
// Seed with high resolution clock.
|
||||||
|
twister.reset(new std::mt19937(get_clock_seed()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What the hell !? A bad PRNG algorithm, seeded with only _32 bit_ of _system time_, used to generate long-lived wallet private keys that store cryptocurrency? 😧
|
||||||
|
|
||||||
|
The group of investigators had to perform a 'double-take' here -- surely `bx` couldn't use this to generate private keys.
|
||||||
|
|
||||||
|
The team investigating the compromise could not believe this to be the case, and setup a simple experiment to validate the hypothesis.
|
||||||
|
Like all good experiments, environmental variables were under the control of the experimenters, in this case -- it was the variable `time` itself that was most relevant in studying the issue at hand.
|
||||||
|
Using the official `bx` version `3.2.0` release binary in combination with the `libfaketime` library to test our theory, and run separate executions under _exactly identical clock_ conditions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ wget https://github.com/libbitcoin/libbitcoin-explorer/releases/download/v3.2.0/bx-linux-x64-qrcode -O ~/.local/bin/bx
|
||||||
|
$ chmod +x ~/.local/bin/bx
|
||||||
|
$ sudo apt install libfaketime
|
||||||
|
$ export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1 FAKETIME_FMT=%s FAKETIME=0
|
||||||
|
|
||||||
|
$ bx seed -b 256 | bx mnemonic-new
|
||||||
|
milk sad wage cup reward umbrella raven visa give list decorate bulb gold raise twenty fly manual stand float super gentle climb fold park
|
||||||
|
|
||||||
|
$ bx seed -b 256 | bx mnemonic-new
|
||||||
|
milk sad wage cup reward umbrella raven visa give list decorate bulb gold raise twenty fly manual stand float super gentle climb fold park
|
||||||
|
```
|
||||||
|
|
||||||
|
💥 Same time - same "random" wallet! Unbelievable.
|
||||||
|
|
||||||
|
A secure and reliable utility would not derive the same mnemonic seed phrase under these circumstances. This was the first hard evidence that the `bx seed` secret generation code in an official release uses the broken time-based pseudorandom function for wallet entropy.
|
||||||
|
|
||||||
|
Digging deeper, the team confirmed that the code uses the standard MT19937 Mersenne Twister PRNG variant which only operates on 32 bits of initial seeding input by design, and not the MT19937-64 extended variant with 64 bits of seeding. So the PRNG can at most have 2^32 starting positions as an upper bound, regardless if it's seeded by `/dev/random` or time.
|
||||||
|
|
||||||
|
To put this in different words: when running `bx seed -b 256` to request 256 bits of non-guessable entropy, the result is 32 bits of high-precision clock time that was put through a blender (or rather: twister 🌪️) and expanded to 256 bit without adding new information. The number of possible key variations would grow exponentially with the size if this were _real_ entropy data, so the difference from the safe expected result (256 bits) and the actual result (32 bits) is of astronomical proportions.
|
||||||
|
|
||||||
|
Anyone can re-compute and find a victim's originally used entropy after a maximum of about 4.29 billion attempts if they have specific characteristics to look for to see if they successfully found a cryptocurrency wallet. In this case, by checking derived wallet addresses that were seen receiving funds on a public blockchain in the past. To put this number into perspective: brute-forcing this key space takes a few days of computation on the average gaming PC, at most. And unfortunately, anyone with sufficient programming skills could do it.
|
||||||
|
|
||||||
|
In terms of cryptocurrency wallet security, this is a pretty catastrophic situation.
|
||||||
|
|
||||||
|
Friday 21 July ends with a handful people who know that everything just got very complicated. Not only did some friends irrevocably lose complete control over their wallet private keys and funds - the ad-hoc group also has a serious disclosure problem on their hands.
|
||||||
|
|
||||||
|
Given that the theft was first identified in the US, early team members wanted to avoid failing to report a crime and get appropriate tax loss reporting for victims, and so an early disclosure of our findings to the FBI was made. We also saw this could be helpful in quickly limiting the fund movements of attackers on major exchanges, if that became necessary.
|
||||||
|
|
||||||
|
## Part II - Context and Impact
|
||||||
|
|
||||||
|
For this section, we'll switch to a non-chronological presentation for readability.
|
||||||
|
|
||||||
|
### Codename "Milk Sad"
|
||||||
|
Faced with the unexpected task of handling an urgent disclosure, we were in need of a project name that was relevant yet told outsiders nothing about the technical issue. The suggestion was made to use the first two words of first BIP39 mnemonic secret generated by `bx` on time zero - `milk sad wage cup reward [...]` -> **Milk Sad**. We found our project name.
|
||||||
|
<img src="/assets/base/milksad_transparent.svg" width="10%"/>
|
||||||
|
|
||||||
|
|
||||||
|
### Not the First Hack: Weak entropy in Cake Wallet
|
||||||
|
|
||||||
|
Very early in our evaluation of the possible flaws that could lead to an issue like this, one of our team members recalled a weak entropy situation in Cake Wallet.
|
||||||
|
|
||||||
|
While no CVE was filed for Cake Wallet as far as we can tell, in May 2021 the Cake wallet team [announced a weak entropy vulnerability in their wallets](https://old.reddit.com/r/Monero/comments/n9yypd/urgent_action_needed_for_bitcoin_wallets_cake/) requiring all users to rotate their mnemonic phrases.
|
||||||
|
|
||||||
|
Some [further investigation](https://old.reddit.com/r/RNG/comments/naljww/critical_rng_flaw_in_cake_walletcryptocurrency/) into this situation by Reddit user Pure-Cricket7485 revealed the glaring flaws in Cake Wallet's entropy sourcing strategy.
|
||||||
|
|
||||||
|
Notably: Cake Wallet [Used Dart's non-secure Random() to generate wallet seeds](https://github.com/cake-tech/cake_wallet/blob/b67bb0664f7268c31c24bd9fb9cbd438c691f5e3/lib/bitcoin/bitcoin_mnemonic.dart#L11-L2):
|
||||||
|
|
||||||
|
```c++
|
||||||
|
Uint8List randomBytes(int length, {bool secure = false}) {
|
||||||
|
assert(length > 0);
|
||||||
|
final random = secure ? Random.secure() : Random();
|
||||||
|
final ret = Uint8List(length);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
ret[i] = random.nextInt(256);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be a real problem considering Dart's Random() [can fall back to 0 or system time](https://github.com/dart-lang/sdk/blob/master/runtime/vm/random.cc#L17-L33)
|
||||||
|
|
||||||
|
```c++
|
||||||
|
Random::Random() {
|
||||||
|
uint64_t seed = FLAG_random_seed;
|
||||||
|
if (seed == 0) {
|
||||||
|
Dart_EntropySource callback = Dart::entropy_source_callback();
|
||||||
|
if (callback != nullptr) {
|
||||||
|
if (!callback(reinterpret_cast<uint8_t*>(&seed), sizeof(seed))) {
|
||||||
|
// Callback failed. Reset the seed to 0.
|
||||||
|
seed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seed == 0) {
|
||||||
|
// We did not get a seed so far. As a fallback we do use the current time.
|
||||||
|
seed = OS::GetCurrentTimeMicros();
|
||||||
|
}
|
||||||
|
Initialize(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
One Reddit user compared this approach to Mersenne Twister, which got us considering if bx had a similar flaw.
|
||||||
|
|
||||||
|
### Not even the second Hack: Mersenne Twister use in Trust Wallet
|
||||||
|
A few days into our disclosure process, we learned of the [Trust Wallet vulnerability](https://community.trustwallet.com/t/browser-extension-wasm-vulnerability-postmortem/750787) that Trust Wallet published in late April 2023. After reading Ledger Donjon's [excellent write-up](https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been-stolen/) on the discovery and research of [CVE-2023-31290](https://nvd.nist.gov/vuln/detail/CVE-2023-31290), we're somewhat shocked: this is _so_ close to the vulnerability in `bx`. And the Trust Wallet publication confirms that attacks on their user's wallets go back as far as December 2022. Why did it take until mid-2023 for money to disappear from `bx`-generated wallets?
|
||||||
|
|
||||||
|
There's a lot to unpack here, so let's start with the vulnerability details.
|
||||||
|
|
||||||
|
The vulnerable Trust Wallet version and `bx` share the same basic, fatal flaw of generating wallet entropy from the unsuited MT19937 Mersenne Twister algorithm. `bx` additionally uses some clock information to seed MT19937, but this doesn't make a big difference to practical offline brute-force attacks by remote attackers. It's still the same limited 32 bits of key space that attackers can comb through completely.
|
||||||
|
|
||||||
|
From what we know, Trust Wallet only creates 12 word BIP39 mnemonic based wallets, which fixes the entropy requirements (and therefore PRNG usage) to 128 bit. `bx` more is flexible and generates 128 bit, 192 bit, 256 bit outputs (and others). More variations means more search space, but do the wallets generated with `bx seed -b 128` overlap with the ones created by Trust wallet?
|
||||||
|
|
||||||
|
As it turns out, they do not - due to a nuance in the PRNG usage. The MT19937 Mersenne Twister algorithm used by Trust Wallet and `bx` is the same, but the output is consumed slightly differently.
|
||||||
|
|
||||||
|
When filling the entropy array with PRNG data, Trust Wallet consumes one MT19937 output of 32 bits, takes the least significant 8 bits of this output to fill the array, and throws away the other 24 bits via the `& 0x000000ff` bitwise-and in [wasm/src/Random.cpp](https://github.com/trustwallet/wallet-core/blob/69b2da9826dfdeb8116be1e5a3747b3e9418592d/wasm/src/Random.cpp#L21):
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// Copyright © 2017-2022 Trust Wallet.
|
||||||
|
//
|
||||||
|
|
||||||
|
[...]
|
||||||
|
|
||||||
|
void random_buffer(uint8_t* buf, size_t len) {
|
||||||
|
std::mt19937 rng(std::random_device{}());
|
||||||
|
std::generate_n(buf, len, [&rng]() -> uint8_t { return rng() & 0x000000ff; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Due to the C++ mechanisms used in `libbitcoin-system` for `pseudo_random::fill()`, its code performs the same 32 bits -> 8 bits truncation, but exactly the other way around - taking the 8 most-significant bits from the PRNG instead.
|
||||||
|
|
||||||
|
As a result, all generated `bx seed` outputs are - to our knowledge - in a completely different key space than what the Trust Wallet code would generate, which means the BIP39 mnemonics are also different.
|
||||||
|
|
||||||
|
To confirm this, here's a quick experiment. The Ledger Donjon write-up contains a specific example wallet that they describe as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
[...]
|
||||||
|
RNG seed: 0x8ec170a8
|
||||||
|
Mnemonic:
|
||||||
|
sorry slush already pass garden decade grid drip machine cradle call put
|
||||||
|
[...]
|
||||||
|
```
|
||||||
|
|
||||||
|
If we re-compute RNG seed `0x8ec170a8` with the `bx` code for a 12 word BIP39 mnemonic, it comes out as the following instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
local chef load churn future essence type leave program weird ancient owner
|
||||||
|
```
|
||||||
|
|
||||||
|
This confirms that the the wallet generation process is indeed different.
|
||||||
|
|
||||||
|
|
||||||
|
In Ledger Donjon's write-up, they close with the following line, which reads a lot like foreshadowing to us:
|
||||||
|
|
||||||
|
> During our investigations, we also noticed that a few addresses were vulnerable while they had been generated a long time before the Trust Wallet release. That probably means this vulnerability exists in some other wallet implementations which is concerning…
|
||||||
|
|
||||||
|
However, we suspect the other wallets seen by Ledger Donjon weren't wallets generated by `bx` `3.x` versions, unless Ledger Donjon experimented a lot with the PRNG outputs and hit this other usage pattern. The presence of more affected wallets in the 12-word Trust Wallet range suggests there are yet other affected wallet generation tools out there which use MT19937, though. Due to time constraints, we did not investigate this further for the time being.
|
||||||
|
|
||||||
|
When comparing our disclosure task on `bx` with that of Ledger on Trust Wallet, it's notable that their disclosure was to a single, commercial, evidently well-funded wallet vendor. Based on their publications, Trust Wallet had direct communication paths to individual users that they could leverage to selectively inform them of vulnerabilities related to their wallet. Additionally, their wallet was only vulnerable for a limited amount of time, and the issue was discovered relatively quickly, with many users still regularly using the app.
|
||||||
|
|
||||||
|
In their [post-mortem](https://community.trustwallet.com/t/browser-extension-wasm-vulnerability-postmortem/750787), the Trust Wallet team lists the total lost funds as $170,000 USD:
|
||||||
|
> Despite our best efforts, two exploits occurred, resulting in a total loss of approximately $170,000 USD at the time of the attack.
|
||||||
|
|
||||||
|
As we understand, Trust Wallet decided to financially incentivize users to move their funds to safety, as well as [reimbursing lost funds](https://community.trustwallet.com/t/reimbursement-process-for-lost-funds-due-to-hacks-from-browser-extension-wasm-vulnerability/750788) for users affected by the theft. Additionally, they paid a significant $100,000 USD bug bounty to Ledger Donjon for the coordinated disclosure.
|
||||||
|
|
||||||
|
In our case, unfortunately, most of these factors did not apply. Given the Libbitcoin project's noncommercial nature, lack of direct communication channels to end users, multiple years of affected wallets and likely widely imported/exported keys and BIP39 mnemonics, the situation was much more difficult on the security researcher side.
|
||||||
|
|
||||||
|
With the Trust Wallet & Ledger Donjon write-ups out there, all `bx seed` private keys compromised, and active on-chain exploitation for `bx` wallets ongoing, it was clear to us that a long coordinated disclosure was not beneficial. If we wanted to give affected `bx` wallet owners the _chance_ to recover their funds, we would have to aim for a publication in days, not months. In this situation, time is on the side of the attackers rather than the victims.
|
||||||
|
|
||||||
|
Well-established security programs like Google Project Zero [have a similar policy](https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html) for actively exploited vulnerabilities:
|
||||||
|
> [...] if Project Zero finds evidence that a vulnerability is being actively exploited against real users "in the wild", a 7-day disclosure policy replaces the 90-day policy. [...]
|
||||||
|
|
||||||
|
### Ongoing On-Chain Thefts - Some Facts
|
||||||
|
|
||||||
|
We decided to focus on Bitcoin as the main coin network for a limited analysis considering our time constraints. Due to the flexibility of `bx seed` output, funds on every BIP39 mnemonic (or even just BIP32 seed) compatible coin under the sun could be affected. With regular day jobs to take care of and limited time & compute resources, doing various steps 100x times wasn't an option. This overview is therefore narrowly focused on Bitcoin, and only presents a partial view of the overall theft actions.
|
||||||
|
Consider the presented figures a lower bound for the overall affected monetary value.
|
||||||
|
|
||||||
|
For Bitcoin, there are two main clusters of transactions that we think were malicious:
|
||||||
|
|
||||||
|
1.) The big 2023-07-12 theft:
|
||||||
|
|
||||||
|
| date | transaction | destination address | moved value | note |
|
||||||
|
|--|--|--|--|--|
|
||||||
|
| 2023-07-12 10:41 | [593e11588a2529ed..](https://mempool.space/tx/593e11588a2529ed26b169e71d46c045eedb6037822641c5fba08c1c34a1c04a) | [3GMQRwh8Yz1WVftL..](https://mempool.space/address/3GMQRwh8Yz1WVftLPiFr9PoYdqrizHcA9R) | ~5.0538 | 14 inputs |
|
||||||
|
| 2023-07-12 10:41 | [81cfe97cc16a4939..](https://mempool.space/tx/81cfe97cc16a49398d6986032ec8f6970ea80df5aa0990dcf0164de87136f5bf) | [3LwDzjA1xH8amCHu..](https://mempool.space/address/3LwDzjA1xH8amCHuvU9YjWST6rsyfPmvmU) | ~9.744 BTC | > 300 inputs |
|
||||||
|
| 2023-07-12 10:41 | [a22b33a9a4ca0de2..](https://mempool.space/tx/a22b33a9a4ca0de2f56ef166298c186c5d71e56b944a255c2ecc52748f8f774b) | [3D2mKf28exn26v7B..](https://mempool.space/address/3D2mKf28exn26v7BCVe9AXrrg4BY7qvYcv) | ~14.847 BTC | > 1200 inputs |
|
||||||
|
|
||||||
|
We attribute these to one well-prepared actor, who stole around ~29.65 BTC worth upwards of $850,000 USD in Bitcoin alone (8/2023 exchange rate). We think it's likely this actor also carried out theft of other coins in the same day. At the time of writing, these funds have not moved away from their new location.
|
||||||
|
|
||||||
|
2.) There is a second, earlier pattern of fund movements that we suspect are also thefts:
|
||||||
|
|
||||||
|
This behavior started May 3rd 2023 and consisted of multiple smaller wallet sweeps which continued until July 15th. In total, we think this sweeping sums up to about 0.33 BTC. The funds have been moved again to further addresses, which differentiates this actor pattern from the first we outlined.
|
||||||
|
|
||||||
|
We have not seen any reports of stolen funds for these addresses. Theoretically, this special actor may be legitimately moving his funds from many distinct wallets (see further analysis) or he is a malicious actor. The addresses reported below are linked by being spent in the same transaction, which indicates that this a single actor or group.
|
||||||
|
|
||||||
|
Suspected Attacker Addresses:
|
||||||
|
- [bc1qdmpx2th8h7l4j0z93sxnrlpuaxfvkkfxlv7n2c](https://mempool.space/address/bc1qdmpx2th8h7l4j0z93sxnrlpuaxfvkkfxlv7n2c) (0.1382 BTC)
|
||||||
|
- [bc1qpnq4q7dcgvpuz8z9dy3d3jp4vuumw0hte4d32n](https://mempool.space/address/bc1qpnq4q7dcgvpuz8z9dy3d3jp4vuumw0hte4d32n) (0.0700 BTC)
|
||||||
|
- [bc1qf9q85mt73sr0vzlqkvy75pyal3g5w2ca7zx3cv](https://mempool.space/address/bc1qf9q85mt73sr0vzlqkvy75pyal3g5w2ca7zx3cv) (0.0045 BTC)
|
||||||
|
- [bc1q5rcm7gcl3n50q93xcwwz5una7xy89unlqhy075](https://mempool.space/address/bc1q5rcm7gcl3n50q93xcwwz5una7xy89unlqhy075) (0.1147 BTC)
|
||||||
|
|
||||||
|
|
||||||
|
Here's a list with some known coins with confirmed thefts (before the publication of this disclosure):
|
||||||
|
* Bitcoin (BTC)
|
||||||
|
* Ethereum (ETH)
|
||||||
|
* Ripple (XRP)
|
||||||
|
* Dogecoin (DOGE)
|
||||||
|
* Solana (SOL)
|
||||||
|
* Litecoin (LTC)
|
||||||
|
* Bitcoin Cash (BCH)
|
||||||
|
* Zcash (ZEC)
|
||||||
|
|
||||||
|
There are likely many more coin types involved, as the additional cost to execute this attack on other coins for the affected wallets is limited to the R&D time required for identification and funds withdrawal. We hope that attackers have some increased risk of getting caught via coin-specific tracing of their identities and methods, but do not have anything to report there.
|
||||||
|
|
||||||
|
Overall, our estimation is that over **$900,000 USD** worth of cryptocurrency assets were moved as part of the overall theft actions (@exchange rate 8/2023), although some of the drained wallets may have been taken over via other vulnerabilities.
|
||||||
|
|
||||||
|
### Searching for Wallets - Motivation and Limits
|
||||||
|
|
||||||
|
The primary reason for rapidly searching for affected Bitcoin wallets was to assess the overall damage in terms of magnitude of stolen & remaining funds, as well as to provide technical confirmation that the vulnerability was real and the sole explanation for the initially analyzed theft. We had some hope that if we came across any substantial remaining funds, there would be a small but non-zero chance to somehow _inform_ the rightful owner so that they could save their funds. In the planning phase, we were considering options to relay a warning message to the wallet owner via a centralized cryptocurrency exchange used for direct deposits, finding wallet owners with publicly listed addresses, or discovering some other back channels. At the very least, we could extend the disclosure time scheduled in that case while searching for options.
|
||||||
|
|
||||||
|
Ultimately **we did not identify** any substantial remaining funds above a threshold of $5000 on the Bitcoin network in the analyzed ranges related to wallets generated with `bx` `3.x` versions, as of the disclosure publication date. It's plausible that larger sums of funds remain on similarly affected wallets of one sort of another, with slightly different derivation paths, but we did not find them with our analysis steps so far. It is also possible that some wallet owners use additional BIP39 passphrases, which provide moderate to strong protections depending on passphrase strength and other factors and make discovery much harder.
|
||||||
|
|
||||||
|
We want to be very clear on the scope of our research-related actions:
|
||||||
|
1. The two originally described victims managed to recover a small portion of their own wallet contents that had not yet been stolen, using their regular wallet setup they had before the theft.
|
||||||
|
2. We did not move any funds on any of the unknown victim wallets, on any coin.
|
||||||
|
3. We do not know or are in any way associated with the attacker(s) that stole the funds.
|
||||||
|
|
||||||
|
**In summary, none of the group members moved any funds that they were not the legal owner of, or knows the identities of the thiefs.**
|
||||||
|
|
||||||
|
As far as we're aware, even under ideal hypothetical circumstances, moving other people's funds to avoid theft by bad actors can become a legal nightmare. Even in the most ideal scenario -- where there is a single victim, clear ownership proofs, well-established identities on both sides, single jurisdiction and reliable private communication options - this carries a lot of risk.
|
||||||
|
|
||||||
|
In our situation, the circumstances were substantially worse than the ideal case in most ways we could think of. Therefore, we saw any scenario that involved us moving anything of any value, which we didn't own before the disclosure, as opening the floodgates of legal problems. (Remember that we're not lawyers, and this is not legal advice; We're also not finance folks, so this is also NOT financial advice)
|
||||||
|
|
||||||
|
For the adventurous reader, who would consider going ahead anyway as a "white knight" in this case, please consider the following dilemma: any attacker can recompute the owner's private keys, which nullifies the value of any cryptographic signatures with those keys. In any scenario that involves moving a wallet owners's funds away to a wallet you control, how do you know the person showing up in your inbox (or on your doorstep) asking to get back "their" funds is the legal owner? Particularly if there is no centralized entity, such as a big cryptocurrency exchange, that backs the claim and can provide circumstantial evidence?
|
||||||
|
|
||||||
|
That's a tough situation! And this dilemma doesn't even touch other problems such as complying with anti-money-laundering (AML) laws, tax declarations, and a host of other aspects.
|
||||||
|
|
||||||
|
In case you are one of the affected victims of the theft: **no, we definitely do not have your funds** or know of a way to get them back. We're sorry.
|
||||||
|
|
||||||
|
If it's any consolation, we've worked hard to give our affected friends and all other victims at least some explanation and closure on why any funds could be stolen in the first place, and help them to avoid getting burned again by the same vulnerability.
|
||||||
|
|
||||||
|
You may use the [lookup service](/lookup.html) to check if your wallet was impacted.
|
||||||
|
|
||||||
|
### Searching for Wallets - Implementation
|
||||||
|
|
||||||
|
Please note that we're intentionally vague on some non-trivial technical details, since we need to make an ethical trade-off between sharing the technical details that are needed to shine a light on this problem, and giving bad actors an unnecessary technical advantage on day 1. Additionally, while we are big fans of Open Source, we will not release custom code we wrote for parts of the on-chain analysis at this time for similar reasons.
|
||||||
|
|
||||||
|
For our exploratory research, we focused on the BIP44, BIP49 and BIP84 standard address formats and derivation paths. Our primary focus was on finding both formerly used as well as currently used Bitcoin wallets generated by `bx seed | bx mnemonic-new` on `bx` `3.x` versions with common BIP39 mnemonic word phrase lengths and settings.
|
||||||
|
|
||||||
|
Specifically, this covers:
|
||||||
|
* 128 bits = 12 words, `bx seed -b 128`
|
||||||
|
* 192 bits = 18 words, `bx seed -b 192` (default)
|
||||||
|
* 256 bits = 24 words, `bx seed -b 256`
|
||||||
|
|
||||||
|
We used a publicly available list of all Bitcoin addresses historically seen by the Bitcoin network and constructed a bloom filter with a very low false positive rate on the data set. Using this filter, we were able to do quick address lookups to query and discard many unused wallet candidates, for which the relevant derived accounts were never seen by the network, without doing costly lookups to a Bitcoin full node.
|
||||||
|
|
||||||
|
We only consider wallets where the first address was used. The standard mandates to scan the first 20 addresses, but computing addresses is a bottle-neck in our search. We also only consider wallets using the standard derivation paths specified by BIP44, BIP49, and BIP84. In total, we discovered over **2600** distinct and actively used Bitcoin cryptocurrency wallets that are based on the bad `bx seed` entropy.
|
||||||
|
|
||||||
|
The majority of these wallets, a group of over **2550**, has an oddly similar usage pattern with small deposits around the same dates in 2018. We think this is the result of some automatic tool use of `bx`, and that these wallets may actually share the same owner. We're not sure what this experiment was about, but they're all in the 256 bit seed output range and have a BIP49 address type ('3' prefix), which helps distinguishing them a bit from other addresses.
|
||||||
|
|
||||||
|
Excluding this large number of special wallets, we've identified less than **50** wallets with more individual usage patterns that we associate with likely use by human wallet owners. Those are distributed across the mentioned ranges and address types. We know this survey to be incomplete, as we have not discovered all wallets involved in the sweeps we observed on the blockchain.
|
||||||
|
|
||||||
|
Within this set of wallets, we were able to find and identify both of the initial victim's wallets. This gives us confidence that our research into the the explanation for the theft is correct.
|
||||||
|
|
||||||
|
A decent portion of the discovered individually used wallets did not have any BTC funds on them since before 2023, so no money could be moved away from them by the attackers. However, we still consider them fully affected for two reasons:
|
||||||
|
1. Any new deposits to them are at risk of immediate, automated theft (as outlined in the Ledger Donjon article).
|
||||||
|
2. Access to the underlying wallet private keys allows reconstructing all derived addresses on all coins, linking the wallet owner's previous actions on all of them, a significant privacy impact.
|
||||||
|
|
||||||
|
### Other Confirmed Victims of this Theft
|
||||||
|
|
||||||
|
Within a few days after the large theft of July 12th 2023, a victim of the theft came forward on Reddit:
|
||||||
|
1. [https://www.reddit.com/r/Bitcoin/comments/158nyuo/mass_hacking_of_over_1000_bitcoin_accounts/](https://www.reddit.com/r/Bitcoin/comments/158nyuo/mass_hacking_of_over_1000_bitcoin_accounts/)
|
||||||
|
|
||||||
|
The Reddit user [u/0n0t0le](https://www.reddit.com/user/0n0t0le/) provides an interesting data point, since he lost ~0.25BTC but was able to move away and recover over 1.05 BTC that the attackers didn't find or take at that point.
|
||||||
|
|
||||||
|
We were able to confirm for case 1) that the user had their funds in a vulnerable wallet in the `bx seed` ranges.
|
||||||
|
|
||||||
|
Please note that BIP39 mnemonic secrets as well as other wallet private keys typically do not contain any clear indication on the software used to generate them. As a format designed to be exportable and importable between different wallet software, it is therefore possible that users do not clearly or correctly recall where a given wallet was created. This can lead to a lot of confusing information on potentially affected other wallet software, and makes fact-finding harder.
|
||||||
|
|
||||||
|
In the interest of correctness, we're currently not listing other open leads that we're following up on, but may update this section at a later point in time.
|
||||||
|
|
||||||
|
### Basic Timeline of Thefts and Our Disclosure
|
||||||
|
|
||||||
|
| Date | Event |
|
||||||
|
| --- | --- |
|
||||||
|
| 2022-11-17 | Ledger Donjon discloses Trust Wallet vulnerability to Binance |
|
||||||
|
| 2022-11-21 | Trust Wallet code patch on GitHub publicly removes Mersenne Twister usage |
|
||||||
|
| 2022-11-21+ | Trust Wallet selectively notifies affected users of the vulnerability |
|
||||||
|
| 2022-12-? | Vulnerable Trust Wallet wallets exploited on-chain (according to vendor) |
|
||||||
|
| 2023-03-? | Vulnerable Trust Wallet wallets exploited on-chain again (according to vendor) |
|
||||||
|
| 2023-04-22 | Trust Wallet publicly discloses Trust Wallet vulnerability |
|
||||||
|
| 2023-04-25 | Ledger Donjon publishes their Trust Wallet vulnerability write-up |
|
||||||
|
| 2023-05-03 | First potential exploitation of `bx` `3.x` wallets |
|
||||||
|
| 2023-07-12 | Main exploitation of `bx` wallets |
|
||||||
|
| 2023-07-21 | We discover the `bx` vulnerability during incident response analysis |
|
||||||
|
| 2023-07-22 | We send initial email to establish communications with the Libbitcoin team |
|
||||||
|
| 2023-07-25 | First Libbitcoin team response, indicating team is too busy for contact |
|
||||||
|
| 2023-07-25 | We send context information without vulnerability details to the Libbitcoin team, asking for followup |
|
||||||
|
| 2023-08-03 | We send technical vulnerability details and detailed disclosure context to the Libbitcoin team |
|
||||||
|
| 2023-08-03 | Libbitcoin team response, indicating they do not feel this is a bug |
|
||||||
|
| 2023-08-04 | We request a CVE for the `bx seed` vulnerability in versions `3.x` from MITRE |
|
||||||
|
| 2023-08-05 | Libbitcoin team response, further detailing they do not feel this is a bug |
|
||||||
|
| 2023-08-07 | MITRE assigns CVE for the `bx` `3.x` issue |
|
||||||
|
| 2023-08-08 | Publication of this article |
|
||||||
|
|
||||||
|
Please note that this timeline focuses on the main events and is not exhaustive.
|
||||||
|
|
||||||
|
<a id="libbitcoin-vendor-response"/>
|
||||||
|
### Libbitcoin Team Response and Context
|
||||||
|
During our accelerated coordinated disclosure to the Libbitcoin team, the Libbitcoin team quickly disputed the relevancy of our findings and the CVE assignment. By our understanding, they consider `bx seed` a command that should never be used productively by any `bx` user since it is sufficiently documented as unsuited for safe wallet generation.
|
||||||
|
|
||||||
|
We do not agree with this assessment.
|
||||||
|
|
||||||
|
Please consider the following timeline and linked resources:
|
||||||
|
|
||||||
|
| Date | Information |
|
||||||
|
| --- | --- |
|
||||||
|
| 2013-07-21 | Libbitcoin Explorer predecessor tool [adds](https://github.com/BWallet/sx/commit/482255da51749300804bfb7d9998bf2604587d3b) a `newseed` command for entropy generation |
|
||||||
|
| 2014-10-16 | First Libbitcoin Explorer wiki [documentation page](https://github.com/libbitcoin/libbitcoin-explorer/wiki/bx-seed/7644353f2ed45f116c595222640240fabc43e953) for `bx seed` |
|
||||||
|
| 2014-12-14 | First Libbitcoin Explorer (bx) release starting with version `2.0.0` |
|
||||||
|
| 2015-12-21 | Libbitcoin Explorer (bx) release `2.2.0` |
|
||||||
|
| 2017-02-10 | Libbitcoin Explorer (bx) release `2.3.0` |
|
||||||
|
| 2015-01-19 | A Libbitcoin team member [adds](https://github.com/bitcoinbook/bitcoinbook/commit/76c5ba8000d6de20b4adaf802329b501a5d5d1db) and updates `bx seed` usage suggestions to "Mastering Bitcoin" (described below) |
|
||||||
|
| 2016-10-21 | The Libbitcoin team changes the seed generation to Mersenne Twister via [PR#559](https://github.com/libbitcoin/libbitcoin-system/pull/559), [commit](https://github.com/libbitcoin/libbitcoin-system/commit/6d5a06e283d81260165e0eab95175069bf03b408) |
|
||||||
|
| 2017-03-08 | Libbitcoin Explorer (bx) [3.0.0](https://github.com/libbitcoin/libbitcoin-explorer/releases/tag/v3.0.0), includes [PR#559](https://github.com/libbitcoin/libbitcoin-system/pull/559) |
|
||||||
|
| 2019-08-29 | Libbitcoin Explorer (bx) [3.6.0](https://github.com/libbitcoin/libbitcoin-explorer/releases/tag/v3.0.0), currently latest official release |
|
||||||
|
| 2021-05-02 | `bx seed` command gets renamed to `bx entropy` on GitHub branch, still based on Mersenne Twister, [PR#628](https://github.com/libbitcoin/libbitcoin-explorer/issues/628), [1](https://github.com/libbitcoin/libbitcoin-explorer/commit/199b34ee359b25befe98317d4f180b7711e6d257#diff-a8950528178320085bc3c31b5920b1992a5c9dbc34e7c25c5029eb16276e2b72), [2](https://github.com/libbitcoin/libbitcoin-explorer/commit/a856bd68777ef8682ea303713618a5ca5bc91a42) |
|
||||||
|
|
||||||
|
We're aware of a single warning note in the `bx seed` documentation page in the wiki:
|
||||||
|
> WARNING: Pseudorandom seeding can introduce [cryptographic weakness](https://cwe.mitre.org/data/definitions/338.html) into your keys. This command is provided as a convenience.
|
||||||
|
|
||||||
|
The wording "can introduce" is quite weak and a user may not be aware that this produces a seed that is completely insecure and should not be used to store anything of value.
|
||||||
|
|
||||||
|
When adding `bx seed` related workflows to the "Mastering Bitcoin" book appendix, Libbitcoin team members described it as follows:
|
||||||
|
> Generate a random "seed" value using the seed command, which uses the operating system’s random number generator. Pass the seed to the `ec-new` command [...]
|
||||||
|
> `$ bx seed | bx ec-new > private_key`
|
||||||
|
> `[...]`
|
||||||
|
|
||||||
|
|
||||||
|
Similarly, in "Mastering Bitcoin" Chapter 4:
|
||||||
|
> You can also use the Bitcoin Explorer command-line tool (see [appdx_bx]) to generate and display private keys with the commands `seed`, `ec-new`, and `ec-to-wif`:
|
||||||
|
> `$ bx seed | bx ec-new | bx ec-to-wif`
|
||||||
|
> `[...]`
|
||||||
|
|
||||||
|
Neither Chapter 4 nor the Appendix contain the warning that `bx seed` does not produce secure random numbers. The examples do not warn the user that wallets created like this are insecure.
|
||||||
|
|
||||||
|
We informed the authors of "Mastering Bitcoin" and they will revise the text.
|
||||||
|
|
||||||
|
The following Libbitcoin documentation includes `bx seed`:
|
||||||
|
|
||||||
|
| Page | Command Excerpt | Last changed |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [wiki landing page](https://github.com/libbitcoin/libbitcoin-explorer/wiki) | `bx seed | bx ec-new | bx ec-to-public | bx ec-to-address` | 7/2018 |
|
||||||
|
| [bx mnemonic-new documentation](https://github.com/libbitcoin/libbitcoin-explorer/wiki/bx-mnemonic-new#example-3) | `bx seed -b 128 | bx mnemonic-new` | 4/2017 |
|
||||||
|
| [bx hd-new documentation](https://github.com/libbitcoin/libbitcoin-explorer/wiki/bx-hd-new#example-4) | `bx seed | bx hd-new` | 4/2022 |
|
||||||
|
| [Random Numbers](https://github.com/libbitcoin/libbitcoin-explorer/wiki/Random-Numbers) | `bx seed -b 256` | 7/2018 |
|
||||||
|
|
||||||
|
All these examples do not contain any warning that the created wallets are insecure.
|
||||||
|
|
||||||
|
Other notable characteristics:
|
||||||
|
* The `bx seed` `-b` parameter cannot be configured to output less than 128 bits of binary output size. Related code comments on this limit include `The minimum safe length of a seed in bits` and `These are soft requirements for security and rationality.`. It seems strange that the design explicitly prevents the user from creating a seed that is too short, but does not prevent him from creating a seed that has not enough randomness.
|
||||||
|
|
||||||
|
General notes:
|
||||||
|
* Dates regarding releases and commits are taken from Git/GitHub timestamp information. This may deviate from the actual dates in some cases.
|
||||||
|
|
||||||
|
|
||||||
|
### Suspected RNG Problems in Older `bx` Versions
|
||||||
|
<a id="bx-other-rng-issues"/>For the main part of our short and busy disclosure, we focused on the Mersenne Twister related issues in `bx` versions released after March 2017.
|
||||||
|
|
||||||
|
However, we recently observed some behavior on older `bx` versions before `3.0.0` that indicate other weak random number generator behavior of `bx seed`, in part depending on the system environment `bx` is executed on. Specifically, the `std::random_device` entropy source in combination with `std::default_random_engine` may not behave securely enough if the random engine uses insufficient seeding and acts as a non-CSPRNG similar to Mersenne Twister.
|
||||||
|
|
||||||
|
| Tool | Version | Release | Status | Details |
|
||||||
|
| -- | -- | -- | -- | -- |
|
||||||
|
| sx | 1.x ? | 2014 | 🔍 likely affected (some systems) | `std::random_device` + `std::default_random_engine` |
|
||||||
|
| bx | 2.0.0 - 2.1.0 | 2014 - 2015 | 🔍 likely affected (some systems) | `std::random_device` + `std::default_random_engine` |
|
||||||
|
| bx | 2.2.0 - 2.3.0 | 2015 - 2017| ❔ unclear, improved behavior | `std::random_device` + `std::uniform_int_distribution` |
|
||||||
|
| bx | 3.0.0 - 3.6.0 | 2017 - now | 🔥 confirmed exploitable | `get_clock_seed()` + `std::mt19937` Mersenne Twister|
|
||||||
|
|
||||||
|
Please regard this as preliminary indications of potential problems. It is possible that some of the public thefts exploit random number generator issues in versions before `bx 3.0.0`, but we have not confirmed this yet.
|
||||||
|
|
||||||
|
We plan to follow up on this with additional research.
|
||||||
|
|
||||||
|
|
||||||
|
## Part III - Summary and Outlook
|
||||||
|
|
||||||
|
<a id="lessons-learned"/>
|
||||||
|
### Basic Lessons Learned
|
||||||
|
* Use BIP39 passphrases for your wallets, ideally with a complex passphrase based on entropy from a separate source.
|
||||||
|
* Trust only heavily audited software to be in your wallet generation path.
|
||||||
|
* Document every wallet generation setup for your future self, this may be very important.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
In this article, we presented technical information of a weak entropy generation function in Libbitcoin Explorer, confirmed the practical use of the weak function for over 2600 cryptocurrency wallets on the Bitcoin Mainnet, and connected it to a recent large theft of cryptocurrency funds on multiple popular blockchains that amounts to an estimated $900k of damages. Additionally, we described the close similarities with another actively exploited vulnerability in Trust Wallet, and provided some background on the Libbitcoin Explorer context and overall timeline.
|
||||||
|
|
||||||
|
### Future Work
|
||||||
|
* We plan further security research into `bx` `2.x` RNG behavior.
|
||||||
|
* We may update this page in the next days as more information becomes available.
|
||||||
|
|
||||||
|
### Additional References
|
||||||
|
|
||||||
|
* [Security bug](https://github.com/intel/pailliercryptolib/issues/2) - Mersenne Twister MT19937 usage in Intel cryptography library
|
||||||
|
|
||||||
|
### Commercial Work
|
||||||
|
|
||||||
|
We have not received any reward for this research and are not accepting donations.
|
||||||
|
|
||||||
|
If you like our work, check out the following commercial services offered by different team members and organizations:
|
||||||
|
|
||||||
|
{% include hiring.html %}
|
||||||
|
|
||||||
|
### Other Notes
|
||||||
|
* Included Libbitcoin code snippets are licensed [under AGPLv3](https://github.com/libbitcoin/libbitcoin-explorer/blob/20eba4db9a8a3476949d6fd08a589abda7fde3e3/COPYING).
|
||||||
|
* For other code snippets, see the included copyright notice.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
{% include credits.html %}
|
|
@ -0,0 +1,121 @@
|
||||||
|
---
|
||||||
|
title: /faq
|
||||||
|
layout: home
|
||||||
|
permalink: /faq.html
|
||||||
|
---
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
|
||||||
|
### Why the name 'Milk Sad'?
|
||||||
|
`milk sad` were the first two words from the first seed phrase of this broken key generation process.
|
||||||
|
|
||||||
|
---
|
||||||
|
### Why this page?
|
||||||
|
This page is meant to increase awareness of this type of vulnerability, involving weak cryptographic systems due to inadequate sources of randomness, and help users protect their funds. Weak randomness is something that's easy to get wrong, and is unfortunately so common that one can frequently find software engineering books teaching this topic incorrectly. We hope this example can motivate developers and users to be more vigilant when dealing with generating sensitive cryptographic material.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### I had my cryptocurrency assets stolen and I believe it may be due to this vulnerability. What should I do?
|
||||||
|
Contact your local authorities. Look into how you generated your wallet in the first place, and if `bx` was involved at all.
|
||||||
|
|
||||||
|
You can use the [lookup service](/lookup.html) we developed to check if your wallet is impacted by this specific problem.
|
||||||
|
|
||||||
|
**We do not have your funds.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Has law enforcement been notified?
|
||||||
|
Yes. If you're in the US, American Federal Agents have been notified of the issue and the extent of the currently known impact. Further disseminating this information to your country's authorities may prove helpful.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Have there been similar vulnerabilities in the past?
|
||||||
|
Yes, this issue is very similar to Trust Wallet, CVE-2023-31290, see the [blog post from Ledger Donjon](https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been-stolen/). Other related vulnerabilities are listed in the [write-up](/disclosure.html).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### I created my wallet with `bx seed`, but my funds are still there -- which versions were vulnerable?
|
||||||
|
All versions of `bx` `3.0.0` to `3.6.0` have weak entropy generation. Additionally, we have [some indication](/disclosure.html#bx-other-rng-issues) that the initial `sx newseed` and some `bx` `2.x` versions behave insecurely on some systems. We recommend quickly moving all funds on wallets created with `bx seed` regardless of the original version and assuming the worst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### I used `bx` to generate my wallets but only use it for non-BTC coins, do I need to worry?
|
||||||
|
Yes. All funds stored on BIP39 mnemonic secrets or BIP32 wallet seeds are affected since the underlying private keys are basically public now.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### I used `bx` to handle some wallet secrets, but generated them elsewhere, do I need to worry?
|
||||||
|
The identified problem is limited to the entropy generation functionality. We're not aware of serious issues in other `bx` commands that operate on existing wallets. If you are 100% sure you generated the wallet elsewhere, you should be okay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Is my hardware wallet affected?
|
||||||
|
- If you _generated_ your BIP39 mnemonic secret on a hardware wallet, you should be fine.
|
||||||
|
- We are not aware of any hardware wallet with similarly broken _wallet generation_ - Trezor, Ledger, and other hardware wallets do not use the affected Libbitcoin code.
|
||||||
|
- If you _imported_ a vulnerable wallet into other software wallets or hardware wallets, they cannot protect you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Is the vulnerability currently fixed in `libbitcoin-explorer`?
|
||||||
|
We are not aware of a fix. At the time of disclosure, our understanding is that the Libbitcoin team considers this not to be a vulnerability. See [this section](disclosure.html#libbitcoin-vendor-response) in our disclosure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Would a security patch fix this problem?
|
||||||
|
Only for new wallets. All funds stored using previously generated wallets will remain vulnerable, and need to be migrated to a new secure wallet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### How do the researchers involved in this project personally protect their crypto assets?
|
||||||
|
The team members use a wide array of commercially available hardware wallets as well as custom built solutions, while some do not have crypto-assets at all.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### In a nutshell, what are your personal "lessons learned"?
|
||||||
|
See [lessons learned](/disclosure.html#lessons-learned).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Is my major exchange cold storage impacted?
|
||||||
|
If it doesn't rely on `bx seed`, then we have no indications for it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Do you have any general advice for avoiding this type of flaw?
|
||||||
|
- If you are non-technical, don't try to store large amounts of crypto assets yourself without consulting security experts.
|
||||||
|
- If you are highly technical and lazy or not well informed on security topics, don't try to store large amounts of crypto assets yourself.
|
||||||
|
- Don't trust closed source solutions, or open source solutions no one credible has reviewed recently.
|
||||||
|
- Use a hardware wallet, but add a [passphrase](https://trezor.io/learn/a/passphrases-and-hidden-wallets) for good measure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Any recommendations for experts?
|
||||||
|
|
||||||
|
We could fit an entire book of advice here, but some top of mind items:
|
||||||
|
|
||||||
|
- Generate keys you care about offline:
|
||||||
|
- Distrust provides [AirgapOS](https://git.distrust.co/public/airgap) for offline operations.
|
||||||
|
- Use a BIP39 passphrase with 128 bit entropy or higher, generated completely independently from your mnemonic.
|
||||||
|
- Ensure your key is actually random:
|
||||||
|
- Ensure /dev/urandom has multiple hardware sources of entropy (Consider using a [TRNG](https://github.com/waywardgeek/infnoise) or similar hardware).
|
||||||
|
- Ensure mnemonic is -actually- seeded with 128-256 bits from /dev/urandom.
|
||||||
|
- Consider generating a mnemonic with [dice](https://github.com/taelfrinn/Bip39-diceware), or a [TRNG](https://github.com/waywardgeek/infnoise).
|
||||||
|
- Derive addresses with two independent platforms:
|
||||||
|
- E.g. hardware wallet + airgapped CLI tools should agree on addresses.
|
||||||
|
- Review the entire code path to your mnemonic/passphrase:
|
||||||
|
- Be aware of the kernel version and `random.c` algorithm you are using.
|
||||||
|
- Document every tool and line of code in your key generation path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Do you accept donations?
|
||||||
|
No.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Can I hire any of you?
|
||||||
|
|
||||||
|
Sure! If you like our work, check out the following commercial services offered by different team members and organizations.
|
||||||
|
|
||||||
|
{% include hiring.html %}
|
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
title: /summary
|
||||||
|
layout: home
|
||||||
|
permalink: /index.html
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="/assets/base/milksad_transparent.svg" width="30%" style="float: right"/>
|
||||||
|
|
||||||
|
# Milk Sad Disclosure
|
||||||
|
|
||||||
|
A practical explanation of how weak entropy can ruin your day - and your savings.
|
||||||
|
|
||||||
|
## Vulnerability [CVE-2023-39910](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-39910)
|
||||||
|
|
||||||
|
- We discovered a cryptographic weakness in the widely utilized `Libbitcoin Explorer` (`bx`) cryptocurrency wallet tool while following up on mysterious wallet thefts.
|
||||||
|
- The `bx seed` subcommand for generation of new wallet private key entropy is flawed and produces insecure output.
|
||||||
|
- On `Libbitcoin Explorer` `3.x` versions, `bx seed` uses the [Mersenne Twister](https://en.wikipedia.org/wiki/Mersenne_Twister#Disadvantages) pseudorandom number generator (PRNG) [initialized](https://github.com/libbitcoin/libbitcoin-system/blob/a1b777fc51d9c04e0c7a1dec5cc746b82a6afe64/src/crypto/pseudo_random.cpp#L77) with 32 bits of system time.
|
||||||
|
- Bad actors have discovered this flaw and are actively exploiting it to steal funds from affected wallets on multiple blockchains.
|
||||||
|
- We have reasons to believe some `Libbitcoin Explorer` versions before `3.0.0` also produce weak `bx seed` output in some system environments.
|
||||||
|
- Think of this as securing your online bank account with a password manager that creates a long random password, but it often creates the same passwords for every user. Malicious people have figured this out and drained funds on any account they can find.
|
||||||
|
|
||||||
|
## How?
|
||||||
|
<div style="float: left; margin-right:40px;margin-left:20px; margin-top:20px;margin-bottom:30px; width: 50%">
|
||||||
|
<img width="100%" src="/assets/base/bx-mastering-bitcoin.png" />
|
||||||
|
<div style="font-size: 12px">
|
||||||
|
Mastering Bitcoin - Second Edition by Andreas M. Antonopoulos LLC is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
- Popular documentation like "[Mastering Bitcoin](https://bitcoinbook.info)" suggests the usage of `bx seed` for wallet generation.
|
||||||
|
- Secure cryptography requires a source of large, non-guessable numbers. If the random number generator is weak, the resulting cryptographic usage is almost always compromised.
|
||||||
|
- For technical people: in this case, practical wallet security is reduced from 128 bit, 192 bit or 256 bit to a mere 32 bit of unknown key information.
|
||||||
|
- A 32 bit key space is 2^32, or 4,294,967,296 different unique combinations of derived [BIP39]((https://en.bitcoin.it/wiki/BIP_0039)) mnemonic phrases or other key formats ([BIP32](https://en.bitcoin.it/wiki/BIP_0032)). Spoiler: That's not as many combinations as it sounds.
|
||||||
|
- With enough optimizations, a decent gaming PC can do a brute-force search through 2^32 wallet combinations in less than a day.
|
||||||
|
- Since `bx` has a configurable output length and can be used in several ways, there are a few variations the attacker needs to test for each case. This slows down practical attacks to a few days.
|
||||||
|
- Once an attacker finds a match of a wallet candidate with an actual wallet used on a blockchain, they are in full possession of the private keys and can steal remaining funds, trace all previous wallet history and sign messages.
|
||||||
|
- The attack works independent of the owner's current copy of the wallet secrets. In other words, even if you keep your paper wallet in a bank safe, your funds can still be stolen remotely. Crazy, right?
|
||||||
|
- Attackers are actively exploiting this and have been draining funds of wallets where the mnemonic was generated using this tool.
|
||||||
|
- Why the silly "Milk Sad" name? Running `bx seed` on `3.x` versions with a system time of 0.0 always generates the following secret:
|
||||||
|
> milk sad wage cup reward umbrella raven visa give list decorate bulb gold raise twenty fly manual stand float super gentle climb fold park
|
||||||
|
|
||||||
|
## When?
|
||||||
|
The main theft occurred around 12 July 2023, although initial exploitation likely began at a smaller scale in May 2023.
|
||||||
|
|
||||||
|
A separate but similar vulnerability in another wallet software was detected in November 2022 and actively exploited shortly after, which may be the prequel to this story.
|
||||||
|
|
||||||
|
## Who?
|
||||||
|
We did not identify who is behind the ongoing thefts from vulnerable wallets.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
[CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)](https://cwe.mitre.org/data/definitions/338.html)
|
||||||
|
|
||||||
|
## Vendor
|
||||||
|
|
||||||
|
[Libbitcoin](https://github.com/libbitcoin)
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
See our [technical writeup](/disclosure.html) and [FAQ](/faq.html).
|
||||||
|
|
||||||
|
[CVE-2023-31290](https://nvd.nist.gov/vuln/detail/CVE-2023-31290) was a similar vulnerability in [Trust Wallet](
|
||||||
|
https://community.trustwallet.com/t/browser-extension-wasm-vulnerability-postmortem/750787
|
||||||
|
), see [Ledger Donjon's technical writeup](https://blog.ledger.com/Funds-of-every-wallet-created-with-the-Trust-Wallet-browser-extension-could-have-been-stolen/
|
||||||
|
).
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
* General requests and comments: email [team@milksad.info](mailto:team@milksad.info)
|
||||||
|
* Press: email [press@milksad.info](mailto:press@milksad.info)
|
||||||
|
|
||||||
|
## Team & Credits
|
||||||
|
|
||||||
|
{% include credits.html %}
|
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
title: /lookup
|
||||||
|
layout: home
|
||||||
|
permalink: /lookup.html
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lookup service
|
||||||
|
|
||||||
|
To help people identify if they are impacted by Milksad, we are providing a web service to check if your mnemonic is in the vulnerable set. Note that this service -only- covers mnemonics impacted by Libbitcoin Explorer (`bx`) versions `3.0.0` to `3.6.0`, though it may be updated over time to cover other related vulnerabilities we are researching.
|
||||||
|
|
||||||
|
## Who should use this service
|
||||||
|
|
||||||
|
* If you know you generated your wallet with bx 3.0.0 or higher (after ca. March 2017)
|
||||||
|
* If you know you generated your wallet with a CLI tool and don't remember which tool
|
||||||
|
|
||||||
|
## Security & Privacy
|
||||||
|
|
||||||
|
We of course do not want to store BIP39 mnemonics for this lookup service, or have people submit their BIP39 mnemonic private keys to us, so we had to sacrifice the overall user experience a bit in order to provide this service safely.
|
||||||
|
|
||||||
|
Our server contains `SHA-256` hashes of all currently known vulnerable mnemonics, and you can submit the `SHA-256 hash` of your own mnemonic and see if it is in our set.
|
||||||
|
|
||||||
|
Please note that it is usually a very, very bad idea to follow invitations from strangers on the Internet when it comes to sharing something about your wallet private keys. Typically they are scammers that have bad intentions. We're aware of this, and want to avoid being a bad example, hence we decided to include no convenient HTML input field that does the hashing for you (and could steal your mnemonics in the process). Users must bring their own sha256 hash of their mnemonic calculated on their own offline machine. If other people offer a similar lookup service, please be very cautious.
|
||||||
|
|
||||||
|
For those wishing to limit metadata sent to us or our server provider, we encourage using Whonix/Tor. That said, we do not have any logging enabled on this service.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Obtain a hash of your mnemonic from an *offline* live-booted linux distribution running on hardware you trust
|
||||||
|
|
||||||
|
Note: Do *not* substitute a virtual machine, WSL, your macbook, or anything else. Assume your workstation is compromised.
|
||||||
|
```
|
||||||
|
unset HISTFILE
|
||||||
|
set +o history
|
||||||
|
printf 'milk sad wage cup reward umbrella raven visa give list decorate bulb gold raise twenty fly manual stand float super gentle climb fold park' | openssl sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Submit your hash to our comparison service on a separate -online- machine with network connection.
|
||||||
|
|
||||||
|
Note: We suggest you transmit the hash to your online system by typing by hand, or copying it to an SD card.
|
||||||
|
|
||||||
|
* Option 1: Browser
|
||||||
|
|
||||||
|
Visit [https://lookup.milksad.info](https://lookup.milksad.info) and paste your sha256 hash
|
||||||
|
|
||||||
|
* Option 2: Terminal
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://lookup.milksad.info/check?sha256=a7278e802055b3d17a66afe498428ac70c828ad69f25d02254c3b76b08d8cf8a
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Inspect the response for ```vulnerable``` or ```no match```
|