2022-12-25 WP

从放假到现在做了一车水题,主要是Buu的,攻防世界近几天才开始做,稍微总结一下思路方法

0xFF 前言

真的是完全从0开始做了一堆re的水题,基本了解了IDA的使用方式,甚至惊喜发现IDA能够使用wine完美运行,遂完全切换至Linux下做题,包括伪代码应该怎么看,伪代码的生成机制,动态调试,远程调试和patch。也收集了一些IDA之外的reverse工具,比如jadx.Net Reflector分别对应java和dotnet的逆向,也很好用

这里放三个略有代表性的题,都很简单

0x00 [ACTF新生赛2020]easyre

ExeinfoPe读入直接报错,无所谓,我会IDA

扔到IDA里面惊喜发现有符号表,感觉应该不难

12-25-eazyre.png.png

直接F5一把梭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int __cdecl main(int argc, const char **argv, const char **envp)
{
_BYTE v4[12]; // [esp+12h] [ebp-2Eh] BYREF
_DWORD v5[3]; // [esp+1Eh] [ebp-22h]
_BYTE v6[5]; // [esp+2Ah] [ebp-16h] BYREF
int v7; // [esp+2Fh] [ebp-11h]
int v8; // [esp+33h] [ebp-Dh]
int v9; // [esp+37h] [ebp-9h]
char v10; // [esp+3Bh] [ebp-5h]
int i; // [esp+3Ch] [ebp-4h]

__main();
qmemcpy(v4, "*F'\"N,\"(I?+@", sizeof(v4));
printf("Please input:");
scanf("%s", v6);
if ( v6[0] != 65 || v6[1] != 67 || v6[2] != 84 || v6[3] != 70 || v6[4] != 123 || v10 != 125 )
return 0;
v5[0] = v7;
v5[1] = v8;
v5[2] = v9;
for ( i = 0; i <= 11; ++i )
{
if ( v4[i] != _data_start__[*((char *)v5 + i) - 1] )
return 0;
}
printf("You are correct!");
return 0;
}

容易看出v7,v8,v9,v10其实都是v6的一部分

看起来是将输入的flag用_data_start__这个数组做了个映射,看下这个数组的内容

12-25-eazyre2.png

果然是简单的映射,直接写脚本,把v4数组的东西映射回去就好了

此处注意,IDA会在字符串中自动加入转义字符,比如v4中的\"就是转义后的"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>

using namespace std;
//不要忘记第一个0x7E是`~`
const char c[] = "~}|{zyxwvutsrqponmlkjihgfedcba`_^]\\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$# !\"";
const char payload[] = "*F'\"N,\"(I?+@";
int main(){
for(int i=0;i<=11;i++){
for(int j=1;j<96;j++){
if(payload[i]==c[j-1]){//注意此处要-1
cout<<char(j)<<"";
break;
}
}
}
cout<<endl;
return 0;
}

输出U9X_1S_W6@T?即为flag

0x01 [GWCTF 2019]pyre

下载附件得到一个.pyc文件,为Python字节码,使用uncompyle6反编译得到

1
2
3
4
5
6
7
8
9
10
11
print "Welcome to Re World!"
print "Your input1 is your flag~"
l = len(input1)
for i in range(l):
num = ((input1[i] + i) % 128 + 128) % 128
code += num
for i in range(l - 1):
code[i] = code[i] ^ code[i + 1]
print code
code = ['\x1f', '\x12', '\x1d', '(', '0', '4', '\x01', '\x06', '\x14', '4', ',', '\x1b', 'U', '?', 'o', '6', '*', ':',
'\x01', 'D', ';', '%', '\x13']

它甚至是Python2的语法

看起来只需要把这个code数组倒着做一次相同的操作,先异或回去再把取模减掉的部分加回来就可以了

1
2
3
4
5
6
7
8
9
10
code = ['\x1f', '\x12', '\x1d', '(', '0', '4', '\x01', '\x06', '\x14', '4', ',', '\x1b', 'U', '?', 'o', '6', '*', ':','\x01', 'D', ';', '%', '\x13']集训
for i in range(len(code) - 2, -1, -1):
code[i] = chr(ord(code[i]) ^ ord(code[(i + 1)]))
c = -1
for i in code:
c = c + 1
if ord(i) - c < 0:
print(chr(ord(i) + 128 - c), end="")
continue
print(chr(ord(i) - c), end="")

得到GWHT{Just_Re_1s_Ha66y!}即为flag

0x02 简单注册器

下载附件得到一个.apk文件,先放到jadx里面看看

很容易就找到了主要函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void onClick(View v) {
int flag = 1;
String xx = editview.getText().toString();
flag = (xx.length() == 32 && xx.charAt(31) == 'a' && xx.charAt(1) == 'b' && (xx.charAt(0) + xx.charAt(2)) + (-48) == 56) ? 0 : 0;
if (flag == 1) {
char[] x = "dd2940c04462b4dd7c450528835cca15".toCharArray();
x[2] = (char) ((x[2] + x[3]) - 50);
x[4] = (char) ((x[2] + x[5]) - 48);
x[30] = (char) ((x[31] + x[9]) - 48);
x[14] = (char) ((x[27] + x[28]) - 97);
for (int i = 0; i < 16; i++) {
char a = x[31 - i];
x[31 - i] = x[i];
x[i] = a;
}
String bbb = String.valueOf(x);
textview.setText("flag{" + bbb + "}");
return;
}
textview.setText("输入注册码错误");
}

看来只需要搞到x数组即可,直接运行这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class solve{
public static void main(String[] args) {
char[] x = "dd2940c04462b4dd7c450528835cca15".toCharArray();
x[2] = (char) ((x[2] + x[3]) - 50);
x[4] = (char) ((x[2] + x[5]) - 48);
x[30] = (char) ((x[31] + x[9]) - 48);
x[14] = (char) ((x[27] + x[28]) - 97);
for (int i = 0; i < 16; i++) {
char a = x[31 - i];
x[31 - i] = x[i];
x[i] = a;
}
String bbb = String.valueOf(x);
System.out.println(bbb);
return;
}
}

得到59acc538825054c7de4b26440c0999dd即为flag

2022-12-31 WP

本周转战攻防世界,也逐渐做了一些有意思的题

0x00 eazy_go

1
2
file easyGo 
easyGo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=AMKR9SkEqpLX9a66kfz-MTfh_nlJaiowVRmWgzjZ/KLNtSXuNfFoiFw0kSD4f/2IbhaQN8zH0h9MRASllS, stripped

一个golang的可执行文件,直接扔到IDA里面,找到主要逻辑main_main函数

直接看逻辑好复杂,开调!

12-31-eazygo1.png

这个函数看起来是比较某个和输入有关的东西和某个和flag有关的东西,打个断点

直接看v0的值试试?

12-31-eazygo2.png

好家伙,明文flag,轻松秒杀

0x01 APK-逆向2

题目名字叫APK,但是我寻思这也没APK啊

1
2
file 4122e391e1574335907f8e2c4f438d0e.exe
4122e391e1574335907f8e2c4f438d0e.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

看起来dotNet可执行文件,扔dnSpy看看咯

12-31-APK1.png

反编译后Main函数长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static void Main(string[] args)
{
string hostname = "127.0.0.1";
int port = 31337;
TcpClient tcpClient = new TcpClient(); //向127.0.0.1:31337 打开一个TCP连接
try
{
Console.WriteLine("Connecting...");
tcpClient.Connect(hostname, port);
}
catch (Exception)
{
Console.WriteLine("Cannot connect!\nFail!");
return;
}
Socket client = tcpClient.Client;
string text = "Super Secret Key";
string text2 = Program.read();
client.Send(Encoding.ASCII.GetBytes("CTF{"));
foreach (char x in text)
{
client.Send(Encoding.ASCII.GetBytes(Program.search(x, text2)));
}
client.Send(Encoding.ASCII.GetBytes("}"));
client.Close();//向127.0.0.1:31337写入一些东西(看起来像flag?)
tcpClient.Close();
Console.WriteLine("Success!");
}

直接开一个127.0.0.1:31337的TCP服务器(嵌入式没白搞

12-31-APK2.png

得到flag这算不算一种偷袭?

0x02 babymips

1
2
file 2104878f0fb046a0a1766a2e80e538c2 
2104878f0fb046a0a1766a2e80e538c2: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-mipsel.so.1, stripped

看起来是mips架构的可执行文件,虽然有设备但是懒得放上去跑了,直接IDA

两个主要函数是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall main(int a1, char **a2, char **a3)
{
int i; // [sp+18h] [+18h] BYREF
char v5[36]; // [sp+1Ch] [+1Ch] BYREF

setbuf((FILE *)stdout, 0);
setbuf((FILE *)stdin, 0);
printf("Give me your flag:");
scanf("%32s", v5);//这里输入flag
for ( i = 0; i < 32; ++i )
v5[i] ^= 32 - (_BYTE)i;//这里对flag进行异或预处理
if ( !strncmp(v5, fdata, 5u) )//这里对预处理后的前五个字节进行比较,fdata:"Q|j{g"
return sub_4007F0(v5);//对剩下的部分进行比较
else
return puts("Wrong");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __fastcall sub_4007F0(const char *a1)
{
char v1; // $v1
size_t i; // [sp+18h] [+18h]

for ( i = 5; i < strlen(a1); ++i )
{
if ( (i & 1) != 0 )
v1 = (a1[i] >> 2) | (a1[i] << 6);//这里把这个字节的左边6位和右边2位对换
else
v1 = (4 * a1[i]) | (a1[i] >> 6);//这里把这个字节的左边2位和右边6位对换,*4等价于<<2
a1[i] = v1;
}
if ( !strncmp(a1 + 5, (const char *)off_410D04, 0x1Bu) )//将处理完的输入与off_410D04进行比较,追踪后dump出数据即可
return puts("Right!");
else
return puts("Wrong!");
}

明确了逻辑即可写出脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <string>

using namespace std;

unsigned char enflag2[] = {'Q','|','j','{','g',0x52, 0xFD, 0x16, 0xA4, 0x89, 0xBD, 0x92, 0x80, 0x13, 0x41, 0x54, 0xA0, 0x8D, 0x45, 0x18, 0x81, 0xDE, 0xFC, 0x95, 0xF0, 0x16, 0x79, 0x1A, 0x15, 0x5B, 0x75, 0x1F, 0};

int main()
{
for (int i = 5; i < 32; i++)
{
if (i & 1!=0)
{
enflag2[i] = (enflag2[i] << 2) | (enflag2[i] >> 6);
}
else
{
enflag2[i] = (enflag2[i] >> 2) | (enflag2[i] << 6);
}
}//先复原位运算
for(int i=0;i<32;i++){
enflag2[i]^=32-i;
cout<<char(enflag2[i]);
}//再复原异或
cout<<endl;
return 0;
}

得到flag

0x03 debug

看这个题目名字就感觉是动态调试秒杀(?

1
2
file 3d43134e9941483e970e936e88c245f2.exe 
3d43134e9941483e970e936e88c245f2.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

依然dotNet可执行文件,直接dnSpy

花费一些时间找到主要逻辑

12-31-debug1.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void ᜀ(string[] A_0)
{
string b = null;
string value = string.Format("{0}", DateTime.Now.Hour + 1);
string a_ = "CreateByTenshine";
ᜅ.ᜀ(a_, Convert.ToInt32(value), ref b);
string a = Console.ReadLine();//输入
if (a == b)//比较,感觉b是flag,这里来个断点直接调
{
Console.WriteLine("u got it!");
Console.ReadKey(true);
}
else
{
Console.Write("wrong");
}
Console.ReadKey(true);
}

12-31-debug2.png

果然是debug就解决了……

0x04 crackme

1
2
file 3fd532458bd248349f3bdba2ccb1c5e8.exe 
3fd532458bd248349f3bdba2ccb1c5e8.exe: PE32 executable (console) Intel 80386, for MS Windows, 3 sections

一般的win32可执行文件

12-31-crackme1.png

有壳,nsPack,想办法脱壳

通过搜索找到脱壳工具,直接脱壳

12-31-crackme2.png

将脱壳后文件扔到IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // eax
char Buffer[52]; // [esp+4h] [ebp-38h] BYREF

memset(Buffer, 0, 50);
printf("Please Input Flag:");
gets_s(Buffer, 0x2Cu);//输入
if ( strlen(Buffer) == 42 )
{
v4 = 0;
while ( (Buffer[v4] ^ byte_402130[v4 % 16]) == dword_402150[v4] )
{
if ( ++v4 >= 42 )
{
printf("right!\n");
return 0;
}//异或后比较,直接追踪过去拿到数据异或回来
}
printf("error!\n");
return 0;
}
else
{
printf("error!\n");
return -1;
}
}

写出脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>

using namespace std;

char Byte[]="this_is_not_flag";
char Dword[]={0x12,4,8,0x14,0x24,0x5C,0x4A,0x3D,0x56,0xA,0x10,0x67,0,0x41,0,1,0x46,0x5A,0x44,0x42,0x6E,0xC,0x44,0x72,0xC,0xD,0x40,0x3E,0x4B,0x5F,2,1,0x4C,0x5E,0x5B,0x17,0x6E,0xC,0x16,0x68,0x5B,0x12,2,0};

int main()
{
for(int i=0;i<42;i++){
Dword[i]^=Byte[i%16];
}
cout<<Dword<<endl;
return 0;
}

0x05 maze

1
2
file bdb2c015b0fd4f74bc4c3e5a6e54bcf4 
bdb2c015b0fd4f74bc4c3e5a6e54bcf4: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=eda1df76eec45447cd0e1ad208a7eff914e86758, stripped

看题目应该是经典的走迷宫题,直接IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
__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);
if ( strlen(&s1) != 24 || strncmp(&s1, "nctf{", 5uLL) || *(&byte_6010BF + 24) != '}' )
{
LABEL_22:
puts("Wrong flag!");
exit(-1);
}
v3 = 5LL;
if ( strlen(&s1) - 1 > 5 )
{
while ( 1 )
{
v4 = *(&s1 + v3);
v5 = 0;
if ( v4 > 78 )
{
if ( (unsigned __int8)v4 == 'O' )
{
v6 = sub_400650(v10); // v10[0]--
goto LABEL_14;
}
if ( (unsigned __int8)v4 == 'o' )
{
v6 = sub_400660(v10); // v10[0]++
goto LABEL_14;
}
}
else
{
if ( (unsigned __int8)v4 == '.' )
{
v6 = sub_400670(&v9); // v9--
goto LABEL_14;
}
if ( (unsigned __int8)v4 == '0' )
{
v6 = sub_400680(&v9); // v9++
LABEL_14:
v5 = v6;
}
}// v10[0]是行,v9是列,v9上下移动,v10[0]左右移动
if ( !(unsigned __int8)sub_400690((__int64)asc_601060, 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]] != 35 )
goto LABEL_20;
v7 = "Congratulations!";
LABEL_21:
puts(v7);
return 0LL;
}

在string里面找到迷宫,在sub_400690得到#是终点, 是路

1
2
3
4
5
6
7
8
  ******
* * *
*** * **
** * **
* *# *
** *** *
** *
********

构造出flag:nctf{o0oo00O000oooo..OO}

0x7F 本周总结

有一些reverse题可以用一些小技巧偷袭简单解出,这可能是出题人的疏忽,也可能是有意为之,要保持灵活的思维,不能只考虑静态分析

本周两个比赛涨了一些见识,catctf的misc好有意思但是好像我的思路都有些问题

2023-01-23 WP

注:本时间为此WP开始写作时间,并非最终上传时间,最终上传时间以论坛/博客时间为准

第一次在自己的WP里面写比赛题

T1 CATCTF Cat_Jump

很有意思的一题,赛后只觉得自己蠢

附件给了一个.vmdk文件

1
2
file cat_jump_clean.vmdk 
cat_jump_clean.vmdk: VMware4 disk image

一个虚拟机硬盘镜像,直观想法应该是挂载它然后看看有什么

直接启动是一个猫猫游戏

找个livecd挂载一下,发现是个alpine linux并且用了openrc,且提取出以下文件

1
2
file termosaur
termosaur: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, with debug_info, not stripped

会不会是个逆向呢,放IDA里面看看

发现如下String

1
catctf=`base64 /sys/firmware/efi/efivars/CatCTF-7c436110-ab2a-4bbb-a880-fe41995c9f82`;echo U2FsdGVkX1/d/fu4H2quw6KimHlfde+UjUBw0jcmkGEKaHnBck8CelJ14JV9buQy | openssl aes-128-cbc -d -pbkdf2 -base64 -k $catctf

看起来是flag的解密脚本,但是这个efivar经过一番努力发现是不可得的,比赛的时候只做到这里

赛后考虑到这是个misc,感觉还是这个vmdk文件有问题,遂拿出原始文件,直接hex editor打开直接搜catctf{

果然搜到了CatCTF{EFI_1sv3ry_funn9}

有种打oi签到题被降智的感觉,我愿称之为CTF的小凯的疑惑

T2 CATCTF CatchCat

仍然是一道misc,GPS数据处理

附件CatchCat.txt纯文本文件,打开以后的内容大概是这样的

1
2
3
$GPGGA,090000.00,3416.48590278,N,10856.86623887,E,1,05,2.87,160.00,M,-21.3213,M,,*7E
...
$GPGGA,090609.00,3416.48362669,N,10856.86416198,E,1,05,2.87,160.00,M,-21.3213,M,,*7D

搜索关键词GPGGA,得到这是一种GPS数据格式,是NMEA-0183协议的一种数据格式

求助万能的Python,搜索Python+NEMA两个关键词

找到一个库pynmea2可以解析NMEA数据,另一个库folium,同时发现有轮子可以直接用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pynmea2
import folium
import os

def draw_gps(locations, output_path, file_name):
m = folium.Map(locations[0], zoom_start=15, attr='default') #确定区域
folium.PolyLine(locations, weight=3, color='orange', opacity=0.8).add_to(m) #划线
folium.Marker(locations[0], popup='<b>Starting Point</b>').add_to(m) #标记起点
folium.Marker(locations[-1], popup='<b>End Point</b>').add_to(m) #标记终点
m.save(os.path.join(output_path, file_name)) #保存为html,默认使用OpenStreetMap图源,本题不涉及具体位置所以够用,不需要考虑坐标偏移

nmea_file=pynmea2.NMEAFile('CatchCat.txt')

poss = list()

for record in nmea_file: #将坐标预处理为二维数组
single = list()
single.append(record.latitude)
single.append(record.longitude)
poss.append(single)
print(poss)
draw_gps(poss,"./","map.html")

得到html文件如图:

1-7-catchcat1.png

阅读轨迹得到flag:CatCTF{GPS_M1ao}

T3 HWS2023WINTER babyre

稍微有一点点难度的android逆向,附件只有一个apk,直接jadx

1-7-babyre1.png

a, b, c, d, MainActivity五个class,先看MainActivity搞清楚逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {
private Button bt;
private EditText input;

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
this.input = (EditText) findViewById(R.id.input);
this.bt = (Button) findViewById(R.id.bt);
this.bt.setOnClickListener(new View.OnClickListener() { // from class: com.me.crackme.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
b.a(MainActivity.this);//关键在于a().check()这个函数,传入的是用户输入,返回一个Bool
if (MainActivity.this.input.getText().toString() != null && new a().check(MainActivity.this.input.getText().toString())) {
Toast.makeText(MainActivity.this, "success", 1).show();
} else {
Toast.makeText(MainActivity.this, "fail", 1).show();
}
}
});
}
}

再看一下a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class a {
public boolean check(String str) {
byte[] bytes = str.getBytes();
if (bytes.length == 38) {
return false;
}
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (bytes[i] + 10);
bytes[i] = (byte) (bytes[i] ^ 102);
bytes[i] = (byte) (bytes[i] << 3);//注意,这一步是不可逆的,因此根据下面的Base64值倒推是不可能的
}
return Base64.encodeToString(bytes, 0).equals("a214bmVqaXlieHpjaXhuc2p4bm5hc20=");
}
}

这里反编译出的a做了一步不可逆的操作,还有b, c, d没看,不急

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class d extends Application {
@Override // android.content.ContextWrapper
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
new c(this);//d实例化了一个c()
}
}

public class c {
public c(Context context) {
try {
//读取了“enc”文件
InputStream open = context.getResources().getAssets().open("enc");
byte[] bArr = new byte[open.available()];
open.read(bArr);
//对它的每个字节做异或
for (int i = 0; i < bArr.length; i++) {
bArr[i] = (byte) (bArr[i] ^ 52);
}
//进入"./odex/"目录
File dir = context.getDir("odex", 0);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir.getAbsolutePath() + File.separator + "classes.dex");
if (!file.exists()) {
file.createNewFile();
}
//将异或后的文件写入到"./odex/classes.dex"文件
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bArr);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

由于class b代码过于冗长,在这里不再贴出,其主要逻辑为利用c中解码出的dex文件修补现有的dex文件

目前已知enc是一个经过编码后的dex文件,将其导出后解码

1
2
3
4
5
6
from Cryptodome.Util.number import long_to_bytes

Dec = open("dec.dex","wb")
with open("enc","rb") as Enc:
for i in Enc.read():
Dec.write(long_to_bytes(i^52))

用jadx直接打开解码后的dec.dex,得到一个新的class a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class a {
public static String Encrypt(String sSrc) {
try {
//秘钥
byte[] raw = "FV8aOaQiak6txP09".getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
//工作模式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
//iv
IvParameterSpec iv = new IvParameterSpec("2Aq7SR5268ZzbouE".getBytes());
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(sSrc.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//对输入数据使用AES/CBC/PKCS5Padding进行加密,且秘钥与偏移向量都已知,直接解密即可
public boolean check(String input) {
//密文
return Encrypt(input).equals("9Kz3YlSdD3lB9KoxeKxXQT4YOEqJTVIuNU+IjW4iFQzjpU+NikF/UqCOsL+1g4eA");
}
}

将参数转换为16进制,扔到CyberChef,得到flag{076a554cef6742b402d74c1013dadde9}

1-7-babyre2.png

T4 HWS2023WINTER sound from somewhere

一道简单的misc,给了一个wav音频文件,学到了QSSTV的使用方式

扔到Audacity看看

1-7-sound1.png

比较标准有规律的波形,结合试听考虑是某种调制过的信号,怀疑是SSTV或者拨号猫

先用SSTV试试,Linux下比较好用的SSTV解析软件大概是QSSTV,发现AUR就有

1-7-sound2.png

打开后界面是这样的,默认是直接使用系统的音频输入

1-7-sound3.png

由于我使用PipeWire作为系统的音频框架,直接使用一个GUI小工具qpwgraph重定向输入输出即可

将当前使用的输出设备的监听接口WI-1000XM2:monitor重定向到qsstv的输入

1-7-sound4.png

鼠标操作即可完成

1-7-sound5.png

在Audacity中播放,得到图片,得到flag{OuTer_Wilds}

1-7-sound6.png

T5 XCTF EASYHOOK

经典的reverse题目,win32可执行文件,只看题目以为是我要hook进程序,结果是程序hook了自己

1
2
file 1c40a4a45618413d83d4724b7f1a5d2f.exe 
1c40a4a45618413d83d4724b7f1a5d2f.exe: PE32 executable (console) Intel 80386, for MS Windows, 3 sections

阅读代码,重命名变量后的main函数大概是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int __cdecl main(int argc, const char **argv, const char **envp)
{
HANDLE FileA; // eax
DWORD NumberOfBytesWritten; // [esp+4h] [ebp-24h] BYREF
char inputflag[32]; // [esp+8h] [ebp-20h] BYREF
puts(aPleaseInputFla);
scanf("%31s", inputflag);//输入一个字符串
if ( strlen(inputflag) == 19 )//flag长度为19
{
sub_401220();
FileA = CreateFileA(FileName, 0x40000000u, 0, 0, 2u, 0x80u, 0);
WriteFile(FileA, inputflag, 0x13u, &NumberOfBytesWritten, 0);
sub_401240(inputflag, &NumberOfBytesWritten);//跟踪进这个函数,代码贴在下面
if ( NumberOfBytesWritten == 1 )
puts(aRightFlagIsYou);
else
puts(aWrong);
system(Command);
return 0;
}
else
{
puts(aWrong);
system(Command);
return 0;
}
}
//sub_401240
int __cdecl sub_401240(const char *a1, _DWORD *a2)
{
int result; // eax
unsigned int v3; // kr04_4
char v4[24]; // [esp+Ch] [ebp-18h] BYREF
result = 0;
strcpy(v4, "This_is_not_the_flag");//显然这不是我们要找的函数
v3 = strlen(a1) + 1;
//省略
return result;
}

注意到main函数中还调用了sub_401220()函数,跟踪进去找找线索

发现是对当前进程的Hook,并且对某个变量进行了赋值操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int sub_401220()
{
HMODULE LibraryA; // eax
DWORD CurrentProcessId; // eax
CurrentProcessId = GetCurrentProcessId();
hProcess = OpenProcess(0x1F0FFFu, 0, CurrentProcessId);
LibraryA = LoadLibraryA(LibFileName);
WriteFile_0 = (BOOL (__stdcall *)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED))GetProcAddress(LibraryA, ProcName);
lpAddress = WriteFile_0;
if ( !WriteFile_0 )
return puts(&unk_40A044);
unk_40C9B4 = *(_DWORD *)lpAddress;
*((_BYTE *)&unk_40C9B4 + 4) = *((_BYTE *)lpAddress + 4);
byte_40C9BC = -23;
//这里进行了一个赋值,跟踪进sub_401080,代码贴在后面
dword_40C9BD = (char *)sub_401080 - (char *)lpAddress - 5;
return sub_4010D0();
}
//sub_401080
int __stdcall sub_401080(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped)
{
int v5; // ebx
v5 = sub_401000((int)lpBuffer, nNumberOfBytesToWrite);
sub_401140();//找到了NumberOfBytesToWrite,同名变量在main函数出现过,遂跟踪进sub_401000
WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
if ( v5 )
*lpNumberOfBytesWritten = 1;
return 0;
}
//sub_401000
int __cdecl sub_401000(int a1, int a2)
{
//真正的flag处理逻辑
char i; // al
char v3; // bl
char v4; // cl
int v5; // eax
for ( i = 0; i < a2; ++i )
{
if ( i == 18 )
{
*(_BYTE *)(a1 + 18) ^= 0x13u;
}
else
{
if ( i % 2 )
v3 = *(_BYTE *)(i + a1) - i;
else
v3 = *(_BYTE *)(i + a1 + 2);
*(_BYTE *)(i + a1) = i ^ v3;
}
}
v4 = 0;
if ( a2 <= 0 )
return 1;
v5 = 0;//此处byte_40A030即编码后的flag
while ( byte_40A030[v5] == *(_BYTE *)(v5 + a1) )
{
v5 = ++v4;
if ( v4 >= a2 )
return 1;
}
return 0;
}

提取出byte_40A030的数据,写脚本逆变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char enflag[]={0x61,0x6A,0x79,0x67,0x6B,0x46,0x6D,0x2E,0x7F,0x5F,0x7E,0x2D,0x53,0x56,0x7B,0x38,0x6D,0x4C,0x6E,0};
char flag[100];
int main()
{
for(int i=0;i<strlen(enflag);i++){
if(i%2){
flag[i]=char((enflag[i]^i)+i);
}
else{
flag[i+2]=char(enflag[i]^i);
}
}
flag[0]='f';
cout<<flag<<endl;
return 0;
}

得到flag{Ho0k_w1th_Fun}

后记:这题如果用动态调试的话难度大大降低,可以直接进入关键函数

T6 XCTF handcrafted-pyc

一个对pyc文件的逆向,考察pyc文件结构和Python字节码

附件是一个Python脚本,此处省略括号中的内容

1
2
3
4
5
6
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import marshal, zlib, base64

exec(marshal.loads(zlib.decompress(base64.b64decode("..."))))

脚本加载了一个pyc文件,先用zlib和base64把它解码后写入到文件

1
2
3
4
5
6
7
import struct, zlib, base64
a = zlib.decompress(base64.b64decode('...'))
tmp = open("a.pyc","wb")
for i in a:
cache = struct.pack("B",i)
tmp.write(cache)
tmp.close();

写入以后用hex editor打开,发现缺少文件头,猜测为Python版本为2,补充八个字节文件头03 F3 0D 0A C8 B9 59 61

1-7-handpyc1.png

补充后用uncompyle6反编译,得到一份Python字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#decr.py
def main--- This code section failed: ---
L. 1 0 LOAD_GLOBAL 0 'chr'
3 LOAD_CONST 108
6 CALL_FUNCTION_1 1 None
9 LOAD_GLOBAL 0 'chr'
12 LOAD_CONST 108
15 CALL_FUNCTION_1 1 None
18 LOAD_GLOBAL 0 'chr'
21 LOAD_CONST 97
24 CALL_FUNCTION_1 1 None
27 LOAD_GLOBAL 0 'chr'
30 LOAD_CONST 67
33 CALL_FUNCTION_1 1 None
36 ROT_TWO
37 BINARY_ADD
38 ROT_TWO
39 BINARY_ADD
40 ROT_TWO
41 BINARY_ADD
42 LOAD_GLOBAL 0 'chr'
45 LOAD_CONST 32
48 CALL_FUNCTION_1 1 None
51 LOAD_GLOBAL 0 'chr'
54 LOAD_CONST 101
57 CALL_FUNCTION_1 1 None
60 LOAD_GLOBAL 0 'chr'
63 LOAD_CONST 109
66 CALL_FUNCTION_1 1 None
69 LOAD_GLOBAL 0 'chr'
72 LOAD_CONST 32
75 CALL_FUNCTION_1 1 None

分析字节码得到以下关键指令

1
2
3
4
5
6
7
8
9
LOAD_GLOBAL
LOAD_CONST
两条指令结合为向栈中加入某类型的一个元素并赋值

BINARY_ADD
将栈顶两个元素弹出,相加,再压入栈中

ROT_TWO
交换栈顶两个元素(此指令可参考以下代码

此处摘录两个函数的的具体实现,其余指令实现均可在CPython源码(注意不同版本opcode有差异)中找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
TARGET_NOARG(BINARY_ADD)
{
w = POP();
v = TOP();
if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
/* INLINE: int + int */
register long a, b, i;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
/* cast to avoid undefined behaviour
on overflow */
i = (long)((unsigned long)a + b);
if ((i^a) < 0 && (i^b) < 0)
goto slow_add;
x = PyInt_FromLong(i);
}
else if (PyString_CheckExact(v) &&
PyString_CheckExact(w)) {
x = string_concatenate(v, w, f, next_instr);
/* string_concatenate consumed the ref to v */
goto skip_decref_vx;
}
else {
slow_add:
x = PyNumber_Add(v, w);
}
Py_DECREF(v);
skip_decref_vx:
Py_DECREF(w);
SET_TOP(x);
if (x != NULL) DISPATCH();
break;
}
TARGET(ROT_TWO) {
PyObject *top = TOP();
PyObject *second = SECOND();
SET_TOP(second);
SET_SECOND(top);
FAST_DISPATCH();
}

只需模拟相应操作即可复原栈中数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def binary_add(nums:list):
tmp = nums.pop()
nums.append(nums.pop()+tmp)
return nums

def rot_two(nums:list):
last=nums.pop()
prev=nums.pop()
nums.append(last)
nums.append(prev)
return nums

with open("decr.py") as decr
lines = decr.readlines()
nums = list()
#手写一个isdigit
isdigit = lambda s: all([data >= '0' and data <= '9' for data in s])

for i in lines:
if len(i.split())==3:
i = i.split()[-1]
if isdigit(i):
nums.append(chr(int(i)))
else:
nums.append(0)
else:
if "BINARY_ADD" in i:
nums = binary_add(nums)
elif "ROT_TWO" in i:
nums = rot_two(nums)
print(nums)

得到hitcon{Now you can compile and run Python bytecode in your brain!}

本周总结

本周打了HWS和杭电Hgame,见到了很多新东西

比赛比较长见识,有时间有能力要多参加比赛,也要防止被降智

遇到没见过的题目不要慌,冷静分析+搜索+跳出固有思维往往可以提供新的思路

有字节码的语言可以考虑了解一下它们的字节码结构

2023-01-12 WP

本次WP题目主要来自Hgame