用手机打 CTF 种什么体验

尝试不用电脑,只拿一台iPhone去参加看雪CTF2020 (娱乐向)

视频地址: https://www.bilibili.com/video/BV1Da4y1p7F3

背景

最近 iSH 在 Apple Store 上架了,之前一直抢不到 testflight 的配额,难得强管控的苹果会让这种 Terminal 类的应用发布,所以第一时间下载来玩玩。测试之后发现可以当做是一个简单的 Linux 虚拟机,至少常用的命令都没什么问题,正好看到看雪论坛有个 CTF,于是就试一下,能不能用手机来打一局 CTF :)

环境

iSH 是一个开源的 Terminal Emulator,在 iOS 中运行,使用用户态的 x86 指令模拟以及 syscall 翻译实现。iSH 中使用的是 Alphine Linux 镜像,用过 Docker 的应该都不会陌生。Alphine 是个非常轻量级的 Linux 发行版,在官网上可以直接下载各个平台预编译的镜像,平时有测试内核需求又不满足于 ramfs 的可以使用下载的镜像进行方便测试。

在 iSH 中已经有了一个简单的 Alphine 环境,可能是是因为苹果商店审核的原因,在其中并没有预置包管理工具。我们可以下载一个完整的 iso 重新挂载,但是有更简单的方法,直接从 Alphine Packages 中下载静态编译版本的 apk-tools-static:

wget http://dl-cdn.alpinelinux.org/alpine/latest-stable/main/x86/apk-tools-static-2.10.5-r1.apk
tar -zxvf apk-tools-static-2.10.5-r1.apk
./sbin/apk.static -X https://mirrors.tuna.tsinghua.edu.cn --initdb add apk-tools
apk update
rm -rf ./sbin

我们用下载的 apk-tools-static 来安装 apk-tools,然后包管理工具 apk 就可以正常使用了。如果初始化的时候没有指定镜像源,可以使用下面的命令修改为清华大学的镜像源,加快国内的访问速度:

sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories

之后需要的工具就可以自行安装了,我这里只需要 radare2 和 Python:

apk add radare2
apk add python3

题目是看雪 CTF2020 的签到题: https://ctf.pediy.com/game-season_fight-158.htm

Write Up

解题过程在文章开头的视频中已经可以看到了,这里还是用文字介绍下解题思路。

首先 rabin2 查看目标程序的信息:

$ rabin2 -I kctf2020.exe
arch     x86
baddr    0x140000000
binsz    12288
bintype  pe
bits     64
canary   false
retguard false
class    PE32+
cmp.csum 0x0000bcb6
compiled Thu Nov 12 03:49:32 2020
crypto   false
dbg_file D:\Users\admin\Documents\Visual Studio 2015\Projects\ctf2020\ConsoleApplication1\x64\Release\ConsoleApplication1.pdb
endian   little
havecode true
hdr.csum 0x00000000
guid     4EF50FB8F5E74CC481D01589FC111B1A1
laddr    0x0
lang     c
linenum  false
lsyms    false
machine  AMD 64
maxopsz  16
minopsz  1
nx       true
os       windows
overlay  false
cc       ms
pcalign  0
pic      true
relocs   false
signed   false
sanitiz  false
static   false
stripped false
subsys   Windows CUI
va       true

可以看到是 PE64 的的程序,鉴于是签到题,所以就直接逆向分析了:

$ r2 kctf2020.exe
 -- 💺
[0x14000154c]> s main
[0x140001000]> af
[0x140001000]> pd 20
            ;-- section..text:
┌ 459: int main (int argc, char **argv, char **envp);
│ bp: 7 (vars 7, args 0)
│ sp: 14 (vars 14, args 0)
│ rg: 0 (vars 0, args 0)
│           0x140001000      4055           push rbp                   ; [00] -r-x section size 8192 named .text
│           0x140001002      488dac24c0fe.  lea rbp, [rsp - 0x140]
│           0x14000100a      4881ec400200.  sub rsp, 0x240
│           0x140001011      488b05e82f00.  mov rax, qword [section..data] ; [0x140004000:8]=0x2b992ddfa232 ; "2\xa2\xdf-\x99+"
│           0x140001018      4833c4         xor rax, rsp
│           0x14000101b      488985300100.  mov qword [var_130h], rax
│           0x140001022      33d2           xor edx, edx
│           0x140001024      488d4c2430     lea rcx, [var_30h]
│           0x140001029      41b800010000   mov r8d, 0x100             ; 256
│           0x14000102f      e8ce0e0000     call 0x140001f02
│           0x140001034      33d2           xor edx, edx
│           0x140001036      488d4d30       lea rcx, [var_bp_30h]
│           0x14000103a      41b800010000   mov r8d, 0x100             ; 256
│           0x140001040      e8bd0e0000     call 0x140001f02
│           0x140001045      488d0dd42100.  lea rcx, str.KCTF_2020     ; 0x140003220 ; "KCTF 2020!\n"
│           0x14000104c      e8ff010000     call 0x140001250
│           0x140001051      488d0dd82100.  lea rcx, str.http:__bbs.pediy.com ; 0x140003230 ; "http://bbs.pediy.com\n"
│           0x140001058      e8f3010000     call 0x140001250
│           0x14000105d      488d0de42100.  lea rcx, str.Please_input_your_flag: ; 0x140003248 ; "Please input your flag: "
│           0x140001064      e8e7010000     call 0x140001250

0x140001250 地址处经常有调用,根据参数可以猜测是打印函数,验证一下:

[0x140001000]> s 0x140001250
[0x140001250]> af
[0x140001250]> pdf
            ; CALL XREFS from main @ 0x14000104c, 0x140001058, 0x140001064, 0x14000119b, 0x1400011ab
┌ 85: int printf (const char *format);
; var int64_t var_20h_2 @ rsp+0x20
; var int64_t var_8h @ rsp+0x50
; var int64_t var_10h @ rsp+0x58
; var int64_t var_18h @ rsp+0x60
; var int64_t var_20h @ rsp+0x68
; arg int64_t arg1 @ rcx
; arg int64_t arg2 @ rdx
; arg int64_t arg3 @ r8
; arg int64_t arg4 @ r9
│           0x140001250      48894c2408     mov qword [var_8h], rcx    ; arg1
│           0x140001255      4889542410     mov qword [var_10h], rdx   ; arg2
│           0x14000125a      4c89442418     mov qword [var_18h], r8    ; arg3
│           0x14000125f      4c894c2420     mov qword [var_20h], r9    ; arg4
│           0x140001264      53             push rbx
│           0x140001265      56             push rsi
│           0x140001266      57             push rdi
│           0x140001267      4883ec30       sub rsp, 0x30
│           0x14000126b      488bf9         mov rdi, rcx
│           0x14000126e      488d742458     lea rsi, [var_10h]
│           0x140001273      b901000000     mov ecx, 1
│           0x140001278      ff150a1f0000   call qword [sym.imp.api_ms_win_crt_stdio_l1_1_0.dll___acrt_iob_func] ; [0x140003188:8]=0x3a98 reloc.api_ms_win_crt_stdio_l1_1_0.dll___acrt_iob_func
│           0x14000127e      488bd8         mov rbx, rax
│           0x140001281      e8baffffff     call fcn.140001240
│           0x140001286      4533c9         xor r9d, r9d
│           0x140001289      4889742420     mov qword [var_20h_2], rsi
│           0x14000128e      4c8bc7         mov r8, rdi
│           0x140001291      488bd3         mov rdx, rbx
│           0x140001294      488b08         mov rcx, qword [rax]
│           0x140001297      ff15e31e0000   call qword [sym.imp.api_ms_win_crt_stdio_l1_1_0.dll___stdio_common_vfprintf] ; [0x140003180:8]=0x3aaa reloc.api_ms_win_crt_stdio_l1_1_0.dll___stdio_common_vfprintf
│           0x14000129d      4883c430       add rsp, 0x30
│           0x1400012a1      5f             pop rdi
│           0x1400012a2      5e             pop rsi
│           0x1400012a3      5b             pop rbx
└           0x1400012a4      c3             ret

果然是打印函数,直接重命名一下,清爽一些:

[0x140001250]> af printf
[0x140001250]> s-
[0x140001000]> pd 20
            ;-- section..text:
┌ 459: int main (int argc, char **argv, char **envp);
│ bp: 7 (vars 7, args 0)
│ sp: 14 (vars 14, args 0)
│ rg: 0 (vars 0, args 0)
│           0x140001000      4055           push rbp                   ; [00] -r-x section size 8192 named .text
│           0x140001002      488dac24c0fe.  lea rbp, [rsp - 0x140]
│           0x14000100a      4881ec400200.  sub rsp, 0x240
│           0x140001011      488b05e82f00.  mov rax, qword [section..data] ; [0x140004000:8]=0x2b992ddfa232 ; "2\xa2\xdf-\x99+"
│           0x140001018      4833c4         xor rax, rsp
│           0x14000101b      488985300100.  mov qword [var_130h], rax
│           0x140001022      33d2           xor edx, edx
│           0x140001024      488d4c2430     lea rcx, [var_30h]
│           0x140001029      41b800010000   mov r8d, 0x100             ; 256
│           0x14000102f      e8ce0e0000     call 0x140001f02
│           0x140001034      33d2           xor edx, edx
│           0x140001036      488d4d30       lea rcx, [var_bp_30h]
│           0x14000103a      41b800010000   mov r8d, 0x100             ; 256
│           0x140001040      e8bd0e0000     call 0x140001f02
│           0x140001045      488d0dd42100.  lea rcx, str.KCTF_2020     ; 0x140003220 ; "KCTF 2020!\n"
│           0x14000104c      e8ff010000     call printf                ; int printf(const char *format)
│           0x140001051      488d0dd82100.  lea rcx, str.http:__bbs.pediy.com ; 0x140003230 ; "http://bbs.pediy.com\n"
│           0x140001058      e8f3010000     call printf                ; int printf(const char *format)
│           0x14000105d      488d0de42100.  lea rcx, str.Please_input_your_flag: ; 0x140003248 ; "Please input your flag: "
│           0x140001064      e8e7010000     call printf                ; int printf(const char *format)

同理,把 scanf 也重命名上,先看程序开头的逻辑:

           0x140001069      41b800010000   mov r8d, 0x100             ; 256
           0x14000106f      488d542430     lea rdx, [var_30h]
           0x140001074      488d0de92100.  lea rcx, [0x140003264]     ; "%s"
           0x14000107b      e860010000     call scanf                 ; int scanf(const char *format)
           0x140001080      488d542430     lea rdx, [var_30h]
           0x140001085      4883c9ff       or rcx, 0xffffffffffffffff
           0x140001089      0f1f80000000.  nop dword [rax]
       ┌─> 0x140001090      48ffc1         inc rcx
          0x140001093      803c0a00       cmp byte [rdx + rcx], 0
       └─< 0x140001097      75f7           jne 0x140001090
           0x140001099      83f90c         cmp ecx, 0xc               ; 12
       ┌─< 0x14000109c      0f8502010000   jne 0x1400011a4
          0x1400010a2      807c243066     cmp byte [var_30h], 0x66
      ┌──< 0x1400010a7      0f85f7000000   jne 0x1400011a4
      ││   0x1400010ad      807c24316c     cmp byte [var_31h], 0x6c
     ┌───< 0x1400010b2      0f85ec000000   jne 0x1400011a4
     │││   0x1400010b8      807c243261     cmp byte [var_32h], 0x61
    ┌────< 0x1400010bd      0f85e1000000   jne 0x1400011a4
    ││││   0x1400010c3      807c243367     cmp byte [var_33h], 0x67
   ┌─────< 0x1400010c8      0f85d6000000   jne 0x1400011a4
   │││││   0x1400010ce      807c24347b     cmp byte [var_34h], 0x7b
  ┌──────< 0x1400010d3      0f85cb000000   jne 0x1400011a4
  ││││││   0x1400010d9      807c243b7d     cmp byte [var_3bh], 0x7d
 ┌───────< 0x1400010de      0f85c0000000   jne 0x1400011a4

var_30h 是输入的字符串,分别进行字节和长度比对,要求输入字符串长度为 12,并且格式为 flag{xxxxxx},这里有个技巧是 radare2 中 VV 模式下的跳转:

  • tab/shift+tab: 跳转到下一个/前一个 basic block
  • t/f: 跳转到 true/false 分支
  • u/U: undo/redo 跳转

然后是 flag 中间字符的判断:

0x1400010e4      440fb74c2439   movzx r9d, word [var_39h]
0x1400010ea      4c8d442420     lea r8, [var_20h]
0x1400010ef      448b542435     mov r10d, dword [var_35h]
0x1400010f4      33c0           xor eax, eax
0x1400010f6      6644894c2424   mov word [var_24h], r9w
0x1400010fc      8bd0           mov edx, eax
0x1400010fe      4489542420     mov dword [var_20h], r10d
0x140001103      0f1f4000       nop dword [rax]
0x140001107      660f1f840000.  nop word [rax + rax]

这里使用 mov dword 来进行拷贝,将 var_35h 拷贝 到 var_20h 中,同时 r8 寄存器指向 var_20h。接着先循环逐个判断字符c - 0x30是否小于 9,如果有大于 9 的就报错:

 ┌─> 0x140001110      410fb608       movzx ecx, byte [r8]
    0x140001114      80e930         sub cl, 0x30               ; 48
    0x140001117      80f909         cmp cl, 9                  ; 9
┌──< 0x14000111a      0f8784000000   ja 0x1400011a4
│╎   0x140001120      ffc2           inc edx
│╎   0x140001122      49ffc0         inc r8
│╎   0x140001125      83fa06         cmp edx, 6                 ; 6
│└─< 0x140001128      72e6           jb 0x140001110
    0x14000112a      ...

接着经过下面的判断,记得 var_30h 是我们的输入,长度为 12,格式为 flag{xxxxxx}:

     0x14000112a      0fb6542437     movzx edx, byte [var_37h]
     0x14000112f      4c8d053e2100.  lea r8, [0x140003274]      ; "2;=EFI"
     0x140001136      0fb64c2436     movzx ecx, byte [var_36h]
     0x14000113b      80ea30         sub dl, 0x30               ; 48
     0x14000113e      80e930         sub cl, 0x30               ; 48
     0x140001141      44885530       mov byte [var_bp_30h], r10b
     0x140001145      4102ca         add cl, r10b
     0x140001148      4180e930       sub r9b, 0x30              ; 48
     0x14000114c      02d1           add dl, cl
     0x14000114e      884d31         mov byte [var_bp_31h], cl
     0x140001151      0fb64c2438     movzx ecx, byte [var_38h]
     0x140001156      80e930         sub cl, 0x30               ; 48
     0x140001159      885532         mov byte [var_bp_32h], dl
     0x14000115c      02ca           add cl, dl
     0x14000115e      488d5530       lea rdx, [var_bp_30h]
     0x140001162      4402c9         add r9b, cl
     0x140001165      884d33         mov byte [var_bp_33h], cl
     0x140001168      0fb64c243a     movzx ecx, byte [var_3ah]
     0x14000116d      80e930         sub cl, 0x30               ; 48
     0x140001170      44884d34       mov byte [var_bp_34h], r9b
     0x140001174      4102c9         add cl, r9b
     0x140001177      884d35         mov byte [var_bp_35h], cl
     0x14000117a      660f1f440000   nop word [rax + rax]
 ┌─> 0x140001180      0fb60c02       movzx ecx, byte [rdx + rax]
    0x140001184      48ffc0         inc rax
    0x140001187      413a4c00ff     cmp cl, byte [r8 + rax - 1]
┌──< 0x14000118c      7516           jne 0x1400011a4
│╎   0x14000118e      4883f807       cmp rax, 7                 ; 7
│└─< 0x140001192      75ec           jne 0x140001180
    0x140001194      488d0de52000.  lea rcx, [0x140003280]     ; "You are winner!\n"
    0x14000119b      e8b0000000     call printf                ; int printf(const char *format)

flag{}中间的内容为flag,这里的逻辑可以写成伪代码:

buf[0] = flag[0]
buf[1] = buf[0] + flag[1] - 48
buf[2] = buf[1] + flag[2] - 48
buf[3] = buf[2] + flag[3] - 48
buf[4] = buf[3] + flag[4] - 48
buf[5] = buf[4] + flag[5] - 48
if buf == "2;=EFI":
  printf("You are winner!\n")

反向计算,可得:

flag[0] = buf[0]
flag[1] = buf[1] - buf[0] + 48
flag[2] = buf[2] - buf[1] + 48
flag[3] = buf[3] - buf[2] + 48
flag[4] = buf[4] - buf[3] + 48
flag[5] = buf[5] - buf[4] + 48

所以计算的 flag 为:

$ ./solve.py
flag: b'292813'

最终答案应该是 flag{292813},如果有 windows 环境的话可以进行动态验证,在 Linux/MacOS 环境中可以用 wine 来进行验证,如下:

$ wine64 kctf2020.exe
0025:err:plugplay:runloop_thread Couldn't open IOHIDManager.
0009:fixme:vcruntime:__telemetry_main_invoke_trigger (0x0)
KCTF 2020!
http://bbs.pediy.com
Please input your flag: 0009:fixme:msvcrt:MSVCRT__stdio_common_vfscanf options 3 not handled
flag{292813}
You are winner!
0009:fixme:vcruntime:__telemetry_main_return_trigger (0x0)

后记

安装了 iSH 之后,我的手机时间分配一度变成了这样:

time

手机上的 Terminal 可以用来做什么呢?这里可以列举一些:

  1. 用来 ssh 登录到服务器进行管理
  2. 用来随时查看 man page
  3. 用来编写和运行自动化脚本
  4. 进行一些简单的命令行操作

其中 ssh 对我来说是个刚需,因为在此之前苹果上一直没有一个好用且免费的 ssh client,只能用 Termius 勉强度日。不过可惜的是由于苹果的策略,iSH 暂时还不能在后台运行,不然直接 ssh -R 还能当做隧道用,岂不美哉?

当然,iSH 还是有很多局限性的,比如只能模拟 x86 的指令集,而且由于是软件模拟,因此运行效率比较低。如果要执行复杂的任务,比如 gcc 编译大型项目,还是最好在 PC 上运行了。