├── qilinglab-x86_64 ├── qilinglab-aarch64 ├── rootfs └── arm64_linux │ ├── proc │ └── sys │ │ └── kernel │ │ └── osrelease │ └── lib │ ├── ld-2.24.so │ ├── libc.so.6 │ └── ld-linux-aarch64.so.1 ├── solve.py └── README.md /qilinglab-x86_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zodf0055980/QilingLab_Writeup/HEAD/qilinglab-x86_64 -------------------------------------------------------------------------------- /qilinglab-aarch64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zodf0055980/QilingLab_Writeup/HEAD/qilinglab-aarch64 -------------------------------------------------------------------------------- /rootfs/arm64_linux/proc/sys/kernel/osrelease: -------------------------------------------------------------------------------- 1 | Linux rasp3 4.14.71-v7+ #1145 SMP Fri Sep 21 15:38:35 BST 2018 armv7l GNU/Linux 2 | -------------------------------------------------------------------------------- /rootfs/arm64_linux/lib/ld-2.24.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zodf0055980/QilingLab_Writeup/HEAD/rootfs/arm64_linux/lib/ld-2.24.so -------------------------------------------------------------------------------- /rootfs/arm64_linux/lib/libc.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zodf0055980/QilingLab_Writeup/HEAD/rootfs/arm64_linux/lib/libc.so.6 -------------------------------------------------------------------------------- /rootfs/arm64_linux/lib/ld-linux-aarch64.so.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zodf0055980/QilingLab_Writeup/HEAD/rootfs/arm64_linux/lib/ld-linux-aarch64.so.1 -------------------------------------------------------------------------------- /solve.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from qiling import * 3 | from qiling.const import QL_VERBOSE 4 | from qiling.os.mapper import QlFsMappedObject 5 | import struct 6 | 7 | sys.path.append("..") 8 | 9 | class Fake_urandom(QlFsMappedObject): 10 | def read(self, size): 11 | if(size > 1): 12 | return b"\x01" * size 13 | else: 14 | return b"\x02" 15 | def fstat(self): # syscall fstat will ignore it if return -1 16 | return -1 17 | def close(self): 18 | return 0 19 | 20 | class Fake_cmdline(QlFsMappedObject): 21 | 22 | def read(self, size): 23 | return b"qilinglab" # type should byte byte, string will error = = 24 | def fstat(self): # syscall fstat will ignore it if return -1 25 | return -1 26 | def close(self): 27 | return 0 28 | 29 | def my_syscall_uname(ql, write_buf, *args, **kw): 30 | buf = b'QilingOS\x00' # sysname 31 | ql.mem.write(write_buf, buf) 32 | 33 | buf = b'30000'.ljust(65, b'\x00') # important!! If not set will `FATAL: kernel too old` 34 | ql.mem.write(write_buf+65*2, buf) 35 | buf = b'ChallengeStart'.ljust(65, b'\x00') # version 36 | ql.mem.write(write_buf+65*3, buf) 37 | regreturn = 0 38 | return regreturn 39 | 40 | 41 | def my_syscall_getrandom(ql, write_buf, write_buf_size, flag , *args, **kw): 42 | buf = b"\x01" * write_buf_size 43 | ql.mem.write(write_buf, buf) 44 | regreturn = 0 45 | return regreturn 46 | 47 | def hook_cmp(ql): 48 | ql.reg.w0 = 1 49 | return 50 | 51 | def hook_rand(ql, *args, **kw): 52 | ql.reg.w0 = 0 53 | return 54 | 55 | def hook_cmp2(ql): 56 | ql.reg.w0 = 0 57 | return 58 | 59 | def hook_sleeptime(ql): 60 | ql.reg.w0 = 0 61 | return 62 | 63 | def hook_tolower(ql): 64 | return 65 | 66 | def find_and_patch(ql, *args, **kw): 67 | MAGIC = 0x3DFCD6EA00000539 68 | magic_addrs = ql.mem.search(ql.pack64(MAGIC)) 69 | 70 | # check_all_magic 71 | for magic_addr in magic_addrs: 72 | # Dump and unpack the candidate structure 73 | malloc1_addr = magic_addr - 8 74 | malloc1_data = ql.mem.read(malloc1_addr, 24) 75 | # unpack three unsigned long 76 | string_addr, _ , check_addr = struct.unpack('QQQ', malloc1_data) 77 | 78 | # check string data 79 | if ql.mem.string(string_addr) == "Random data": 80 | ql.mem.write(check_addr, b"\x01") 81 | break 82 | return 83 | 84 | def midr_el1_hook(ql, address, size): 85 | 86 | # 001013ec 00 00 38 d5 mrs x0,midr_el1 87 | 88 | if ql.mem.read(address, size) == b"\x00\x00\x38\xD5": 89 | # if any code is mrs x0,midr_el1 90 | # Write the expected value to x0 91 | ql.reg.x0 = 0x1337 << 0x10 92 | # Go to next instruction 93 | ql.reg.arch_pc += 4 94 | # important !! Maybe hook library 95 | # see : https://joansivion.github.io/qilinglabs/ 96 | return 97 | 98 | if __name__ == "__main__": 99 | ql = Qiling(["qilinglab-aarch64"], "rootfs/arm64_linux" 100 | ,verbose=QL_VERBOSE.OFF 101 | ) 102 | 103 | # challenge 1 104 | 105 | # need to align the memory offset and address for mapping. 106 | # size at least a multiple of 4096 for alignment 107 | ql.mem.map(0x1337//4096*4096, 4096) 108 | ql.mem.write(0x1337,ql.pack16(1337) ) 109 | 110 | # challenge 2 111 | ql.set_syscall("uname", my_syscall_uname) 112 | 113 | # challenge 3 114 | ql.add_fs_mapper('/dev/urandom', Fake_urandom()) 115 | ql.set_syscall("getrandom", my_syscall_getrandom) 116 | 117 | # challenge 4 118 | 119 | base_addr = ql.mem.get_lib_base(ql.path) # get pie_base addr 120 | 121 | # 00100fd8 e0 1b 40 b9 ldr w0,[sp, #local_8] 122 | # 00100fdc e1 1f 40 b9 ldr w1,[sp, #local_4] 123 | # 00100fe0 3f 00 00 6b cmp w1,w0 124 | 125 | ql.hook_address(hook_cmp, base_addr + 0xfe0) 126 | 127 | # callenge 5 128 | ql.set_api("rand", hook_rand) 129 | 130 | # challenge 6 131 | ql.hook_address(hook_sleeptime, base_addr + 0x001118) 132 | 133 | # challenge 7 134 | ql.hook_address(hook_cmp, base_addr + 0x1154) 135 | 136 | # challenge 8 137 | 138 | # 001011d0 e0 17 40 f9 ldr magic_string,[sp, #local_8] 139 | # 001011d4 e1 0f 40 f9 ldr x1,[sp, #local_18] 140 | # 001011d8 01 08 00 f9 str x1,[magic_string, #0x10] 141 | # 001011dc 1f 20 03 d5 nop <- hook 142 | # 001011e0 fd 7b c3 a8 ldp x29=>local_30,x30,[sp], #0x30 143 | # 001011e4 c0 03 5f d6 ret 144 | ql.hook_address(find_and_patch, base_addr + 0x011dc) 145 | 146 | 147 | # challenge 9 148 | ql.set_api("tolower", hook_tolower) 149 | 150 | # challenge 10 151 | ql.add_fs_mapper('/proc/self/cmdline', Fake_cmdline()) 152 | 153 | # challenge 11 154 | ql.hook_code(midr_el1_hook) 155 | 156 | # end and run 157 | ql.run() 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qiling lab writeup 2 | 3 | shielder 在 2021/7/21 發布了 [QilingLab](https://www.shielder.it/blog/2021/07/qilinglab-release/) 來幫助學習 [qiling framwork](https://github.com/qilingframework/qiling) 的用法,剛好最近有用到,順手解了一下並寫了一下 writeup。 4 | 5 | ## 前情提要 6 | [Qiling](https://github.com/qilingframework/qiling) 是一款功能強大的模擬框架,和 qemu user mode 類似,但可以做到更多功能,詳情請見他們的 [github](https://github.com/qilingframework/qiling) 和[網站](https://qiling.io/)。 7 | 8 | 他們有[官方文件](https://docs.qiling.io/en/latest/),解此題目前建議看一下。 9 | 10 | 我所解的為 aarch64 的 [challenge](https://www.shielder.it/blog/2021/07/qilinglab-release/ 11 | ),使用的 rootfs 為 qililng 所提供的 [arm64_linux](https://github.com/qilingframework/rootfs 12 | )。 13 | 14 | 逆向工具用 [ghidra](https://ghidra-sre.org/),因為我沒錢買 idapro。 15 | 16 | 17 | ## First 18 | 19 | 先隨手寫個 python 用 qiling 執行 challenge binary。 20 | 21 | ```python 22 | import sys 23 | from qiling import * 24 | from qiling.const import QL_VERBOSE 25 | 26 | sys.path.append("..") 27 | 28 | 29 | if __name__ == "__main__": 30 | ql = Qiling(["qilinglab-aarch64"], "rootfs/arm64_linux",verbose=QL_VERBOSE.OFF) 31 | ql.run() 32 | 33 | ``` 34 | 35 | 可以看到結果是 binary 會不正常執行,此為正常現象,有些 Challenge 沒解完會導致錯誤或是無窮迴圈。 36 | 37 | ``` 38 | Welcome to QilingLab. 39 | Here is the list of challenges: 40 | Challenge 1: Store 1337 at pointer 0x1337. 41 | Challenge 2: Make the 'uname' syscall return the correct values. 42 | Challenge 3: Make '/dev/urandom' and 'getrandom' "collide". 43 | Challenge 4: Enter inside the "forbidden" loop. 44 | Challenge 5: Guess every call to rand(). 45 | Challenge 6: Avoid the infinite loop. 46 | Challenge 7: Don't waste time waiting for 'sleep'. 47 | Challenge 8: Unpack the struct and write at the target address. 48 | Challenge 9: Fix some string operation to make the iMpOsSiBlE come true. 49 | Challenge 10: Fake the 'cmdline' line file to return the right content. 50 | Challenge 11: Bypass CPUID/MIDR_EL1 checks. 51 | 52 | Checking which challenge are solved... 53 | Note: Some challenges will results in segfaults and infinite loops if they aren't solved. 54 | [x] 55 | 56 | [x] x0 : 0x0 57 | [x] x1 : 0x0 58 | [x] x2 : 0x1 59 | [x] x3 : 0x0 60 | [x] x4 : 0x0 61 | 62 | ``` 63 | 64 | ## Challenge 1 65 | 把 0x1337 的位置的值改成 1337 66 | ![](https://i.imgur.com/WmHLBSY.png) 67 | 68 | 用 qiling 把該位置的 memory 讀出來,再進行改寫,要注意 align 問題。詳情請見[文件](https://docs.qiling.io/en/latest/memory/)。 69 | 70 | ``` 71 | ql.mem.map(0x1337//4096*4096, 4096) 72 | ql.mem.write(0x1337,ql.pack16(1337) ) 73 | 74 | ``` 75 | ## Challenge 2 76 | 改掉 uname 此 system call 的 return。 77 | 78 | ![](https://i.imgur.com/FelecVT.png) 79 | 80 | 可以看到他去比對 uname.sysname 和 uname.version 是否為特定值。我採用對 system call 進行 [hijack](https://docs.qiling.io/en/latest/hijack/)。 81 | 82 | 去翻 linux [文件](https://man7.org/linux/man-pages/man2/uname.2.html) 可以看到 uname 回傳的格式為 : 83 | ``` 84 | struct utsname { 85 | char sysname[]; /* Operating system name (e.g., "Linux") */ 86 | char nodename[]; /* Name within "some implementation-defined 87 | network" */ 88 | char release[]; /* Operating system release 89 | (e.g., "2.6.28") */ 90 | char version[]; /* Operating system version */ 91 | char machine[]; /* Hardware identifier */ 92 | #ifdef _GNU_SOURCE 93 | char domainname[]; /* NIS or YP domain name */ 94 | #endif 95 | }; 96 | ``` 97 | 98 | 依照此文件把相對應的位置改掉。注意如果 release 改太小或是沒給,會噴錯。 99 | 100 | ```python 101 | def my_syscall_uname(ql, write_buf, *args, **kw): 102 | buf = b'QilingOS\x00' # sysname 103 | ql.mem.write(write_buf, buf) 104 | buf = b'30000'.ljust(65, b'\x00') # important!! If not sat will `FATAL: kernel too old` 105 | ql.mem.write(write_buf+65*2, buf) 106 | buf = b'ChallengeStart'.ljust(65, b'\x00') # version 107 | ql.mem.write(write_buf+65*3, buf) 108 | regreturn = 0 109 | return regreturn 110 | 111 | ql.set_syscall("uname", my_syscall_uname) 112 | ``` 113 | 114 | 115 | 116 | 117 | ## Challenge 3 118 | 從`/dev/random`,從中讀取兩次,確保第一次的值和 getrandom 得到的值相同,且其中沒有第二次讀到值。 119 | 120 | ![](https://i.imgur.com/ZpxCW2z.png) 121 | 122 | 查了一下 getrandom 是一 system call。因此對 `/dev/random` 和 getrandom() 進行 [hijack](https://docs.qiling.io/en/latest/hijack/) 即可 123 | 124 | ```python 125 | class Fake_urandom(QlFsMappedObject): 126 | def read(self, size): 127 | if(size > 1): 128 | return b"\x01" * size 129 | else: 130 | return b"\x02" 131 | def fstat(self): # syscall fstat will ignore it if return -1 132 | return -1 133 | def close(self): 134 | return 0 135 | 136 | def my_syscall_getrandom(ql, write_buf, write_buf_size, flag , *args, **kw): 137 | buf = b"\x01" * write_buf_size 138 | ql.mem.write(write_buf, buf) 139 | regreturn = 0 140 | return regreturn 141 | 142 | ql.add_fs_mapper('/dev/urandom', Fake_urandom()) 143 | ql.set_syscall("getrandom", my_syscall_getrandom) 144 | ``` 145 | 146 | 147 | ## Challenge 4 148 | 進入不能進去的迴圈 149 | 150 | ![](https://i.imgur.com/BQSbfOm.png) 151 | 152 | 直接 hook `cmp` 的位置讓 reg w0 是 1 即可,位置記得要加上 pie。 153 | 154 | ``` 155 | # 00100fd8 e0 1b 40 b9 ldr w0,[sp, #local_8] 156 | # 00100fdc e1 1f 40 b9 ldr w1,[sp, #local_4] 157 | # 00100fe0 3f 00 00 6b cmp w1,w0 <- hook 158 | ``` 159 | 160 | ```python 161 | def hook_cmp(ql): 162 | ql.reg.w0 = 1 163 | return 164 | 165 | base_addr = ql.mem.get_lib_base(ql.path) # get pie_base addr 166 | ql.hook_address(hook_cmp, base_addr + 0xfe0) 167 | ``` 168 | 169 | ## Challenge 5 170 | rand() 出來的值和 0 比較要通過 171 | ![](https://i.imgur.com/HowFwVM.png) 172 | 173 | 直接 hijack rand() 讓他回傳都是 0 即可。 174 | 175 | ```python 176 | def hook_cmp(ql): 177 | ql.reg.w0 = 1 178 | return 179 | 180 | ql.set_api("rand", hook_rand) 181 | 182 | ``` 183 | ## Challenge 6 184 | 185 | 解開無窮迴圈 186 | 187 | ![](https://i.imgur.com/wfK2XV0.png) 188 | 189 | 和 Challenge 4 同想法,hook cmp。 190 | 191 | ```python 192 | def hook_cmp2(ql): 193 | ql.reg.w0 = 0 194 | return 195 | 196 | ql.hook_address(hook_cmp2, base_addr + 0x001118) 197 | 198 | ``` 199 | ## Challenge 7 200 | 不要讓他 sleep。 201 | ![](https://i.imgur.com/re9QVoa.png) 202 | 解法很多,可以 hook sleep 這個 api,或是看 sleep linux [文件](https://man7.org/linux/man-pages/man3/sleep.3.html)能知道內部處理是用 [nanosleep](https://man7.org/linux/man-pages/man2/nanosleep.2.html),hook 他即可。 203 | 204 | ```python 205 | def hook_sleeptime(ql): 206 | ql.reg.w0 = 0 207 | return 208 | ql.hook_address(hook_sleeptime, base_addr + 0x1154) 209 | ``` 210 | 211 | ## Challenge 8 212 | 裡面最難的一題,他是建立特殊一個結構長這個樣子。 213 | 214 | ``` 215 | struct something(0x18){ 216 | string_ptr -> malloc (0x1e) -> 0x64206d6f646e6152 217 | long_int = 0x3DFCD6EA00000539 218 | check_addr -> check; 219 | } 220 | ``` 221 | 222 | ![](https://i.imgur.com/eDINYNq.png) 223 | 224 | 由於他結構內部有 0x3DFCD6EA00000539 這個 magic byte,因此可以直接對此作搜尋並改寫內部記憶體。這邊要注意搜尋可能找到其他位置,因此前面可以加對 string_ptr 所在位置的判斷。 225 | 226 | ```python 227 | def find_and_patch(ql, *args, **kw): 228 | MAGIC = 0x3DFCD6EA00000539 229 | magic_addrs = ql.mem.search(ql.pack64(MAGIC)) 230 | 231 | # check_all_magic 232 | for magic_addr in magic_addrs: 233 | # Dump and unpack the candidate structure 234 | malloc1_addr = magic_addr - 8 235 | malloc1_data = ql.mem.read(malloc1_addr, 24) 236 | # unpack three unsigned long 237 | string_addr, _ , check_addr = struct.unpack('QQQ', malloc1_data) 238 | 239 | # check string data 240 | if ql.mem.string(string_addr) == "Random data": 241 | ql.mem.write(check_addr, b"\x01") 242 | break 243 | return 244 | 245 | ql.hook_address(find_and_patch, base_addr + 0x011dc) 246 | ``` 247 | 248 | 另一種解法則是由於該結構在 stack 上,因此直接讀 stack 即可。 249 | 250 | ## Challenge 9 251 | 252 | 把一字串轉用[tolower](https://www.programiz.com/c-programming/library-function/ctype.h/tolower)小寫,再用 strcmp 比較。 253 | 254 | ![](https://i.imgur.com/DmjFuFw.png) 255 | 256 | 解法一樣很多種,我是 hijack tolower() 讓他啥事都不做。 257 | 258 | ```python 259 | def hook_tolower(ql): 260 | return 261 | 262 | ql.set_api("tolower", hook_tolower) 263 | ``` 264 | 265 | ## Challenge 10 266 | 267 | 打開不存在的文件,讀取的值需要是 `qilinglab` 268 | 269 | ![](https://i.imgur.com/gXu6jxO.png) 270 | 和 Challenge 3 作法一樣,這邊要注意的是 return 要是 byte,string 會出錯。 = = 271 | 272 | ```python 273 | class Fake_cmdline(QlFsMappedObject): 274 | 275 | def read(self, size): 276 | return b"qilinglab" # type should byte byte, string will error = = 277 | def fstat(self): # syscall fstat will ignore it if return -1 278 | return -1 279 | def close(self): 280 | return 0 281 | 282 | ql.add_fs_mapper('/proc/self/cmdline', Fake_cmdline()) 283 | ``` 284 | 285 | ## Challenge 11 286 | 287 | 可以看到他從 [MIDR_EL1]( 288 | https://developer.arm.com/documentation/ddi0595/2020-12/AArch64-Registers/MIDR-EL1--Main-ID-Register) 取值,而此為特殊的暫存器。 289 | 290 | ![](https://i.imgur.com/phrLOoU.png) 291 | 292 | 這邊解法是去 hook code,我選擇 hook 這段 293 | 294 | ``` 295 | # 001013ec 00 00 38 d5 mrs x0,midr_el1 296 | ``` 297 | 298 | 去搜尋所有記憶體為 `b"\x00\x00\x38\xD5"` ,讓他執行時把 x0 暫存器改寫,並更改 pc。 299 | 300 | ```python 301 | def midr_el1_hook(ql, address, size): 302 | if ql.mem.read(address, size) == b"\x00\x00\x38\xD5": 303 | # if any code is mrs x0,midr_el1 304 | # Write the expected value to x0 305 | ql.reg.x0 = 0x1337 << 0x10 306 | # Go to next instruction 307 | ql.reg.arch_pc += 4 308 | # important !! Maybe hook library 309 | # see : https://joansivion.github.io/qilinglabs/ 310 | return 311 | 312 | ql.hook_code(midr_el1_hook) 313 | ``` 314 | 315 | 316 | ## Done 317 | ![](https://i.imgur.com/THCInVp.png) 318 | 319 | ## Thanks 320 | Thanks [MANSOUR Cyril](https://twitter.com/MansourCyril) release his [writeup](https://joansivion.github.io/qilinglabs/), help me alot. 321 | --------------------------------------------------------------------------------