GitHub Actions로 Magisk Patch 자동화하기
Last updated: Oct 2, 2021
GitHub Actions를 이용하여 Magisk 패치를 자동화 해본 경험을 쓰고자 한다.
기존 방법
Magisk 패치 방법은 다음과 같다.
- Factory Image를 다운받는다.
- 압축 해제 후, boot.img를 추출한다.
- boot.img를 휴대폰에 옮긴 후, Magisk 앱에서 boot.img를 넣고 패치를 진행한다.
- 패치된 boot.img를 다시 컴퓨터에 옮긴 후, 휴대폰을 fastboot 모드로 진입시킨다.
fastboot
명령어를 이용하여 boot 영역에 패치된 boot.img를 플래시(flashing) 한다.- 재부팅 하면 완료
안드로이드 폰은 매달 보안 업데이트가 나오는데, root 권한을 획득하기 위하여 매달 한 번은 위와 같은 프로세스를 반복해야 한다. 간단하다고 생각할 수는 있겠으나 개인적으로 boot.img를 폰으로 옮겼다가 컴퓨터로 다시 옮기는 이러한 과정은 매우 비효율적이라고 생각했고, 매달 반복하다 보니 이제는 귀찮아졌다.
단축시킨 방법 (요약)
우선 가장 비효율적이라고 생각한 Magisk boot.img 패치하는 것부터 자동화를 하고자 했다.
- JSON 파일을 하나 만든다. 이는 수동으로 자동화 프로세스에서 변수를 통제하기 위함이다.
{
"name": "crosshatch",
"build_number": "RQ3A.210905.001",
"link": "https://dl.google.com/dl/android/aosp/crosshatch-rq3a.210905.001-factory-94be46cc.zip",
"checksum": "94be46cc1873c273aaf16df6af2020980f90a402ace9f3327c1b839093cc133c",
"magisk": "https://github.com/topjohnwu/Magisk/releases/download/v23.0/Magisk-v23.0.apk"
}
이름, 빌드번호, factory image 다운로드 링크, checksum, Magisk 앱 다운로드 링크를 입력하였다.
GitHub에 레포를 하나 만들고, GitHub Actions를 이용하여 workflow를 구축한다. Workflow 구축은
.github/workflows
에 yml 파일을 만들면 된다.작업 순서는 다음과 같다.
- Magisk 패치에 필요한 파일 받기
- Factory image 다운받아서 boot.img 추출하기
- Boot.img를 패치하고, 패치된 boot.img를 업로드하기
a번과 b번은 종속성이 없기 때문에, 병렬로 처리하기 위해 job을 분리하고, c번은 a번과 b번이 끝나야 시작할 수 있기 때문에 따로 종속성을 부여한 job을 생성하였다.
Workflow를 작동시키고, 결과물을 releases 탭에서 확인한다.
결과물 태그는
코드네임-빌드번호
로 설정하였다. 혹시나 모를 상황에 대비하여 작업하기 전 순정 boot.img 도 첨부해 준다.
구체적인 작동 원리
구제적으로 어떻게 구현했는지 알아보자. 우선 Magisk가 어떻게 boot.img를 패치하는지부터 설명해야 한다.
Magisk가 boot.img를 패치하는 방법
일단 Magisk가 boot.img를 패치하는 코드를 찾아야 한다. 패치하는 코드는 scripts/boot_patch.sh
에 있다. 구체적으로 어떤 패치가 이루어지는지는 이번 포스트에서 다루지 않겠다. 해당 파일의 주석을 읽어보면 사용방법을 설명해 놓았다.
#!/system/bin/sh
#######################################################################################
# Magisk Boot Image Patcher
#######################################################################################
#
# Usage: boot_patch.sh <bootimage>
#
# The following flags can be set in environment variables:
# KEEPVERITY, KEEPFORCEENCRYPT, RECOVERYMODE
#
# This script should be placed in a directory with the following files:
#
# File name Type Description
#
# boot_patch.sh script A script to patch boot image for Magisk.
# (this file) The script will use files in its same
# directory to complete the patching process
# util_functions.sh script A script which hosts all functions required
# for this script to work properly
# magiskinit binary The binary to replace /init
# magisk(32/64) binary The magisk binaries
# magiskboot binary A tool to manipulate boot images
# chromeos folder This folder includes the utility and keys to sign
# (optional) chromeos boot images. Only used for Pixel C.
#
#######################################################################################
해당 스크립트를 돌리기 위해 반드시 필요한 파일들을 적어 놓았다. 기재된 파일들을 같은 디렉터리에 넣고 해당 스크립트를 돌리기만 하면 끝이다. 하지만 여기서 문제가 발생한다. 바로 binary 들이다.
Magisk binaries
위에서 봤듯이 boot.img를 패치하기 위해서는 binary 파일이 3개나 필요하다. 하지만 자동화 과정에서 빌드를 하게 되면 시간이 너무 소모되기 때문에 prebuilt binary를 사용할 예정이다. 하지만 Magisk는 따로 prebuilt binary를 제공하지 않는다. Magisk v22.0 이전에는 Magisk Manager라는 안드로이드 앱과 패치를 할 때 사용하는 magisk.zip이라는 패치 파일로 제공을 했었다. magisk.zip에는 우리가 지금 필요한 binary들이 x86과 ARM 버전으로 들어가 있었으나, v22.0부터 Magisk Manager가 Magisk로 통합되면서 바이너리는 따로 제공하지 않게 되었다. 현재는 Magisk.apk 하나만 제공하고 있다.
하지만 v22.0부터 바뀐 설치방법에 대한 문서를 보면 deprecated 됐지만 커스텀 리커버리를 사용하는 경우에 설치 방법에 대한 설명이 나온다. 해당 설명을 읽어 보면 apk 파일을 zip으로 확장자를 변경하고 커스텀 리커버리에서 플래싱이 가능하다고 적혀있다…! 이 뜻은 apk 파일 안에 binary가 포함되어 있음을 알린다. 그래서 apk 파일을 압축을 풀어보았다.
├── AndroidManifest.xml
├── META-INF
│ ├── CERT.RSA
│ ├── CERT.SF
│ ├── MANIFEST.MF
│ ├── com
│ │ ├── android
│ │ │ └── build
│ │ │ └── gradle
│ │ │ └── app-metadata.properties
│ │ └── google
│ │ └── android
│ │ ├── update-binary
│ │ └── updater-script
│ └── services
│ ├── kotlinx.coroutines.CoroutineExceptionHandler
│ └── kotlinx.coroutines.internal.MainDispatcherFactory
├── assets
│ ├── addon.d.sh
│ ├── boot_patch.sh
│ ├── chromeos
│ │ ├── futility
│ │ ├── kernel.keyblock
│ │ └── kernel_data_key.vbprivk
│ ├── uninstaller.sh
│ └── util_functions.sh
├── classes.dex
├── lib
│ ├── arm64-v8a
│ │ └── libstub.so
│ ├── armeabi-v7a
│ │ ├── libbusybox.so
│ │ ├── libmagisk32.so
│ │ ├── libmagisk64.so
│ │ ├── libmagiskboot.so
│ │ └── libmagiskinit.so
│ ├── x86
│ │ ├── libbusybox.so
│ │ ├── libmagisk32.so
│ │ ├── libmagisk64.so
│ │ ├── libmagiskboot.so
│ │ └── libmagiskinit.so
│ └── x86_64
│ └── libstub.so
├── org
│ └── commonmark
│ └── internal
│ └── util
│ └── entities.properties
├── res
│ ├── --.xml
│ ├── 엄청 뭐가 많아서 생략
│ ├── ...
│ └── zq.xml
└── resources.arsc
lib<파일이름>.so
파일들이 우리가 찾는 바이너리이다…! x86 바이너리도 존재한다. 바로 로컬에서 테스트를 해봐야겠다.
Ubuntu에서 패치해보기
Ubuntu에서 패치가 가능하다면 당연히 GitHub Actions로 구현이 가능하다. 바로 테스트를 해봤더니…
이런 결과가 나왔다. 우선 보이는 것부터 살펴보자면 dos2unix 가 없다고 뜬다. 찾아보니 이는 Ubuntu에 존재하는 패키지이기에 해결 가능한 것 같다.
하지만 여기서 getprop
이 문제가 된다. getprop
은 안드로이드에만 존재하는 커맨드이다. 휴대폰의 속성(properties)를 가져올 때 사용하는 커맨드인데, Ubuntu에 이게 존재할 리가 없다. 따라서 getprop
커맨드를 직접 구현하기로 했다.
getprop
커맨드 구현하기
우선 안드로이드에서 getprop
을 실행하면 어떻게 나오는지 알아보자.
[속성 키]: [속성 값]
순서로 차례대로 나온다. 근데 getprop
실행 시 나오는 속성들이 무려 570개가 넘는데 이를 다 구현하는 건 무리가 있다. 또한 모든 값들이 Magisk 패치할 때 필요하지 않기 때문에 호출하는 값들만 찾아봤다.
[ro.crypto.state]: [encrypted]
[ro.build.ab_update]: [true]
[ro.boot.slot_suffix]: [_a]
[init.svc.vold]: [running]
[ro.build.version.sdk]: [30]
[ro.product.cpu.abi]: [arm64-v8a]
스크립트를 찾아본 결과 위에 있는 6개 값들만 참조한다. 따라서 해당 출력 결과를 GitHub Actions에서 사용할 수 있게끔 GETPROP_OUTPUT
이라는 파일명으로 저장한다.
다음은 getprop
구현 차례이다. getprop
은 추가 인자가 없을 시엔 모든 속성 키와 속성 값을 출력하고, 특정 속성 키를 인자로 넣을 시 속성 값만 배출한다.
딱 이까지만 구현할 예정이다. 패치 스크립트에서 그 이상 쓰이지 않기 때문이다.
#!/bin/bash
# Ubuntu does not have command `getprop`. Only android does.
# This function is a simple implementation of getprop command, but
# gets value from GETPROP_OUTPUT file. <- You should copy/paste it from your phone.
# However is not fully implemented. Only shows property values when argument exists.
getprop() {
if [ -f GETPROP_OUTPUT ]; then
local prop_output=`cat GETPROP_OUTPUT`
if [ $# -eq 0 ]; then
# No specific property. Return whole getprop
echo "$prop_output"
else
local key="$1"
local key_value=`grep $key GETPROP_OUTPUT`
# IFS does not work in zsh!
IFS=':'
value=($key_value)
unset IFS
local result=${value[1]}
echo $result | sed 's/[][]//g'
fi
fi
}
export -f getprop
해당 스크립트를 실행함으로써 작성한 함수를 마지막에 export까지 해준다. 테스트해보자.
완벽하게 작동한다! 이제 아까 돌렸던 패치 스크립트를 다시 돌려보자.
interpreter error가 난다. 이는 boot_patch.sh
에 첫번째 줄에 interpreter가 /system/bin/sh
으로 되어있기 때문이다. 원래 Magisk 앱에서 돌리던 스크립트이다 보니 안드로이드 sh 경로로 맞춰져 있다.
sed
를 이용하여 Ubuntu에 알맞게 /bin/bash
로 바꿔준다.
성공이다! 이제 위에서 한 작업을 GitHub Actions로 옮기면 끝이다.
GitHub Actions에서 패치하기
GitHub Actions에서 구성한 workflow 코드는 너무 길어서 링크로 대체한다. 위에서 설명한 것과 같이 순서대로 진행하면 된다.
이 글을 쓰면서 추가적으로 보완하여 커밋 한 후 돌아간 workflow이다. 3분 만에 패치가 끝났다.
패치된 boot.img 테스트 해보기
진짜로 잘 패치가 됐는지 테스트를 해봐야 한다. 패치된 boot.img를 다운로드하고 fastboot 모드를 킨다.
성공은 했으나… 무한부팅이 되었다…ㅠㅠ 다시 순정 boot.img를 패치해 주었다.
원인은 간단했다. boot.img를 x86 바이너리로 패치해서 문제가 생겼던 거였다. 따라서 armeabi-v7a 폴더에 있는 binary를 이용해서 패치를 해줘야 한다. 하지만 armeabi-v7a는 ARM 디바이스이기 때문에 ARM 가상머신을 GitHub Actions에서 띄우고, 가상머신에서 작업해주어야 한다. 때마침 GitHub Actions 써드파티 패키지 중에서 ARM 가상머신을 지원하는 패키지가 있었다! 바로 적용시킨 후 테스트하니 정상 부팅이 되었고, Magisk 인식도 완벽했다.
해결해야 하는 것
이 게시글은 이 블로그뿐만이 아니라 VoLTE 관련 카페에서도 공유를 했다. 그중에서도 활발하게 활동하시는 누리로 님이 댓글을 달아 주셨는데, 내용은 다음과 같다.
하고싶은 얘기가 여러 가지 있지만, 다른 건 다 생략하고 가장 큰 것 하나만 이야기하겠습니다.
매지스크를 설치하려는 기기의 Magisk app에서 직접 boot.img를 패치해야 하는 중요한 이유가 있습니다. 그렇게 해야 순정 boot.img 파일이 /data 에 백업됩니다. 안 하면 백업이 안 생깁니다.
매지스크를 설치하려는 기기에서 Magisk 앱으로 직접 boot.img를 패치하지 않고, 다른 데서 미리 만들어둔 magisk_patched.img를 boot 파티션에 플래싱해도 일단 매지스크 설치는 됩니다. 또한 매지스크의 기능을 사용하는 데에도 문제가 없습니다. 하지만 그렇게 하면 나중에 매지스크를 제거하려고 할 때 문제가 생깁니다.
이미지 복구 - 순정 boot.img 파일의 백업이 없으므로 이미지 복구가 되지 않습니다.
완전히 제거 - 다른 과정은 문제없으나, 역시 순정 boot.img 파일의 백업이 없으므로 boot 파티션을 순정으로 복구하지 못합니다. 이 경우 fallback으로 램디스크의 변경사항을 일일히 되돌리는 방식으로 복구되는데, 이렇게 하면 boot 파티션이 순정과 100% 같아지지 않습니다. 그러면
- 재부팅 이후 bootloop가 발생하거나,
- 재부팅에 성공하더라도 나중에 OTA 시스템 업데이트가 되지 않는 문제가 발생합니다. 이렇게 한참 지나서 다른 곳에서 문제가 터지면 굉장히 당황하게 되며 문제의 원인이 무엇인지 생각해내기가 쉽지 않습니다.
그러므로 만일, 순정 boot.img를 Magisk app에서 직접 패치하지 않고 미리 패치된 magisk_patched.img를 플래싱해서 매지스크를 설치했다면, 매지스크 제거는
순정 boot.img 파일을 수동으로 boot 파티션에 플래싱하거나 (이건 ‘이미지 복구’와 같은 것이죠)
Magisk app에서 순정 boot.img를 패치
–> Magisk app 완전종료 (Recents screen에서 Magisk app을 스와이프하여 제거)
–> 이후 Magisk app을 다시 실행하고 7-8초 wait
이렇게 하여 순정 boot.img의 백업이 /data 에 생성되도록 한 다음, Magisk app에서 [제거] / [이미지 복구] 또는 [제거] / [완전히 제거] 를 해야 합니다.
만드신 것과 비슷한 tool에 대한 아이디어는 저도 예전에 잠깐 생각해 본 적이 있는데, boot.img가 백업되지 않는 문제를 깔끔하고 아름답게 해결할 방법이 머리속에 떠오르지 않았습니다.
혹시 이 문제는 어떻게 해결하실 계획인지, 가능하다면 그 부분에 대한 생각을 좀 듣고 싶습니다.
해당 부분을 해결하는 과정은 다음 포스팅으로 남길 예정이다.