XCTF - Reverse 全关卡笔记

XCTF - Reverse

TODO: 补挂掉的图片

目录

参考:

XCTF—WriteUp

关于汇编跳转指令的说明

Visual C++ 32 位和 64 位编译器可识别类型

对LOWORD, HIWORD, LOBYTE, HIBYTE的理解

高字节和低字节

C强制类型转换总结

C++指针类型间强制转换

C++ 学习——char * ,char a[ ],char ** ,char *a[] 的区别

dw、db、dd

第一题:simple-unpack

0x01.查壳和程序的详细信息

照着套路扔到PEID中查看信息
在这里插入图片描述
无果,想起可能是linux的ELF可执行文件,扔到exeinfo中,

img

发现有upx壳。

注:windows下的文件是PE文件,Linux/Unix下的文件是ELF文件

0x02.UPX 脱壳

img

upx -d 即可对upx壳进行脱壳

image-20220503104853291

0x03.载入IDA

img

还是从main函数开始分析,结果我们再右侧发现了意外惊喜

运行程序,输入我们看到的flag:flag{Upx_1s_n0t_a_d3liv3r_c0mp4ny}

img

本writeup参考来自:吉林省信睿网络,https://blog.csdn.net/xiao__1bai/article/details/119395006

第二题: logmein

0x01.查壳和查看程序的详细信息

img

发现程序是一个ELF文件,将其放入Linux环境中进行分析

img

发现程序是64位的,使用静态分析工具IDA进行分析

0x02.IDA

img

从main函数开始分析,使用F5查看伪代码

img

发现main函数的整个运算逻辑

先是,将指定字符串复制到v8

s是用户输入的字符串,先进行比较长度,如果长度比v8小,则进入sub_4007c0函数

img

可以看出输出字符串Incorrect password,然后,退出

如果长度大于或等与v8则进入下面的循环

看到判断如果输入的字符串和经过运算后的后字符串不等,则进入sub_4007c0,输出Incorrect password,

如果想得,则进入sub_4007f0函数

img

证明输入的字符串就是flag

接下来写脚本

0x03.Write EXP

我们的目标是计算

v8 = ":\"AL_RT^L*.?+6/46";
v7 = 28537194573619560LL;
v6 = 7;

for ( i = 0; i < strlen(s); ++i )
  {
  		s[i] = (char)(*((_BYTE *)&v7 + i % v6) ^ v8[i]) )
  }

首先要理解这个表达式:

(_BYTE *)&v7是字节型指针,其中BYTE可以理解为unsigned char, 一个字节存储8位无符号数,储存的数值范围为0-255。而一开始v7是一个long long 类型的整数,因此这里需要将其转换为字符串。(_BYTE *)&v7相当于将v7转化为字符数组(串)的首元素的地址,str_v7[0]

然后(_BYTE *)&v7 + i % v6 // v6 = 7,i = 0,1,...则依次代表v7的前v6个元素的地址,自然*((_BYTE *)&v7 + i % v6)就代表循环遍历字符数组v7的前v6个元素,也就是str_v7[i % v6]

然后的异或,转换类型,应该不必多说,只是需要注意不同环境下实现方式的不同。

接下来将详细探讨如何将long long 型v7转化为字符串

方法一:手动转换

image-20220503142424210

方法二:010editior

先将v7转换为16进制,然后在010editior里按CTRL + shift + v,然后再想办法翻转

image-20220503142615687

为什么要翻转呢?

先将v7的值转化为16进制。v7=0x65626d61726168。

在这里插入图片描述

如图所示,x86架构,v7在栈中是小端存储,即字节序是little-endian,所以v7的高位数据放在高址,低位数组放在低址。

最后是将计算开始那个表示式

方法一:利用python

v6 = 7
v7 = "harambe"
v8 = ":\"AL_RT^L*.?+6/46"

for i in range(len(v8)):
	x = chr(ord(v7[i % v6]) ^ ord(v8[i]))
	print(x, end = '')
    
# ord():是将字符串转换为ascii格式,为了方便运算
# chr():是将ascii转换为字符串

方法二:利用C语言(最优雅,简单,甚至不用管v7的字符串是什么)

#include<stdio.h>
#include<string.h>
#include<windows.h>	//这头文件和(BYTE*)是第一次见,学着吧

const char v8[] = ":\"AL_RT^L*.?+6/46";
const long long int v6 = 7;	
// 注意由于v6和v7要一起进行运算,因此v6也得开long long 
const long long v7 = 0x65626D61726168LL;

int main(void){
	
	for(int i = 0; i < strlen(v8); i ++ ){
		char x = (char)(*((BYTE *)&v7 + i % v6) ^ v8[i]);
		printf("%c", x);
	} 
	
	return 0;
}

参考文章:

https://blog.csdn.net/qq_43394612/article/details/84839170

XCTF - Writeup

第三题:insanity

思路一:

IDA后shift + F12

img

思路二:

查看伪代码,发现是先设置随机数再输出strs里面的一个字符串,于是可以双击strs看看里面都有什么

img

第四题:getit

0x01.查壳

在这里插入图片描述
是linux的文件。没加壳

0x02.拖入ida

在这里插入图片描述

中间对部分生成了一个字符串,然后将它写入文件中,经过四个f函数,最后又把文件删掉了,因此我们什么都看不到。

百度可知,那几个f函数的作用是把原来的数据覆盖掉。

思路一:在四个f函数之前设置断点,查看相关数据。

0x03:GDB:我们这时候通过IDA查看汇编代码

img

然后我们向下追踪,追踪到for循环的位置,因为,flag是在这里存入文件的,所以,我们可以在内存中找到正要存储的字符串

img

我们将鼠标指向strlen(),在下面可以看到汇编所在的地址,然后我们根据大概的地址去看汇编代码

img

可以看到这是调用strlen()函数的汇编指令

我们通过上一个图片,可以知道经过for()的判断条件后,还要进行一步fseek函数,所以,根据汇编代码,可以确定jnb loc_4008B5就是fseek()函数,那么,mov eax,[rbp+var_3C]肯定就是最后要得到的flag了

0x04.GDB:这里我们用linux下的动态调试工具gdb进行动态调试

这里介绍一下,对gdb进行强化的两个工具peda和pwndbg,这两个工具可以强化视觉效果,可以更加清楚的显示堆栈,内存,寄存机的情况

先加载程序

img

然后,用b 下断点

img

然后,运行 R

img

这里我们可以看出,程序停止在0x400832的位置,然后,要被移动的字符串在RDX的位置

注:

这里介绍一下一下RDX,RDX存的是i/0指针,0x6010e0,这个位置存的字符串是最后的flag:SharifCTF{b70c59275fcfa8aebf2d5911223c6589}

以为这里涉及的是程序读写函数,所以涉及的就是i/o指针

另外也可以就直接在strlen处设置断掉,效果如下:

image-20220503170037620

所以我们能得到最后的flag: SharifCTF{b70c59275fcfa8aebf2d5911223c6589}

本writeup参考来自:吉林省信睿网络

思路二:用代码实现伪代码中的生成字符串的功能

#include<stdio.h>

int main(void)
{
	char s[] = "c61b68366edeb7bdce3c6820314b7498";
	int v6 = 0;
	while ( v6 < strlen(s) )
	{
		int v3;
		if ( v6 & 1 )
			v3 = 1;
		else
			v3 = -1;
		char x = s[v6] + v3;
		v6 ++ ;
		printf("%c", x);
	}

	return 0;
}

结果为:b70c59275fcfa8aebf2d5911223c6589

然后再和SharifCTF{}进行拼接

(这是在IDA中,通过shift + F12发现的)。

值得注意的是 t 中存放的是harictf{??????????????????}. 开始t字符串初始化是'0x53',即S,可能有某种奇怪的规则吧。

第五题:python-trade

0x01.下载附件

img

注:

python文件在被import运行的时候会在同目录下编译一个pyc的文件(为了下次快速加载),这个文件可以和py文件一样使用,但无法阅读和修改;python工具支持将pyc文件反编译为py文件(可能会存在部分文件无法反编译)。

支持的python版本:1.0、 1.1、 1.3、 1.4、 1.5、 1.6、 2.0、 2.1、 2.2、 2.3、 2.4、 2.5、 2.6、 2.7、 3.0、 3.1、 3.2、 3.3、 3.4、 3.5、 3.6、 3.7、 3.8、 3.9、 3.10。

0x02.在线Python反编译

https://tool.lu/pyc/

#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 2.7

import base64


def encode(message):
    s = ""
    for i in message:
        x = ord(i) ^ 32
        x = x + 16
        s += chr(x)
    return base64.b64encode(s)


correct = "XlNkVmtUI1MgXWBZXCFeKY+AaXNt"
flag = ""
print "Input flag:"
flag = raw_input()
if encode(flag) == correct:
    print "correct"
else:
    print "wrong"

这是生成的py文件

然后,对这个文件的运算逻辑进行逆向

0x03.写EXP

#!/usr/bin/env python

import base64

ori = "XlNkVmtUI1MgXWBZXCFeKY+AaXNt"
ori = base64.b64decode(ori)

ans = ""

for i in ori:
    ans += chr((ord(i) - 16) ^ 32)

print(ans)

先对字符串进行b64decode,然后,再进行xor运算得到最后的flag:nctf{d3c0mpil1n9_PyC}

本writeup参考来自:吉林省信睿网络

第六题:re1

思路一:IDA静态分析

IDA F5查看伪代码

img

这是整个main函数的运算逻辑

可以看到一个关键的字符串,print(aFlag),那么证明这就是输入正确flag,然后,会输出aFlag证明你的flag正确,然后,继续往上分析,可以看到v3的值,是由strcmp()决定的,比较v5和输入的字符串,如果一样就会进入后面的if判断,所以,我们继续往上分析,看看哪里又涉及v5,可以看到开头的_mm_storeu_si128(),对其进行分析发现它类似于memset(),将xmmword_413E34的值赋值给v5,所以,我们可以得到正确的flag应该在xmmword_413E34中,然后,我们双击413E34进行跟进

img

可以看到一堆十六进制的数

这时,我们使用IDA的另一个功能 R ,能够将十进制的数转换为字符串。

img

根据小端存储的规则可以得到flag

但是不知道为什么不同IDA分析出来的伪代码差别比较大,本题我是用IDA pro7.6做的,而IDA pro 6.8则不行,连main函数都找不到,奇怪。

思路二:Ollydbg中文搜索

image-20220503210347962

这种思路我觉得比较碰巧,类似的用010editor打开也能看到类似的字符串,不过OD可能更容易发现一点

但是不知道为什么IDA的shift + F12 看不到

第七题:game(TODO)

第八题:Hello,CTF

0x01.运行程序

img

输入正确的flag,才会显示正确

0x02.查壳

img

是32位的程序,并且是Microsoft Visual C++编译,而且没有加壳

0x03.IDA

img

照旧,依旧先从main开始分析,然后,对main函数进行F5查看伪代码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // ebx
  char v4; // al
  int result; // eax
  int v6; // [esp+0h] [ebp-70h]
  int v7; // [esp+0h] [ebp-70h]
  char Buffer[2]; // [esp+12h] [ebp-5Eh] BYREF
  char v9[20]; // [esp+14h] [ebp-5Ch] BYREF
  char v10[32]; // [esp+28h] [ebp-48h] BYREF
  __int16 v11; // [esp+48h] [ebp-28h]
  char v12; // [esp+4Ah] [ebp-26h]
  char v13[36]; // [esp+4Ch] [ebp-24h] BYREF

  strcpy(v13, "437261636b4d654a757374466f7246756e");// 将字符串复制到v13的位置
  while ( 1 )
  {
    memset(v10, 0, sizeof(v10));
    v11 = 0;
    v12 = 0;
    sub_40134B(aPleaseInputYou, v6);
    scanf("%s", v9);            // 输入一个字符串
    if ( strlen(v9) > 0x11 )    // 输入的字符串长度不能大于17(0x11)
      break;
    for ( i = 0; i < 17; ++i )
    {
      v4 = v9[i];
      if ( !v4 )
        break;
      sprintf(Buffer, "%x", v4);// Buffer的定义:char Buffer[2];猜测是将v4以十六进制的形式输出到Buffer中
      strcat(v10, Buffer);      // 拼接v10, Buffer
    }
    if ( !strcmp(v10, v13) )    // 最后,进行比较,看输入的字符串是否和v10的字符串相等,如果相等,则说明输入了正确的flag
      sub_40134B(aSuccess, v7);
    else
      sub_40134B(aWrong, v7);
  }
  sub_40134B(aWrong, v7);
  result = --Stream._cnt;
  if ( Stream._cnt < 0 )
    return _filbuf(&Stream);
  ++Stream._ptr;
  return result;
}

0x04.将字符串转换为十六进制

image-20220506104043203

得到了最后的flag是:CrackMeJustForFun

第九题:open - source

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 4) {
    	printf("what?\n");
    	//exit(1);
    }

    unsigned int first = atoi(argv[1]);	// argv[1] = 51966
    if (first != 0xcafe) {
    	printf("you are wrong, sorry.\n");
    	exit(2);
    }

    unsigned int second = atoi(argv[2]);
    if (second % 5 == 3 || second % 17 != 8) {
    	printf("ha, you won't get it!\n");
    	exit(3);
    }

    if (strcmp("h4cky0u", argv[3])) {	// argv[3] = h4cky0u
    	printf("so close, dude!\n");
    	exit(4);
    }

    printf("Brr wrrr grr\n");

    unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv[3]) - 1615810207;

    printf("Get your key: ");
    printf("%x\n", hash);
    return 0;
}

逐个分析即可。

#include<stdio.h>
#include<string.h>

int main(void){
	unsigned int first = 51966;	// 0xcafe(16) →51966(10)  
	
	unsigned int second; 		// 25
	for (unsigned int i = 0; ;i ++ ){
		if (i % 5 != 3 && i % 17 == 8){
			second = i;
			// printf("%d\n", (second % 17) * 11); // 88 
			break;
		}
	}
	char argv[10] = "h4cky0u";
	unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv) - 1615810207;
    printf("Get your key: ");
    printf("%x\n", hash);
	return 0;
} 

第十题:no-strings-attached

法一:动态分析

0x01.查壳和查看程序的详细信息

img

说明程序是ELF文件,32位

这个软件要放在Linux下执行,值得注意的是,我的Linux是64位的,一开始显示找不到文件,原因是没有安装32位的编译环境,在网上查找相关教程后才能运行。

0x02.使用静态分析工具IDA进行分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setlocale(6, &locale);
  banner();
  prompt_authentication();
  authenticate();
  return 0;
}

一个一个分析

setlocale

看名字像初始化,看了代码也看不出什么

int banner()
{
  unsigned int v0; // eax

  v0 = time(0);
  srand(v0);
  wprintf((int)&unk_80488B0);
  rand();
  return wprintf((int)&unk_8048960);
}

出现了wprintf()函数,我们可以康康输出了什么

双击&unk_80488B0(其它地址也是)

image-20220506143156195

可以看到一个一个的字母,我们用IDA python来输出,运行以下脚本

// IDA python 查看 wprintf();的内容
addr = here();
ans = ""
for i in range(50):
    ans += get_bytes(addr + i * 4, 1).decode(errors='ignore')
print(ans)

可以得到banner()中输出了"Welcome to cyber malware control software.","Currently tracking %d bots worldwide"

prompt_authentication();

同理可以发现,这个函数输出了“Please enter authentication details:”

还没有一些实质性的操作

authenticate()

void authenticate()
{
  int ws[8192]; // [esp+1Ch] [ebp-800Ch] BYREF
  wchar_t *s2; // [esp+801Ch] [ebp-Ch]

  s2 = decrypt((wchar_t *)&s, (wchar_t *)&dword_8048A90);//decrypt:加密
  if ( fgetws(ws, 0x2000, stdin) )	// 从标准输入流输入
  {
    ws[wcslen(ws) - 1] = 0;			// 谜之操作
    if ( !wcscmp(ws, s2) )			// 对比字符串
      wprintf((int)&unk_8048B44);	// Success! Welcome back!
    else
      wprintf((int)&unk_8048BA4);	// Access denied!
  }
  free(s2);
}

分析可得,这段代码是将我们的输入与s2进行对比,因此我们不妨先把s2找出来

0x03.GDB动态调试

首先我们在authenticate()中查看decrypt函数

image-20220506144710540

image-20220506144559694

它应该往eax写入了返回结果,我们待会重点关注。

用GDB调试,我在蓝色的地方设置了断点,然后运行,最后查看eax寄存器(以字符串形式输出)

gdb hhh
b *0x08048728
r
x/6sw $eax

最后一步详见:【原创】GDB之examine命令(即x命令)详解

image-20220506145129698

得到flag:9447{you_are_an_international_myster

法二:静态分析

s2 = decrypt((wchar_t *)&s, (wchar_t *)&dword_8048A90);知,我们可以分析s与dword_8048A90这两个数组的值,然后模拟一遍decrypt()函数,则有可能得到答案

字符串s

脚本法:67;zqxcfsgbes`kqxjspdxnppdpdn{vxjs{

image-20220506152838934

Shift + E法

image-20220506153059187

另一个字符串

image-20220506153223135

注意:两个参数的类型都是wchar_t 类型(长度16位或32位,本机32位,4字节)由于有大量的0,所以不能用char类型的数组,否则读到第三位直接结束。此外,删除后面4 个字节的0,因为字符串的结尾默认加0。

ps: wchar_t是C/C++的字符类型,是一种扩展的存储方式。wchar_t类型主要用在国际化程序的实现中,但它不等同于unicode编码。unicode编码的字符一般以wchar_t类型存储。——百度百科

#include<stdio.h>
#include<string.h>

int main(void)
{
	unsigned char s[] =
	{
		0x3A, 0x14, 0x00, 0x00, 0x36, 0x14, 0x00, 0x00, 0x37, 0x14,
		0x00, 0x00, 0x3B, 0x14, 0x00, 0x00, 0x80, 0x14, 0x00, 0x00,
		0x7A, 0x14, 0x00, 0x00, 0x71, 0x14, 0x00, 0x00, 0x78, 0x14,
		0x00, 0x00, 0x63, 0x14, 0x00, 0x00, 0x66, 0x14, 0x00, 0x00,
		0x73, 0x14, 0x00, 0x00, 0x67, 0x14, 0x00, 0x00, 0x62, 0x14,
		0x00, 0x00, 0x65, 0x14, 0x00, 0x00, 0x73, 0x14, 0x00, 0x00,
		0x60, 0x14, 0x00, 0x00, 0x6B, 0x14, 0x00, 0x00, 0x71, 0x14,
		0x00, 0x00, 0x78, 0x14, 0x00, 0x00, 0x6A, 0x14, 0x00, 0x00,
		0x73, 0x14, 0x00, 0x00, 0x70, 0x14, 0x00, 0x00, 0x64, 0x14,
		0x00, 0x00, 0x78, 0x14, 0x00, 0x00, 0x6E, 0x14, 0x00, 0x00,
		0x70, 0x14, 0x00, 0x00, 0x70, 0x14, 0x00, 0x00, 0x64, 0x14,
		0x00, 0x00, 0x70, 0x14, 0x00, 0x00, 0x64, 0x14, 0x00, 0x00,
		0x6E, 0x14, 0x00, 0x00, 0x7B, 0x14, 0x00, 0x00, 0x76, 0x14,
		0x00, 0x00, 0x78, 0x14, 0x00, 0x00, 0x6A, 0x14, 0x00, 0x00,
		0x73, 0x14, 0x00, 0x00, 0x7B, 0x14, 0x00, 0x00, 0x80, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00,
		0x75, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x63, 0x00,
		0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00,
		0x73, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x20, 0x00,
		0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00,
		0x6C, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x6F, 0x00,
		0x00, 0x00, 0x6D, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00,
		0x20, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x61, 0x00,
		0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x6B, 0x00, 0x00, 0x00,
		0x21, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00,
		0x63, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x73, 0x00,
		0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
		0x64, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6E, 0x00,
		0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00,
		0x64, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x0A, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x01, 0x1B, 0x03, 0x3B, 0x50, 0x00, 0x00, 0x00, 0x09, 0x00,
		0x00, 0x00, 0x88, 0xF8, 0xFF, 0xFF, 0x6C, 0x00, 0x00, 0x00,
		0x1C, 0xFA, 0xFF, 0xFF, 0x90, 0x00, 0x00, 0x00, 0x5B, 0xFA,
		0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x70, 0xFA, 0xFF, 0xFF,
		0xD0, 0x00, 0x00, 0x00, 0x20, 0xFB, 0xFF, 0xFF, 0xF4, 0x00,
		0x00, 0x00, 0xC1, 0xFB, 0xFF, 0xFF, 0x14, 0x01, 0x00, 0x00,
		0xF8, 0xFB, 0xFF, 0xFF, 0x34, 0x01, 0x00, 0x00, 0x68, 0xFC,
		0xFF, 0xFF, 0x70, 0x01, 0x00, 0x00, 0x6A, 0xFC, 0xFF, 0xFF,
		0x84, 0x01, 0x00, 0x00
	};

	unsigned char a2[] =
	{
		0x01, 0x14, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x03, 0x14,
		0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x05, 0x14, 0x00, 0x00,
	};

	int v4 = 0;
	int ans[1000] = {};
	for (int i = 0; i < 150; i ++ ){
		ans[i] = s[i] - a2[i % 20];		 // 两个循环可以简化成一个
		if(ans[i]) printf("%c", ans[i]); // 不加if,会出现大量空格 
	}
	return 0;
}

运行得到flag:9447{you_are_an_international_myster

本文参考:XCTF-writeup

第十一题 :csaw2013reversing2

0x01:查壳

image-20220508082617281

0x02: 思路一:静态分析

这里我用不同版本的IDA会生成不同的反汇编代码,其中v6的更容易读懂,这里我们选择v6,但是二者要结合在一起看。

main函数的反汇编代码

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // ecx@1
  LPVOID lpMem; // [sp+8h] [bp-Ch]@1				// 在v7中,它是char *类型的
  HANDLE hHeap; // [sp+10h] [bp-4h]@1

  hHeap = HeapCreate(0x40000u, 0, 0);
  lpMem = HeapAlloc(hHeap, 8u, MaxCount + 1);		// 看不懂,应该是赋一个初始值
  memcpy_s(lpMem, MaxCount, &unk_409B10, MaxCount);	// 类似strcpy,将字符串unk_409B10到地址lpMem处
  if ( sub_40102A() || IsDebuggerPresent() )	// 看名字可以猜测,有代码防止Debugger,动态调试要注意,我们静态调试就不管了 
  {
    __debugbreak();								// 看名字可以猜测,运行到这里就会退出 
    sub_401000(v3 + 4, (int)lpMem);				// 这是一段被“保护”的代码,可以猜测是生成flag的函数,另外第一个参数没有起作用
    ExitProcess(0xFFFFFFFF);					// 看名字可以猜测,运行到这里也会退出
  }
  MessageBoxA(0, (LPCSTR)lpMem + 1, "Flag", 2u);// 结合名字可以知道,应该是输出Flag
  HeapFree(hHeap, 0, lpMem);
  HeapDestroy(hHeap);
  ExitProcess(0);
}

main函数的逻辑大概是:

  1. 定义一个字符指针lpMen,往该地址写入字符串unk_409B10
  2. 如果经过几个检测debug的函数,将lpMen作为参数,调用生成Flag的函数
  3. 退出程序(这段要修改)
  4. 如果没有生成flag,就输出flag

很明显,如果是动态调试,那就得各种绕过,但我们现在只要关注生成flag的函数即可。

sub_401000(v3 + 4, (int)lpMem)函数的反汇编代码

unsigned int __fastcall sub_401000(int a1, int a2)	// 传入两个整数型参数 
{
  int v2; // esi
  unsigned int v3; // eax
  unsigned int v4; // ecx
  unsigned int result; // eax

  v2 = dword_409B38;	// 给v2赋值一个dword值的地址 
  v3 = a2 + 1 + strlen((const char *)(a2 + 1)) + 1;
  v4 = 0;
  result = ((strlen((const char *)(a2 + 1))) >> 2) + 1;
  if ( result )			// 结合下文猜测result是控制范围的 
  {
    do
      *(_DWORD *)(a2 + 4 * v4++) ^= v2;			// 最重要的一行 
    while ( v4 < result );
  }
  return result;
}

这段代码主要是各种运算,我们要解决一些概念问题,整个问题就迎刃而解:

  1. lpMem的值的问题

    传入的第二个参数是a2 = (int)lpMem,但是lpMem是一个地址,而地址那段代码又看不懂,不知道lpMem具体是什么。而代码中又要用到a2,这该怎么处理?

    经过分析可以发现,lpMem的值不会影响我们解题,因为其中可能造成影响的a2被消去了,下面具体分析涉及到lpMem的语句。

    首先是lpMem的初始化,第一行我们看不懂,但是经查阅,第二行的作用类似strcpy,将地址unk_409B10处开始的字符串复制到地址lpMem处,复制的元素是SourceSize个

    lpMem = (CHAR *)HeapAlloc(hHeap, 8u, SourceSize + 1);
    memcpy_s(lpMem, SourceSize, &unk_409B10, SourceSize);
    

    然后在传入参数时,令a2 = (int)lpMem

    具体使用a2时,有三处。

    第一处:v3 = a2 + 1 + strlen((const char *)(a2 + 1)) + 1;

    第二处:result = ((v3 - (a2 + 2)) >> 2) + 1;

    第三处:*(_DWORD *)(a2 + 4 * v4++) ^= v2;

    这里有两种用法,一种是把a2当作整数,另一种又把a2变成了指针。

    对于后者,这样的操作等价于对一个字符串进行操作,与具体的值无关。

    对于前者,我们可以发现它只影响v3和result的取值。对于v3,我们无法得知具体的值,后面只有求result时要用到。那我们就具体分析result,我们惊讶地发现,计算result时,a2被消掉了,因此我们可以计算出result的值,其它地方也用不到v3,至此我们可以放心的说a2对我们不会造成影响。

    所以我们可以直接定义一个字符数组:

    	unsigned char lpMem[] =	//lpMem
    	{
    		0xBB, 0xCC, 0xA0, 0xBC, 0xDC, 0xD1, 0xBE, 0xB8, 0xCD, 0xCF,
    		0xBE, 0xAE, 0xD2, 0xC4, 0xAB, 0x82, 0xD2, 0xD9, 0x93, 0xB3,
    		0xD4, 0xDE, 0x93, 0xA9, 0xD3, 0xCB, 0xB8, 0x82, 0xD3, 0xCB,
    		0xBE, 0xB9, 0x9A, 0xD7, 0xCC, 0xDD
    	};
    
  2. _DWORD类型,理解*(_DWORD *)(a2 + 4 * v4++) ^= v2;

    经查阅,它的定义是

    A dword, which is short for "double word," is a data type definition that is specific to Microsoft Windows. When defined in the file windows.h, a dword is an unsigned, 32-bit unit of data. It can contain an integer value in the range 0 through 4,294,967,295.

    它占有相当4个字节/char的空间,这段代码的含义应该是对a2所指向的地址处的Dword类型进行操作,即和v2所指向的Dword(v2被定义为int,但作用却是指针)进行异或。

    具体来说,v2处的定义是v2 = dword_409B38;,这就是一个dword的地址,点进去可以看到它的值是0DDCCAABBh或者写成c代码unsigned char v2[] ={ 0xBB, 0xAA, 0xCC, 0xDD };

    然后是异或,两个Dword怎么异或?

    应该可以使用windows.h来进行相关操作。

    这里我们可以把dword当作4个char来操作,即让我们上面定义的lpMem数组的每一个元素,轮换着和v2进行异或

  3. result的值的问题

    int a2 = (int)lpMem;
    int v3 = a2 + 1 + strlen(lpMem + 1) + 1;
    int result = ((v3 - (a2 + 2)) >> 2) + 1;
    printf("\n%u", result);
    

    计算可得result是9,也就是遍历一遍lpMem,所以对于char型的lpMem,范围就是36

0x03: 编写脚本生成flag

于是最终代码如下

#include<stdio.h>
int main(void)
{
	unsigned char lpMem[] =	//lpMem
	{
		0xBB, 0xCC, 0xA0, 0xBC, 0xDC, 0xD1, 0xBE, 0xB8, 0xCD, 0xCF,
		0xBE, 0xAE, 0xD2, 0xC4, 0xAB, 0x82, 0xD2, 0xD9, 0x93, 0xB3,
		0xD4, 0xDE, 0x93, 0xA9, 0xD3, 0xCB, 0xB8, 0x82, 0xD3, 0xCB,
		0xBE, 0xB9, 0x9A, 0xD7, 0xCC, 0xDD
	};
	unsigned char v2[] =
	{
		0xBB, 0xAA, 0xCC, 0xDD
	};
	unsigned char ans[100];
	for (int i = 0; i < 36 ; i ++ )
	{
		ans[i] = (lpMem[i]) ^ v2[i % 4];
		printf("%c", ans[i]);
	}
	return 0;
}

输出是 flag{reversing_is_not_that_hard!}

其它思路

参考文章:https://blog.csdn.net/weixin_43784056/article/details/103655968

第十二题:maze

0x01.查壳和详细信息

img

可以看到程序是ELF文件,64位

0x02:IDA静态分析

拖入IDA,F5查看main函数

p.s.右键数字,可以把数字转换成字符

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v3; // rbx
  int v4; // eax
  char v5; // bp
  char v6; // al
  const char *v7; // rdi
  unsigned int v9; // [rsp+0h] [rbp-28h] BYREF
  int v10[9]; // [rsp+4h] [rbp-24h] BYREF

  v10[0] = 0;
  v9 = 0;
  puts("Input flag:");
  scanf("%s", &s1); // s1 是我们输入的字符串,也就是flag了
  if ( strlen(&s1) != 24 || strncmp(&s1, "nctf{", 5uLL) || *(&byte_6010BF + 24) != '}' ) // 开头是“nctf{”,字符串长度是24,减去nctf{},还剩18个字符
  {
LABEL_22:
    puts("Wrong flag!");
    exit(-1);
  }
  v3 = 5LL;
  if ( strlen(&s1) - 1 > 5 )    // 长度得大于6
  {
    while ( 1 )
    {
      v4 = *(&s1 + v3);         // 即'{'之后的字符,转换为整数(ASCII)
      v5 = 0;
      if ( v4 > 78 )            // N(78)
      {
        if ( (unsigned __int8)v4 == 79 )    // 'O'
        {
          v6 = sub_400650(v10); // int v1 = (*a1)--; return v1 > 0; 返回v10指向的值是否为正,并且让v10指向的元素减一
          goto LABEL_14;
        }
        if ( (unsigned __int8)v4 == 111 )   // 'o'
        {
          v6 = sub_400660(v10); // int v1 = *a1 + 1; *a1 = v1; return v1 < 8; 让v10指向的值加一,然后判断其是否小于8
          goto LABEL_14;
        }
      }
      else
      {
        if ( (unsigned __int8)v4 == 46 )    // '.'
        {
          v6 = sub_400670(&v9); // int v1 = (*a1)--; return v1 > 0; 返回v9是否为正,并且让v9减一
          goto LABEL_14;
        }
        if ( (unsigned __int8)v4 == 48 )    // '0'
        {
          v6 = sub_400680(&v9); // int v1 = *a1 + 1; *a1 = v1; return v1 < 8; 先让v9加一,然后再判断其是否小于8
LABEL_14:
          v5 = v6;
        }
      }
      
      if ( !(unsigned __int8)sub_400690(asc_601060, (unsigned int)v10[0], v9) )	// 状态检测
        goto LABEL_22;
      
      if ( ++v3 >= strlen(&s1) - 1 )
      {
        if ( v5 )
          break;
LABEL_20:
        v7 = "Wrong flag!";
        goto LABEL_21;
      }
    }
  }
  if ( asc_601060[8 * v9 + v10[0]] != '#' )  // 8 * v9 + v10[0] 得是 36
    goto LABEL_20;
  v7 = "Congratulations!";
LABEL_21:
  puts(v7);
  return 0LL;
}

程序逻辑

  1. 输入一个字符串,要求开头是“nctf{”,字符串长度是24,最后一个字符是'}'

  2. 对字符串的每一个字符依次进行if判断,v4只能是{'O', 'o', '.', '0'}的其中一个。在判断的同时有以下操作

    1. 会改变两个量:v9,v10[0]。变化是加一或者减一。
    2. 会对v9,v10[0]的值的范围进行一个判断,将结果储存在v6(v5)中
  3. 判断结束后,会用sub_400690函数判断v10[0], v9的值是否合法,不合法则退出程序

  4. 当遍历完最后一个元素时,v5也会作为状态判断的标准,通过之后则退出遍历

  5. 最后以一个判断语句,判断v9,v10[0]是否符合要求,符合则输出Congratulations,说明我们输入了正确的flag。

其中sub_400690函数的作用是,判断数组asc_601060的特定元素,是否为指定元素

0x03.模拟代码

#include<iostream>
using namespace std;

const char pos[5] = {'O', 'o', '.', '0'}; 
const int len = 18;
char ans[19];

unsigned char a1[] =
{
  0x20, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x20, 
  0x20, 0x20, 0x2A, 0x20, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x20, 
  0x2A, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x20, 0x20, 0x2A, 0x20, 
  0x2A, 0x2A, 0x2A, 0x20, 0x20, 0x2A, 0x23, 0x20, 0x20, 0x2A, 
  0x2A, 0x2A, 0x20, 0x2A, 0x2A, 0x2A, 0x20, 0x2A, 0x2A, 0x2A, 
  0x20, 0x20, 0x20, 0x20, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 
  0x2A, 0x2A, 0x2A, 0x2A, 0x00
};

bool check (unsigned char* a1, int a2, int a3)
{
  char result; // rax
  result = *(unsigned char *)(a1 + a2 + 8LL * a3);
  bool x = ((result == 32) || (result == 35));    // 32 是空格(0x20),35 是'#'(0x23)
  return x;
}

void dfs(int n, int v9, int v10){
	if (n == len){
		if (v10 <= 0 || v10 >= 8 || v9 <= 0 || v9 >= 8) return;
		if (8 * v9 + v10 != 36) return;
		
		for (int i = 0; i < len; i ++ )	printf("%c", ans[i]);
		printf("\n");
		return;
	}
	if (!check(a1, v10, v9)) return;
	
	ans[n] = pos[0];
	dfs(n + 1, v9, v10 - 1);
	ans[n] = pos[1];
	dfs(n + 1, v9, v10 + 1);
	ans[n] = pos[2];
	dfs(n + 1, v9 - 1, v10);
	ans[n] = pos[3];
	dfs(n + 1, v9 + 1, v10);
    /*
   		意识到这是个迷宫之后,可以将z代码修改如下
    	const char dx[4] = {0, 0, -1, 1};
		const char dy[4] = {-1, 1, 0, 0};
    	for (int i = 0; i < 4; i ++ ){
		ans[n] = pos[i];
		dfs(n + 1, v9 + dx[i], v10 + dy[i]);
	}
    */
	return;
}

int main(void){
	dfs(0, 0, 0);
	return 0;
} 
posted @ 2022-05-08 16:16  tsrigo  阅读(222)  评论(0编辑  收藏  举报