在推文中我提到了一嘴,研究apple music里提到的各种so,可以帮助我们获得最接近原生的登录方法。
Tweet: https://x.com/0x88FFA357/status/2042842471999377841
并且看到了这样一个wrapper程序用来下载apple music。但是它用了两套二进制,一个放在外面不用ndk,纯glibc,另一个放在chroot里,ndk的实现用来适配android环境。这是必须的吗?
FROM scratch
显然不是,我们知道,docker实际上可以被理解成一个分层的沙盘。在Dockerfile构建时,每一个命令(RUN,COPY,..etc)都是一层,每层中保存了这一层被放进来的内容。这样的增量保存是Docker广受好评的原因。那么,最开始的系统镜像是怎么打包的呢?
答案就是本节标题`FROM scratch`,这里会给一个空白的沙盘,文件系统为空。所以我们只需要把wrapper/rootfs直接放进根目录,我们就不再需要一个单独的wrapper来启动chroot了。
这样就构造了一个“伪”android环境。于是我们写一个简单程序放进去就能用了!
但是这还不是我想要的效果。可以看到另一个程序‣中用到了mp4box这种重依赖glibc的程序。大家第一想法显然都应该是连接成static放到容器中就能用了。但是,能不能不这样呢?
INTERPRETER,LOADER
这里是linux的另一个有意思的点。在启动linux程序时,走exec*的系统调用来启动。启动时,linux会检查程序的头标志符,如果存在PT_INTERP说明程序依赖动态加载的loader。在glibc环境下是/lib64/ld-linux-x86-64.so.2,在android上是/system/bin/loader64。内核会启动这个程序来进行依赖的加载。最后再运行程序。
所以在我们打ctf的时候会用到patchelf这个程序来修改程序头,让它使用我们期望的libc。
💡 不过patchelf是给nixos写的谁能想到…
所以怎么让glibc和bionic libc共存呢?
实际上我们只需要把两边都loader和so文件都放进来到对应的位置,实际上直接运行就能运行了。位置
下面是我写的测试Dockerfile
FROM debian:bookworm-slim AS mp4boxARG GPAC_APT_URI="https://dist.gpac.io/gpac/linux/debian"ARG GPAC_APT_COMPONENT="nightly"RUN apt-get update \ && set -eux \ && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ binutils \ && install -m 0755 -d /etc/apt/keyrings \ && curl -fsSL https://dist.gpac.io/gpac/linux/gpg.asc -o /etc/apt/keyrings/gpac.asc \ && chmod a+r /etc/apt/keyrings/gpac.asc \ && printf 'Types: deb\nURIs: %s\nSuites: %s\nComponents: %s\nSigned-By: /etc/apt/keyrings/gpac.asc\n' \ "$GPAC_APT_URI" \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" \ "$GPAC_APT_COMPONENT" \ > /etc/apt/sources.list.d/gpac.sources \ && apt-get update \ && apt-get install -y --no-install-recommends gpac \ && rm -rf /var/lib/apt/lists/* \ && mp4box_path="$(command -v MP4Box || command -v mp4box)" \ && test -x "$mp4box_path" \ && libgpac_path="$(find /usr/lib /lib -name 'libgpac.so*' ! -name '*.a' | sort | head -n 1)" \ && test -n "$libgpac_path" \ && test -e "$libgpac_path" \ && interp="$(readelf -l "$mp4box_path" | sed -n 's/.*Requesting program interpreter: \(.*\)]/\1/p')" \ && test -n "$interp" \ && module_paths="$(if test -d /usr/lib/gpac; then find /usr/lib/gpac -type f -name '*.so' | sort; fi)" \ && deps="$( \ { \ ldd "$mp4box_path" "$libgpac_path"; \ if test -n "$module_paths"; then ldd $module_paths; fi; \ } | awk ' \ /=> \// { print $3 } \ /^\// && $1 !~ /:$/ { print $1 } \ ' \ | sort -u \ )" \ && install -d /out/usr/bin /out/usr/lib /out/lib64 /out/etc/ssl/certs \ && ln -s usr/lib /out/lib \ && cp -a "$mp4box_path" /out/usr/bin/MP4Box \ && if test -e /usr/bin/mp4box; then cp -a /usr/bin/mp4box /out/usr/bin/mp4box; fi \ && if test -d /usr/lib/gpac; then cp -a /usr/lib/gpac /out/usr/lib/gpac; fi \ && for dep in "$interp" "$libgpac_path" $deps; do \ clean_dep="${dep%:}"; \ real="$(readlink -f "$clean_dep")"; \ install -d "/out$(dirname "$real")"; \ cp -a "$real" "/out$real"; \ if test -L "$clean_dep"; then \ case "$clean_dep" in \ /lib/*) \ link_path="/usr$clean_dep"; \ ;; \ *) \ link_path="$clean_dep"; \ ;; \ esac; \ install -d "/out$(dirname "$link_path")"; \ cp -a "$clean_dep" "/out$link_path"; \ fi; \ done \ && for alt in \ /etc/alternatives/libblas.so.3-x86_64-linux-gnu \ /etc/alternatives/liblapack.so.3-x86_64-linux-gnu; do \ if test -L "$alt"; then \ alt_target="$(readlink "$alt")"; \ install -d "/out$(dirname "$alt")" "/out$(dirname "$alt_target")"; \ cp -a "$alt" "/out$alt"; \ cp -a "$alt_target" "/out$alt_target"; \ fi; \ done \ && if test -f /etc/ssl/certs/ca-certificates.crt; then \ cp -a /etc/ssl/certs/ca-certificates.crt /out/etc/ssl/certs/ca-certificates.crt; \ fiFROM scratchCOPY --from=mp4box /out/ /ENTRYPOINT ["/usr/bin/MP4Box"]CMD ["-version"]经过测试这也能运行。
于是我们只需要把刚刚的两个结合到一起,就能获得glibc和bionic libc共存的程序。
细节代码见
FROM debian:bookworm-slim AS ffmpegARG FFMPEG_STATIC_URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl xz-utils \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /opt/ffmpeg \ && curl -fsSL "$FFMPEG_STATIC_URL" -o /tmp/ffmpeg.tar.xz \ && tar -xJf /tmp/ffmpeg.tar.xz -C /opt/ffmpeg --strip-components=1 \ && test -x /opt/ffmpeg/ffmpeg \ && test -x /opt/ffmpeg/ffprobe \ && rm -f /tmp/ffmpeg.tar.xzFROM node:22-bookworm AS frontend-builderWORKDIR /app/frontendCOPY frontend/package.json frontend/package-lock.json ./RUN npm ciCOPY frontend ./RUN npm run buildFROM rust:bookworm AS builderARG ANDROID_NDK_URL="https://dl.google.com/android/repository/android-ndk-r25c-linux.zip"ARG GIT_COMMITRUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl unzip \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /opt/android-ndk \ && curl -fsSL "$ANDROID_NDK_URL" -o /tmp/android-ndk.zip \ && unzip -q /tmp/android-ndk.zip -d /opt/android-ndk \ && ndk_dir="$(find /opt/android-ndk -mindepth 1 -maxdepth 1 -type d | head -n 1)" \ && mv "$ndk_dir" /opt/android-ndk/current \ && rm -f /tmp/android-ndk.zipENV ANDROID_NDK_HOME=/opt/android-ndk/currentENV GIT_COMMIT=${GIT_COMMIT}RUN cargo install --locked cargo-ndkRUN rustup target add x86_64-linux-androidWORKDIR /appCOPY . .COPY --from=frontend-builder /app/frontend/dist /app/frontend/distRUN cargo ndk -t x86_64 build --release --bin mainFROM debian:bookworm-slim AS rootfsARG ROOTFS_ARCHIVE_URL="https://github.com/WorldObservationLog/wrapper/archive/refs/heads/main.tar.gz"# Android's dynamic loader must stay executable. Containerd refuses to start# `/main` when the archived rootfs ships `system/bin/linker64` without the# execute bit.RUN apt-get update \ && apt-get install -y --no-install-recommends...这就是神奇的linux魅力啊,哪怕最后删到一个文件没有里它还是能正常运行的。