-
-
[原创]首届“安洵杯”国际赛站-WMCTF2022 官方WP之MISC
-
2022-8-30 11:02 6850
-
首届“安洵杯”国际赛站-WMCTF2022 官方WP
MISC
1!5!
1.打开流量,可以发现由两部分组成,由quic和tcp组成,先看看底部tcp
2.可以看见有websockets流量,并且可以发现每段都发送了加密数据
3.将其全部提取作为备用,手工或者脚本都可以,可以看得出来是一些加密内容,但是不知道加密方式,先留着,看看内存
4.分析内存,发现是lime镜像,意味着是linux系统镜像
1 | strings memory.mem |grep 'Linux version' |
获得关键信息,Linux version 4.19.0-21-amd64,且发现是debian系统,经过内核搜索
Deep Security 12.0 Supported Linux Kernels (trendmicro.com),可以得知是debian10的系统
5.下载其iso并安装后,制作其对应的符号表镜像 以方便后续取证
1 2 3 4 5 6 7 8 | git clone https: / / github.com / volatilityfoundation / volatility.git cd volatility pip2 install pycrypto pip2 install distorm3 cd tools / linux make cd .. / .. / zip volatility / plugins / overlays / linux / Debian10. zip tools / linux / module.dwarf / boot / System. map - 4.19 . 0 - 21 - amd64 |
最后使用python2 vol.py --info | grep debian即可发现符号表制作成功
6.进行内存取证,查看一下历史命令
1 | python2 vol.py - f .. / memory.mem - - profile = Linuxdebian10x64 linux_bash |
7.根据历史记录,可以发现服务器运行另一个http3的一个服务端,这与流量的quic吻合,并且记录了SSLKEYLOGFILE的路径,可以看见是在桌面上的,然后在最后发现了eval.js,这比较奇怪
8.看一看进程
说明运行了apache2的服务器,
再次遇见了eval.js说明比较重要,尝试使用linux_find_file指令进行查找
9.出于vol的弊端,并不能通过linux_find_file来找到目标文件的缓存地址,
由于内存镜像本质的原理,而且我们已经掌握了关键信息,我们通过strings来快速过滤我们的关键内容
10.通过strings memory.mem|grep eval,来快速定位一下我们的相关信息
可以看到大量的eval.js相关内容,可以注意到一个比较奇特的一串eval开头的js代码
结合前后数据的位置,可以确认该段js就是eval.js的内容,将其赋值出来进行反混淆。
11.通过对其反混淆,可以获得如下代码
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 | function randomString(e) { e = e || 32 ; var t = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678" , a = t.length, n = ""; for (i = 0 ; i < e; i + + ) n + = t.charAt(Math.floor(Math.random() * a)); return n } function encrypto(a, b, c) { if (typeof a ! = = 'string' || typeof b ! = = 'number' || typeof c ! = = 'number' ) { return } let resultList = []; c = c < = 25 ? c : c % 25 ; for (let i = 0 ; i < a.length; i + + ) { let charCode = a.charCodeAt(i); charCode = (charCode * 1 ) ^ b; charCode = charCode.toString(c); resultList.push(charCode) } let splitStr = String.fromCharCode(c + 97 ); let resultStr = resultList.join(splitStr); return resultStr } var b1 = new Encode() var ws = new WebSocket( "ws://localhost:2303/flag" ); ws.onopen = function(a) { console.log( "Connection open ..." ); ws.send( "flag" ) }; ws.onmessage = function(a) { var b = randomString( 5 ) n = a.data res = n.padEnd( 9 , b) s1 = encrypto(res, 15 , 25 ) f1 = b1.encode(s1) ws.send(f1) console.log( 'Connection Send:' + f1) }; ws.onclose = function(a) { console.log( "Connection closed." ) }; function Encode() { _keyStr = "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC" ; this.encode = function(a) { var b = ""; var c, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0 ; a = _utf8_encode(a); while (i < a.length) { c = a.charCodeAt(i + + ); chr2 = a.charCodeAt(i + + ); chr3 = a.charCodeAt(i + + ); enc1 = c >> 2 ; enc2 = ((c & 3 ) << 4 ) | (chr2 >> 4 ); enc3 = ((chr2 & 15 ) << 2 ) | (chr3 >> 6 ); enc4 = chr3 & 63 ; if (isNaN(chr2)) { enc3 = enc4 = 64 } else if (isNaN(chr3)) { enc4 = 64 } b = b + _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4) } return b } _utf8_encode = function(a) { a = a.replace( / \r\n / g, "\n" ); var b = ""; for (var n = 0 ; n < a.length; n + + ) { var c = a.charCodeAt(n); if (c < 128 ) { b + = String.fromCharCode(c) } else if ((c > 127 ) && (c < 2048 )) { b + = String.fromCharCode((c >> 6 ) | 192 ); b + = String.fromCharCode((c & 63 ) | 128 ) } else { b + = String.fromCharCode((c >> 12 ) | 224 ); b + = String.fromCharCode(((c >> 6 ) & 63 ) | 128 ); b + = String.fromCharCode((c & 63 ) | 128 ) } } return b } } |
12.经过分析,可以发现其流程为:websocket连接服务端,向其发送flag字段,然后服务端向html发送明文flag,通过加密再次发送出去
加密流程:首先随机生成字符串补在flag字段后面,然后进行了异或的加密,最后进行了换表的base64操作。
至此我们可以同样写一串js代码来解密其字段
13.解密代码
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 | <script> var str1 = "待解密的字符串" function Base64() { var _keyStr = "/128GhIoPQROSTeUbADfgHijKLM+n0pFWXY456xyzB7=39VaqrstJklmNuZvwcdEC" ; this.decode = function( input ) { var output = ""; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0 ; input = input .replace( / [^A - Za - z0 - 9 \ + \ / \ = ] / g, ""); while (i < input .length) { enc1 = _keyStr.indexOf( input .charAt(i + + )); enc2 = _keyStr.indexOf( input .charAt(i + + )); enc3 = _keyStr.indexOf( input .charAt(i + + )); enc4 = _keyStr.indexOf( input .charAt(i + + )); chr1 = (enc1 << 2 ) | (enc2 >> 4 ); chr2 = ((enc2 & 15 ) << 4 ) | (enc3 >> 2 ); chr3 = ((enc3 & 3 ) << 6 ) | enc4; output = output + String.fromCharCode(chr1); if (enc3 ! = 64 ) { output = output + String.fromCharCode(chr2); } if (enc4 ! = 64 ) { output = output + String.fromCharCode(chr3); } } output = _utf8_decode(output); return output; } var _utf8_decode = function (utftext) { var string = ""; var i = 0 ; var c = 0 ; var c1 = 0 ; var c2 = 0 ; while (i < utftext.length) { c = utftext.charCodeAt(i); if (c < 128 ) { string + = String.fromCharCode(c); i + + ; } else if ((c > 191 ) && (c < 224 )) { c1 = utftext.charCodeAt(i + 1 ); string + = String.fromCharCode(((c & 31 ) << 6 ) | (c1 & 63 )); i + = 2 ; } else { c1 = utftext.charCodeAt(i + 1 ); c2 = utftext.charCodeAt(i + 2 ); string + = String.fromCharCode(((c & 15 ) << 12 ) | ((c1 & 63 ) << 6 ) | (c2 & 63 )); i + = 3 ; } } return string; } } function decrypto( str , xor, hex ) { if ( typeof str ! = = 'string' || typeof xor ! = = 'number' || typeof hex ! = = 'number' ) { return ; } let strCharList = []; let resultList = []; hex = hex < = 25 ? hex : hex % 25 ; let splitStr = String.fromCharCode( hex + 97 ); strCharList = str .split(splitStr); for ( let i = 0 ; i<strCharList.length; i + + ) { let charCode = parseInt(strCharList[i], hex ); charCode = (charCode * 1 ) ^ xor; let strChar = String.fromCharCode(charCode); resultList.push(strChar); } let resultStr = resultList.join(''); return resultStr; } var base = new Base64() b64 = base.decode(str1) console.log(b64) s1 = decrypto(b64, 15 , 25 ) console.log(s1[ 0 ]) < / script> |
14.成功解密前半段,获得前半段flag:WMCTF{LOL_StR1ngs_1s_F@ke_BUT
15.流量还有quic流量需要解密,结合前面得知的sslkeylog.txt,我们同样可以通过strings来快速锁定
此处考察对sslkeylog关键字段知识点的了解,此处文章:NSS Key Log Format — Firefox Source Docs documentation (mozilla.org)
那么只需要一次通过strings来过滤如下
1 2 3 4 | CLIENT_HANDSHAKE_TRAFFIC_SECRET SERVER_HANDSHAKE_TRAFFIC_SECRET CLIENT_TRAFFIC_SECRET_0 SERVER_TRAFFIC_SECRET_0 |
四个关键段即可
16.最终拼接的sslkeylog的内容为
1 2 3 4 | CLIENT_HANDSHAKE_TRAFFIC_SECRET 1002eec63c7da0d66827ebc83af50e00550704d76420b1d039f9ef2222641dd2 48f1197d22ef93778c14f15ddbbf9a53df20cf74c9c68b9f3073fa9f405da995 SERVER_HANDSHAKE_TRAFFIC_SECRET 1002eec63c7da0d66827ebc83af50e00550704d76420b1d039f9ef2222641dd2 38b4671e9ded337c7066e3830563f4519f3bf4effb13d046c2e62847329f0787 CLIENT_TRAFFIC_SECRET_0 1002eec63c7da0d66827ebc83af50e00550704d76420b1d039f9ef2222641dd2 457d3990a971aad9a308ea0af62db5745d99a75e0c484487289f9e760b33a43f SERVER_TRAFFIC_SECRET_0 1002eec63c7da0d66827ebc83af50e00550704d76420b1d039f9ef2222641dd2 dc730355e51308929f66eabb06458080459810bdd6b27de884a1c1fdc5385b1e |
17.最后在wireshark 编辑 首选项 TLS设置一下即可
便可以成功解开http3的流量
18.最后获得后半段flag,
19.最终flag为:WMCTF{LOL_StR1ngs_1s_F@ke_BUT_HTTP3_1s_C000L}
hilbert_wave
首先是一堆音频,au查看后可以看见有间隙的波纹点
直接用wave读一下数据可以发现其值都不大于255(ps:原始数据是49152的一维信息,但是通过声道可以知道是RGB三个颜色分别分到了三个音轨上面),易得其本来为图片,且可以发现49152=128*128*3
再根据题目名称hilbert_wave可以知道其通过了希尔伯特的处理,逆处理一波可以得到图像(ps:下面脚本为更好的可以图片ocr,进行了二值化处理)
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 | import wave import matplotlib.pyplot as plt import numpy as np from PIL import Image import time from tqdm import tqdm def wav_to_pic(wav,pic): plt.rcParams[ 'font.sans-serif' ] = [ 'SimHei' ] plt.rcParams[ 'axes.unicode_minus' ] = False f = wave. open (wav, "rb" ) params = f.getparams() nchannels, sampwidth, framerate, nframes = params[: 4 ] str_data = f.readframes(nframes) # print(nchannels, sampwidth, framerate, nframes) f.close() wave_data = np.fromstring(str_data, dtype = np.short).reshape(( 16384 , 3 )) def _hilbert(direction, rotation, order): if order = = 0 : return direction + = rotation _hilbert(direction, - rotation, order - 1 ) step1(direction) direction - = rotation _hilbert(direction, rotation, order - 1 ) step1(direction) _hilbert(direction, rotation, order - 1 ) direction - = rotation step1(direction) _hilbert(direction, - rotation, order - 1 ) def step1(direction): next = { 0 : ( 1 , 0 ), 1 : ( 0 , 1 ), 2 : ( - 1 , 0 ), 3 : ( 0 , - 1 )}[direction & 0x3 ] global x, y x.append(x[ - 1 ] + next [ 0 ]) y.append(y[ - 1 ] + next [ 1 ]) def hilbert(order): global x, y x = [ 0 ,] y = [ 0 ,] _hilbert( 0 , 1 , order) return (x, y) x, y = hilbert( 7 ) inx = [] for i in range ( len (x)): inx.append((x[i],y[i])) inx = np.array(inx) new_p = Image.new( 'RGB' , ( 128 , 128 )) for i in range ( len (inx)): if tuple (wave_data[i]) ! = ( 255 , 255 , 255 ): new_p.putpixel(inx[i], ( 0 , 0 , 0 )) else : new_p.putpixel(inx[i], ( 255 , 255 , 255 )) new_p.save(pic) for i in tqdm( range ( 104 )): wav_to_pic( 'wavs/' + str (i) + '.wav' , 'res/' + str (i) + '.png' ) |
然后可以发现上面有部分是缺省了一个数字而有的没有缺省,把没有缺省的代入0,缺省的代入缺省的数字这里用了百度的ocr(图片不多,也可以人工去查看),把所有数字凑起来以后long_to_bytes即可得到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 | res2 = [] import requests,base64,json from urllib.parse import quote_from_bytes import time from tqdm import tqdm requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS = 'ALL' url = 'https://aip.baidubce.com' path = '/rest/2.0/ocr/v1/accurate_basic' headers = {} headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded;charset=UTF-8' headers[ 'Host' ] = 'aip.baidubce.com' params = {} params[ 'access_token' ] = '***********************************************************' for ii in tqdm( range ( 104 )): time.sleep( 1 ) body = 'image=' + quote_from_bytes(base64.b64encode( open ( 'res/' + str (ii) + '.png' , 'rb' ).read())) r = requests.Session() rr = r.post(url + path,data = body,headers = headers,params = params,verify = False ) res = json.loads(rr.text) f_res = "" print (res) for i in range ( len (res[ "words_result" ])): f_res + = res[ "words_result" ][i][ "words" ] print (f_res) res2.append( str (f_res)) print (res2) res3 = '' for i in res2: for j in [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ]: flag = 1 if str (j) not in i: res3 + = str (j) flag = 0 break if flag: res3 + = '0' print (res3) from Crypto.Util import number print (number.long_to_bytes( int (res3))) |
Hacked_by_L1near
这里我们可以知道是tomcat的websocket,基本上都是默认开启了permessage-deflate,然后分析数据包我们也可以知道其中的permessage-deflate的开启情况,我们总的可以通过RFC 7692 - Compression Extensions for WebSocket (ietf.org)此处的协议来编写脚本,中间有些数据会失真,我们无法解出,但是使用cyberchef仍然可以看到部分数据,比如第4个流:
exp.py:
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 | from Crypto.Util.number import * import zlib def unmark(masked_data,mask_key,payload_length): res = b'' for i in range ( len (masked_data)): res + = long_to_bytes(masked_data[i] ^ mask_key[i % 4 ]) payload = hex (bytes_to_long(res))[ 2 :] fin_payload = _fill(payload,payload_length) return fin_payload def _fill(payload,payload_length): if payload.__len__()! = payload_length * 2 : payload = payload.zfill(payload_length * 2 ) payload = payload[ 0 ] + hex ( int (payload[ 1 ], 16 ) + 1 )[ 2 :] + payload[ 2 :] return payload f = open ( '1.txt' , 'r' ).read().split( '\n' ) # print(f) for ff in f: try : websocket_info = bin ( int (ff[: 4 ], 16 ))[ 2 :] mode = websocket_info[ 1 ] if mode = = '1' : print ( 'permessage-deflate' ) else : # print('no permessage-deflate') continue payload_length = int (websocket_info[ - 7 :], 2 ) mask = websocket_info[ 8 ] if mask = = '1' : print ( 'marked' ) if payload_length ! = 0 : if payload_length = = 126 : payload_length = int (ff[ 4 : 8 ], 16 ) print ( 'payload_length:' ,payload_length) mask_key = long_to_bytes( int (ff[ 8 : 16 ], 16 )) # print(ff[8:16]) masked_data = long_to_bytes( int (ff[ 16 :], 16 )) payload = unmark(masked_data,mask_key,payload_length) print ( 'payload:' ,payload) data = long_to_bytes( int (payload, 16 )) fin = zlib.decompress(data, - 15 ) print (fin.decode()) else : print ( 'payload_length:' ,payload_length) mask_key = long_to_bytes( int (ff[ 4 : 12 ], 16 )) # print(mask_key) masked_data = long_to_bytes( int (ff[ 12 :], 16 )) payload = unmark(masked_data,mask_key,payload_length) print ( 'payload:' ,payload) data = long_to_bytes( int (payload, 16 )) fin = zlib.decompress(data, - 15 ) print (fin.decode()) else : print ( 'payload_length:' ,payload_length) print () else : print ( 'unmarked' ) if payload_length ! = 0 : if payload_length = = 126 : payload_length = int (ff[ 4 : 8 ], 16 ) print ( 'payload_length:' ,payload_length) payload = hex ( int (ff[ 8 :], 16 ))[ 2 :] fin_payload = _fill(payload,payload_length) print ( 'payload:' ,fin_payload) data = long_to_bytes( int (fin_payload, 16 )) fin = zlib.decompress(data, - 15 ) print (fin.decode()) else : print ( 'payload_length:' ,payload_length) payload = hex ( int (ff[ 4 :], 16 ))[ 2 :] fin_payload = _fill(payload,payload_length) print ( 'payload:' ,fin_payload) data = long_to_bytes( int (fin_payload, 16 )) fin = zlib.decompress(data, - 12 ) print (fin.decode()) else : print ( 'payload_length:' ,payload_length) print () except : print () pass |
nano
- PaxHeader以高度准确的方式记录了文件的创建时间,当使用tar解压原附件时,我们可以使用命令stat来显示每个图像的不同创建时间。
- 挑战的描述中说:"看看这些雪!"。哦,等一下......"。为了观看nanoTV(?),我们需要根据图像的创建时间来排序。
- 为了方便观看,我们可以制作一个每秒30帧的GIF,并观看它。然后我们就可以看到flag从右向左漂移了!(大致可以看到flag从右向左漂移。(大致可以看到中间的几秒钟)。
https://cache.nan.pub/imgs/flag.gif
nanoStego
1、找到两个IEND PNG_CHUNK,分割得到两个png文件。
2、检查IDAT PNG_CHUNK。根据PNG的结构,通过对IDAT内的数据进行连接和解压,可以得到原始图像数据。然而,zlib是用来解压的,它并不关心原始图像数据后面的额外数据。注意到这一点,你就可以解压这部分,得到一个Python脚本和一个.ttf字体。
例如:图像中被框住的部分是Python脚本的压缩位置。
3、Python脚本在这里。实现了一个盲水印的功能,所以我们需要写一个盲水印的解码程序。
4、有一个解码程序是不够的。盲水印的实现还涉及到阿诺德的猫图,我们需要A和B的值。水印的大小是150*150,所以0<=A,B<150。
试着列举出A和B的值来进行解码。当A和B的值都不对时,你会得到一个随机的图像。但当A或B的值正确时,你可以看到一些有图案的图像。
经过这一步,我们可以得到A和B的正确值。
5、最后你会得到这个水印。但是为什么我们看不到flag呢?这是因为L43的代码做了一个类型强制转换,导致水印中值为255的像素被保留,而其他低于255的像素值被平移为0。
6、如何解决这个问题?我们可以从短到长不断地猜测flag。用相同的参数和相同的字体文件在图像上打印所有被猜中的flag,并选择像素匹配数较高的flag,然后继续进行。完成了!
[招生]科锐逆向工程师培训46期预科班将于 2023年02月09日 正式开班