REVERSE-PRACTICE-BUUCTF-17
[网鼎杯 2020 青龙组]jocker
exe程序,运行后提示输入flag,无壳,用ida分析
main函数平衡栈后,F5反汇编
主要逻辑为
读取输入,检验输入长度是否为24,对输入进行变换,与已知数组比较,执行一段从encrypt函数起始地址开始的SMC,分析可知,finally函数也会完全被SMC,未经变换的输入作为参数,调用encrypt函数和finally函数
先写input经过wrong变换和omg验证的逆运算脚本,得到的是假flag
往下走,在ida中使用idapython脚本完成SMC自修改代码
from idaapi import *
from idautils import *
start_addr = 0x401500
key = 0x41
for i in range(start_addr,start_addr+187):
PatchByte(i,Byte(i)^key)
SMC前,encrypt函数和finally函数各自包含了一大段数据
SMC后,encrypt函数和finally函数的指令间都有一些db指令,实际上为花指令
encrypt函数:
finally函数:
手动nop掉多余的db指令,让ida能够自动分析成代码
完成nop后,发现encrypt函数,在两端黑色代码中间插有一段红色代码
这时的处理方法是,在encrypt函数起始地址0x401500处,右键->undefine,变成黄色的数据,再按c转成代码,这时,encrypt函数起始地址与finally函数起始地址之间的代码全部为红色,再在encrypt函数起始地址0x401500处,右键->create function,平衡栈后,F5反汇编
这里encrypt函数反汇编后,好像没有用到未经变换的input,可能是patch代码的时候出错了,不过可以猜测为input和字符串“hahahaha_do_you_find_me?”异或,再与已知的unk_403040数组比较
写encrypt函数的逆运算脚本,得到部分flag,之所以是部分flag,是因为长度为24的unk_403040的最后5个元素为0,“d_me?”与0异或不变,或者v6=19说明了只异或了19次,而且提交失败
同encrypt函数的解析步骤,对finally函数,先nop掉多余的db指令,undefine掉黑色代码,转成数据,按c再转成代码,右键->create function,F5反汇编
没看懂这个函数,看了其他师傅的wp,说这里也是异或,能看到的5个值[37,116,112,38,58]是异或后要比较的值,由于input的最后一位一定是“}”,它异或一个值(“71”)后与58相等,而且它之前的四位也是和这个值(“71”)异或,结果是58之前的四个值
写finally函数的逆运算脚本得到后半部分的flag,替换掉“flag{d07abccf8a410cd_me?”的后5位,flag即为“flag{d07abccf8a410cb37a}”
[2019红帽杯]childRE
exe程序,运行后直接输入,无壳,ida分析
交叉引用字符串“flag{MD5(your input)}”来到sub_140001610函数
sub_140001610函数主要的逻辑为
读取输入,验证输入的长度是否为31,对输入进行位置变换处理,位置变换的结果放到v2(name),经调试可知,输入为abcdefghijklmnopqrstuvwxyz12345时,v2(name)的内容为pqhrsidtujvwkebxylz1mf23n45ogca
对v2(name)调用UnDecorateSymbolName函数处理,结果放到outputstring,验证outputstring的长度62及其内容
signed __int64 sub_7FF6DC281610()
{
signed __int64 input_len; // rax
_QWORD *v1; // rax
const CHAR *v2; // r11
__int64 v3; // r10
__int64 v4; // r9
const CHAR *v5; // r10
signed __int64 outputstring_len; // rcx
__int64 v7; // rax
signed __int64 result; // rax
unsigned int v9; // ecx
__int64 v10; // r9
int v11; // er10
__int64 v12; // r8
__int128 input; // [rsp+20h] [rbp-38h]
__int128 v14; // [rsp+30h] [rbp-28h]
input = 0i64;
v14 = 0i64;
sub_7FF6DC281080((__int64)"%s", &input); // 读取输入
input_len = -1i64;
do
++input_len;
while ( *((_BYTE *)&input + input_len) );
if ( input_len != 31 ) // 输入的长度为31
{
while ( 1 )
Sleep(0x3E8u);
}
v1 = sub_7FF6DC281280(&input); // 对input的处理,通过调试可知,从31行到41行,是对input的位置变换,变换后的结果放入v2,也就是name
// 例如,输入abcdefghijklmnopqrstuvwxyz12345,name=“pqhrsidtujvwkebxylz1mf23n45ogca”
v2 = name; // v2==name
if ( v1 )
{
sub_7FF6DC2815C0((unsigned __int8 *)v1[1]);
sub_7FF6DC2815C0(*(unsigned __int8 **)(v3 + 16));
v4 = dword_7FF6DC2857E0;
v2[v4] = *v5;
dword_7FF6DC2857E0 = v4 + 1;
}
UnDecorateSymbolName(v2, outputString, 0x100u, 0);// v2,也就是name,经过UnDecorateSymbolName函数处理,结果放到outputstring中
outputstring_len = -1i64;
do
++outputstring_len;
while ( outputString[outputstring_len] );
if ( outputstring_len == 62 ) // outputstring的长度为62
{
v9 = 0;
v10 = 0i64;
do // do循环体在验证outputstring的内容
{
v11 = outputString[v10];
v12 = v11 % 23;
if ( table[v12] != *(_BYTE *)(v10 + 0x7FF6DC283478i64) )// (_@4620!08!6_0*0442!@186%%0@3=66!!974*3234=&0^3&1@=&0908!6_0*&
_exit(v9);
if ( table[v11 / 23] != *(_BYTE *)(v10 + 0x7FF6DC283438i64) )// 55565653255552225565565555243466334653663544426565555525555222
_exit(v9 * v9);
++v9;
++v10;
}
while ( v9 < 0x3E );
sub_7FF6DC281020("flag{MD5(your input)}\n", v11 / 23, v12, v10);
result = 0i64;
}
else
{
v7 = sub_7FF6DC2818A0(std::cout);
std::basic_ostream<char,std::char_traits<char>>::operator<<(v7, sub_7FF6DC281A60);
result = 0xFFFFFFFFi64;
}
return result;
}
先写验证outputstring的长度62及其内容的逆运算脚本,得到outputstring,为一个未修饰的C++符号名
UnDecorateSymbolName函数反修饰指定已修饰的 C++ 符号名,参考:UnDecorateSymbolName
第1个参数为已修饰的 C++ 符号名,此名称能以始终为问号 (?) 的首字符鉴别,本题中为v2(name),第2个参数指向字符串缓冲区的指针,该缓冲区接收未修饰的名字,本题中为outputstring
可知需要对outputstring符号名修饰才能得到v2(name),参考:c/c++函数名修饰规则
或者通过写C++代码,调用__FUNCDNAME__宏(FUNCDNAME:只有在函数内部才有效,返回该函数经编译器修饰后的名字。如果编译器选项中设定了/EP或/P,则__FUNCDNAME__是未定义。)直接得到函数经编译器修饰后的符号名,需要注意的是函数所在的类,域,返回类型,函数名,参数类型都必须和已知完全相同
#include<iostream>
using namespace std;
class R0Pxx {//函数所在的类
public:
R0Pxx() {//该类的构造函数
unsigned char a;
My_Aut0_PWN(&a);
}
private://函数属于私有域
//函数的返回类型 函数名 以及参数类型
char * My_Aut0_PWN(unsigned char*) {
char * ret = NULL;
//调用宏 输出该函数经编译器修饰后的符号名
printf("%s", __FUNCDNAME__);
return ret;
}
};
int main() {
new R0Pxx();
getchar();
return 0;
}
运行结果即为v2(name),注意是在x86架构下运行的结果
写逆位置变换脚本即可得到输入,对输入进行md5散列即可得到flag
[MRCTF2020]PixelShooter
apk文件,jadx-gui打开,什么都没发现
用Apktool Box反编译apk后,在PixelShooter->lib->armeabi-v7a目录下发现3个.so文件,依次分析,同样什么都没发现
突然想到这是个安卓unity游戏,而安卓unity游戏的核心逻辑一般位于assets\bin\Data\Managed\Assembly-CSharp.dll
用dnSpy打开,在类UIController的GameOver方法中找到flag
[ACTF新生赛2020]SoulLike
elf文件,无壳,ida分析
main函数,读取输入,验证输入的前5个字符是否为“actf{”,将输入{}内的字符放入v8,可知输入{}内的长度为12,调用sub_83A函数验证v8,且输入的最后一个字符为“}”
分析sub_83A函数,可以看到是对input{}内12个字符的超多异或运算
在sub_83A函数的最后,是input{}内的12个字符经过超多异或运算后的值与已知的v4到v15比较,因为会有类似input[3]^=input[2]的情况,所以不能通过调试得到每个字符最后等效的异或值是什么
不过当比较为不相同时,程序会打印输入是哪个位置的字符错了,于是可以写脚本爆破出flag
爆破脚本
#coding:utf-8
from itertools import *
import subprocess
flag=""
t=""
for i in range(12):
for j in range(32,126):
flag ="actf{"+t+chr(j)+"}"
p = subprocess.Popen(["./SoulLike"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(flag)
p.stdin.close()
out=p.stdout.read()
p.stdout.close()
if "#"+str(i) not in out:
t+=chr(j)
break
print(flag)
运行结果