看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

栏目: 编程工具 · 发布时间: 5年前

内容简介:七月的风,八月的雨,亲爱的伙伴,你在哪里?是否分得清干湿垃圾?是否解得开KCTF的赛题?将雨未雨,迎着七月清晨的微风,伸伸胳膊,我们又迎来全新的一天。小区楼下,猝不及防,收到来自清洁阿姨直击灵魂的一问,“你是什么垃圾?”“额,我……”微笑jpg.

七月的风,八月的雨,亲爱的伙伴,你在哪里?是否分得清干湿垃圾?是否解得开KCTF的赛题?

将雨未雨,迎着七月清晨的微风,伸伸胳膊,我们又迎来全新的一天。小区楼下,猝不及防,收到来自清洁阿姨直击灵魂的一问,“你是什么垃圾?”“额,我……”微笑jpg.

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

玩笑过后,我们来步入正题,昨天我们对无人攻破的部落冲突进行了解析,今天我们一起来聆听下迷雾中的琴声,看你是否能猜透其中奥秘~

题目简介

题目背景:

清晨的薄雾,湿润了空气,沁人心脾。花骨朵上还残留着晶莹剔透的露珠,顺着花瓣慢慢滴落在地上的甲壳虫身上。可是如此放松的时刻却十分短暂。雾气并没有随着太阳的升起而逐渐消散,反而越发浓重,不一会就变成了“雾里看花”。

突然正前方的花丛里发出了一阵迷乱的琴声,走过去发现有一群猴子正在弹琴。原来,这是一个外星的琴师养的一群猴子。由于长期的潜移默化,猴子们对琴师的16弦琴产生了浓厚的兴趣,经常上去弹弹。

显然,猴子们的弹奏是有规律的,只是地球人听不懂而已。而要想通过此关,你就必须要让这曲子弹出特定的韵味。于是你决定改编一下琴弦的程序,让猴子们变成真正的程序猿。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

本题共有1420人围观,截至比赛结束只有1人攻破此题。是第二赛段很有难度的一道题。

攻破此题的唯一战队是:

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

接下来我们来对题目进行解析,破解琴声中的奥秘。

看雪评委crownless点评

这道题可以从两个线索入手。

第一个线索是赛题程序的资源节是可执行的,而正常编译出的程序的资源节没有执行权,从这种角度入手难度较大。

第二个线索是用肉眼看一看这个程序的资源,可以发现异常的彩色点,进而找到分析程序的突破口。

以上两条线索,任意发现一条都可以破解程序,饶有趣味。

出题团队简介

本题出题战队  Archaia 

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

团队简介:我们是刚入坑的萌新,请大佬带带我们~

设计思路

程序没有加壳,可以直接跟踪调试。

首先通过断Messagebox找到程序各个判断的地方,改掉验证字符长度,验证字符输入和验证CRC的分支后我们发现程序最后跳进了0x004044C8。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

继续跑,程序直接异常了。

重新打开程序,对这个地址下断,根据看内存的值可以判断这里是一个16B的数组, 而最后程序会跳到这个数组里执行代码。

通过对程序的分析,可以知道里面有一个CRC48校验,要构造出能弹出Win的shellcode不难,但碰不上CRC校验。所以此路不通。

要构造出CRC校验,就只能用10B完成弹出Win的任务,然后用剩下的6B满足CRC校验。然而,当时的寄存器中也并没有存放”Win”的地址,光是call Messagebox 和push 字符串地址就占掉了10B了。所以,光靠这10B想完成任务也是行不通的。

因此可以肯定:这10B最终将跳转到其他的地方执行代码,而剩下的6B是根据这10B和CRC校验反推出来的。

那么我们就得找其他的地方是否可以执行出弹出带”Win”的Messagebox代码,作者是否留下了什么线索或漏洞。

线索一:这个exe的资源节是可执行的!!!

众所周知,正常编译出的exe的资源节没有执行权。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

然而,这个exe的4个节区都是可读可写可执行。这说明了数据区和资源区是可以在运行中修改数据并且可以执行代码的!

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

简单观察可以发现:资源节中4个字节为一组,第4个字节固定是FF,明显有规律。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

但是,这里很明显有不符合规律的数据出现在了资源里面。

这个地址是:0x1A390h + 2000h + 400000h = 41C390h

在Debug里面反汇编就能看出这里是一段弹出messagebox的代码。

显然,那10B应该跳到这里来!

如果觉得查看程序各节的权限,属于高难度技术手段,还有更简单的线索二:

线索二:用肉眼看一看这个程序的资源

这个程序没有什么其它资源,基本上就是图标了。用 工具 提取icon资源区图标,看看!

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

这里其他图片都没有彩色的点,这里却有彩色的点。 有彩色点的图片是Icon的第6张。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

找到资源icon的第6个图标资源区,注意这个。

根据上下格式这里有一段二进制和正常的icon数据不同(正常的格式4字节一组第4字节为FF),这也说明了为什么图片上有莫名的斑点。

这里可以拿到文件地址 1A390h 。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

程序没有开启随机基址 基址位置 0x00400000h。

这个可疑的代码地址就有了 0x1A390h + 2000h + 400000h = 41C390h。

以上两条线索,任意发现一条,都能找到这段代码的地址41C390h。

接下来就能看到:

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

这里是一段执行弹窗的程序。 但它只是弹窗,却没有‘Win’。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

我们在程序中搜索”Win”的ASCII(调用的A版),发现程序中找不到这个字符串, 那么程序里是通过其他运算来算出这个字符串的。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

这里push了字符串而且push之前还有一个4字节异或寄存器eax的操作,  [0x403FC] 的值是主窗口的窗口句柄,当参数传给MessageBoxA了, 并且 弹框结束后jmp回 0x00401331。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

0x00401331 这个地址正好在校验完成并跳入数组执行代码之后,可以肯定图片里的函数是执行正确序列号用的。

这里是序列号成功弹出”Win”的执行代码, ”Win”的字符串长度正好又是4个字节, 那么41c38b地址的值异或eax 后为 Win的ascii码加个’\0’。

57 69 6E 00 xor 2F 3F 5A 12 = 78 56 34 12

由于上面的 and al,0xFF 执行后 eax不可能是 78 56 34 12 所以锁定函数入口在0x0041c398。

Eax 是人为写入的, 所以得出数组里的执行10字节代码 mov eax, 0x12345678  jmp 0x0041C398。

对应的机器码B8 78563412 E9 C67E0100。

剩余6字节可从CRC结果中恢复,恢复算法是:

具体步骤和普通的CRC相同,已知被除数B878563412E9C67E0100????????????,可从crackme中得到除数0x180000030000 (对应恢复时0x80000c000001),余数0xb2a289cf038b0000,容易解出余下6字节。

下一步,  分析CrackMe可知,程序首先接收序列号,用于初始化数组,交给5个线程,经过300轮变换后,这个16字节的数组将被作为代码执行。

线程每轮修改数组需要知道两部分参数。

所有线程都共用一个相同的PRNG算法1和种子产生的随机数列,把每轮产生的随机数作为参数传给一个固定的洗牌算法,得到在[0,15]中的两个数x,y,决定修改数组中哪两个位置的值。

每个线程各自还有一个PRNG算法2和不同的种子,根据每轮产生的随机数得到L,R和q,由L,R决定线程如何修改这两处的值。5个线程的q合在一起改变PRNG内部的状态。

线程对数组的变换具体是:

将数组x位上的数字取出,加上L,记为x0

将数组y位上的数字取出,加上R,记为y0

计算  x1=3*x0+y0

y1=x0-2*y0

然后将x1放到x位 ,将y1放到y位。

显然只要知道<x,y,L,R>就可以从本轮数组还原出上一轮修改之前的数组的值。

破解者可以自己实现5个线程,使用和CrackMe中相同的算法和初始种子,并在每轮计算完成后记录下本轮的<x,y,L,R>。

用记录下来的<x,y,L,R>还原300轮后的正确的16个字节,得到300轮之前的16个字节,就可以得到正确的序列号。

解题思路

本题解题思路由看雪论坛   风间仁   提供:

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

sn长度为32

.text:00401250 call ds:GetWindowTextLengthA
.text:00401256 cmp eax, 20h
.text:00401259 jz short loc_401296

hex2bin(大写的16进制)

sn = hex2bin(sn);

sn ^= key;

.text:00401299 call x_init_sn
.data:0040401C g_xor_key db 0E9h, 4Fh, 6Eh, 20h, 78h, 1Ah, 7, 0Fh, 0, 17h, 36h, 9, 0Ah, 7, 1Fh, 0Ch

创建5个线程分别计算, 5个线程的一轮都计算结束后才会进行下一轮计算, 共300轮。

每个线程更新sn中的两个字节(二元一次方程), 把计算中用到的sn的2个索引值和1个中间变量保存下来, 用来做逆运算(不用管随机数什么的)。

.text:00401900 x_start_check

计算过程:

DWORD xorshift32(DWORD x)
{
 x ^= x << 13;
 x ^= x >> 17;
 x ^= x << 5;
 return x;
}
 
// m2, m3
DWORD transform1(DWORD a, DWORD b)
{
 DWORD m = a ^ (a >> 7);
 DWORD n = b ^ (m << 7);
 return m ^ b ^ (n << 6);
}
 
void swap8(BYTE& a, BYTE& b)
{
 BYTE t = a;
 a = b;
 b = t;
}
 
string g_temp_data;
 
void one_round(DWORD id)
{
 WORD v_00;
 WORD v_01;
 WORD v_02;
 DWORD v_03;
 // wait for g_h_ary[id]
 if (g_thread_inited[id])
 {
 v_00 = (WORD)g_4045EC ^ g_rnd0[id];
 v_01 = (WORD)g_4045EC ^ g_rnd1[id];
 v_02 = (WORD)g_4045EC ^ g_rnd2[id];
 v_03 = g_4045EC ^ g_rnd_xorshift[id];
 } else
 {
 g_thread_inited[id] = 1;
 v_00 = 3923;
 v_01 = 28;
 v_02 = 1;
 v_03 = id + 0x1D4B42;
 }
 
 BYTE v36[16] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
 
 // Wichmann–Hill
 g_rnd0[id] = 171 * v_00 % 30269;
 g_rnd1[id] = 172 * v_01 % 30307;
 g_rnd2[id] = 170 * v_02 % 30323;
 g_rnd_xorshift[id] = xorshift32(v_03);
 
 WORD rnd_sum = (WORD)(g_rnd0[id] + g_rnd2[id] + g_rnd1[id]);
 g_40402C[id] += rnd_sum;
 g_404040[id] -= rnd_sum;
 g_404068[id] *= rnd_sum;
 g_40407C[id] -= rnd_sum;
 g_404054[id] += rnd_sum;
 
 DWORD v20;
 DWORD v44;
 DWORD v38;
 
 DWORD v47 = g_404068[id];
 DWORD m0 = g_404040[id];
 DWORD m1 = g_40407C[id];
 DWORD m2 = g_40402C[id];
 DWORD m3 = g_404054[id];
 DWORD v15 = g_404068[id];
 
 for (int k = 0; k < 32; k++)
 {
 v20 = transform1(m2, m3);
 v44 = transform1(m0, v20);
 
 
 DWORD v22 = v47;
 v47 = m3;
 m2 = v22;
 
 BYTE v21 = (BYTE)(v20 * ((v15 >> 2) + m0 + 1)) & 0x0F;
 BYTE v26 = ((BYTE)v44 * ((BYTE)m2 + (BYTE)(m1 >> 2) + 1)) & 0xF;
 swap8(v36[v26], v36[v21]);
 
 m0 = m1;
 v38 = m1;
 m1 = v20;
 m3 = v44;
 v15 = v47;
 }
 
 g_40407C[id] = v20;
 g_404054[id] = v44;
 g_40402C[id] = m2;
 g_404040[id] = v38;
 g_404068[id] = v47;
 
 BYTE v28 = (BYTE)g_rnd_xorshift[id];
 WORD v28_2 = v28 * v28;
 BYTE v33 = g_buf256[(BYTE)(v28_2 >> 8) ^ g_buf256[(BYTE)v28_2]];
 BYTE sn1_idx = v36[2 * id];
 BYTE sn0_idx = v36[2 * id + 1];
 g_temp_data.append((char *)&v28, 1);
 g_temp_data.append((char *)&sn1_idx, 1);
 g_temp_data.append((char *)&sn0_idx, 1);
 
 BYTE sn0 = g_sn[sn0_idx];
 BYTE sn1 = g_sn[sn1_idx];
 g_sn[sn1_idx] = sn0 + 3 * (v33 + sn1);
 g_sn[sn0_idx] = (BYTE)(v33 + sn1 + g_buf256[v28] - 2 * sn0);
 
 g_4045F8[id] = v33 ^ g_buf256[v28];
 // release g_h
}
 
void one_round_rev(PBYTE temp_cur, int i, int k)
{
 BYTE v28 = temp_cur[0];
 BYTE sn1_idx = temp_cur[1];
 BYTE sn0_idx = temp_cur[2];
 WORD v28_2 = v28 * v28;
 BYTE v33 = g_buf256[(BYTE)(v28_2 >> 8) ^ g_buf256[(BYTE)v28_2]];
 
 BYTE new_sn0 = g_sn[sn0_idx];
 BYTE new_sn1 = g_sn[sn1_idx];
 g_sn[sn1_idx] = (2*new_sn1 + new_sn0 - 7*v33 - g_buf256[v28]) * 0xB7;
 g_sn[sn0_idx] = (new_sn1 - 3*new_sn0 + 3*g_buf256[v28]) * 0xB7;
}
 
void test2()
{
 string str = util::hex2bin("11121314212223243132333441424344");
 memcpy(g_sn, str.c_str(), str.size());
 xor_buf(g_sn, sizeof(g_sn), g_xor_key);
 for (int i = 0; i < 300; i++)
 {
 int k;
 for (k = 0; k < 5; k++)
 {
 one_round(k);
 }
 g_4045EC = 0;
 for (k = 0; k < 5; k++)
 {
 g_4045EC += g_4045F8[k];
 }
 }
 print_buf(g_sn, sizeof(g_sn));
 // 0x21, 0x19, 0x4A, 0xB9, 0x7E, 0x63, 0x04, 0x5F, 0xEA, 0xC3, 0xFC, 0x7C, 0x70, 0xC4, 0x80, 0xB2
 util::SaveFile(g_temp_data, "temp_data.dat");
}
 
void test2_rev()
{
 BYTE buf[16] = {
 0x21, 0x19, 0x4A, 0xB9, 0x7E, 0x63, 0x04, 0x5F, 0xEA, 0xC3, 0xFC, 0x7C, 0x70, 0xC4, 0x80, 0xB2
 };
 memcpy(g_sn, buf, sizeof(buf));
 util::LoadFile(g_temp_data, "temp_data.dat");
 PBYTE temp = (PBYTE)g_temp_data.c_str();
 
 for (int i = 300 - 1; i >= 0; i--)
 {
 for (int k = 5 - 1; k >= 0; k--)
 {
 PBYTE temp_cur = temp + i * 5 * 3 + k * 3;
 one_round_rev(temp_cur, i, k);
 }
 }
 xor_buf(g_sn, sizeof(g_sn), g_xor_key);
 printf("%s\n", util::bin2hex(g_sn, sizeof(g_sn)).c_str());
}

校验sn的hash

.text:00401A39 lea edi, [esp+60h+var_31]
...
.text:00401B1B xor eax, 18000h
.text:00401B20 xor ecx, 0C00h
.text:00401B26 shld ecx, eax, 1
.text:00401B2A add eax, eax
.text:00401B2C sub edx, 1
.text:00401B2F jnz short loc_401B11
.text:00401B31 cmp eax, 38B0000h
.text:00401B36 jnz short loc_401B55
.text:00401B38 cmp ecx, 0B2A289CFh
.text:00401B3E jnz short loc_401B55
 
// multi -> one
__int64 hash_v(__int64 v) {
 for (int i = 0; i < 64; i++)
 {
 if (v < 0)
 {
 v ^= 0x00000C0000018000;
 }
 v <<= 1;
 }
 return v;
}
 
__int64 hash_sn(PBYTE sn) {
 __int64 v1 = util::byteswap64(*(__int64*)(sn));
 __int64 v2 = util::byteswap64(*(__int64*)(sn+8));
 __int64 v;
 v = hash_v(v1);
 v ^= v2;
 v = hash_v(v);
 return v;
}

校验通过后, 跳向sn执行弹框(界面上提示弹出Win表示正确)。

.text:00401321 mov ecx, [ebp+hDlg]
.text:00401324 mov g_hDlg, ecx
.text:0040132A mov eax, offset g_sn
.text:0040132F jmp eax
.text:00401331 mov edx, [ebp+hDlg]
.text:00401334 push edx ; hWnd
.text:00401335 call ds:DestroyWindow
.text:0040133B jmp short loc_401380

尝试构造shellcode(未通过hash验证)。

004044C8 83C0 0C add eax, 0C
004044CB 6A 00 push 0
004044CD 50 push eax
004044CE 50 push eax
004044CF - E9 1ECEFFFF jmp 004012F2
004044D4 Win
 
83 C0 0C 6A 00 50 50 E9 1E CE FF FF 57 69 6E 00

观察所有MessageBoxA的调用,  其中第1个参数都是mov ecx, hDlg; push ecx的形式。

而00401324处却把hDlg额外保存到了全局变量g_hDlg中:

.text:004012E8 push offset Caption ; lpCaption
.text:004012ED push offset Text ; lpText
.text:004012F2 mov ecx, [ebp+hDlg]
.text:004012F5 push ecx ; hWnd
.text:004012F6 call ds:MessageBoxA
 
.text:00401321 mov ecx, [ebp+hDlg]
.text:00401324 mov g_hDlg, ecx
.text:0040132A mov eax, offset g_sn
.text:0040132F jmp eax
 
.data:004043FC g_hDlg

搜索g_hDlg的引用(搜索16进制FC434000), 另外找到1处引用(位于资源6.ico中, 所有区段都是RWE的)。

根据提示要弹出Win, 所以跳到这里时eax必须为0x125A3F2F ^ 0x006E6957(Win) = 0x12345678。

.rsrc:0041C398 xor ds:dword_41C3BB, eax
.rsrc:0041C39E push 0
.rsrc:0041C3A0 push offset dword_41C3BB
.rsrc:0041C3A5 push offset dword_41C3BB
.rsrc:0041C3AA push g_hDlg
.rsrc:0041C3B0 call ds:MessageBoxA
.rsrc:0041C3B6 jmp loc_401331
.rsrc:0041C3BB dword_41C3BB dd 125A3F2Fh

再次构造shellcode(未通过hash验证)。 

004044C8 B8 78563412 mov eax, 12345678
004044CD 3105 BBC34100 xor dword ptr [41C3BB], eax
004044D3 - E9 C67E0100 jmp 0041C39E
 
B8 78 56 34 12 31 05 BB C3 41 00 E9 C6 7E 01 00

看来得用到hash验证了, 固定前10个字节, 后面6个字节用z3约束求解

运行脚本得到: b878563412e9c67e0100a6e11213382f。

逆运算得到sn(正解): 8CF4BD334ACF8F1222B70EA1FF8085D6。

from z3 import *
 
 
def fn(n):
 v = n
 for i in range(64):
 bit = (v >> 63) & 1
 v ^= bit * 0xC0000018000
 v <<= 1
 v &= 0xFFFFFFFFFFFFFFFF
 return v
 
 
def big_int64_to_str(v):
 s = ''
 for i in range(8):
 s += chr((v >> ((7 - i) * 8)) & 0xFF)
 return s
 
 
def test1():
 a = BitVec('a', 64)
 b = BitVec('b', 64)
 v = fn(a)
 v ^= b
 v = fn(v)
 solver = Solver()
 solver.add(v == 0xB2A289CF038B0000)
 solver.add(a == 0xB878563412E9C67E)
 solver.add(((b >> 56) & 0xFF) == 1)
 solver.add(((b >> 48) & 0xFF) == 0)
 print solver.check()
 m = solver.model()
 print (big_int64_to_str(m[a].as_long()) + big_int64_to_str(m[b].as_long())).encode('hex')
 return
 
 
def test():
 test1()
 return
 
 
test()

这里的mov与jmp并不是一定得紧挨着的,  另外0041C396那一句也可以利用。

两个多解:

sn: 6A0E470F088F93B27E3366C375FCE132

sn: 8AE0942B5CE0778A643DDBAAE3E46CC3

mov eax,0x12345678
 jmp 0041C398
 
.rsrc:0041C396 and al, 0FFh
.rsrc:0041C398 xor ds:dword_41C3BB, eax

END

1、 【英雄榜单】看雪.纽盾 KCTF 晋级赛Q2 排行榜出炉!

2、 看雪.纽盾 KCTF 2019 Q2 | 第一题点评及解题思路

3、看雪.纽盾 KCTF 2019 Q2 | 第二题点评及解题思路

4、 看雪.纽盾 KCTF 2019 Q2 | 第三题点评及解题思路

5、 看雪.纽盾 KCTF 2019 Q2 | 第四题点评及解题思路

6、 看雪.纽盾 KCTF 2019 Q2 | 第五题点评及解题思路

7、 看雪.纽盾 KCTF 2019 Q2 | 第六题点评及解题思路

8、 看雪.纽盾 KCTF 2019 Q2 | 第七题点评及解题思路

主办方

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪学院(www.kanxue.com)是一个专注于PC、移动、智能设备安全研究及逆向工程的开发者社区!创建于2000年,历经19年的发展,受到业内的广泛认同,在行业中树立了令人尊敬的专业形象。平台为会员提供安全知识的在线课程教学,同时为企业提供智能设备安全相关产品和服务。 

合作伙伴

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

上海纽盾科技股份有限公司( www.newdon.net )成立于2009年,是一家以“网络安全”为主轴,以“科技源自生活,纽盾服务社会”为核心经营理念,以网络安全产品的研发、生产、销售、售后服务与相关安全服务为一体的专业安全公司,致力于为数字化时代背景下的用户提供安全产品、安全服务以及等级保护等安全解决方案。

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

10大议题正式公布!第三届看雪安全开发者峰会重磅来袭!

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路 小手一戳,了解更多

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com

看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

戳原文,查看更多精彩writeup!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Anatomy

Web Anatomy

Robert Hoekman Jr.、Jared Spool / New Riders / 2009-12-11 / USD 39.99

At the start of every web design project, the ongoing struggles reappear. We want to design highly usable and self-evident applications, but we also want to devise innovative, compelling, and exciting......一起来看看 《Web Anatomy》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具