记某项目的二顾茅庐5K实战
一顾茅庐
漏洞一:存在逻辑缺陷导致无限发布新动态和可修改动态问题
可以看到此时发布了一个动态,还可以发布两个动态。
点击发布新动态,填写好信息点击提交并抓包
可以发现成功发布,回到动态页面可以看到可发布次数还是2,并且新发布的动态比正常发布的还多了一个修改的功能,可以正常使用此功能进行修改已经发布的动态。
再发一次,字段还是-1,下面的展界改为135,正常发布动态是无法选择展界的,此时可以成功的任意修改数据,并且可发布数量还是为2,证明了可以无限次发布新动态。
漏洞二:存在突破发布数量限制的问题
目前还剩一次发布动态的机会,点击发布新动态并填写信息
点击提交并抓包,进行并发测试,并发结束后再将原包正常放回
可以发现是成功了一定的数量的,回到页面可以看到成功发布了7条动态,超过了系统的限制
漏洞三:查询处因设计缺陷存在拒绝服务攻击漏洞
正常查询的时候需要时间是834ms
可能图有点糊看不清,接口请求如下默认请求接口:xxx/query?current=1&size=10&title=&type=&read=&r=1695628867371将参数调大进行测试:xxx/query?current=1000&size=1000000&title=&type=10000000&read=&r=1695628867371然后发包,发现服务器的回显时间变得很长,达到了46337ms
此时再配合多线程就可以对服务器造成拒绝服务攻击致使服务器瘫痪,对所有用户的使用造成影响,危害大。这里为了不影响正常业务不进行下一步利用。
二顾茅庐
漏洞一:敏感信息泄露
随便点点点,来到人员证件申请处,点击查询并抓包,如下
抓取数据包
将value置空
url编码后重新发送,返回了大量敏感信息
漏洞二:未授权+越权
可以看到目前账号只有一个动态如图所示:
刷新此页面,然后进行抓包
逐个放包直到获取到此包,可以得到本人的动态信息
然后我们将companyId置空
可以未授权看到很多其它企业的敏感信息,并且可以看到我们所要的动态值
点击删除并抓包
然后放包,可以看到成功的删除掉了动态。此处存在水平越权
利用子域的System权限通往父域
前言
最近翻阅笔记发现一篇文章提到通过子域的System权限可以突破获取到父域权限,本文将对此技术进行尝试复现研究。
利用分析
环境信息:
子域:187、sub.cs.org
父域:197、cs.org
首先通过在子域的域控机器上打开mmc.exe->连接ADSI->配置来查看子域的配置命名上下文:
从配置中可以看到配置命名上下文的域名实际上是父域cs.org,因此判断子域中看到的信息可能是父域的副本:
继续查看配置对象的安全描述符中的ACL,发现子域没有权限去变更:
但是可以看到除了域用户、域管用户以外,还有一个特权ACL条目叫SYSTEM,该条目拥有完全控制权限:
SYSTEM属于一个特殊用户,不属于域内用户,因此理论上只要能做到是SYSTEM权限就能控制对象条目而不用关注是不是域内管理员。因此尝试使用SYSTEM权限继续打开配置命名上下文:
可以看到当子域拥有了SYSTEM权限后就可以修改来自父域副本的配置对象:
利用方式
既然可以控制父域的配置命名上下文,那如何利用呢?网上提到有几种方式,一种是通过GPO、还有的是提到给父域添加一个自己可控的证书模板(ESC1),这里以GPO组策略为例。先在子域域控上创建一个GPO:
New-GPO jumbo_gpo_test
设置计划任务:
通过SYSTEM权限把子域的GPO link到父域:
PS C:\Windows\system32> Get-ADDomainController -Server cs.org | select HostNane, ServerObjectDN
HostNane ServerObjectDN
-------- --------------
{} CN=10_4_45_197,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=cs,DC=orgPS C:\Windows\system32> New-GPLink -Name "jumbo_gpo_test" -Target "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=cs,DC=org" -Server sub.cs.org
GpoId : 76606696-cd03-4349-b0f2-0a45bdf305d4
DisplayName : jumbo_gpo_test
Enabled : True
Enforced : False
Target : CN=Default-First-Site-Name,cn=Sites,CN=Configuration,DC=cs,DC=org
Order : 1
父域刷新组策略可以看到子域链接过来的GPO:
父域更新组策略成功执行计划任务notepad.exe:
gpupdate /force
刷新组策略后通过gpresult /r命名也可以看到添加的GPO:
总结
本文介绍了除SidHistory以外还可以通过子域的System权限进行突破到父域的攻击手法。
LLVM IR 深入研究分析
前置知识
LLVM是C++编写的构架编译器的框架系统,可用于优化以任意程序语言编写的程序。
LLVM IR可以理解为LLVM平台的汇编语言,所以官方也是以语言参考手册(Language Reference Manual)的形式给出LLVM IR的文档说明。既然是汇编语言,那么就和传统的CUP类似,有特定的汇编指令集。但是它又与传统的特定平台相关的指令集(x86,ARM,RISC-V等)不一样,它定位为平台无关的汇编语言。也就是说,LLVM IR是一种相对于CUP指令集高级,但是又是一种低级的代码中间表示(比抽象语法树等高级表示更加低级)。
LLVM IR即代码的中间表示,有三种形式:
.ll 格式:人类可以阅读的文本(汇编码) -->这个就是我们要学习的IR
.bc 格式:适合机器存储的二进制文件
内存表示
下面给出.ll格式和.bc格式生成及相互转换的常用指令清单:
.c -> .ll:clang -emit-llvm -S a.c -o a.ll
.c -> .bc: clang -emit-llvm -c a.c -o a.bc
.ll -> .bc: llvm-as a.ll -o a.bc
.bc -> .ll: llvm-dis a.bc -o a.ll
.bc -> .s: llc a.bc -o a.s
那么我们以一道CTF赛题来分析实验,学习LLVM IR
实验解析
题目附件直接给出了中间表示.II文件
打开查看一下汇编码,毕竟.II文件是人类可以阅读的文本,这边笔者使用的是Sublime Text(使用VScode查看即可)代码量不多,大概600行
题目初步分析
我们直接寻找一下main函数
我们可以看出题目经历了两次RC4,然后Base64,我们从上面可以看到密文,RC4_key,我们直接一把锁,cyberchef启动,会发现解不出来,那么程序应该做了其他的操作,最朴素的,我们可以想到把RC4魔改了,base64魔改等等。
So!继续学习研究ing
.II详细分析
所以本着学习的态度,我们这时候应该掏出LLVM Language Reference Manual(官方文档)来简单了解学习一些常见指令、符号标识以及特性。这边给出一些分析 .ll 中间文件的算法流程
@ - 全局变量
% - 局部变量
alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存
i32 - 32位4字节的整数
align - 对齐
load - 读出,store写入
icmp - 两个整数值比较,返回布尔值
br - 选择分支,根据条件来转向label,不根据条件跳转的话类型goto
label - 代码标签
call - 调用函数
首先看到一些全局变量,知道了RC4_key = llvmbitccipher = "TSzkWKgbMHszXaj@kLBmRrnTxsNtZsSOtZzqYikCw="
我们继续分析,重点分析各个function
b64encode
b64encode 魔改
每三个字符,24位,切分成4断,每段6位。
将6位对应的值 (value+ 59)&0xff 则是编码后的值。
%22 = getelementptr inbounds i8, i8* %19, i64 %21 // 取出当前处理字符
%23 = load i8, i8* %22, align 1
%24 = zext i8 %23 to i32 // 类型强制转化
%25 = ashr i32 %24, 2 // 算数右移两位 input[i]>>2
%26 = add nsw i32 %25, 59 // input[i]+59
%27 = trunc i32 %26 to i8 // 强制转化 相当于 &0xff
%28 = load i8*, i8** %6, align 8
%29 = load i32, i32* %9, align 4
%30 = sext i32 %29 to i64
%31 = getelementptr inbounds i8, i8* %28, i64 %30 // 存储base64 编码串
store i8 %27, i8* %31, align 1
%32 = load i8*, i8** %4, align 8
%33 = load i32, i32* %7, align 4
%34 = sext i32 %33 to i64
%35 = getelementptr inbounds i8, i8* %32, i64 %34
%36 = load i8, i8* %35, align 1
%37 = zext i8 %36 to i32
%38 = and i32 %37, 3 // 获取第一个字符 低两位
%39 = shl i32 %38, 4 // 左移四位
RC4_init
RC4_init 正常,无魔改
define dso_local void @Rc4_Init(i8*, i32) #0 { //RC4_init function
%3 = alloca i8*, align 8
%4 = alloca i32, align 4
%5 = alloca i32, align 4
%6 = alloca i32, align 4
store i8* %0, i8** %3, align 8
store i32 %1, i32* %4, align 4 //初始化S,T盒
call void @llvm.memset.p0i8.i64(i8* align 16 getelementptr inbounds ([256 x i8], [256 x i8]* @s, i64 0, i64 0), i8 0, i64 256, i1 false)
call void @llvm.memset.p0i8.i64(i8* align 16 getelementptr inbounds ([256 x i8], [256 x i8]* @t, i64 0, i64 0), i8 0, i64 256, i1 false)
store i32 0, i32* %5, align 4
br label %7
7: ; preds = %26, %2
%8 = load i32, i32* %5, align 4
%9 = icmp slt i32 %8, 256
br i1 %9, label %10, label %29 //如果 %9 为真(即 %8 小于 256),跳转到标签 %10;否则跳转到标签 %29,根据t打乱s盒
10: ; preds = %7
%11 = load i32, i32* %5, align 4
%12 = trunc i32 %11 to i8
%13 = load i32, i32* %5, align 4
%14 = sext i32 %13 to i64
%15 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %14
store i8 %12, i8* %15, align 1
%16 = load i8*, i8** %3, align 8
%17 = load i32, i32* %5, align 4
%18 = load i32, i32* %4, align 4
%19 = urem i32 %17, %18
%20 = zext i32 %19 to i64
%21 = getelementptr inbounds i8, i8* %16, i64 %20
%22 = load i8, i8* %21, align 1
%23 = load i32, i32* %5, align 4
%24 = sext i32 %23 to i64
%25 = getelementptr inbounds [256 x i8], [256 x i8]* @t, i64 0, i64 %24
store i8 %22, i8* %25, align 1
br label %26
26: ; preds = %10
%27 = load i32, i32* %5, align 4
%28 = add nsw i32 %27, 1
store i32 %28, i32* %5, align 4
br label %7
29: ; preds = %7
store i32 0, i32* %6, align 4
store i32 0, i32* %5, align 4
br label %30
30: ; preds = %54, %29
%31 = load i32, i32* %5, align 4
%32 = icmp slt i32 %31, 256
br i1 %32, label %33, label %57
33: ; preds = %30
%34 = load i32, i32* %6, align 4
%35 = load i32, i32* %5, align 4
%36 = sext i32 %35 to i64
%37 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %36
%38 = load i8, i8* %37, align 1
%39 = zext i8 %38 to i32
%40 = add nsw i32 %34, %39
%41 = load i32, i32* %5, align 4
%42 = sext i32 %41 to i64
%43 = getelementptr inbounds [256 x i8], [256 x i8]* @t, i64 0, i64 %42
%44 = load i8, i8* %43, align 1
%45 = zext i8 %44 to i32
%46 = add nsw i32 %40, %45
%47 = srem i32 %46, 256
store i32 %47, i32* %6, align 4
%48 = load i32, i32* %5, align 4
%49 = sext i32 %48 to i64
%50 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %49
%51 = load i32, i32* %6, align 4
%52 = sext i32 %51 to i64
%53 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %52
call void @swap(i8* %50, i8* %53) //call swap function
br label %54
RC4_enc
RC4_enc 魔改 多了一层xor 89
define dso_local void @Rc4_Encrypt(i8*, i32) #0 { //RC4_enc function
%3 = alloca i8*, align 8
%4 = alloca i32, align 4
%5 = alloca i8, align 1
%6 = alloca i8, align 1
%7 = alloca i8, align 1
%8 = alloca i8, align 1
store i8* %0, i8** %3, align 8
store i32 %1, i32* %4, align 4
store i8 0, i8* %6, align 1
store i8 0, i8* %7, align 1
store i8 0, i8* %8, align 1
br label %9
9: ; preds = %14, %2
%10 = load i8, i8* %8, align 1
%11 = zext i8 %10 to i32
%12 = load i32, i32* %4, align 4
%13 = icmp ult i32 %11, %12
br i1 %13, label %14, label %64
14: ; preds = %9
%15 = load i8, i8* %6, align 1
%16 = zext i8 %15 to i32
%17 = add nsw i32 %16, 1
%18 = srem i32 %17, 256
%19 = trunc i32 %18 to i8
store i8 %19, i8* %6, align 1
%20 = load i8, i8* %7, align 1
%21 = zext i8 %20 to i32
%22 = load i8, i8* %6, align 1
%23 = zext i8 %22 to i64
%24 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %23 //生成密钥流
%25 = load i8, i8* %24, align 1
%26 = zext i8 %25 to i32
%27 = add nsw i32 %21, %26
%28 = srem i32 %27, 256
%29 = trunc i32 %28 to i8
store i8 %29, i8* %7, align 1
%30 = load i8, i8* %6, align 1
%31 = zext i8 %30 to i64
%32 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %31
%33 = load i8, i8* %7, align 1
%34 = zext i8 %33 to i64
%35 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %34 //经典Swap了再加
call void @swap(i8* %32, i8* %35)
%36 = load i8, i8* %6, align 1
%37 = zext i8 %36 to i64
%38 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %37
%39 = load i8, i8* %38, align 1
%40 = zext i8 %39 to i32
%41 = load i8, i8* %7, align 1
%42 = zext i8 %41 to i64
%43 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %42
%44 = load i8, i8* %43, align 1
%45 = zext i8 %44 to i32
%46 = add nsw i32 %40, %45
%47 = srem i32 %46, 256
%48 = sext i32 %47 to i64
%49 = getelementptr inbounds [256 x i8], [256 x i8]* @s, i64 0, i64 %48
%50 = load i8, i8* %49, align 1
store i8 %50, i8* %5, align 1
%51 = load i8, i8* %5, align 1
%52 = zext i8 %51 to i32
%53 = xor i32 %52, 89 //xor 89
%54 = load i8*, i8** %3, align 8
%55 = load i8, i8* %8, align 1
%56 = zext i8 %55 to i64
%57 = getelementptr inbounds i8, i8* %54, i64 %56
%58 = load i8, i8* %57, align 1
%59 = zext i8 %58 to i32
%60 = xor i32 %59, %53 //xor k
%61 = trunc i32 %60 to i8
store i8 %61, i8* %57, align 1
%62 = load i8, i8* %8, align 1
%63 = add i8 %62, 1
store i8 %63, i8* %8, align 1
br label %9
64: ; preds = %9
ret void
}
main
main函数逻辑cipher -->RC4_init-->RC4_enc-->RC4_enc-->b64encode需要注意一下在RC4_enc的参数中,传入的数据块长度是固定的16,所以说程序进行两次RC4_enc的原因也就确定了,是为了分两次对程序进行加密,也算是一点点小手段,总之,即使让你好好分析.II代码,考察对软件分析的细节,耐心,嘻嘻。
OK,理清楚逻辑,就可以试着敲代码解密啦。
解密
逆向分析过程明了之后,那么写代码就简单多了
#include<stdio.h>
unsigned char s[300],t[300];
void b64decode(unsigned char * enc,unsigned char* dec);
void Rc4_dec1(int len, unsigned char *enc);
void Rc4_Init(char *key,int len);
void Rc4_dec2(int len, unsigned char *enc);
int main() {
unsigned char enc[50]="TSz`kWKgbMHszXaj`@kLBmRrnTxsNtZsSOtZzqYikCw=";
unsigned char dec1[50]={0x00};
char key[10] ="llvmbitc";
unsigned char a[50];
int i=0;
b64decode(enc,dec1);
Rc4_Init(key,8);
Rc4_dec1(16,&dec1[16]);
for(i=0;i<16;i++) {
dec1[i+16]^=dec1[i];
}
Rc4_Init(key,8);
Rc4_dec2(16,dec1);
printf("%s",dec1);
return 0;
}
void b64decode(unsigned char * enc,unsigned char* dec) {
int i=0,j=0;
for(i=0;i<40;i+=4) {
dec[j] = ((enc[i]-59)<<2)&0xfc | (((enc[i+2]-59)>>4))&3;
dec[j+1] = (((enc[i+2]-59)&0xf)<<4) | (((enc[i+1]-59)>>2)&0xf);
dec[j+2] = (((enc[i+1]-59)&3)<<6) | ((enc[i+3]-59)&0x3f);
j+=3;
}
dec[j] = ((enc[i]-59)<<2)&0xfc | (((enc[i+1]-59)>>4))&3;
dec[j+1] = (((enc[i+2]-59)>>2)&0xf) | (((enc[i+1]-59)<<4)&0xf0);
dec[j+2]=0;
}
void Rc4_Init(char *key,int len) {
int i=0,v5=0;
unsigned char temp;
for(i=0;i<256;i++) {
s[i] =i;
t[i] = key[i%len];
}
for(i=0;i<256;i++) {
v5=(s[i]+t[i]+v5)%256;
temp = s[i];
s[i]= s[v5];
s[v5]=temp;
}
}
void Rc4_dec1(int len, unsigned char *enc) {
int v3=0,v5=0,i,j;
unsigned char temp;
for(i=0;i<len;i++) {
v3=(v3+1)%256;
v5=(s[v3]+v5)%256;
temp=s[v3];
s[v3]=s[v5];
s[v5]=temp;
}
v5=v3=0;
for(i=0;i<len;i++) {
v3=(v3+1)%256;
v5 = (s[v3]+v5)%256;
temp = s[v3];
s[v3]=s[v5];
s[v5]=temp;
enc[i]^=s[(s[v5]+s[v3])%256]^0x59;
}
}
void Rc4_dec2(int len, unsigned char *enc) {
int v3=0,v5=0,i,j;
unsigned char temp;
v5=v3=0;
for(i=0;i<len;i++) {
v3=(v3+1)%256;
v5 = (s[v3]+v5)%256;
temp = s[v3];
s[v3]=s[v5];
s[v5]=temp;
enc[i]^=s[(s[v5]+s[v3])%256]^0x59;
}
}
flag{Hacking_for_fun@reverser$!}
总结
通过这么一道CTF题目,深入学习LLVM IR的冰山一角,认真实验,细细分析,相信会对你有极大帮助。当然,如果单从解题来说,对于解决这道题有很多的办法,比如说将.II转化为可执行文件,然后IDA分析,但我们旨在学习LLVM IR,这里不再过多赘述。
MFC框架软件逆向研究
MFC框架简介
什么是mfc?
MFC库是开发Windows应用程序的C++接口。MFC提供了面向对象的框架,采用面向对象技术,将大部分的Windows API 封装到C++类中,以类成员函数的形式提供给程序开发人员调用。
简单来说,MFC是一种面向对象,用于开发windows应用程序的框架,突出特点是封装了大部分windows API,便于开发人员使用(写win挂方便)。
MFC程序的运行过程分为以下四步:
利用全局应用程序对象theApp启动应用程序。
调用全局应用程序对象的构造函数,从而调用基类(CWinApp)的构造函数,完成应用程序的一些初始化工作,并将应用程序对象的指针保存起来。
进入WinMain函数。在AfxWinMain函数中获取子类的指针,利用指针实现上述的三个函数,从而完成窗口的创建注册等工作。
进入消息循环,一直到WM_QUIT。
那么问题来了,我们如何逆向mfc程序呢?因为其封装了大部分windows API,逆向起来也复杂了不少,因为需要了解大量的windows api 并且熟悉windows编程。下面进行讲解。
MFC如何逆向
如下图,是MFC框架软件的基本界面,可以看到,就是一堆button,主要逆向也是check button。
那么,对于MFC逆向,我们主要需要知道的是,当我们执行某个操作(点击某个按钮)的时候,程序会执行什么处理函数。在mfc中,程序是使用消息机制来实现操作响应的,这个是消息映射表的代码:
struct AFX_MSGMAP{
AFX_MSGMAP * pBaseMessageMap;
AFX_MSGMAP_ENTRY * lpEntries;
}
struct AFX_MSGMAP_ENTRY{
UINT nMessage; //Windows Message
UINT nCode //Control code or WM_NOTIFY code
UINT nID; //control ID (or 0 for windows messages)
UINT nLastID; //used for entries specifying a range of control id's
UINT nSig; //signature type(action) or pointer to message
AFX_PMSG pfn; //routine to call (or specical value)
}
其中这个AFX_MSGMAP_ENTRY中的最后一个成员AFX_PMSG就是一个函数指针,指向了当前控件绑定的函数。同时,这个nID成员描述的是当前控件的ID,利用这个ID就能确定我们所寻找的控件。然后这个AFX_MSGMAP结构体则会记录一个指向AFX_MSGMAP_ENTRY的指针,于是查找控件的注册函数的思路可以缩小为:
找到AFX_MSGMAP
找到控件的ID --- 关键就是找ID
那么,我们又该怎么找到控件ID呢,俗话说“工欲善其事,必先利其器”,作为逆向分析人员,肯定要选择好分析的工具了,很庆幸,我们站在巨人的肩膀上,针对mfc软件程序的逆向分析,前辈们已经开发了一些非常好用的小工具,我们可以直接使用它们。例如:
xspy
ResourceHacker
彗星小助手
其中我们主要用的是xspy,mfc分析利器如下图所示
逆向实验-以CTF赛题为例讲解
demo1 - MFC初探
打开程序软件
程序的标题Flag就在控件中,然后界面内容是让我们找一个key。很明显,我们需要找到两个东西
标题找Flag(也就是找窗口句柄)
内容找key
根据这些内容,告诉我们我们去找控件,然后这时候就要掏出xspy了。不然的话,我们如果使用老一套经典分析流程,die+ida对用架构分析,会发生下面这样的事。首先die查个架构,查个壳
好家伙,VMP壳,PE32ida走起,如下图,emmm....
这样的话,我们很难继续往下分析,所以我们使用xspy分析。使用方法如下图
首先我们找到了Flag_enc(944c8d100f82f0c18b682f63e4dbaa207a2f1e72581c2f1b)我们知道特定的,窗口句柄叫 HWND
然后我们可以发现一条特殊的onMsgOnMsg:0464,func= 0x00402170(MFC1.exe+ 0x002170 )为什么特殊呢,因为只有它并不是以宏的形式出现,应该是作者自定义的消息,没有button等东西,所以程序怎么点击都无法触发任何效果;并且传入一个特殊数字0464,来触发效果。
那么,我们需要去发送这条消息来出发func函数以获取我们需要的key
#include<Windows.h>
#include<stdio.h>
int main()
{
HWND h = FindWindowA(NULL, "Flag就在控件里");
if (h)
{
SendMessage(h, 0x0464, 0, 0);
printf("success");
}
else printf("failure");
}
使用 API FindWindow 获取窗口句柄,SendMessage发送消息,得到了key{I am a Des key}
最后DES解密即可
flag{thIs_Is_real_kEy_hahaaa}
Junk_instruction-西湖论剑
下面,再讲解一道大型比赛的赛题来实验打开,看到这个朴素的界面可以鉴定是MFC框架。
我们看到了一个input,还有一个check button,很明显,我们首先就需要去找check button的id&注册函数。
xspy-MFC分析
check按钮的id为03e9,同时窗口存在OnCommand: notifycode=0000 id=03e9,func= 0x00C72420(Junk_Instruction.exe+ 0x002420 )函数。那么对应的check逻辑肯定在基址+偏移0x002420处。打开ida,找到check函数 sub_402420 ,如下图
可以看到有一个条件判断:if ( (unsigned __int8)sub_402600(v2 + 16) )。一眼顶针,两个分支分别是弹出正确和错误的对话框,为什么呢?if else函数体内容基本一样。当然我们还是动态调试一下
所以enc函数很明显就是sub_402600这个函数中就出现了很多垃圾指令了,也就对应上题目名称Junk_instruction了。
去花-IDA分析
爆红
花指令,经典call $+5起手,就是先用一个call压好返回地址,再把栈里的返回地址弹出来,改一下,压回去,如此反复。去掉也很简单,我们把下述累死指令块全部nop掉即可,有好几处,一模一样。
当然,我们使用idapython脚本自动去花
from ida_bytes import get_bytes, patch_bytes
import re
addr = 0x402600
end = 0x402fe3
buf = get_bytes(addr, end-addr)
def nopp(s):
s = s.group(0)
print("".join(["%02x"%i for i in s]))
s = b"\x90"*len(s)
return s
pattern = b"\xe8\x00\x00\x00\x00\x58\x89.*?\xc3.*?\x22"
buf = re.sub(pattern , nopp, buf, flags=re.I)
patch_bytes(addr, buf)
print("Done")
加密
去除花指令,简单审计发现是对程序进行RC4加密,最后还对输入进行了个倒叙
去花后,整理一下,代码如下
char __thiscall sub_402600(void *this, int a2)
{
const WCHAR *v2; // eax
void *v3; // eax
char v5[511]; // [esp+9h] [ebp-4BBh] BYREF
int v6; // [esp+208h] [ebp-2BCh]
char *v7; // [esp+20Ch] [ebp-2B8h]
int v8; // [esp+210h] [ebp-2B4h]
size_t Count; // [esp+214h] [ebp-2B0h]
int v10; // [esp+218h] [ebp-2ACh]
size_t v11; // [esp+21Ch] [ebp-2A8h]
char *v12; // [esp+220h] [ebp-2A4h]
char *v13; // [esp+224h] [ebp-2A0h]
int v14; // [esp+228h] [ebp-29Ch]
char v15[4]; // [esp+22Ch] [ebp-298h] BYREF
char *Source; // [esp+230h] [ebp-294h]
void *v17; // [esp+234h] [ebp-290h]
char cipher[32]; // [esp+238h] [ebp-28Ch]
const char *v19; // [esp+258h] [ebp-26Ch]
char *v20; // [esp+25Ch] [ebp-268h]
int i; // [esp+260h] [ebp-264h]
char *p_Destination; // [esp+264h] [ebp-260h]
char v23; // [esp+26Dh] [ebp-257h]
char v24; // [esp+26Eh] [ebp-256h]
char v25; // [esp+26Fh] [ebp-255h]
char v26[28]; // [esp+270h] [ebp-254h] BYREF
char v27[256]; // [esp+28Ch] [ebp-238h] BYREF
char key[256]; // [esp+38Ch] [ebp-138h] BYREF
char Destination; // [esp+48Ch] [ebp-38h] BYREF
char v30[39]; // [esp+48Dh] [ebp-37h] BYREF
int v31; // [esp+4C0h] [ebp-4h]
v17 = this;
v31 = 3;
cipher[0] = 91;
cipher[1] = -42;
cipher[2] = -48;
cipher[3] = 38;
cipher[4] = -56;
cipher[5] = -35;
cipher[6] = 25;
cipher[7] = 126;
cipher[8] = 110;
cipher[9] = 62;
cipher[10] = -53;
cipher[11] = 22;
cipher[12] = -111;
cipher[13] = 125;
cipher[14] = -1;
cipher[15] = -81;
cipher[16] = -35;
cipher[17] = 118;
cipher[18] = 100;
cipher[19] = -80;
cipher[20] = -9;
cipher[21] = -27;
cipher[22] = -119;
cipher[23] = 87;
cipher[24] = -126;
cipher[25] = -97;
cipher[26] = 12;
cipher[27] = 0;
cipher[28] = -98;
cipher[29] = -48;
cipher[30] = 69;
cipher[31] = -6;
v2 = (const WCHAR *)sub_401570(&a2);
v14 = sub_4030A0(v2);
v10 = v14;
v3 = (void *)sub_401570(v14);
sub_403000(v3);
sub_4012A0(v15);
Source = (char *)unknown_libname_1(v26);
v20 = Source;
v13 = Source + 1;
v20 += strlen(v20);
v11 = ++v20 - (Source + 1);
Count = v11;
Destination = 0;
memset(v30, 0, sizeof(v30));
strncpy(&Destination, Source, v11);
if ( sub_402AF0(&Destination) )
{
v23 = 0;
v25 = 0;
LABEL_7:
v24 = v25;
}
else
{
strcpy(key, "qwertyuiop"); // key
memset(&key[11], 0, 0xF5u);
memset(v27, 0, sizeof(v27));
memset(v5, 0, sizeof(v5));
v19 = key;
v7 = &key[1];
v19 += strlen(v19);
v6 = ++v19 - &key[1];
RC4_init((int)v27, key, v19 - &key[1]); // RC4_init
p_Destination = &Destination;
v12 = v30;
p_Destination += strlen(p_Destination);
v8 = ++p_Destination - v30;
RC4_crypt((int)v27, (int)&Destination, p_Destination - v30);// RC4_crypto
for ( i = 31; i >= 0; --i )
{
if ( v30[i - 1] != cipher[i] ) // 倒叙
{
v25 = 0;
goto LABEL_7;
}
}
v24 = 1;
}
LOBYTE(v31) = 0;
sub_403060(v26);
v31 = -1;
sub_4012A0(&a2);
return v24;
}
解密
首先提取密文,利用插件Lazy_ida 5BD6D026C8DD197E6E3ECB16917DFFAFDD7664B0F7E58957829F0C009ED045FA
key-->qwertyuiop
cyberchef 得解
flag{973387a11fa3f724d74802857d3e052f}
游戏安全入门-扫雷分析&远程线程注入
前言
无论学习什么,首先,我们应该有个目标,那么入门windows游戏安全,脑海中浮现出来的一个游戏 -- 扫雷,一款家喻户晓的游戏,虽然已经被大家分析的不能再透了,但是我觉得自己去分析一下还是极好的,把它作为一个小目标再好不过了。
我们编写一个妙妙小工具,工具要求实现以下功能:时间暂停、修改表情、透视、一键扫雷等等。
本文所用工具:
Cheat Engine、x32dbg(ollydbg)、Visual Studio 2019
扫雷游戏分析
游戏数据在内存中是地址,那么第一个任务,找内存地址
打开CE修改器
修改时间->时间暂停
计数器的时间是一个精确的值,所以我们通过精确数值扫描出来,游戏开始之前计数器上的数是0,所以我们扫描0。
时间在变化,选择介于什么数值之间再次扫描
可得 0x100579c --- winmine.exe+579C
我们发现这个数据都是直接通过基址 + 固定偏移能直接得到的。
然后我们对数据去找 是什么改写了这个地址,得到一个指令和指针:
时间:0x100579c
修改表情 - 没啥用
修改表情这个功能怎么搞我觉得还是很容易想到的,这个按钮的作用是重新开始游戏,开始游戏,游戏胜利,游戏失败。
(表情的状态被分成了两个变量(4byte)来控制)
所以它是一种状态,所以我们通过0和1进行扫描,游戏进行状态输入1进行扫描,还原游戏之后输入0进行扫描。
首先是游戏进行状态,输入1进行扫描
再点击表情,将游戏还原,输入0开始扫描
如此反复进行扫描,得到表情的内存地址
0x1005164 -- winmine.exe+5164
但是嘞,修改成2或者3,表情没有心得反应,所以控制游戏胜利和游戏失败的是其他的地址,我们知道,一般来说,一个功能的代码在内存中基本上都是连续的,(就像你修改一个游戏的血量,浏览血量内存块,你可以发现怒气,蓝量等内存地址)
所以,我们浏览内存
0x1005164-4 = 0x1005160
修改为3,发现出现了戴墨镜的表情(游戏胜利)
但是这个胜利知识一个状态,并不能说明扫雷完成.
表情:0x1005160与0x1005164
透视 - 显示雷区
思考游戏结束的时候会自动显示所有的雷,因此我们动态调试,看看在哪个函数调用之后会显示所有的雷
经过几次的动态调试之后发现:0x2F80函数是我们要找的结果。
一键扫雷
通过透视,我们玩一把游戏,使得游戏胜利(点完最后一个)
然后后两个函数,是破纪录跟英雄榜的函数
ret来到了这儿,游戏通关了,来到了这儿,可以知道,这个0x347c就是判断输赢的函数
并且通过调试发现由一个参数 0 1 来控制,所以跟透视差不多,带个参数线程回调就完了
编写妙妙小工具
怎么实现这个工具呢,当然是选择DLL注入
那么dll 怎么注入进去呢,这里选择远程线程注入
这里先简单介绍下什么是远程线程注入
前置知识-动态调用dll
主要就是这几个个 API:
LoadLibraryA
加载指定 DLL 并返回模块句柄,参数为字符串,就是 dll 的路径。
GetProcAddress
获取指定 dll 的导出函数的地址。
第一个参数是模块句柄,第二个参数是模块函数,返回值为函数的地址。
通过这两个函数,我们可以拿到所有函数的地址,然后就能进行调用。
CreateThread - 远程线程注入
里面几乎只有一个参数,那就是线程回调函数,然后当然还有返回地址,返回线程 id 啥的,这里我们都可以不用管,几乎是与 Linux 的创建线程函数一致。
还有一个远程版本的叫 CreateRemoteThread,它可以给别的进程创建一个线程并可以在本进程创建那个进程调用的回调函数。我们可以在回调函数中加载指定的 dll,在 dllmain 的入口当中,有一个 switch 的四个选项。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,//指向自身的句柄
DWORD ul_reason_for_call,//调用原因
LPVOID lpReserved//隐式加载or显式加载
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH://附加到进程上时执行
case DLL_THREAD_ATTACH://附加到线程上时执行
case DLL_THREAD_DETACH://从线程上剥离时执行
case DLL_PROCESS_DETACH://从进程上剥离时执行
break;
}
return TRUE;
}
我们可以在 DLL_PROCESS_ATTACH 的选项中加入代码,让它在加载的时候调用执行。
那么我们的步骤是:
打开指定进程获得句柄
开辟远程进程的空间,分配可读可写段。
调用 WriteProcessMemory 将 dll 路径写入该内存区域。
创建远程线程,回调函数使用 LoadLibrary 加载指定 dll。
等待返回(loadLibrary返回)
释放空间
释放句柄
返回结果
demo:
void Inject(DWORD ProcessId, const char* szPath)
{
//1.打开目标进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
printf("进程句柄:%p\n", hProcess);
//2.在目标进程体内申请空间
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//3.写入DLL路径
SIZE_T dwWriteLength = 0;
WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
//4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
//5.等待返回(loadLibrary返回)
WaitForSingleObject(hThread, -1);
//6.释放空间
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
//7.释放句柄
CloseHandle(hProcess);
CloseHandle(hThread);
//返回结果
AfxMessageBox(L"完成");
}
编写DLL注入器
#include<windows.h>
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>
DWORD FindProcess() {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32 = { sizeof(pe32) };
BOOL ret = Process32First(hSnap, &pe32);
while (ret)
{
if (!wcsncmp(pe32.szExeFile, L"mine.exe", 11)) {
printf("Find winmine.exe Process %d\n", pe32.th32ProcessID);
return pe32.th32ProcessID;
}
ret = Process32Next(hSnap, &pe32);
}
return 0;
}
void Inject(DWORD ProcessId, const char* szPath)
{
//1.打开目标进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
printf("进程句柄:%p\n", hProcess);
//2.在目标进程体内申请空间
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//3.写入DLL路径
SIZE_T dwWriteLength = 0;
WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
//4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
//5.等待返回(loadLibrary返回)
WaitForSingleObject(hThread, -1);
//6.释放空间
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
//7.释放句柄
CloseHandle(hProcess);
CloseHandle(hThread);
}
int main() {
DWORD ProcessId = FindProcess();
while (!ProcessId) {
printf("未找到扫雷程序,等待两秒中再试\n");
Sleep(2000);
ProcessId = FindProcess();
}
printf("开始注入进程...\n");
Inject(ProcessId, "E:\\CODE\\wimine\\Mine\\release\\Mine.dll");
printf("注入完毕\n");
}
编写DLL
这里我们采用MFC DLL 基于对话框 (dialog)的方式编写(简单),使用静态编译的方式
然后我们需要在资源窗体,新建一个 Dialog ,简单包装一个界面
这样我们在加载窗体的时候需要创建一个窗体类对象用它的 DoModal 方法去显示,用线程回调的方式加载并且初始化InitInstance
DWORD WINAPI DlgThreadCallBack(LPVOID lp) {
MineDlg* Dlg;
Dlg = new MineDlg();
Dlg->DoModal();
delete Dlg;
FreeLibraryAndExitThread(theApp.m_hInstance, 1);
return 0;
}
// CMineApp 初始化
BOOL CMineApp::InitInstance()
{
CWinApp::InitInstance();
::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL);
return TRUE;
}
时间暂停
上面我们找到了它控制时间增加的指令,我们把它们全部 NOP 掉,就可以实现时间暂停
写两个按钮,创建下面的事件实现时间暂停开关。
DWORD GetBaseAddr() {
HMODULE hMode = GetModuleHandle(nullptr);
//LPWSTR s = (LPWSTR)malloc(0x100);
//wsprintf(s, L"基址:%p", hMode);
//AfxMessageBox(s);
return (DWORD)hMode;
}
void MineDlg::OnBnClickedButton1() // 时间暂停
{
// TODO: 在此添加控件通知处理程序代码
auto BaseAddr=GetBaseAddr();
DWORD TimeOffset = 0x579C;
DWORD TimeInsOffset = 0x2FF5;
DWORD InsLen = 6;
DWORD old;
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
BYTE INS[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
memcpy((void *)(BaseAddr + TimeInsOffset), INS, InsLen);
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
void MineDlg::OnBnClickedButton2() // 恢复字节即可取消时间暂停
{
// TODO: 在此添加控件通知处理程序代码
auto BaseAddr = GetBaseAddr();
DWORD TimeOffset = 0x579C;
DWORD TimeInsOffset = 0x2FF5;
DWORD InsLen = 6;
DWORD old;
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
BYTE INS[] = { 0xFF,0x05,0x9C,0x57,0x00,0x01 };
memcpy((void*)(BaseAddr + TimeInsOffset), INS, 6);
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
测试
透视
经过上面动态调试我们得出结论:0x2F80函数是踩雷函数。
我们如果调用这个函数,是不是就能够实现透视了呢?
我们依旧采取线程回调的方式
void MineDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码
DWORD ESPOffset = 0x2f80;
DWORD FuncAddr = GetBaseAddr() + ESPOffset;
// 创建不带参数的线程
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FuncAddr, NULL, 0, NULL);
}
测试
一键扫雷
跟透视差不多,只不过创建带参数的线程回调
void MineDlg::OnBnClickedButton4()
{
// TODO: 在此添加控件通知处理程序代码
DWORD ESPOffset = 0x347C;
DWORD FuncAddr = GetBaseAddr() + ESPOffset;
//创建带参数的线程
struct { int a; } s = { 0 };
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL);
}
测试
总结
通过这个小项目,对WIN游戏安全有初步的认识,并且加强对软件的逆向思维,增强动态调试的能力,找到软件关键的基地址,通过CE修改器,初步pojie软件,了解软件的状态,修改时间(时间暂停等等),理解几个重要的API,FindWindow获取句柄,WriteProcessMemory写入内存信息,LoadLibraryA加载指定 DLL 并返回模块句柄,GetProcAddress,获取指定 dll 的导出函数的地址,CreateThread 线程回调函数等等。多写,多做,多调,多实验,加油,互勉。
【总结】注册码泄露原理以及例题
引言
题目给了小明的机器码:1653643685031597
用户user_id:xiaoming
可以看到题目采用了SIMD指令集
该指令格式在CTF和攻防对抗中经常出现,可以提高执行效率的同时也可以增加逆向的难度。
对于此类指令和题目,我们分析的方法是:遇到查意思,查的多了就跟看正常代码一样,采用动态分析。
机器码修改
将内置的机器码改为题目给的:1653643685031597
修改成功:
得到flag的时候跟machine这个有很大关系
动态分析
machine_id处理
在这个加密函数中
发现了MD5特征
经过动调拿到函数加密后的结果
与我们的猜测是相符的
可以发现最终md5(机器码)变成
user_id处理
和调试machine加密一样,最终MD5(user_id)变成:
最终处理
经过之前相同的加密
变成这个数字:1228240365737281
然而这还没完,居然进行两次相同加密
再次加密后的结果:0502036271810858
可以发现此题出的很好,利用了密码比较的漏洞,没有将密文给出,而是将生成的密文在中间给出,从而造成了数据泄露。
得到flag
回顾加密流程,可以发现
f(key) = f( f( f(md5(machine)) + f(md5(user_id)) ) )
那么题目给了得到flag的machine和user_id,可以得出
Key =f( f(md5(machine)) + f(md5(user_id)) )
所以最终flag:
flag{1228240365737281}
jwt伪造身份组组组合拳艰难通关
前言
现在的攻防演练不再像以往那样一个漏洞直捣黄龙,而是需要各种组合拳才能信手拈来,但是有时候使尽浑身解数也不能称心如意。
前期信息收集
首先是拿到靶标的清单
访问系统的界面,没有什么能利用的功能点
首先进行目录扫描,扫描发现存在xxx.zip的文件放置在web目录上
一般zip文件大部分情况都是开发运维人员做系统维护时留下的备份文件,在系统上线后并没有将其删除,于是底裤(即源代码)都直接给到了攻击者
来到这一步都以为是一路高歌,轻松拿下,没想象到是跌宕起伏伏伏伏伏......
先使用wget下载zip文件,文件总共200+mb,很有概率是源代码的打包
从文件内容可判断,该系统是使用的.net开发,可通过dnspy进行审计
文件上传漏洞审计
拿到源码后的第一个思路是寻找文件上传漏洞
果不其然在源码中找到uploadimg接口,发现未对上传的文件格式进行过滤
实际访问接口发现,怎么改变文件格式、文件内容、Content-Type、还是各种变种传输都无济于事
返回包永远是{"Status":1,"Data""null}
运维实在是坏呀~
Sql注入漏洞审计
第二个思路就是找注入
但是代码中定义了一个SqlChecker全局的类,强制处理所有用户传参,找注入这个方向有有点难啃了
系统用户信息遍历
找到/api/user/getusers接口
接口没有做鉴权,构造请求包发送,返回包返回系统所有用户信息
其中用户信息包括姓名、出生日期、微信账号、手机号码、邮箱、密码等等
伪造jwt_token获取系统管理员-拿下靶标
源码获取到jwt_token的secret
但是该secret不是可读性文本,估计是随机生成的byte字节序列,因此不能自行使用cyberchief或者其他工具将token直接生成
这里有个坑点:开始是使用gpt生成的脚本进行secret的读取和token的生成,发现gpt在处理字节上面有点问题,生成的jwt_token不能使用,于是自行编写了个py脚本进行jwt_token的构造,首先我们将字节序列做16进制的转化,为了python能够使用bytes.fromhex()函数读取16进制化的secret,然后根据上面读出的用户信息,伪装admin账号身份,并设置一个较长的ExpireTime
拿到jwt_token之后,要如何使用才能拿到后台呢,这里首先要明白该系统的登录鉴权机制
由于他存在注册功能,我们便可在自行注册一个账号,然后进行登录,查看认证处理流程
从数据包里面得知,登录成功后会返回jwt_token和一些与用户相关的一些信息,前端会根据返回的身份信息,跳转到对应的页面,并且功能接口都会带上jwt_token进行请求以便获取系统数据
了解清楚后,就开始进行身份伪造,首先去后台登录系统
将登录返回包的内容替换为管理员账号的token(从python脚本中生成)和管理员用户的身份信息
通过鉴权后,终于成功获取管理员后台,靶标5000分到手,哈哈
总结
本次渗透从惊喜到怀疑到失落,总的来说就是“山穷水尽疑无路,柳暗花明又一村”。
如果只是死磕文件上传、SQL注入这些能够快速获取权限的洞,反而有时会错过一些有用的信息,毕竟比赛中分数才是最要紧的,如何高效快速拿下靶标才是第一要领。
同时,代码审计的过程中要结合系统功能来多方面评估,本次挖洞也是先认真理解了系统的登录认证机制,才知道有jwt鉴权这种方式,从而萌生在代码中找jwt secret的想法,也才能把快到手的分数牢牢抓在自己手中。
【实战】文件加密器进行逆向
前言
实战可以大大提高自己,学习技术的目的就是能够在实战中运用。
本次实战与实际息息相关,该软件具有加密某文件的功能。
界面还挺好看的,功能很简单,输入文件和PIN(4位)进加解密。
这是被加密的文件
需要将其进行解密,拿到flag
思路
因为PIN是4位,因此可以写一个python脚本,对其进行爆破。
关键在于得出加密的算法,此时就需要我们进行逆向分析了
分析
先尝试进行加密
根据关键词:encrypted 进行定位
发现是我们需要的信息
跟踪进去,发现了花指令
去花指令
发现堆栈不平衡,将所有代码选中,然后C键,重新分析
发现了单指令花指令,无非nop掉即可
从头选到下一个函数开始的位置
按下C键
analyze即可
还是rust编译的
此时就可以分析函数了。处理其他函数也是相同的道理
初步分析
使用Fincrypto发现了salsa20加密
salsa20:32位字符构成的key,二是随机生成的8位nonce。算法使用key和nonce生成一个2^70长度的序列,并与明文进行异或加密
sub_140073A70
发现main_func就是获取我们的输入的文件内容
sub_140039890
而函数sub_140039890才是关键的加密函数
0x61707865、0x3320646E均为Salsa20算法的固定参数
解密
由于密码只有0000~9999这10000种可能
加密后文件名又是flag.png.enc,所以原文件是个png文件
使用png固有文件头89504E47来判断解密是否成功
Python
from Cryptodome.Cipher import Salsa20
cipher = open("flag.png.enc", "rb").read()
for i in range(10000):
key = str(i).rjust(4, '0').ljust(32, 'x00')
nonce = b'x24x24x24x24x24x24x24x24'
sal = Salsa20.new(key=key, nonce=nonce)
plain = sal.decrypt(cipher)
if plain.find(b"x89x50x4Ex47")>=0:
open("flag.png", "wb").write(plain)
break
后门函数技术在二进制对抗中的应用
本次题目跟第七届HWS线下的re2有类似的地方,均有后门函数。
二进制后门可以理解为:我们只需要修改某个字节或某个函数,就可以将加密的过程变成解密的过程,大大节省逆向成本。
本题先对内置的dll进行解密,然后调用其加密函数对我们的txt进行加密,如果我们将加密的函数nop为解密函数,就可以直接解密,类比与RC4动态解密技术。
1、初次分析
0地址异常反调试
本题的一大亮点就是有访问0地址的异常反调试,小伙伴们在做的时候有没有发现调试异常艰难呢
故意访问0地址
然后走作者自定义的处理函数,如果我们在IDA动调的时候不经过处理函数,程序就会卡在哪里不能继续运行。
做法很简单:将访问0地址的代码和异常处理函数完全给nop掉
(说白了:就是将所有跟异常有关的汇编都给nop掉就完事)
处理函数也是完整nop
返回处也nop,跟开头相对应
main函数
分析main函数,发现反编译爆红
很正常,查看汇编代码,发现了异常反调试和异常花指令干扰分析
做法很简单:直接nop即可
具体做法参考:上面一小节,0地址异常反调试
nop
成功生成函数
TLS回调函数
尝试运行,发现直接退出,发现了TLS反调试函数
nop即可
生成函数
将exit函数nop掉即可,不用管反调试的事情了
2、内置DLL资源解密
使用工具打开file_encrypt
发现内置 pe程序,猜测key为0x33,解密
这是程序使用0x33解密
发现了很多加密函数和解密函数(Crypt开头),因此本题程序使用本dll进行加密和解密操作
在后面的分析中,也发现了函数加载了我们的dll
3、关键函数分析
sub_401320
使用IDA动调发现了很多bug,莫名其妙断下,改用x64dbg
sub_402000
路径和盘符有关,比如我在C盘
C:......\document\1.txt
找到1.txt
sub_4017E0
加载dll
sub_4013E0
4、解密
既然使用了encrypto,那么我们改为decrypto就可以啦
I added the missing CryptDecrypt call to the binary's import table and patched the executable to decrypt the files. The decryption call takes one parameter less than the encryption one, so I NOP'ed one push to the
stack as well:
某个OA系统的代码审计
2023年HVV中爆出来的洞了,但是有一些漏洞点修复了,刚好地市级的攻防演练中遇到了一个,想着把可能出现问题的点全部审计一下,顺便熟悉一下.net代码审计。ps:感兴趣的师傅们可以自行根据poc搜索源码。
0x1 反编译
好吧,当我没说,下载dnspy反编译即可,但是首先要找到web逻辑代码才能开始审计,因为这套oa是使用了mvc开发模式,简单介绍一下mvc,其实就是model,controller,view,其中的view是视图也就是html等展示给用户看的东西,model是模型也就是控制数据库的代码。controller是控制器负责执行代码的逻辑,也就是我们需要审计的地方了。
然后找到controller就是web的主要逻辑了。
0x2 身份校验绕过
首先可以随便点入一个controller,发现filesController继承自TopVisionApi。
然后我们发现IsAuthorityCheck()这个函数用于判断权限。
首先看到第一行代码getByValue这个函数,其实Request.Properties["MS_HttpContext"]).Request[value]就是获取http请求中的某个参数,而value就是调用传过来的参数,在这里是token,那么这段代码就是获取http中的token参数。
然后if判断了token是不是空值然后再判断token参数的值是不是等于"zxh",如果登录则直接返回一个UserInfo对象。
然后回到filesController的身份判断,发现只判断了IsAuthorityCheck返回是否为null,所以只需要让token参数是zxh的时候,那么就可以绕过身份校验了。
0x3 任意文件下载
还是 filesController 这个控制器 DownloadRptFile方法。这时我们已经绕过了身份认证,所以只需要看之后代码即可。requestFileName就是我们传递的http参数,
然后跟进代码。并未发现任何过滤../的行为,直接传递给getBinaryFile函数
getBinaryFile函数如下。
结果证明: (读取文件内容会以base64返回)
0x4 信息泄露
发现GetCurrentUserList方法查询了所有用户信息。并且返回给前台。
<UserInfo>是c#中的泛型,这里是用来查询数据库的。可以看到遍历了dicUserList这个数组。这个数组就是初始化的用户信息数组了。
直接访问:
0x5 任意文件删除
发现DeleteFile2方法是一个删除文件方法。这里也没有发现过滤../以及过滤删除文件的后缀名。
虽然是有限制了文件路径,但是全然没有过滤../,而且filename参数也是完全可控的。所以这里其实是存在任意文件删除漏洞的。
ps: 这里就不放验证截图了,感兴趣的师傅们可以自行本地验证。
0x6 任意文件上传
UploadFile2方法中获取了各种参数,然后传入UploadFile2
跟进该方法。pathType就是限制文件上传到哪个文件夹的。
pathType详解:
fs参数是我们传递的byte数组也就是文件的内容。
startPoint等于0就好这样才能创建一个新的文件,datasize则是数组的长度。
漏洞验证:
0x7 SQL注入
InventoryController的GetProductInv方法,直接从参数获取boxNoName未经过过滤直接通过string.Format拼接至sql语句中,导致了sql注入。
验证:直接sqlmap即可
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

