TOC

crackRTF

是一个资源题,以前没见过,记录一下。
反编译一下:

int __cdecl main_0(int argc, const char **argv, const char **envp)  
{  
DWORD v3; // eax  
DWORD v4; // eax  
CHAR String; // [esp+4Ch] [ebp-310h]  
int v7; // [esp+150h] [ebp-20Ch]  
CHAR String1; // [esp+154h] [ebp-208h]  
BYTE pbData; // [esp+258h] [ebp-104h]  
  
memset(&pbData, 0, 0x104u);  
memset(&String1, 0, 0x104u);  
v7 = 0;  
printf("pls input the first passwd(1): ");  
scanf("%s", &pbData);  
if ( strlen((const char *)&pbData) != 6 )  
{  
printf("Must be 6 characters!\n");  
ExitProcess(0);  
}  
v7 = atoi((const char *)&pbData);  
if ( v7 < 100000 ) //输入的password可以转化为小于100000的数字  
ExitProcess(0);  
strcat((char *)&pbData, "@DBApp");  
v3 = strlen((const char *)&pbData);  
sub_40100A(&pbData, v3, &String1);  //求哈希值  
if ( !_strcmpi(&String1, "6E32D0943418C2C33385BC35A1470250DD8923A9") )  
{  
printf("continue...\n\n");  
printf("pls input the first passwd(2): ");  
memset(&String, 0, 0x104u);  
scanf("%s", &String);  
if ( strlen(&String) != 6 )  
{  
  printf("Must be 6 characters!\n");  
  ExitProcess(0);  
}  
strcat(&String, (const char *)&pbData);  
memset(&String1, 0, 0x104u);  
v4 = strlen(&String);  
sub_401019((BYTE *)&String, v4, &String1);  
if ( !_strcmpi("27019e688a4e62a649fd99cadaafdb4e", &String1) )  
{  
  if ( !sub_40100F(&String) )  
  {  
    printf("Error!!\n");  
    ExitProcess(0);  
  }  
  printf("bye ~~\n");  
}  
}  
return 0;  
}  

第一个密码是一个小于100000的数字,数字后面加上@DBApp后求哈希值,求哈希值的函数如下:

int __cdecl sub_401230(BYTE *pbData, DWORD dwDataLen, LPSTR lpString1)  
{  
int result; // eax  
DWORD i; // [esp+4Ch] [ebp-28h]  
CHAR String2; // [esp+50h] [ebp-24h]  
char v6[20]; // [esp+54h] [ebp-20h]  
DWORD pdwDataLen; // [esp+68h] [ebp-Ch]  
HCRYPTHASH phHash; // [esp+6Ch] [ebp-8h]  
HCRYPTPROV phProv; // [esp+70h] [ebp-4h]  
  
if ( !CryptAcquireContextA(&phProv, 0, 0, 1u, 0xF0000000) )  
return 0;  
if ( CryptCreateHash(phProv, 0x8004u, 0, 0, &phHash) )  
{  
if ( CryptHashData(phHash, pbData, dwDataLen, 0) )  
{  
  CryptGetHashParam(phHash, 2u, (BYTE *)v6, &pdwDataLen, 0);  
  *lpString1 = 0;  
  for ( i = 0; i < pdwDataLen; ++i )  
  {  
    wsprintfA(&String2, "%02X", (unsigned __int8)v6[i]);  
    lstrcatA(lpString1, &String2);  
  }  
  CryptDestroyHash(phHash);  
  CryptReleaseContext(phProv, 0);  
  result = 1;  
}  
else  
{  
  CryptDestroyHash(phHash);  
  CryptReleaseContext(phProv, 0);  
  result = 0;  
}  
}  
else  
{  
CryptReleaseContext(phProv, 0);  
result = 0;  
}  
return result;  
}  

CryptCreateHash函数的第二个参数用来确定哈希函数的类型
0x8004 -> SHA1, 0x800c -> SHA256, 0x8003 -> MD5
所以第一个密码是爆破SHA1,爆破范围是0-100000,爆破结果是123321
第二个输入是6字节的字符串,然后求MD5,我一开始的想法是爆破后来才知道超过4字节爆破是在想peach
继续往下走,在sub_40100F函数中调用了sub_4014D0

char __cdecl sub_4014D0(LPCSTR lpString)  
{  
LPCVOID lpBuffer; // [esp+50h] [ebp-1Ch]  
DWORD NumberOfBytesWritten; // [esp+58h] [ebp-14h]  
DWORD nNumberOfBytesToWrite; // [esp+5Ch] [ebp-10h]  
HGLOBAL hResData; // [esp+60h] [ebp-Ch]  
HRSRC hResInfo; // [esp+64h] [ebp-8h]  
HANDLE hFile; // [esp+68h] [ebp-4h]  
  
hFile = 0;  
hResData = 0;  
nNumberOfBytesToWrite = 0;  
NumberOfBytesWritten = 0;  
hResInfo = FindResourceA(0, (LPCSTR)0x65, "AAA");  
if ( !hResInfo )  
return 0;  
nNumberOfBytesToWrite = SizeofResource(0, hResInfo);  
hResData = LoadResource(0, hResInfo);  
if ( !hResData )  
return 0;  
lpBuffer = LockResource(hResData);  
sub_401005(lpString, (int)lpBuffer, nNumberOfBytesToWrite);  
hFile = CreateFileA("dbapp.rtf", 0x10000000u, 0, 0, 2u, 0x80u, 0);  
if ( hFile == (HANDLE)-1 )  
return 0;  
if ( !WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) )  
return 0;  
CloseHandle(hFile);  
return 1;  
}  

FindResourceA(0, (LPCSTR)0x65, “AAA”)函数用于寻找名称为AAA的资源文件,下面是关于windows中资源文件的的一些资料.
VC使用自定义资源,FindResource,LoadResource,UnLockResource
Windows MFC工程起步
使用ResourceHacker可以查找文件中的resource:

看到资源AAA中是一系列的字符串
sub_401005中将AAA中的字节与输入的6字节字符串进行异或,最终将结果写入dbapp.rtf中,用写字板随便写一个文件然后查看RTF文件格式

取前6个与AAA中的字符串异或就能得到passwd2

babyre

是SCTF2019的一道RE题,当时没做出来看了师傅的WP后明白了,后来看到Ex大佬的WP感觉大佬的做法好特别,记录一下。
首先是调试:因为前面有花指令无法F5只有看汇编+调试.文件是动态加载的, 无法确定断点位置, 我之前没调过这种, 查资料发现了attach的方法. 先运行可执行文件, 然后使用ps -aux | grep filename的方法查看进程号, 使用sudo gdb filename的方法运行gdb, 然后使用attach PID附加进程. 如果程序停留在库函数中可以使用finish命令快速跳过.
还有一些实用的指令:

1. 反向调试: 要求平台支持记录回放  
reverse-step <--- 反向运行程序到上一次被执行的源代码行。  
reverse-stepi <--- 反向运行程序到上一条机器指令  
watch a <--- 给变量a设置检查点,每次执行一条机器指令会打印出a在指令执行前后的值  
record <--- 启动进程回放  
reverse-next <--- 让程序倒退回到上一步的状态  
  
2. 在启动gdb的时候附加的选项  
-symbols <file> -s <file> 从指定文件中读取符号表。  
-se file 从指定文件中读取符号表信息,并把他用在可执行文件中。  
core <file> -c <file> 调试时core dump的core文件。  
  
3.打印  
x 按十六进制格式显示变量。  
d 按十进制格式显示变量。  
u 按十六进制格式显示无符号整型。  
o 按八进制格式显示变量。  
t 按二进制格式显示变量。  
a 按十六进制格式显示变量。  
c 按字符格式显示变量。  
f 按浮点数格式显示变量。  
(gdb) p i  
$21 = 101  
(gdb) p/a i  
$22 = 0x65  
(gdb) p/c i  
$23 = 101 'e'  
  
查看文件中某变量的值:  
file::variable  
function::variable  
可以通过这种形式指定你所想查看的变量,是哪个文件中的或是哪个函数中的。例如,查看文件f2.c中的全局变量x的值:  
(gdb) p 'f2.c'::x  
查看数组的值, 比如数组的一段,或是动态分配的数据的大小。可以使用GDB的“@”操作符,“@”的左边是第一个内存的地址的值,“@”的右边是想查看内存的长度。  
例如,程序中有这样的语句:  
int *array = (int *) malloc (len * sizeof (int));  
于是,在GDB调试过程中,可以以如下命令显示出这个动态数组的取值:  
p *array@len  
二维数组打印 ----> p **array@len  
如果是静态数组的话,可以直接用print数组名,就可以显示数组中所有数据的内容了。  
http://www.cppblog.com/chaosuper85/archive/2009/08/04/92123.html  
  
4.断点   
gdb断点分类:  
以设置断点的命令分类:  
break 可以根据行号、函数、条件生成断点。  
watch 监测变量或者表达式的值发生变化时产生断点。  
catch 监测信号的产生。例如c++的throw,或者加载库的时候。  
可以借助catch命令来反反调试:  
`catch syscall ptrace`会在发生ptrace调用的时候停下,因此在第二次停住的时候`set $rax=0`,从而绕过程序中`ptrace(PTRACE_TRACEME, 0, 0, 0) ==-1`的判断  
  
gdb中的变量从1开始标号,不同的断点采用变量标号同一管理,可以 用enable、disable等命令管理,同时支持断点范围的操作,比如有些命令接受断点范围作为参数。  
例如:disable 5-8  
  
break,tbreak ----> 可以根据行号、函数、条件生成断点。tbreak设置方法与break相同,只不过tbreak只在断点停一次,过后会自动将断点删除,break需要手动控制断点的删除和使能。  
  
多文件设置断点:  
在进入指定函数时停住:  
C++中可以使用class::function或function(type,type)格式来指定函数名。如果有名称空间,可以使用namespace::class::function或者function(type,type)格式来指定函数名。  
break filename:linenum ---> 在源文件filename的linenum行处停住   
break filename:function ---> 在源文件filename的function函数的入口处停住  
break class::function或function(type,type)  (个人感觉这个比较方便,b 类名::函数名,执行后会提示如:  
>>b GamePerson::update  
Breakpoint 1 at 0x46b89e: file GamePerson.cpp, line 14.  
在类class的function函数的入口处停住  
break namespace::class::function ---> 在名称空间为namespace的类class的function函数的入口处停住  
  
until  
until line-number  继续运行直到到达指定行号,或者函数,地址等。  
until line-number if condition  
   
info break <--- 查看断点信息  
(gdb) bt <--- 查看函数堆栈。backtrace 打印当前的函数调用栈的所有信息。  
#0  func (n=250) at tst.c:5  
#1  0x080484e4 in main () at tst.c:24  
#2  0x400409ed in __libc_start_main () from /lib/libc.so.6  

一共有三关, 第一关是一个三维迷宫
第二关输入经过特定表表转换之后,不断左移累加,最终的转换结果需要与字符串“sctf_9102”相等
调试发现是一个4字节到3字节的转换关系, 进行爆破.
一般的爆破流程:

data = [0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000003E, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000003F, 0x00000034, 0x00000035, 0x00000036, 0x00000037, 0x00000038, 0x00000039, 0x0000003A, 0x0000003B, 0x0000003C, 0x0000003D, 0x0000007F, 0x0000007F, 0x0000007F, 0x00000040, 0x0000007F, 0x0000007F, 0x0000007F, 0x00000000, 0x00000001, 0x00000002, 0x00000003, 0x00000004, 0x00000005, 0x00000006, 0x00000007, 0x00000008, 0x00000009, 0x0000000A, 0x0000000B, 0x0000000C, 0x0000000D, 0x0000000E, 0x0000000F, 0x00000010, 0x00000011, 0x00000012, 0x00000013, 0x00000014, 0x00000015, 0x00000016, 0x00000017, 0x00000018, 0x00000019, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000001A, 0x0000001B, 0x0000001C, 0x0000001D, 0x0000001E, 0x0000001F, 0x00000020, 0x00000021, 0x00000022, 0x00000023, 0x00000024, 0x00000025, 0x00000026, 0x00000027, 0x00000028, 0x00000029, 0x0000002A, 0x0000002B, 0x0000002C, 0x0000002D, 0x0000002E, 0x0000002F, 0x00000030, 0x00000031, 0x00000032, 0x00000033, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F, 0x0000007F]  
# Python>'sctf_9102'.encode('hex')  
# 736374665f39313032  
pass2 = ''  
dest = [0x736374,0x665f39,0x313032]  
for d in range(3):  #三个输入,每个输入有四字节  
for i in range(0x20,0x80):  
    for j in range(0x20,0x80):  
        for k in range(0x20,0x80):  
             for l in range(0x20,0x80):  
                h = (((((data[i]<<6)|data[j])<<6)|data[k])<<6)|data[l]  
                if h == dest[d]:  
                    print chr(i) + chr(j) + chr(k) + chr(l)  
                    pass2 += chr(i) + chr(j) + chr(k) + chr(l)  
print pass2  

Ex大佬直接利用源程序里的流程进行爆破, 我觉得这样可以并不清楚具体实现细节,出错的概率也更小.具体的映射细节在loc_C22中:

// gcc -fPIC -O3 -shared hook.c -c -o hook.o  
// ld -shared -ldl hook.o -o hook.so  
#include <stdio.h>  
#include <dlfcn.h>  
  
typedef void (*FUNC)(char *, char *);  
  
void _init()  
{  
FUNC func;  
char *printable = "_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";  
#define LENGTH 63  
char in[8], out[8];  
char *image_base = *(void **)dlopen(NULL, 1);  
printf("Image base: %p\n", image_base);  
func = image_base + 0xC22;  
*(size_t *)in = 0;  
for (int i = 0; i < LENGTH; i++)  
{  
    in[0] = printable[i];  
    for (int ii = 0; ii < LENGTH; ii++)  
    {  
        in[1] = printable[ii];  
        for (int iii = 0; iii < LENGTH; iii++)  
        {  
            in[2] = printable[iii];  
            for (int iiii = 0; iiii < LENGTH; iiii++)  
            {  
                in[3] = printable[iiii];  
                *(size_t *)out = 0;  
                func(in, out);  
                switch(*(size_t *)out)  
                {  
                case 0x746373:  
                    fprintf(stderr, "%s -> %s\n",in, out);  
                    break;  
                case 0x395F66:  
                    fprintf(stderr, "%s -> %s\n",in, out);  
                    break;  
                case 0x323031:  
                    fprintf(stderr, "%s -> %s\n",in, out);  
                    break;  
                }  
            }  
        }  
    }  
}  
printf("over\n");  
exit(0);  
}  

运行:

$ LD_PRELOAD=./hook.so ./babygame  
Image base: 0x559ccb697000  
c2N0 -> sct  
MTAy -> 102  
Zl85 -> f_9  
over  

主要关键在于LD_PRELOAD, 找到一篇介绍文章如下:
https://www.cnblogs.com/net66/p/5609026.html
说明在babygame最开始调用_init函数的时候其实会调用hook.c中的_init函数, _init是一个爆破的函数,调用func时会调用loc_C22
还有关于dlopen()函数的问题
https://www.cnblogs.com/youxin/p/5109520.html

第三问是一个可逆函数。

array[30]  
array[x+4] = array[x+0] ^ f(array[x+1] ^ array[x+2] ^ array[x+3])  

输入分别分位4个32bit的整数,赋值给array[0]-array[3],然后根据异或变换和f(x)转换得到array[30],最后四个字节分别为0xD8BF92EF,0x9FCC401F,0xC5AF7647,0xBE040680
f(x)函数依赖于x。所以直接将公式倒置即可。

// gcc -fPIC -O3 -shared hook2.c -c -o hook2.o  
// ld -shared -ldl hook2.o -o hook2.so  
#include <stdio.h>  
#include <dlfcn.h>  
  
typedef int (*FUNC)(int);  
  
void _init()  
{  
FUNC func;  
int array[0x100] = {0};  
array[0] = 0xD8BF92EF;  
array[1] = 0x9FCC401F;  
array[2] = 0xC5AF7647;  
array[3] = 0xBE040680;  
  
char *image_base = *(void **)dlopen(NULL, 1);  
printf("Image base: %p\n", image_base);  
func = image_base + 0x1464;  
for (int i = 0; i < 26; i++)  
{  
    array[i + 4] = array[i] ^ func(array[i+1] ^ array[i+2] ^ array[i+3]);  
}  
  
for(int i=3;i>=0;i--)  
{  
    printf("%08X\n", array[26 + i]);  
    printf("%c%c%c%c\n", ((char *)&array[26 + i])[0], ((char *)&array[26 + i])[1], ((char *)&array[26 + i])[2], ((char *)&array[26 + i])[3]);  
}  
printf("over\n");  
exit(0);  
}  

运行实例:

$ LD_PRELOAD=./hook2.so ./babygame  
Image base: 0x562b0a9e9000  
67346C66  
fl4g  
5F73695F  
_is_  
755F3073  
s0_u  
21793167  
g1y!  
over  

creakme

在main函数中,首先进行了一些预处理。sub_402320添加了SEH
添加SEH的基本步骤

在上图中1步骤之前和步骤2之后的SEH链分别如下:

安装完SEH后通过call ds:DebugBreak调用DebugBreak函数进入SEH, 在调试过程中发生中断会优先交给调试器处理,需要在SEH函数开始下断点后用Shift + F9运行才可以在SEH函数处暂停.
SEH函数如下:

最终会调用0x4023EF函数,这个函数首先使用call ds:CheckRemoteDebuggerPresentcall ds:IsDebuggerPresent来检查程序是否被调试, 如果没有调试则进入sub_402450函数,这个函数会将0x404000后的0x200个字节与sycloversyclover进行异或

import idc  
addr = 0x404000  
sy = "sycloversyclover"  
length = len(sy)  
for i in range(0x200):  
b = int(idc.Byte(addr + i))  
idc.PatchByte(addr+i, ~(b ^ ord(sy[i % length])))  

sub_4024A0首先使用__readfsdword(0x30u)检查程序是否被调试,然后调用404000函数,SMC后404000成了一个函数,用于修改最后的对比字符串。
最后在sub_4020D0中,对输入进行AES_CBC_128加密。密钥为’sycloversyclover’,IV为’sctfsctfsctfsctf’。这里的AES流程并不是正常的算法流程,这里使用了查表法,对比查表参数可以看出来是AES.
关于AES查表法的识别有下面的文章可以参考
逆向分析及识别恶意代码中的AES算法

BJDCTF_easy

下面代码中v2中用来存储v14转化为二进制的数值,v2[0]存储v14的最高位

while ( SHIDWORD(v14) > 0 || v14 >= 0 && v14 )  
{  
  v2[v16++] = ((SHIDWORD(v14) >> 31) ^ (((SHIDWORD(v14) >> 31) ^ v14) - (SHIDWORD(v14) >> 31)) & 1)  
            - (SHIDWORD(v14) >> 31);  
  v14 /= 2LL;  
}  

#define SHIDWORD(x) (*((int32*)&(x)+1))
取变量X地址的下一个字节的地址并且解引用,最后得到的是地址中的值,通俗点讲就是X所在内存中的邻居