Skip to content

zodf0055980/QilingLab_Writeup

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

qiling lab writeup

shielder 在 2021/7/21 發布了 QilingLab 來幫助學習 qiling framwork 的用法,剛好最近有用到,順手解了一下並寫了一下 writeup。

前情提要

Qiling 是一款功能強大的模擬框架,和 qemu user mode 類似,但可以做到更多功能,詳情請見他們的 github網站

他們有官方文件,解此題目前建議看一下。

我所解的為 aarch64 的 challenge,使用的 rootfs 為 qililng 所提供的 arm64_linux

逆向工具用 ghidra,因為我沒錢買 idapro。

First

先隨手寫個 python 用 qiling 執行 challenge binary。

import sys
from qiling import *
from qiling.const import QL_VERBOSE

sys.path.append("..")


if __name__ == "__main__":
    ql = Qiling(["qilinglab-aarch64"], "rootfs/arm64_linux",verbose=QL_VERBOSE.OFF)
    ql.run()

可以看到結果是 binary 會不正常執行,此為正常現象,有些 Challenge 沒解完會導致錯誤或是無窮迴圈。

Welcome to QilingLab.
Here is the list of challenges:
Challenge 1: Store 1337 at pointer 0x1337.
Challenge 2: Make the 'uname' syscall return the correct values.
Challenge 3: Make '/dev/urandom' and 'getrandom' "collide".
Challenge 4: Enter inside the "forbidden" loop.
Challenge 5: Guess every call to rand().
Challenge 6: Avoid the infinite loop.
Challenge 7: Don't waste time waiting for 'sleep'.
Challenge 8: Unpack the struct and write at the target address.
Challenge 9: Fix some string operation to make the iMpOsSiBlE come true.
Challenge 10: Fake the 'cmdline' line file to return the right content.
Challenge 11: Bypass CPUID/MIDR_EL1 checks.

Checking which challenge are solved...
Note: Some challenges will results in segfaults and infinite loops if they aren't solved.
[x]	

[x]	x0	:	 0x0
[x]	x1	:	 0x0
[x]	x2	:	 0x1
[x]	x3	:	 0x0
[x]	x4	:	 0x0

Challenge 1

把 0x1337 的位置的值改成 1337

用 qiling 把該位置的 memory 讀出來,再進行改寫,要注意 align 問題。詳情請見文件

    ql.mem.map(0x1337//4096*4096, 4096)
    ql.mem.write(0x1337,ql.pack16(1337) )

Challenge 2

改掉 uname 此 system call 的 return。

可以看到他去比對 uname.sysname 和 uname.version 是否為特定值。我採用對 system call 進行 hijack

去翻 linux 文件 可以看到 uname 回傳的格式為 :

struct utsname {
               char sysname[];    /* Operating system name (e.g., "Linux") */
               char nodename[];   /* Name within "some implementation-defined
                                     network" */
               char release[];    /* Operating system release
                                     (e.g., "2.6.28") */
               char version[];    /* Operating system version */
               char machine[];    /* Hardware identifier */
           #ifdef _GNU_SOURCE
               char domainname[]; /* NIS or YP domain name */
           #endif
};

依照此文件把相對應的位置改掉。注意如果 release 改太小或是沒給,會噴錯。

def my_syscall_uname(ql, write_buf, *args, **kw):
    buf = b'QilingOS\x00' # sysname
    ql.mem.write(write_buf, buf)
    buf = b'30000'.ljust(65, b'\x00') # important!! If not sat will `FATAL: kernel too old`
    ql.mem.write(write_buf+65*2, buf)
    buf = b'ChallengeStart'.ljust(65, b'\x00') # version
    ql.mem.write(write_buf+65*3, buf)
    regreturn = 0
    return regreturn

ql.set_syscall("uname", my_syscall_uname)

Challenge 3

/dev/random,從中讀取兩次,確保第一次的值和 getrandom 得到的值相同,且其中沒有第二次讀到值。

查了一下 getrandom 是一 system call。因此對 /dev/random 和 getrandom() 進行 hijack 即可

class Fake_urandom(QlFsMappedObject):
    def read(self, size):
        if(size > 1):
            return b"\x01" * size
        else:
            return b"\x02"
    def fstat(self): # syscall fstat will ignore it if return -1
        return -1
    def close(self):
        return 0

def my_syscall_getrandom(ql, write_buf, write_buf_size, flag , *args, **kw):
    buf = b"\x01" * write_buf_size
    ql.mem.write(write_buf, buf)
    regreturn = 0
    return regreturn
    
ql.add_fs_mapper('/dev/urandom', Fake_urandom())
ql.set_syscall("getrandom", my_syscall_getrandom)

Challenge 4

進入不能進去的迴圈

直接 hook cmp 的位置讓 reg w0 是 1 即可,位置記得要加上 pie。

    # 00100fd8 e0 1b 40 b9     ldr        w0,[sp, #local_8]
    # 00100fdc e1 1f 40 b9     ldr        w1,[sp, #local_4]
    # 00100fe0 3f 00 00 6b     cmp        w1,w0    <- hook         
def hook_cmp(ql):
    ql.reg.w0 = 1
    return

base_addr = ql.mem.get_lib_base(ql.path) # get pie_base addr
ql.hook_address(hook_cmp, base_addr + 0xfe0)

Challenge 5

rand() 出來的值和 0 比較要通過

直接 hijack rand() 讓他回傳都是 0 即可。

def hook_cmp(ql):
    ql.reg.w0 = 1
    return
    
ql.set_api("rand", hook_rand)

Challenge 6

解開無窮迴圈

和 Challenge 4 同想法,hook cmp。

def hook_cmp2(ql):
    ql.reg.w0 = 0
    return
    
ql.hook_address(hook_cmp2, base_addr + 0x001118)

Challenge 7

不要讓他 sleep。 解法很多,可以 hook sleep 這個 api,或是看 sleep linux 文件能知道內部處理是用 nanosleep,hook 他即可。

def hook_sleeptime(ql):
    ql.reg.w0 = 0
    return
ql.hook_address(hook_sleeptime, base_addr + 0x1154)

Challenge 8

裡面最難的一題,他是建立特殊一個結構長這個樣子。

struct something(0x18){ 
 string_ptr -> malloc (0x1e) ->  0x64206d6f646e6152
 long_int = 0x3DFCD6EA00000539
 check_addr -> check;
}  

由於他結構內部有 0x3DFCD6EA00000539 這個 magic byte,因此可以直接對此作搜尋並改寫內部記憶體。這邊要注意搜尋可能找到其他位置,因此前面可以加對 string_ptr 所在位置的判斷。

def find_and_patch(ql, *args, **kw):
    MAGIC = 0x3DFCD6EA00000539
    magic_addrs = ql.mem.search(ql.pack64(MAGIC)) 

    # check_all_magic
    for magic_addr in magic_addrs:
        # Dump and unpack the candidate structure
        malloc1_addr = magic_addr - 8
        malloc1_data = ql.mem.read(malloc1_addr, 24)
        # unpack three unsigned long
        string_addr, _ , check_addr = struct.unpack('QQQ', malloc1_data)

        # check string data        
        if ql.mem.string(string_addr) == "Random data":
            ql.mem.write(check_addr, b"\x01")
            break
    return
    
ql.hook_address(find_and_patch, base_addr + 0x011dc)

另一種解法則是由於該結構在 stack 上,因此直接讀 stack 即可。

Challenge 9

把一字串轉用tolower小寫,再用 strcmp 比較。

解法一樣很多種,我是 hijack tolower() 讓他啥事都不做。

def hook_tolower(ql):
    return
    
ql.set_api("tolower", hook_tolower)

Challenge 10

打開不存在的文件,讀取的值需要是 qilinglab

和 Challenge 3 作法一樣,這邊要注意的是 return 要是 byte,string 會出錯。 = =

class Fake_cmdline(QlFsMappedObject):

    def read(self, size):
        return b"qilinglab" # type should byte byte, string will error = =
    def fstat(self): # syscall fstat will ignore it if return -1
        return -1
    def close(self):
        return 0

ql.add_fs_mapper('/proc/self/cmdline', Fake_cmdline())

Challenge 11

可以看到他從 MIDR_EL1 取值,而此為特殊的暫存器。

這邊解法是去 hook code,我選擇 hook 這段

# 001013ec 00 00 38 d5     mrs        x0,midr_el1

去搜尋所有記憶體為 b"\x00\x00\x38\xD5" ,讓他執行時把 x0 暫存器改寫,並更改 pc。

def midr_el1_hook(ql, address, size):  
    if ql.mem.read(address, size) == b"\x00\x00\x38\xD5":
        # if any code is mrs        x0,midr_el1
        # Write the expected value to x0
        ql.reg.x0 = 0x1337 << 0x10
        # Go to next instruction
        ql.reg.arch_pc += 4
    # important !! Maybe hook library
    # see : https://joansivion.github.io/qilinglabs/
    return

ql.hook_code(midr_el1_hook)

Done

Thanks

Thanks MANSOUR Cyril release his writeup, help me alot.

About

QilingLab challenge writeup

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages