UofTCTF

Last updated on January 27, 2024 am

记录JS逆向以及Html&Css逆向

UofTCTF-2023

这里是题目

第一步首先找到关键的函数,可以先找按钮的位置,再找按钮的触发事件

显然是这个checkPassword,checkPassword是被混淆了的

截屏2024-01-17 23.49.39

丢去这个网站可以解混淆,下面是混淆后的结果

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
function checkPassword(password) {
try {
if (password.length !== 21) {
return false;
}
if (password.slice(1, 2) !== (String.fromCodePoint + '')[parseInt((parseInt + '').charCodeAt(3), 16) - 147] ||
password[(parseInt(41, 6) >> 2) - 2] !== String.fromCodePoint(123) ||
password[4].charCodeAt(0) !== password[7].charCodeAt(0) + 72 ||
JSON.stringify(Array.from(password.slice(5, 7).split('').reverse().join(), _0x2d4d73 => _0x2d4d73.codePointAt(0)).map(_0x5b85c5 => _0x5b85c5 + 213)) !== JSON.stringify([285, 257, 297])) {
return false;
}
let password_8_12_rev = password.slice(8, 12).split('').reverse();
try {
for (let i = 0; i < 5; i++) {
password_8_12_rev[i] = password_8_12_rev[i].charCodeAt(0) + i + getAdder(i);
}
} catch (_0x1fbd51) {
password_8_12_rev = password_8_12_rev.map(_0x24cda7 => _0x24cda7 += _0x1fbd51.constructor.name.length - 4);
}
if (MD5(String.fromCodePoint(...password_8_12_rev)) !== '098f6bcd4621d373cade4e832627b4f6') {
return false;
}
if (MD5(password.charCodeAt(12) + '') !== '812b4ba287f5ee0bc9d43bbf5bbe87fb') {
return false;
}
password_8_12_rev = (password[8] + password[11]).split('');
password_8_12_rev.push(password_8_12_rev.shift());
if (password.substring(14, 16) !== String.fromCodePoint(...password_8_12_rev.map(_0x5b5ec8 => Number.isNaN(+_0x5b5ec8) ? _0x5b5ec8.charCodeAt(0) + 5 : 48)) || password[password[7] - password[10]] !== atob('dQ==') || password.indexOf(String.fromCharCode(117)) !== password[7] - password[17] || JSON.stringify(password.slice(2, 4).split('').map(_0x7bf0a6 => _0x7bf0a6.charCodeAt(0) ^ getAdder.name[password[7]].charCodeAt(0))) !== JSON.stringify([
72,
90
].map(_0x40ab0d => _0x40ab0d ^ String.fromCodePoint.name[password[17] - 1].charCodeAt(0)))) {
return false;
}
if (String.fromCodePoint(...password.split('').filter((_0x5edfac, _0x2965d2) => _0x2965d2 > 15 && _0x2965d2 % 2 == 0).map(_0x2ffa6d => _0x2ffa6d.charCodeAt(0) ^ password.length + password[7])) !== atob('g5Go')) {
return false;
}
if (password[password.length - 2] !== String.fromCharCode(Math.floor((({} + '').charCodeAt(0) + 9) / 3)) || password[1 + password[7]] !== giggity()[5]) {
return false;
}
return true;
} catch (_0x4d4983) {
return false;
}
}
function getAdder(_0x430c9d) {
switch (_0x430c9d) {
case 0:
return 34;
case 1:
return 44;
case 2:
return 26;
case 3:
return 60;
}
return 101;
}
function giggity() {
return giggity.caller.name;
}

这里面有些变量作了重新命名,便于审计。

整体的验证流程都很简单,就是把flag给拆分开来进行判断,

比如首先验证长度为21

1
2
3
if (password.length !== 21) {
return false;
}

然后逐步逐步验证

1
2
3
4
5
6
if (password.slice(1, 2) !== (String.fromCodePoint + '')[parseInt((parseInt + '').charCodeAt(3), 16) - 147] ||
password[(parseInt(41, 6) >> 2) - 2] !== String.fromCodePoint(123) ||
password[4].charCodeAt(0) !== password[7].charCodeAt(0) + 72 ||
JSON.stringify(Array.from(password.slice(5, 7).split('').reverse().join(), _0x2d4d73 => _0x2d4d73.codePointAt(0)).map(_0x5b85c5 => _0x5b85c5 + 213)) !== JSON.stringify([285, 257, 297])) {
return false;
}

截屏2024-01-17 23.59.19

对于其中的一些语句可以直接使用控制台打印调试,整体都是这个思路,这里贴上官方wp

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
function checkPassword(_0x38d32a) {
try {
// Password length is 21.
if (_0x38d32a.length !== 21) {
return false;
}
if (
_0x38d32a.slice(1, 2) !==
(String.fromCodePoint + "")[
parseInt((parseInt + "").charCodeAt(3), 16) - 147
] /* password[1] = 'p' */ ||
_0x38d32a[(parseInt(41, 6) >> 2) - 2] !==
String.fromCodePoint(123) /* password[4] = '{' */ ||
_0x38d32a[4].charCodeAt(0) !==
_0x38d32a[7].charCodeAt(0) + 72 /* password[7] = '3'. */ ||
JSON.stringify(
Array.from(
_0x38d32a.slice(5, 7).split("").reverse().join(),
(_0x2d4d73) => _0x2d4d73.codePointAt(0)
).map((_0x5b85c5) => _0x5b85c5 + 213)
) !==
JSON.stringify([
285, 257, 297,
]) /* password[5] = 'T', password[6] = 'H' */
) {
return false;
}
/* For password[8], password[9], password[10], password[11] */
let _0x3c7a5c = _0x38d32a.slice(8, 12).split("").reverse();

try {
for (let _0x396662 = 0; _0x396662 < 5; _0x396662++) {
_0x3c7a5c[_0x396662] =
_0x3c7a5c[_0x396662].charCodeAt(0) + _0x396662 + getAdder(_0x396662);
}
} catch (_0x1fbd51) {
_0x3c7a5c = _0x3c7a5c.map(
(_0x24cda7) => (_0x24cda7 += _0x1fbd51.constructor.name.length - 4)
);
}

if (
MD5(String.fromCodePoint(..._0x3c7a5c)) !==
"098f6bcd4621d373cade4e832627b4f6" /* password[8] = '0', password[9] = 'R', password[10] = '3', password[11] = 'M' */
) {
return false;
}

if (
MD5(_0x38d32a.charCodeAt(12) + "") !==
"812b4ba287f5ee0bc9d43bbf5bbe87fb" /* password[12] = '_' */
) {
return false;
}
_0x3c7a5c = (_0x38d32a[8] + _0x38d32a[11]).split("");
_0x3c7a5c.push(_0x3c7a5c.shift());
if (
_0x38d32a.substring(14, 16) !==
String.fromCodePoint(
..._0x3c7a5c.map((_0x5b5ec8) =>
Number.isNaN(+_0x5b5ec8) ? _0x5b5ec8.charCodeAt(0) + 5 : 48
)
) /* password[14] = 'R' password[15] = '0' */ ||
_0x38d32a[_0x38d32a[7] - _0x38d32a[10]] !==
atob("dQ==") /* password[0] = 'u' */ ||
_0x38d32a.indexOf(String.fromCharCode(117)) !==
_0x38d32a[7] - _0x38d32a[17] /* password[17] = '3' */ ||
JSON.stringify(
_0x38d32a
.slice(2, 4)
.split("")
.map(
(_0x7bf0a6) =>
_0x7bf0a6.charCodeAt(0) ^
getAdder.name[_0x38d32a[7]].charCodeAt(0)
)
) !==
JSON.stringify(
[72, 90].map(
(_0x40ab0d) =>
_0x40ab0d ^
String.fromCodePoint.name[_0x38d32a[17] - 1].charCodeAt(0)
)
) /* password[2] = 'f', password[3] = 't' */
) {
return false;
}
if (
String.fromCodePoint(
..._0x38d32a
.split("")
.filter(
(_0x5edfac, _0x2965d2) => _0x2965d2 > 15 && _0x2965d2 % 2 == 0
)
.map(
(_0x2ffa6d) =>
_0x2ffa6d.charCodeAt(0) ^ (_0x38d32a.length + _0x38d32a[7])
)
) !==
atob(
"g5Go"
) /* password[16] = 'V', password[18] = 'D', password[20] = '}' */
) {
return false;
}
if (
_0x38d32a[_0x38d32a.length - 2] !==
String.fromCharCode(Math.floor((({} + "").charCodeAt(0) + 9) / 3)) ||
_0x38d32a[1 + _0x38d32a[7]] !== giggity()[5] /* password[19] = ! */
) {
return false;
}
return true;
} catch (_0x4d4983) {
return false;
}
}
function getAdder(_0x430c9d) {
switch (_0x430c9d) {
case 0:
return 34;
case 1:
return 44;
case 2:
return 26;
case 3:
return 60;
}
return 101;
}
function giggity() {
return giggity.caller.name;
}

UofTCTF-2024

这里是题目

题目只有一个html文件,打开后是这个样子,大意是需要控制上面19个字节使得下面的灯全亮即可获得正确flag。

阅读html文件得知,这里实际上就是有css控制。

这里是控制每一个比特的样式,

这里是用鼠标点击作为控制,当点击latch_set按钮时,这个按钮下的子元素latch_state就往左移;当点击latch_reset按钮时,这个按钮下的子元素latch_state就往右移,

截屏2024-01-18 15.10.18

截屏2024-01-18 15.12.39

这里则是控制整一个字节的样式

截屏2024-01-18 15.15.22

这里则是控制LED灯的样式

截屏2024-01-18 15.14.48

最关键的则是控制LED的代码,这里可以这样理解

wrapper类中,若第一个byte子元素里面的第七个latch子元素的latch_resetactive,那么checker的父元素的具有相同第二个类型元素的第一个子元素做操作。

结合html文件的树形结构会更好理解。即每一个byte里面的每一个比特都控制一个checker里面的一个checker_state元素,我们就需要把所有的checker_state都用操作translateX(-100%)给挪走,这样就可以使得覆盖在原checker上面的checker_state消失,从而暴露出原来的checker,那么就会点亮这个LED

截屏2024-01-18 15.16.12

这里就直接手动操作完所有的比特了

1
2
3
4
5
6
7
8
9
10
flag = [0b01000011, 0b01110011, 0b01010011, 0b01011111, 0b01101100, 0b00110000, 0b01100111, 0b00110001, 0b01100011,
0b01011111, 0b01101001, 0b01110011, 0b01011111, 0b01100110, 0b01110101, 0b01101110, 0b01011111, 0b00110011,
0b01101000]

for i in flag:
print(chr(i), end='')

# {CsS_l0g1c_is_fun_3h}


JS总结

  1. map函数会自动捕捉上下文的变量,比如arr.map(x=>x+1)就会把arr里面的元素作为变量进行相同运算
截屏2024-01-18 00.08.53
  1. 数字与字符相加会直接在后面添加
截屏2024-01-18 00.15.34
  1. 数组的filter方法使用箭头函数会有两种使用方式

arr.filter((v) => xxx),v是具体的值

arr.filter((v,index) => xxx),index是每一项的索引

截屏2024-01-18 00.22.27

CSS总结

  1. 选择子

    1
    2
    3
    4
    5
    6
     .latch {
    position: relative;
    display: inline-flex;
    width: 200px;
    height: 50px;
    }

    这就是选择所有类latch的元素,设置它们的样式

  2. 操作

    1
    2
    3
    4
    .latch__reset:active~.latch__state {
    transform: translateX(0%);
    transition: transform 0s;
    }

    这里是选择latch_reset元素,如果点击这个元素active,那么选择这个元素后第一个子元素为latch_state,设置它的样式

  3. 条件操作

    1
    2
    3
    4
    .wrapper:has(.byte:nth-child(1) .latch:nth-child(7) .latch__reset:active) .checker:nth-of-type(2) .checker__state:nth-child(1) {
    transform: translateX(0%);
    transition: transform 0s;
    }

    wrapper类中,若第一个byte子元素里面的第七个latch子元素的latch_resetactive,那么checker的父元素的具有相同第二个类型元素的第一个子元素做操作。


UofTCTF
http://example.com/2024/01/17/UofTCTF_JS/
Author
yring
Posted on
January 17, 2024
Licensed under