Contents

Github Enterprise 授权验证流程全解析

概述

Github Enterprise 是 Github 为企业用户量身定制的代码托管与项目协同平台,提供公有云、私有云两种不同的部署方式。本文将详细讲解 Github Enterprise (后文将简称为 GHE)license 的生成与验证的流程与逆向分析的思路,同时分享我编写的通杀一键注册机,开箱即用。希望文中分享的一些思路,也会为大家在 Linux 系统取证、应用逆向分析、授权绕过时快速找到突破口提供一定的帮助。

Github Enterprise 的界面与 Github 完全一样,而且包含几乎所有 Github 的功能特性(缺少的功能目前我看到的只有项目页面中的 Discussion),并额外提供了其他 Github 没有的高级功能。列举几个我印象比较深的。

  1. SAML/CAS/LDAP SSO(没有 OAuth)
  2. 行为审计(粒度非常细!)
  3. 访问权限控制(包括 Fork、Issue、PR 等等各种功能的使用权限、项目可见度等等)
  4. 代码安全扫描(Github Advanced Security,包括代码扫描、密码私钥扫描、依赖库三大块)
  5. 与 Github 互联互通(像 Bing 一样可以在 Github 上搜索企业内部数据)

通过本文,读者可以了解到的内容大致有:

  1. Github Enterprise 的安装操作流程(至于 GHE 的功能使用,基本与 Github 相同,大家自行试用即可)。
  2. Github Enterprise 的授权验证机制、信任链、验证机制实现、(一个并不是很强的)程序混淆算法,在不破坏 OTA 功能的前提下绕过授权验证机制的方法。
  3. 如何可靠地、future-proof 地破解一个以完整操作系统形式交付的软件产品。实现方法:打包一个专用的 Linux 发行版 ISO,对目标系统盘进行旁路注入,修改必要的文件(如授权文件)并注入二阶段 Payload。
  4. bash 脚本的高级用法:彩色输出、多进程/子进程监控/进程间通信/输出捕获、校验容器/镜像/镜像内文件/容器内文件的完整性等等。
  5. 一个完全开源的 Github Enterprise 破解工具!

定位关键信息

首先,我们从官网下载 GHE 安装包。至本文截稿时(2022.02),官网最新版本为 3.4.0.rc1(后文所分享的注册机工具,经过多次测试,能够保证能够在此版本上稳定运行)。

官网提供了三种部署方式,Hyper-V (VHD)、OpenStack KVM (QCOW2)、VMWare ESXi/vSphere (OVA)。这里我选择 OVA 格式并将其部署到 vSphere 上,读者也可以将 OVA 导入到 VMWare Workstation 或 Virtualbox 进行试用,一定要注意资源需求。我部署时使用了 2CPU、16G RAM、200G 系统盘 + 150G 数据盘的配置,官方推荐(Installing GitHub Enterprise Server on VMware - GitHub Docs)的资源配置是 4CPU、32G RAM。

文件系统与发行版信息

部署后,我们先不急着进入系统。用 Kali 启动盘启动虚拟机,并查看分区情况。系统盘被分成两块,我们可以当作 A/B 分区。既然是个 Linux 系统,我们得知道它是哪个发行版。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220215122044-15updlk.png

阅读 lsb_release 的源码可知,发行版信息保存于 /usr/lib/os-release 文件中。读取这个文件得知当前系统是 Debian 10。

$ cat /media/kali/_/usr/lib/os-release
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
...

系统服务与关键目录

既然是 Debian 的话,无论它用了什么服务进行任务调度,它必然需要通过 systemd 进行开机自启。我们翻一翻 /etc/systemd/system/*.target.wants,这里会包含用户配置(或者说第三方配置)的开机启动项。很容易发现,其中 ghe- 开头的一大票服务非常显眼,ghe 就是 Github Enterprise 的简称。(在后续的分析中我还发现 nomad 这个服务也很重要,这个留到后面讲)

我本来以为 Github Enterprise 应该简称为 GE 的,还觉得怪怪的,果然官方的缩写 GHE 看起来更舒服一点。

$ ls /etc/systemd/system/*.target.wants
/etc/systemd/system/getty.target.wants:
getty@tty1.service

/etc/systemd/system/github-enterprise.target.wants:
graphite-web.service

/etc/systemd/system/multi-user.target.wants:
chrony.service           enterprise-manage.service          grafana-server.service         open-vm-tools.service
console-setup.service    ghe-create-log-dirs.service        haproxy-cluster-proxy.service  rdnssd.service
consul.service           ghe-loading-page.service           haproxy-data-proxy.service     remote-fs.target
consul-template.service  ghe-reconfigure.service            haproxy.service                rsync.service
containerd.service       ghe-secrets.service                lo-enable-ipv6.service         ssh.service
cron.service             ghe-user-disk.service              networking.service             syslog-ng.service
dnsmasq.service          ghe-wait-for-certificates.service  nomad-jobs.service             sysstat.service
docker.service           github-enterprise.target           nomad.service                  ufw.service

/etc/systemd/system/network-online.target.wants:
networking.service

/etc/systemd/system/sockets.target.wants:
dm-event.socket  docker.socket

/etc/systemd/system/sysinit.target.wants:
blk-availability.service  keyboard-setup.service  lvm2-monitor.service  systemd-timesyncd.service
ghe-welcome.service       lvm2-lvmpolld.socket    resolvconf.service    virt-what.service

/etc/systemd/system/timers.target.wants:
apt-daily.timer  logrotate.timer  man-db.timer

随便点开几个服务,可以看到 /usr/local/share/enterprise 似乎就是一个关键目录。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220215123346-c80ouv7.png

这些启动项有什么用?等开机了我们才能看到。不过在这个目录下,我发现一个有意思的脚本 /usr/local/share/enterprise/ghe-license-info。看一眼就知道,这个脚本相当于直接告诉我们,授权文件安装在 /data/user/common/enterprise.ghl是一个 gpg 套起来的 tar 文件,压缩包里面包含一个名为 metadata.json 的文件,里面就是注册码信息

#!/bin/bash
#/ Usage: ghe-license-info
set -e

GHE_LICENSE_FILE=${GHE_LICENSE_FILE:-/data/user/common/enterprise.ghl}

[ "$(whoami)" = "root" ] || [ -r "$GHE_LICENSE_FILE" ] || {
  exec sudo -u root GHE_LICENSE_FILE="$GHE_LICENSE_FILE" "$0" "$@"
  echo Run this script as the root user. >&2
  exit 1
}

if [ ! -f "$GHE_LICENSE_FILE" ]; then
  echo "No license file found ($GHE_LICENSE_FILE)." >&2
  exit 1
fi

info=$(gpg --skip-verify --no-permission-warning --decrypt "$GHE_LICENSE_FILE" 2>/dev/null |
  tar xO metadata.json |
  jq -c 'del(.customer_public_key,.customer_private_key)')

if [ "$1" == "-j" ]; then
  echo "$info"
else
  echo "$info" |
  jq -r 'to_entries | sort[] | "\(.key | tojson) : \(.value | tojson)"'
fi

代码混淆算法

全盘搜索名称包含 license 的文件,除了一些开源组件的开源 license 之外,这几个文件比较显眼。

/data/enterprise-manage/current/lib/manage/models/license_settings.rb
/data/enterprise-manage/current/lib/manage/license.rb
/data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto/license.rb
/data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto/license_vault.rb

但是打开会发现,这些文件都被混淆了

__ruby_concealer__ "x\x9C]\x9A\xFB[\xDAz\xB3\xF6\xBF9\x90\x84\x9CH
...
+\f\xC1\xF8\xF3\xDD\x84\x82\"\xD9\xF9\x1D\xC00\xA1\xB7mGMp7~|\xD9\xFF\a\xBD\xE1Yb"

解开这些文件也很容易。使用 zlib 解压(zlib inflate)后,使用以下字符串作为 Key 异或(XOR)混淆过的字符串。注意末尾有一个空格。

"This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. "

混淆函数打包在 ruby 中。这里不作分析。

$ strings /usr/local/bin/ruby | grep encryption
This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken.

使用以下脚本解密。

require 'zlib'

def decrypt(s)
    i, plaintext = 0, ''
    key = "This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. "

    Zlib::Inflate.inflate(s).each_byte do |c|
        plaintext << (c ^ key[i%key.length].ord).chr
        i += 1
    end
    plaintext
end

content = File.open(ARGV[0], "r").read
content.sub! %Q(__ruby_concealer__), " decrypt "
plaintext = eval content

puts plaintext

与之对应的加密脚本。其实加密脚本咱用不着,代码改完不加密直接放进去也没问题。就是图一乐。

require 'zlib'

def encrypt(s)
    i, ciphertext = 0, ''
    key = "This obfuscation is intended to discourage GitHub Enterprise customers from making modifications to the VM. We know this 'encryption' is easily broken. "

    s.each_byte do |c|
        ciphertext << (c ^ key[i%key.length].ord).chr
        i += 1
    end
    Zlib::Deflate.deflate(ciphertext)
end

content = File.open(ARGV[0], "r").read
ciphertext = encrypt(content)
puts "__ruby_concealer__ #{ciphertext.inspect}"

将这个文件保存为 /tmp/dec.rb,使用以下 bash 命令就可以在 GHE 系统分区(可以从 Kali Live 中 chroot 进去)中 dump 所有被加密的程序。

cd /data/enterprise-manage/current
find lib/ -name '*.rb' | xargs -I% sh -c ' mkdir -p /tmp/$(dirname %) && grep -o  __ruby_concealer__ % && ruby /tmp/dec.rb % > /tmp/% || cp % /tmp/%'^C

受混淆保护的文件列表

搜索 .rb 文件并检测文件内容包含 __ruby_concealer__ 即可。完整列表太长了。

授权文件结构

我们现在还没注册码,系统都没启动呢,就已经获取了这么多信息了。按照国际惯例,我们不得整一个注册码?

随便糊弄一个邮箱从 Github Enterprise 官网注册,然后就可以申请一个 45 天的试用 License。通过前面获知的文件解压方法,我们可以看看里面的内容。

安装 GnuPG (windows 可以安装 gpg4win)并用 gpg 解密之前从官网拿到的 ghl 授权文件。说是解密,其实并不需要密码。解密后用 7-Zip 解压 tar 即可得到里面的文件。可以看到里面除了 metadata.json 之外,还包含了两组 key。

gpg --skip-verify --no-permission-warning --decrypt github-enterprise-195560.ghl.tar github-enterprise-195560.ghl
github-enterprise-195560.tar
   metadata.json
├───customer
       pubring.gpg
       secring.gpg
└───license
        pubring.gpg
        secring.gpg

这两个 key 的 fingerprint 是完全一样的,且 metadata.json 中 customer_private_keycustomer_public_key 字段内容也和这这两对 key 完全相同。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220221135134-uvji73d.png

去掉上面两个字段后, metadata.json 的内容如下。下面文件中的 Company 是我填写的企业名称,

{
    "reference_number": "13eb75",
    "company": "Company",
    "ssh_allowed": true,
    "cluster_support": false,
    "croquet_support": false,
    "custom_terms": false,
    "support_key": null,
    "unlimited_seating": true,
    "perpetual": false,
    "evaluation": true,
    "learning_lab_seats": 0,
    "learning_lab_evaluation_expires": null,
    "insights_enabled": false,
    "insights_expire_at": null,
    "advanced_security_enabled": false,
    "advanced_security_seats": 0,
    "seats": 0,
    "expire_at": "2022-03-31T23:59:59-07:00"
}

那岂不是我改一下这个 JSON 的内容,然后用 GPG 打个包放进去就成了?

对,也不对。

授权校验算法

众所周知,GPG 是用来校验数字签名的,就像 CA 证书体系一样,需要构建一个完整的信任链。但 CA 是集中制的,正如其名称中的 Authority,所有用户靠出厂就印在操作系统里的 CA 列表验证彼此间的真实性。

而 GPG 就靠大家互相信任,A 为 B 的证书签名,就能够信任 B 证书签名的所有数据。但 A 必须自己检查 B 的证书是不是通过可信的渠道发过来的。

而 Github 的做法就是,把 B 证书硬编码在自己的代码里,这样不就安全了吗?

真是个大聪明!

那我们生成授权文件的时候,就需要为这个授权文件添加由可信密钥签署的数字签名。系统里当然只会存一份公钥,为了生成签名,我们需要自己生成一个私钥,并将对应的公钥替换到系统中,从而接管整个信任链。

聪明的读者,到这里肯定已经有思路了,从这里开始,我将重点陈述整理之后的结果与一些关键信息,过程性描述将尽量忽略。

源代码与库命名空间映射

类名 用途 ruby require 文件位置
Enterprise::Crypto 授权文件与 OTA 包读写、验证 enterprise/crypto /data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto.rb
Enterprise::Crypto::Vault 装 key 的容器,每个 key 一个容器,都经过 master key 校验的子 key enterprise/crypto /data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto/vault.rb
Manage 后台主程序,里面初始化了授权模块 manage /data/enterprise-manage/current/lib/manage.rb
Manage::License 后台程序里负责授权的模块 manage/license /data/enterprise-manage/current/lib/manage/license.rb

Manage 模块是管理后台,通过 haproxy 转发给 nginx 转发给 unicorn 的后台程序,从 8443 端口访问。这个程序是 GHE 安装时的后端程序。

Crypto 模块是一个公共模块,所有的签名校验、文件读写(包括授权文件的解压、OTA package 的解压)都是由这个模块来进行的。

Vault 是模块中的一个关键的类,这个类负责公私钥的存储,根密钥的签名和指纹的校验,它的三个子类 LicenseVault、CustomerVault、PackageVault 顾名思义负责具体的功能。

关键密钥

用于签名验证的根密钥硬编码于以下文件中。

/data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto/vault.rb

根密钥的指纹为 E422F81088AE9ABD5A6CBF7D1AF590C895016367这个指纹就是 GHE 的信任根

pub   rsa4096/1AF590C895016367 2011-08-06 [SC]
      E422F81088AE9ABD5A6CBF7D1AF590C895016367
uid                 [ unknown] GitHub Master Key <support@github.com>
sig 3        1AF590C895016367 2011-08-06  GitHub Master Key <support@github.com>
sub   rsa4096/4ED44822D1ED9253 2011-08-06 [E]
sig          1AF590C895016367 2011-08-06  GitHub Master Key <support@github.com>

根密钥签名了 3 个二级密钥。package 是签名升级包的,license 是签名

这三个路径于 /data/enterprise-manage/current/lib/manage.rb 中引用。

/data/enterprise/package.gpg
/data/enterprise/license.gpg
/data/enterprise/customer.gpg

从 gpg 的输出中可以看到,这三个 key 的都包含了 Master Key 的签名。

pub   rsa4096/8BDF429EF29F9D7E 2011-08-23 [SC]
      74708F1DFE79BF5FE2DDEC458BDF429EF29F9D7E
uid                 [ unknown] GitHub Customer Key <support@github.com>
sig 3        8BDF429EF29F9D7E 2011-08-23  GitHub Customer Key <support@github.com>
sig          1AF590C895016367 2011-08-23  GitHub Master Key <support@github.com>
sub   rsa4096/FC78CDE72E0420D4 2011-08-23 [E]
sig          8BDF429EF29F9D7E 2011-08-23  GitHub Customer Key <support@github.com>

pub   rsa4096/1E9E2B9CD80BFEB7 2011-08-23 [SC]
      C49B7FA81A0BC6D26DEAC2BA1E9E2B9CD80BFEB7
uid                 [ unknown] GitHub License Key <support@github.com>
sig 3        1E9E2B9CD80BFEB7 2011-08-23  GitHub License Key <support@github.com>
sig          1AF590C895016367 2011-08-23  GitHub Master Key <support@github.com>
sub   rsa4096/4AAC02EA164A2313 2011-08-23 [E]
sig          1E9E2B9CD80BFEB7 2011-08-23  GitHub License Key <support@github.com>

pub   rsa4096/B2B935E31729A28E 2011-08-18 [SC]
      663BE6981E181CC121BC4CADB2B935E31729A28E
uid                 [ unknown] GitHub Package Key <support@github.com>
sig 3        B2B935E31729A28E 2011-08-18  GitHub Package Key <support@github.com>
sig          1AF590C895016367 2011-08-18  GitHub Master Key <support@github.com>
sub   rsa4096/47443DAD9E39C3C3 2011-08-18 [E]
sig          B2B935E31729A28E 2011-08-18  GitHub Package Key <support@github.com>

而授权文件中包含的公私钥对,则由 Customer Key 签名。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220221152819-j9ujt25.png

整体验证流程

整体的验证流程如下所示。以管理后台的初始化流程为例。

  1. Manage.setup 初始化三个 Vault,将原始文件的内容(new 函数的所有参数作为数组)读到对应 Vault 内部的 key_data 中。

    Enterprise::Crypto.license_vault  = LicenseVault.new(license_key)
    Enterprise::Crypto.customer_vault = CustomerVault.new(customer_key)
    Enterprise::Crypto.package_vault  = PackageVault.new(package_key)
    
  2. 在 Vault 进行验签操作(调用 decrypt_and_verify)前,在 Crypto.with_vault 上下文初始化时,读取 key_data 并校验 Vault 中的子密钥。

    1. Enterprise::Crypto.with_vault -> Enterprise::Crypto::Vault.open!

      1. Enterprise::Crypto::Vault.load_master_public_key硬编码的 master public key blob 载入到 GPG 中

      2. Enterprise::Crypto::Vault.load_vault

        1. key_data 的内容挨个载入到 GPG 中,并确保它们 fingerprint 相同,保存 @fingerprint 变量
        2. 如果导入了私钥(通过 @fingerprint 变量查询),确保私钥与公钥签名相同 Enterprise::Crypto::Vault.verify_key_fingerprint!
        3. 如果导入了公钥(通过 @fingerprint 变量查询),确保公钥由主密钥签名 Enterprise::Crypto::Vault.verify_key_signature!
      3. Enterprise::Crypto::VaultValidator.validate! 重复 2.1.2,对 key 再次进行验证。

        1. Enterprise::Crypto::Vault.verify_key_fingerprint! 确保 master pubkey 与硬编码的 fingerprint 相同。
        2. Enterprise::Crypto::Vault.verify_key_signature! 如果 2.1.2 步骤中导入了公钥,确保公钥由主密钥签名
        3. Enterprise::Crypto::Vault.verify_key_fingerprint! 如果 2.1.2 步骤中导入了私钥,确保私钥与公钥签名相同
  3. 进行授权验证,如 License 载入或 OTA 包解压。 以下两个例子都调用了 Enterprise::Crypto::Vault.decrypt_and_verify

    # Manage::License
          @crypto = begin
            Enterprise::Crypto::License.load(data)
                    rescue Enterprise::Crypto::Error
                      nil
          end
    # Enterprise::Crypto::Package
          def self.extract(package_path, output_path, vault = Crypto.package_vault)
            tar_file = Crypto.with_vault(vault) do
              vault.read_package(File.open(package_path))
            end
    
            from_tar(tar_file.path, output_path)
    
            verify_extraction(output_path)
    

上文中标记粗体的就是我们的突破点。实际上,修改 vault.rb 中的两个函数就够了。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220221160434-iy6x3s3.png

如果让所有校验都返回 True,这样不够安全。直接修改 MASTER_FINGERPRINT 的话,这会使在线升级功能完全失效,下载下来的升级包过不了校验。只插入 fingerprint 的方式不需要替换完整的 master pubkey data,减少了需要更改的补丁量方便生成 patch。这个修改方式是最简洁且有效的。

不过这样修改的话,需要将我们自己生成的 Master Key fingerprint 和 SubkeyID 填进修改后的 vault.rb。因此我们还需要先生成 key 再打包所有的补丁。

生成授权文件

将以下两个脚本分别保存为 keygen.sh、keygen.rb,与上一步修改好的 vault.rb 一起复制进 GHE chroot 目录中。

其中,第一个脚本用于生成 GPG key,第二个脚本用于生成授权文件。

#!/bin/bash
# keygen.sh

script_path=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
key_path="./keys"

GNUPGHOME="$script_path/gpg"

mkdir -p "$GNUPGHOME" && chmod go-rwx "$GNUPGHOME"
mkdir -p "$key_path"

makekey () {
  echo "Creating key for $@" >&2
  gpg --batch --gen-key 2> >(perl -n -e '/\/([A-F0-9]{40}).rev/ && print $1') <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: $1
Name-Email: $2
Expire-Date: 0
EOF
}

signkey () {
  echo "Signing $2 with signature of $1"
  printf 'y\nsave\n' | gpg --command-fd 0 --status-fd 2 --local-user "$1" --edit-key "$2" sign
}

exportkey () {
  gpg --export --armor "$1" > "$key_path/$2.gpg"
}

exportseckey () {
  gpg --export-secret-keys --armor "$1" > "$key_path/$2.gpg"
}

mkey=$(makekey "Github Master Key" "test@github.com")
lkey=$(makekey "Github License Key" "test@github.com")
ckey=$(makekey "Github Customer Key" "test@github.com")


set -x
signkey "$mkey" "$lkey"
signkey "$mkey" "$ckey"


exportkey	"$mkey" master
exportseckey	"$mkey" master_sec
exportkey	"$lkey" license
exportseckey	"$lkey" license_sec
exportkey	"$ckey" customer
exportseckey	"$ckey" customer_sec

keygen.rb 调用 GHE 内部模块生成授权文件。

# keygen.rb
# frozen_string_literal: true
require "enterprise/crypto"


def main()
  license = generate_example_license(0, Time.now + days, license_metadata)
  File.open(ENV["LICENSE_FILENAME"] || "license.ghl", "wb") { |f| f << license.to_bin }
end

def generate_example_license(seats, expire_at, metadata = {})
  customer_sec_key = File.read("keys/customer_sec.gpg")
  customer_pub_key = File.read("keys/customer.gpg")
  license_sec_key  = File.read("keys/license_sec.gpg")
  license_pub_key  = File.read("keys/license.gpg")

  name  = ENV["LICENSE_NAME"] || "Test User"
  email = ENV["LICENSE_EMAIL"] || "test@example.com"

  Enterprise::Crypto.customer_vault =
    Enterprise::Crypto::CustomerVault.new(customer_sec_key, customer_pub_key, blank_password: true)

  Enterprise::Crypto.license_vault =
    Enterprise::Crypto::LicenseVault.new(license_sec_key, license_pub_key, blank_password: true)

  customer = Enterprise::Crypto::Customer.generate(name, email)
  license = customer.generate_license(seats, expire_at, support_key, metadata)
end

def license_metadata
  company            = ENV["LICENSE_COMPANY"] || "Baidu Inc."
  ref_number         = ENV["LICENSE_KEY_REFERENCE_NUMBER"] || "0b634b"
  license_ref_number = ENV["LICENSE_REFERENCE_NUMBER"] || "2df6d8"
  order_ref_number   = ENV["LICENSE_ORDER"] || "81e4b7"

  {
    reference_number: ref_number,
    order: order_ref_number,
    license: license_ref_number,
    company: company,
    ssh_allowed: true,
    support_key: support_key,
    cluster_support: true,
    advanced_security_enabled: true,
    unlimited_seating: true,
    perpetual: true
  }
end

def seats
  (ENV["LICENSE_SEATS"] || 100).to_i
end

def days
  60 * 60 * 24 * (ENV["LICENSE_DAYS"] || 365 * 10).to_i
end

if __FILE__ == $0
  main()
end

别急着运行。把文件拷贝过去以后,记得把 /dev 给 mount 进去。生成 key 的时候需要调用这个目录里面的随机数生成器。进入 chroot shell 内运行 keygen.sh

GHE_ROOT=/media/root/_

cp keygen.sh keygen.rb "$GHE_ROOT/root/"
mount /dev $GHE_ROOT/dev
chroot $GHE_ROOT bash

# 在 chroot shell 内
export SHELL=/bin/bash PATH="/usr/share/rbenv/shims:$PATH" RBENV_VERSION=$(cat /etc/github/ruby_version) GEM_PATH=/data/enterprise-manage/current/vendor/gems/ruby/2.7.0
cd /root/
bash keygen.sh

运行 keygen.sh 后,当前目录下的 keys 子目录会包含 master 公私钥和 customer 和 license 两对子密钥。

使用 gpg 命令获取 Master Key fingerprint 和 SubkeyID,以我生成的这对密钥为例, fingerprint 为 6805C30E66B00B085D24D9EC985269A0B58C25A5,SubkeyID 为 985269A0B58C25A5

$ ls keys/
customer.gpg
customer_sec.gpg
license.gpg
license_sec.gpg
master.gpg
master_sec.gpg

$ gpg --with-fingerprint --keyid-format LONG --import-options show-only --import ./keys/master.gpg
pub   rsa4096/985269A0B58C25A5 2022-02-17 [SCEA]
      Key fingerprint = 6805 C30E 66B0 0B08 5D24  D9EC 9852 69A0 B58C 25A5
uid                            Github Master Key <security@github.com>
sub   rsa4096/D633FCD383735413 2022-02-17 [SEA]

将这两个数据填进修改后的 vault.rb,并拷贝到原来文件的位置上(patch 它之后校验才能通过)。然后通过环境变量填入适当的参数运行 keygen.rb 即可。

需要注意的是,LICENSE_NAME 的值将会作为 GHE 界面上显示的企业名称,在授权文件创建后就没办法修改了。

ruby enc.rb vault.rb > vault.enc.rb  # optional
cp vault.enc.rb /data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto/vault.rb

LICENSE_NAME="Known Rabbit" \
LICENSE_EMAIL="sales@github.com" \
LICENSE_COMPANY="KR" \
LICENSE_KEY_REFERENCE_NUMBER=$(dd if=/dev/urandom bs=1 count=3 | xxd -ps) \
LICENSE_REFERENCE_NUMBER=$(dd if=/dev/urandom bs=1 count=3 | xxd -ps) \
LICENSE_ORDER=$(dd if=/dev/urandom bs=1 count=3 | xxd -ps) \
ruby keygen.rb

运行后,目录中将生成一个 license.ghl,因为我们完全是调用 GHE 自身代码创建的授权文件,所以只要文件创建成功,那这个文件就必然能被 GHE 校验为有效授权文件。将这个文件拷贝出来后面就可以用了。

需要覆盖的文件

总的来说,我们需要将我们修改的 vault.rb 覆盖到所有调用了这个文件的位置。

由于 GHE 的整体架构较为复杂,我们之前覆盖的位置只能够保证我们顺利通过 GHE 初始设置(位于 8443 端口的管理后台服务)。GHE 的大部分服务都位于 Docker 容器内,由 Nomad 负责这些服务的生命周期管理。

rootfs 中的文件位置:

# Patch Management Console
/data/enterprise-manage/current/vendor/gems/ruby/2.7.0/gems/enterprise-crypto-0.4.22/lib/enterprise/crypto/vault.rb

其中,验证了授权信息的容器包括 github-env-<RANDOM_UUID> (GHE 初始化时用它运行数据库结构迁移)与 github-unicorn-<RANDOM_UUID> (GHE 前台 Web 服务),其中 RANDOM_UUID 在每次服务启动时随机生成,里面的程序一旦关掉 (docker stop)后,就会自动被 nomad 删掉(docker rm)并创建一个新容器。所以必须在 image 里覆盖文件进行破解。在目前最新版 3.4.0.rc1 中,容器镜像名为 github:4780f0371bdde4e035b372fdd8f373c11a3ef9c2 。容器内部的 vault 文件位置:

/github/vendor/gems/2.7.1/ruby/2.7.0/gems/enterprise-crypto-0.4.23/lib/enterprise/crypto/vault.rb

在每次虚拟机启动、或通过管理后台修改系统设定时,nomad 会调用 /usr/local/share/enterprise/ghe-docker-load-images ,通过 docker load 的方法载入位于以下位置的镜像备份,日志记录于 /data/user/common/ghe-config.log

/data/docker-images/github:4780f0371bdde4e035b372fdd8f373c11a3ef9c2#sha256:c837ca7816c67152409c4f5446d0061e9d5b6329d6cd0d62bc504c0df416adc7.tar

因此,要么把这个镜像备份文件给删掉,让系统无法覆盖我们 patch 过的镜像。要么将这个镜像备份也给 patch 掉。patch 镜像备份涉及的细节颇多,不再赘述,具体方法请参见我的注册机中的安装脚本。

注册机的打包与封装——“破解专用操作系统”

破解 GHE 与其他的软件最大的不同在于,这个软件本身以一个完整操作系统的形式进行分发。

破解一个 PE 文件,我们只需要改几个汇编语句。 破解一个 Docker 容器,我们只需要把补丁文件 bind mount 进去。 但破解一个操作系统呢?如果我们想在操作系统启动前破解它,那只能用魔法打败魔法——另起一个操作系统来破解它了。

现在考考大家,体积最迷你的 Linux 发行版是什么?

Arch Linux?

不不不,那东西可太大坨了,Live CD 就六百多兆,我一顿精简后最小也还 480M。Alpine Linux 或许还靠点谱,但我们一般都拿它做容器的底包,加上内核打包起来也有一百多兆。

现在我们请 Slitaz Linux 坐到主席台上来。Slitaz Linux 那可就太厉害了。整个 rootfs 压缩前是 29M,加上 Linux 内核压缩到一起,输出的 ISO 只有 16M!

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220219125952-cxemkmf.png

这才是适合制作咱们“破解专用操作系统”的基础镜像。我们命名为 ghec-os(Github Enterprise Crack OS

Slitaz Linux 提供了一个名为 Tazlito 的 ISO 定制工具,简而言之,我们只需要用它生成一个模板(flavor),在模板中按目录结构填入我们额外添加的文件,并在配置文件中填写我们需要添加的软件包即可。官方文档描述的总体流程如下

Tazlito automates the process of building a customized ISO, and the method is quite straight-forward:

  1. find a template (flavor) to work on ($ tazlito list-flavors)
  2. download the template ($ tazlito get-flavor flavor_name)
  3. look at the flavor’s packages ($ tazlito show-flavor flavor_name)
  4. extract the flavor ($ tazlito extract-flavor flavor_name)
  5. put in a rootfs folder (make one by copying in files from /etc/ to /home/slitaz/distro )
  6. add additional files using the folder /home/slitaz/distro/addfiles (optional)
  7. change options ($ tazlito configure)
  8. create the ISO image ($ tazlito gen-distro)
  9. burn to CD or USB stick ($ tazusb gen-iso2usb)

打包 ghec-os 的过程,也是一个漫长的、遇到各种问题并解决问题的过程。

简而言之,在 ghec-os 中,我在官方 base flavor 的基础上,添加了一个修改 GHE 根文件系统的程序(crack.sh),这个程序替换根密钥、中间密钥并将另一个程序(crack-step2.sh)注入 GHE 启动项,在 Nomad 启动 Docker 容器后对容器进行检测,并按需破解容器镜像与容器镜像备份,同时将实时状态显示在 VM 终端上(未破解、部分破解、破解运行中、破解成功待重启、已破解)。

除此之外,我还修改了默认用户密码为 root:root,修改 motd 登录提示信息,还在 /usr/bin 目录下添加了一个软链接,这样登录 root 用户后,直接运行 crack 即可完成破解。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220222234059-65zvlfg.png

打包过程中遇到的一些问题点总结:

  1. Slitaz 系统的包管理器安装源域名,114dns 无法解析,换成阿里DNS可解决
  2. 打包时必须添加名为 linux-scsi 的包。VMWare 使用的 SCSI 控制器需要这个包中的驱动,否则识别不到盘。
  3. 运行 tazlito gen-distro 时必须添加名为 stripped=1 环境变量,这是 tazlito 的 bug,否则不能正常识别 distro-packages.list 中的包列表。
  4. distro-packages.list 这个文件中不能包含空行,否则构建 ISO 时会闪退。名字里带 - 减号的包不能位于列表末尾,需要插在中间。

在编写破解 Shell 脚本的过程中,我也遇到了不小的挑战。和我一样有下面这些疑问的朋友,可以自行提取注册机中的程序,看看我是如何实现下面的需求。

  1. 没有 bash、没有 tput 的时候,怎么在 Shell 中显示彩色的文字?
  2. 如何验证一个文件的完整性?如何验证一个 container 内单个文件的完整性?如何验证一个 image 内单个文件的完整性?如何验证一个镜像整体的完整性?
  3. 如何用 Bash 实现多进程?如何实现子进程的状态监控?如何在进程间、函数间传递消息?如何像 systemd 一样,维护程序状态、捕获输出与子进程状态码?
  4. 如何保证一个稳定的程序状态?例如对抗目标程序的更新,破解后跑起来不崩,保证破解程序多次运行时结果的一致性(避免备份被覆盖、双重打补丁等等)。

注册机搞定了,我们简单来讲讲怎么用。

注册机的使用

按照官方教程导入 GHE 的 OVA 后,在虚拟机 BIOS 中设置 CDROM 为优先启动项,进入 Slitaz Linux 启动界面后,一顿回车进入系统。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223222231-yhexxi6.png

登陆后,运行命令 crack。第一个操作前会提示用户确认,输入 y 回车即可。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223222416-bxi858n.png

运行到最后提示 Crack module loaded 即可。在虚拟机设置中,取消光驱“在启动时连接“的选项。然后重新启动虚拟机即可。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223222615-kpwwq6w.png

重启后,访问 GHE 的 IP,上传 License 并完成首次初始化配置。强烈建议在初始化配置时添加一个 SSH Key,这样后面出错的时候可以进去看日志。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223223202-szi69dv.png

完成初始化配置后,会进入下图右侧的等待界面,此时可以注意到 Storage 中显示的占用空间会越来越大,这个时候,GHE 在解压 docker 镜像。当空间增长到 13G 左右时,GHE 会启动所有的 docker 容器开始数据库结构迁移。注册机在这个时候开始破解 docker 镜像,同时显示一行蓝色的字指示运行状态和子进程的 PID。

当破解运行完成后,在 VMWare 中选择”重新启动客户机”,别点强制重启。右边此时会运行到 Running migrations。不用等它运行完成,不重启它会报错的。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223225233-0ehxfg8.png

重启后,GHE 会继续运行配置过程。等着就好啦。如果一切正常的话,配置页面和终端中都是一片绿。

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223225514-4u672yh.png

然后点 Visit your instance 就能看到主界面啦!

/zh/2022/github-enterprise-reverse-engineering/assets/image-20220223231149-bcpuc73.png

关于获取注册机:关注公众号 Rabbit Unknown,在后台回复:ghec-os 获取下载地址。md5sum:

8c053c113054c4d2e089b2192e393147  ghe-crack.iso

结语

GHE 是一个充满了雄心壮志的产品,从最开始单纯的 Git 托管,到后来的 CI/CD (Actions)、Package Repo、Discussion、Security Advisory,到现在全功能的 on-premise 版本。在试用 GHE 的过程中,从产品的角度,微软一定致力于将它打造为 Gitlab EE 的有力竞品。我个人就认为,Github Actions 比 Gitlab CI/CD Pipelines 要整整先进一个时代。

比较有趣的是,Github Enterprise 的主体框架是 ruby 写的,但 Github Actions 部分却是用 C# 加 Powershell 写的。看来果然是微软之收购后的产物呀。

在享受 Github 带来的所有便利的同时,客观的来讲,我也发现 GHE 也存在非常非常多的问题。

  1. 整个系统对运维人员非常不友好。

    1. 看日志非常不方便。服务全跑在 Docker 里,但日志却不在 Docker logs 里,且分散在各个目录里,官方没有文档说明。
    2. 没办法配置各个组件的开关和实例伸缩,比如 Actions 功能即使不使用,后台的 docker 服务也不会关。
    3. 用了 Docker,服务调度却不是常见的 compose、swarm、k8s,而是 nomad。
  2. 资源占用极为夸张,开机占用 15G 内存、70G 硬盘,运行 12 小时后磁盘写入量 10G——这还是实例中一个项目都没有的情况下。

  3. 不能忍的小 bug。

    1. 数据库似乎没开时区支持,访问所有与时间相关的设置都会 HTTP 500 崩溃(定时提醒、限时个人状态等)。怎么外国人不配用是吧?
    2. Github Actions 与 Packages 功能需要 S3 Storage,但分析容器里面的 Powershell 脚本发现,部署服务里硬编码了一个 region code us-east-1 导致 Action 部署过程死循环。怎么外国人不配用是吧?
    3. 修改任何配置都需要重启所有服务,重启时间以小时计算。最长的一次用了 12 小时。这种 down time 怕是运维的电话都得被打爆了。
  4. 后端是 Ruby 和 C# 缝起来的,虽然前端看起来集成度其实蛮高,但跑了 MySQL + MSSQL 两个数据库直接导致内存占用飙升,MSSQL 服务还会随时间推移逐渐占满内存(16G)。真有你的呀 MS。

  5. 支持企业内部 CA(证书手动导入),但不支持企业内部的私有 ACME 服务,修改脚本里硬编码的 ACME 服务器地址能搞定,但不是全世界都用 LetsEncrypt 的好吗?

最后想说的是,个人自用的话,除非家里有机房,否则功能本身带来的生产力提升收益,远远抵不上计算/存储资源(包括电费)消耗的成本。我与大家分享这个注册机,也是希望通过大家一起学(bai)习(piao)它,有机会给微软提提 bug,也许它能好好优化优化吧!