在推文中我提到了一嘴,研究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了。

Docker
FROM debian:bookworm-slim AS rootfsARG ROOTFS_ARCHIVE_URL="https://github.com/WorldObservationLog/wrapper/archive/refs/heads/main.tar.gz"RUN apt-get update \    && apt-get install -y --no-install-recommends ca-certificates curl tar \    && rm -rf /var/lib/apt/lists/* \    && mkdir -p /tmp/rootfs /out \    && curl -fsSL "$ROOTFS_ARCHIVE_URL" -o /tmp/rootfs/rootfs.tar.gz \    && tar -xzf /tmp/rootfs/rootfs.tar.gz -C /tmp/rootfs \    && src_dir="$(find /tmp/rootfs -mindepth 2 -maxdepth 2 -type d -name rootfs | head -n 1)" \    && test -n "$src_dir" \    && cp -a "$src_dir" /out/rootfsFROM scratchCOPY --from=rootfs /out/rootfs /

这样就构造了一个“伪”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

Docker
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共存的程序。

细节代码见

Dockerfile

Text
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魅力啊,哪怕最后删到一个文件没有里它还是能正常运行的。