DASCTF八月挑战赛
littlejunk太痛苦了,在学长的精心提示下还搞了一天半,小丑就是我自己了,ida欺负我就算了,dev也给我卡bug。
RE
1、py签到
经典python打包的exe程序,pyinstxtractor解包后,对名称为py的无后缀文件修复文件头,再用uncompyle6直接反编译出源码。
def encode(s):
str = ''
for i in range(len(s)):
res = ord(s[i]) ^ 32
res += 31
str += chr(res)
return str
m = 'ek`fz13b3c5e047b`bd`0/c268e600e7c5d1`|'
strings = ''
strings = input('Input:')
if encode(strings) == m:
print 'Correct!'
else:
print 'Try again!'
# okay decompiling py.pyc
简单逆运算即可
m='ek`fz13b3c5e047b`bd`0/c268e600e7c5d1`|'
for i in range(len(m)):
print(chr((ord(m[i])-31)^32),end='')
2、apkre
Android逆向题,自己目前就会常见的思路,拿到apk文件,直接解压,找到class.dex文件,用d2j-dex2jar.bat把dex反编译为jar文件,之后用jd-gui查看源码。 Android killer有点慢…
native层的逆向,主要就是一个mycheck函数来进行检验。
去lib文件夹内找到x86下的so文件直接拖入ida,在函数窗口搜索mycheck就能定位。
在对s盒初始化的时候有很多sse指令,之后可以学一学,但当时看到类似rc4,之后又秘钥有明文直接套个脚本就出了。不过在线网站有点香,手速题。
总结
Android逆向现在一知半解,还要了解一下结构和常见逆向思路什么的,对于这题调试一下出流秘钥那就更快了,目前我不是太会。
3、littlejunk
①动静态分析
比赛期间没做出来,单纯认为是个Tea加密,最后key还搞错了,不过这个题还是有很多坑的,搞了1天半才摸索出。
首先在main函数里面就有俩花指令,比较常见的一种类型,因为是跳转到一个标号+1,所以标号处的第一个字节始终不起作用,所以可以nop掉。
接着发现一个比较可疑的地方,有ctf的字样,并且函数1对一个地址处的字节做了异或操作。
函数1
根据做题经验,这是通过异或掩盖了函数的内容,只有动调到这里才能看到函数原本的面貌。听学长说这是一种smc,自修改技术,没怎么了解,别骂了,太懒了。
所以我们要动调到这,让他自己异或,看一看异或后的函数内容。
当然,直接调的话会有个system函数,就认为他是一种比较low的反调试吧,下个断点,修改eip为箭头指向的地方,或者其他语句的开始即可,可别乱改写中间,ida会崩的。
当然,在要观察的函数附近下个断点,绕过system就直接F9过去。
通过调试发现,这个ipAddress就是指向loc_14000函数的第一个字节,正如猜测他是修改的下面的函数。
修改前: 啥也不是,不要乱nop。F8单步以后,进去把数据按C转代码,再p重新改一下这个函数。
非代码段里面放了代码,第一次见挺吓人的。
这里要注意,这个函数里面没有一点花指令,全是有用代码,可能不能直接c,或者c不动的话,先按u进行分析,或者d转数据之后就可以按c了!!!
一开始我就是在这里面不能c的都nop了,结果越看越迷。
刚开始弄完是这样,有点奇怪,后面会解决,先单步跟进来看看是什么情况。
点进去第一个和第二个函数看一看,里面是print 让你输入flag,接着就是输入flag,并且flag必须是38位,然后格式flag{}.
用python构造出一个flag即可,也方便跟踪和计算的。
flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} //根据个人喜好构造flag
下一个函数跟进后,又是一个自解密的函数,结构这么怪就与这里有关。
单步完这个自修改函数后,进入第四个函数,里面蛮花的,汇编窗口按照u+c整理整理。
整理后:注意重新定义一下函数p,没有花指令
一个类似RC4的函数,但最后的算法有一丢丢怪。
接着来到外面,F5一下,出现了一个爆红的指令,是自修改后出现的部分,汇编窗口table跟进,继续整理。
主要是在sub_14000函数标号的地方,u或d一下之后再p一下,直到出现上图的情况算是正常,再提示一遍,这里没有花指令!!!
这是1400函数,我改了改函数名,就是先输入flag然后进行了一个类似rc4的加密,反正是异或只要调出key就可以了。
直接F8单步,看一下rc4 return的全局变量,这里还有一个坑!!!已经不知道是第几个了。
进去一看很懵逼,处理的字符串变成了一个,注意下面有个align100,按一下u。
一共32个,每个取第一个字节,这里5其实是高位的,但是那个转大端序的函数把5放到了第四个地址也就是变成了低字节。这里获得的32个数据是key[i] ^ord(‘a’),在异或一下ord(‘a’)就拿到了rc4的key。
接着是最后一个函数,跟进去也是乱的,这里又有两处花指令,和main函数处的原理一样。
不在多做解释,自己更改即可,注意修改后重新定义一下函数。
Tea加密,进化版,可知v13和v11是一组,v12和v14是一组,delta没有改是0x9E3779B9,但是原本的Tea加密是明文两个4字节,秘钥4个四字节。这个是明文两个8字节,并且写在了一个循环里,秘钥4个8字节,我愿称之为巨型Tea。
动调到a2这里,能看到a2的地址,0x12ffe20,在hex窗口G到这个地址。
为了证明ida的障眼法,手机拍下了证据,真正的秘钥key是从0x12ffe20开始的4个8字节,注意小端序要转成正确顺序,从内存中提取出的key才是真正的key。
0x54466076484c5476 , 0x4550504f765f4344 , 0x5a796f755f6d6179 , 0x5f6e6565645f7468
这是密文,也是最终的check,不过他这个256bit的Tea加密最终的比较是第一个数是低位第二个数是高位,两两一组就能拼出最中加密后的4个值,之后再两两一组进行解密即可。
②写脚本的辛酸经历
#include<iostream>
#include<iomanip>
#include<cstring>
using namespace std;
void tea_decode(unsigned __int64*s,unsigned __int64 *key);
int main(){
unsigned __int64 m[2]={0x2FDE61CCEFC70FF8,0x56BC19E119C8B07B};
//第一组:0xE990A522BE80F786
unsigned __int64 key[4]={0x54466076484c5476 , 0x4550504f765f4344 , 0x5a796f755f6d6179 , 0x5f6e6565645f7468};
tea_decode(m,key);
unsigned long long a=0x9e3779b9L * 0x20L;//测试
cout<<hex<<a<<endl; //测试
return 0;
}
void tea_decode(unsigned __int64 *s,unsigned __int64 *key){
unsigned __int64 v0=s[0];
unsigned __int64 v1=s[1];
unsigned __int64 sum=0x13c6ef3720; // 写的0x9e3779b9*0x20 devc只给我保留低四个字节
unsigned __int64 defalt=0x9e3779b9;
unsigned __int64 k0=key[0],k1=key[1],k2=key[2],k3=key[3];
for(int i=0;i<0x20;i++){
v1-=((v0<<4)+k2)^(v0+sum)^((v0>>5)+k3);
v0-=((v1<<4)+k0)^(v1+sum)^((v1>>5)+k1);
sum-=defalt;
}
s[0]=v0;
s[1]=v1;
cout<<v0<<","<<v1<<endl;
}
注意:Tea解密时的sum直接写算出来的结果0x13c6ef3720,不要写0x9e3779b9*0x20,如果第二种写法则结果只保留低32bit,即便你开辟了8字节的大小,还是会有这个坑。
Tea解密之后流密码解密
from Crypto.Util.number import *
#print('flag{'+'a'*32+'}')
a=[ 0x05, 0x00, 0x00, 0x00, 0x5A, 0x00, 0x00, 0x00, 0x18, 0x00,
0x00, 0x00, 0xC6, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00,
0x4F, 0x00, 0x00, 0x00, 0xCE, 0x00, 0x00, 0x00, 0x83, 0x00,
0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0xAC, 0x00, 0x00, 0x00,
0xF3, 0x00, 0x00, 0x00, 0x3D, 0x00, 0x00, 0x00, 0x7F, 0x00,
0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x60, 0x00,
0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0xD3, 0x00, 0x00, 0x00,
0x1F, 0x00, 0x00, 0x00, 0xA5, 0x00, 0x00, 0x00, 0xFA, 0x00,
0x00, 0x00, 0xEB, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00,
0x63, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x0F, 0x00,
0x00, 0x00, 0xF6, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00,
0x42, 0x00, 0x00, 0x00, 0xA5, 0x00, 0x00, 0x00]
key=[]
for i in range(len(a)//4):
key.append(a[4*i]^97)
#dump出了 rc4的key
#tea解密后
cc1=[5790024677284421248,265414871944557825,3760601914643164344,675928458074199027]
temp=int.to_bytes(cc1[0],8,byteorder='big') #已经帮我们小端转大端了,不许要我们自行转换了
temp+=int.to_bytes(cc1[1],8,byteorder='big')
temp+=int.to_bytes(cc1[2],8,byteorder='big')
temp+=int.to_bytes(cc1[3],8,byteorder='big')
for i in range(len(temp)):
print(chr(temp[i]^key[i]),end='')
#flag{4a5c0f1b5cc7f60e918611721c87ba07}
反观这个rc4,其实是做了手脚的,最后本应该算出的v4,原版应该是s[v4]^m[i] 但题中确是m[i]^s[i],中间那三步还是个交换函数,如下python示例。
def fun(a,b):
a = b & ~a | a & ~b
b = b & ~a | a & ~b
a = b & ~a | a & ~b
print(a,b)
fun(3,4) -> 4 3
当然对于流密码来说,动调能拿到流秘钥后便能快速进行解密。
总结:
其实复现一遍来看,并不是那么复杂,花指令就4处,2处自解密算法,最后跟了一个rc4和一个巨型TEA,其实一开始做题的时候没这么清晰,直接去看了字符串窗口,也能看到很多信息,可以在那些地方下断点什么,当然一道送分题扣了那么久还真是蛮好笑的,但也学到了很多细节问题,比如符号处理,devc的bug,自解密等,拿我的idol的话来说,别总想着套,多去了解,10min出才是基础扎实。haha,慢慢爬叭!
Crypto
整体难度不大,本期的主题就是赛题复现么,不过也挺有意思的
1、easymath
原题,谷歌能直接搜出来,之前校赛也搞过,主要还是求逆元。
assert(len(open('flag.txt', 'rb').read()) < 50)
assert(str(int.from_bytes(open('flag.txt', 'rb').read(), byteorder='big') << 10000).endswith(
'1862790884563160582365888530869690397667546628710795031544304378154769559410473276482265448754388655981091313419549689169381115573539422545933044902527020209259938095466283008'))
endwith函数是判断是否以指定的字符串结尾,即尾部为括号内的内容,是175位数。
即
c = (flag<<10000)%(10^175)
左移10000也就是乘 2**10000次方 原式 c = flag*(2**10000) % 10^175 如果2**10000在10^175下有逆元
就直接能解出flag,但2**10000 和 10^175不是互素的,但和5^175互素,而10^175是5^175的倍数,所以
(c%(10^175)) 再%5^175 和 直接c%(5^175)的值是一样的。
直接求个逆元,变能拿到flag
import gmpy2
from Crypto.Util.number import *
d=gmpy2.invert(2**10000,5**175)
c=...
f=long_to_bytes(c*d%(5**175))
print(f)
2、let’s play with rsa~
from sympy import isprime,nextprime
from Crypto.Util.number import getPrime as getprime ,long_to_bytes,bytes_to_long,inverse
flag='flag{***************}'
def play():
p=getprime(1024)
q=getprime(1024)
n=p*q
e=65537
print "Hello,let's play rsa~\n"
print 'Now,I make some numbers,wait a second\n'
n1=getprime(200)
n2=getprime(200)
number=n1*n2
print "Ok,i will send two numbers to you,one of them was encoded.\n"
print "Encode n1:%d,\n"%(pow(n1,e,n))
print "And n2:%d.\n"%n2
print "Information that can now be made public:the public key (n,e):(%d,%d)\n"%(n,e)
while True:
try:
c=int(raw_input("ok,now,tell me the value of the number (encode it for safe):"))
except:
print "Sorry,the input is illeagal, and the integer is accept~"
else:
break
d=inverse(e,(p-1)*(q-1))
m=pow(c,d,n)
if m==number:
print "It's easy and interesting,didn't it?\n"
print "This is the gift for you :"+flag
else:
print "Emmmmm,there is something wrong, bye~\n"
if __name__ == '__main__':
play()
芜湖,正好三星的ctf的教程里面就有关这个的攻击,大致流程是e和n给你了,然后他自己生产了n1,n2,number=n1*n2,我们拿到了(pow(n1,e,n)) 和 n2,之后让我们转入一个数,并且他进行解密m=pow(c,d,n)后和number进行check。
((n1^e)%n * (n2^e)%n)%n =(n1*n2)^e %n = number^e %n
#就是带模运算 乘法的那个式子,(n1^e)%n n2已经知 直接构造求即可
print((pow(n2,e,n)*n1)%n)
nc 连上 传入即得到flag
总结
有些时候,服务器可能会限制我们的输入,比如传入一个hello,里面是个rsa的加密机制,并且会自动加密和自动解密,他要求我们传入的密文解密后加入等于hello,他就会给我们flag。
但是我们直接传入hello进行加密的时候呢,他又进制了这个输入,这时候我们可以把hello,转为数值,进行因式分解,即m1*m2==m,然后我们再传入m1和m2,这个体系就会给我们返还pow(m1,e,n)和pow(m2,e,n),之后将两者的乘积再%n,这样就构造出了hello的密文,之后传入便能拿到flag。
3、ezRSA
from secret import flag
from Crypto.Util.number import *
from random import getrandbits
from hashlib import sha256
class EzRsa:
def __init__(self):
self.E = 0x10001
self.P = getPrime(1024)
self.Q = getPrime(1024)
while GCD((self.P-1)*(self.Q-1), self.E) != 1:
self.Q = getPrime(1024)
self.N = self.P*self.Q
def encrypt(self):
f = getrandbits(32)
c = pow(f, self.E, self.N)
return (f, c)
def encrypt_flag(self, flag):
f = bytes_to_long(flag)
c = pow(f, self.E, self.N)
return c
def proof():
seed = getrandbits(32)
print(seed)
sha = sha256(str(seed).encode()).hexdigest()
print(f"sha256({seed>>18}...).hexdigest() = {sha}")
sha_i = input("plz enter seed: ")
if sha256(sha_i.encode()).hexdigest() != sha:
exit(0)
if __name__ == "__main__":
proof()
print("welcome to EzRsa")
print("""
1. Get flag
2. Encrypt
3. Insert
4. Exit
""")
A = EzRsa()
coin = 5
while coin > 0:
choose = input("> ")
if choose == "1":
print(
f"pow(flag,e,n) = {A.encrypt_flag(flag)}\ne = 0x10001")
exit(0)
elif choose == "2":
f, c = A.encrypt()
print(f"plain = {f}\ncipher = {c}")
coin -= 1
elif choose == "3":
q = getrandbits(1024)
n = A.P*q
f = getrandbits(32)
c = pow(f, 0x10001, n)
print(f"plain = {f}\ncipher = {c}")
coin -= 1
elif choose == "4":
print("bye~")
else:
print("wrong input")
print("Now you get the flag right?")
第一个sha256的check有点离谱,seed都给了,直接复制粘贴一遍便过了。
coin初始为5,每次选择会-1,最多能循环5次。
选1的话,是拿到flag的密文,但选了1就会退出。
选2的话是给你明文和加密后的密文,e的话是0x10001
选3的话,也是给一组明文和密文,e是0x10001不过n和2的不一样,这里的n是取出的P乘上一个随机数。
…谁没事会选4呢???
根据2,由 m^e=c+k*n (m^e -c =k*n) 拿到两组2的话 就能通过求公因数泄露k*n
同理,根据 m^e -c =k*P*q 取公因数可以拿到 k*q
注意,这里一般都是 k*n 或者 k*q k可以在小范围内爆破,毕竟明文比较短,n比较大
所以我们只需取 2组 2来泄露n,取两组3来泄露P就能分解n,进而正常解密rsa了
解密脚本
import gmpy2
from Crypto.Util.number import *
from random import getrandbits
e= 0x10001
p1=1751297511
c1=
p2=3438100521
c2=
#以上数据用于泄露n 连续取2次 2
p3=1584259449
c3=
p4=3829043104
c4=
#泄露p 连续取两次3
enc=
n=gmpy2.gcd(pow(p1,e)-c1,pow(p2,e)-c2) #可能k*n的k也有公因数 可以小范围的缩一下
#for i in range(2,100):
# if n%i==0:
# print(i)
n=n//22 #i求出来是22
p=gmpy2.gcd(pow(p3,e)-c3,pow(p4,e)-c4) #因为3 的q是随机的 所以公因数为k*q
#for i in range(2,100):
# if p%i==0:
# print(i)
p=p//3
#if n%p==0:
# print('yes')
q=n//p
d=gmpy2.invert(e,(p-1)*(q-1))
print(long_to_bytes(pow(enc,d,n)))
漏洞还是比较明显的,主要是gcd预算,n和p在小范围内跑一下有没有k即可。
总结
idol:10min 做不出来就是基础不行,确实都是送分题。
hh对我还挺难受的,对于littlejunk,确实是基础不牢,做题经验也不行,老是采坑,Android逆向也了解不深,rsa有关线代的一些知识还不了解,真是tcl,还要继续加油。
总之,热爱的话就一起爬叭~